Add -sync option to slothfs-populate.

This option creates a workspace from a freshly dereferenced manifest,
and populates the checkout with symlinks to the checkout.

Change-Id: I0e8d63f5bb05e4fe1a066627c2a7b88920e4300e
diff --git a/cmd/slothfs-deref-manifest/main.go b/cmd/slothfs-deref-manifest/main.go
index c58b508..6d3e1e5 100644
--- a/cmd/slothfs-deref-manifest/main.go
+++ b/cmd/slothfs-deref-manifest/main.go
@@ -16,14 +16,11 @@
 
 import (
 	"flag"
-	"fmt"
 	"log"
 	"os"
 
 	"github.com/google/slothfs/gitiles"
-	"github.com/google/slothfs/manifest"
-
-	git "github.com/libgit2/git2go"
+	"github.com/google/slothfs/populate"
 )
 
 func main() {
@@ -37,15 +34,15 @@
 		log.Fatalf("NewService: %v", err)
 	}
 
-	mf, err := fetchManifest(service, *repo, *branch)
+	mf, err := populate.FetchManifest(service, *repo, *branch)
 	if err != nil {
-		log.Fatalf("fetchManifest: %v", err)
+		log.Fatalf("FetchManifest: %v", err)
 	}
 
 	mf.Filter()
 
-	if err := derefManifest(service, *repo, mf); err != nil {
-		log.Fatalf("derefManifest: %v", err)
+	if err := populate.DerefManifest(service, mf); err != nil {
+		log.Fatalf("DerefManifest: %v", err)
 	}
 
 	xml, err := mf.MarshalXML()
@@ -55,63 +52,3 @@
 
 	os.Stdout.Write(xml)
 }
-
-func fetchManifest(service *gitiles.Service, repo, branch string) (*manifest.Manifest, error) {
-	project := service.NewRepoService(repo)
-
-	// When checking this out, it's called "manifest.xml". Go figure.
-	c, err := project.GetBlob(branch, "default.xml")
-	if err != nil {
-		return nil, err
-	}
-	mf, err := manifest.Parse(c)
-	if err != nil {
-		return nil, err
-	}
-
-	return mf, nil
-}
-
-func derefManifest(service *gitiles.Service, manifestRepo string, mf *manifest.Manifest) error {
-	branchSet := map[string]struct{}{}
-
-	var todoProjects []int
-	for i, p := range mf.Project {
-		rev := mf.ProjectRevision(&p)
-		if _, err := git.NewOid(rev); err == nil {
-			continue
-		}
-
-		branchSet[rev] = struct{}{}
-		todoProjects = append(todoProjects, i)
-	}
-
-	var branches []string
-	for k := range branchSet {
-		branches = append(branches, k)
-	}
-
-	repos, err := service.List(branches)
-	if err != nil {
-		return err
-	}
-	for _, i := range todoProjects {
-		p := &mf.Project[i]
-
-		proj, ok := repos[p.Name]
-		if !ok {
-			return fmt.Errorf("server list doesn't mention repo %s", p.Name)
-		}
-
-		p.CloneURL = proj.CloneURL
-
-		branch := mf.ProjectRevision(p)
-		commit, ok := proj.Branches[branch]
-		if !ok {
-			return fmt.Errorf("branch %q for repo %s not returned", branch, p.Name)
-		}
-
-		p.Revision = commit
-	}
-	return nil
-}
diff --git a/cmd/slothfs-populate/main.go b/cmd/slothfs-populate/main.go
index b7fcedb..ce50744 100644
--- a/cmd/slothfs-populate/main.go
+++ b/cmd/slothfs-populate/main.go
@@ -15,16 +15,88 @@
 package main
 
 import (
+	"bufio"
 	"flag"
+	"io/ioutil"
 	"log"
 	"os"
+	"path/filepath"
+	"strings"
 	"time"
 
+	"github.com/google/slothfs/gitiles"
 	"github.com/google/slothfs/populate"
 )
 
+// findSlothFSMount guesses where slothfs might be mounted.
+func findSlothFSMount() string {
+	f, err := os.Open("/proc/mounts")
+	if err != nil {
+		// We're probably on OSX.
+		return ""
+	}
+	defer f.Close()
+
+	scanner := bufio.NewScanner(f)
+	for scanner.Scan() {
+		line := scanner.Text()
+		fields := strings.Split(line, " ")
+		if len(fields) >= 3 && fields[2] == "fuse.slothfs" {
+			return fields[1]
+		}
+	}
+	return ""
+}
+
+// syncManifest fetches a manifest file, and configures a workspace
+// for it.
+func syncManifest(opts *gitiles.Options, mountPoint, repo, branch string) (string, error) {
+	service, err := gitiles.NewService(*opts)
+	if err != nil {
+		return "", err
+	}
+
+	mf, err := populate.FetchManifest(service, repo, branch)
+	if err != nil {
+		return "", err
+	}
+
+	mf.Filter()
+
+	if err := populate.DerefManifest(service, mf); err != nil {
+		return "", err
+	}
+
+	xml, err := ioutil.TempFile("", "")
+	if err != nil {
+		return "", err
+	}
+
+	xmlBytes, err := mf.MarshalXML()
+	if err != nil {
+		return "", err
+	}
+	if err := ioutil.WriteFile(xml.Name(), xmlBytes, 0644); err != nil {
+		return "", err
+	}
+
+	name := strings.Replace(time.Now().Format("S"+time.RFC3339), ":", "_", -1)
+
+	log.Printf("fetched manifest; configuring workspace %s", name)
+	if err := os.Symlink(xml.Name(), filepath.Join(mountPoint, "config", name)); err != nil {
+		return "", err
+	}
+
+	return filepath.Join(mountPoint, name), nil
+}
+
 func main() {
-	mount := flag.String("ro", "", "path to slothfs-multifs mount.")
+	gitilesOptions := gitiles.DefineFlags()
+	newROWorkspace := flag.String("ro", "", "Set path to slothfs-multifs mount.")
+	mount := flag.String("mount", "", "Set slothfs mountpoint for -sync option. Autodetected if empty.")
+	sync := flag.Bool("sync", false, "Sync checkout to latest manifest version.")
+	syncBranch := flag.String("sync_branch", "master", "Use this branch for -sync.")
+	syncRepo := flag.String("sync_repo", "platform/manifest", "Use this repo for -sync.")
 	flag.Parse()
 
 	dir := "."
@@ -34,7 +106,28 @@
 		log.Fatal("too many arguments.")
 	}
 
-	added, changed, err := populate.Checkout(*mount, dir)
+	if *sync {
+		if *mount == "" {
+			*mount = findSlothFSMount()
+			if *mount == "" {
+				log.Fatal("could not autodetect mount point. Pass --mount option.")
+			}
+		}
+
+		var err error
+		*newROWorkspace, err = syncManifest(gitilesOptions, *mount, *syncRepo, *syncBranch)
+		if err != nil {
+			log.Fatalf("syncManifest: %v", err)
+		}
+	}
+
+	if *newROWorkspace == "" {
+		log.Fatalf("no readonly checkout given. Specify -ro DIR or -sync.")
+	}
+
+	log.Printf("creating symlinks to %s", *newROWorkspace)
+
+	added, changed, err := populate.Checkout(*newROWorkspace, dir)
 	if err != nil {
 		log.Fatalf("populate.Checkout: %v", err)
 	}
diff --git a/docs/manual.md b/docs/manual.md
index 6791204..4c1f17e 100644
--- a/docs/manual.md
+++ b/docs/manual.md
@@ -106,6 +106,9 @@
 This populate the `checkout` directory with symlinks into `/slothfs`, yielding a
 full check out.
 
+If there were symlinks to a previous checkout in the workspace, this will also
+update timestamps to make incremental builds work.
+
 
 Syncing
 =======
@@ -113,14 +116,11 @@
 Advancing your checkout to a different timestamp uses the same commands. To sync
 to the current state of the Android tree, do the following
 
-    SYNC=$(date -Iminutes)
-    slothfs-deref-manifest > /tmp/${SYNC}.xml
-    ln -s /tmp/${SYNC}.xml /slothfs/config/${SYNC}
-    slothfs-populate -ro /slothfs/${SYNC} .
+    slothfs-populate -sync .
 
-this will reroute symlinks in your check to your newly created
-workspace. `slothfs-populate` will also update timestamps so incremental builds
-keep working.
+This fetches the latest manifest file, finds the project revisions, sets up a
+workspace for the manifest, and updates the symlinks from your read/write
+checkout.
 
 
 Removing a workspace
diff --git a/populate/deref.go b/populate/deref.go
new file mode 100644
index 0000000..7e82553
--- /dev/null
+++ b/populate/deref.go
@@ -0,0 +1,96 @@
+// Copyright 2016 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package populate
+
+import (
+	"fmt"
+
+	"github.com/google/slothfs/gitiles"
+	"github.com/google/slothfs/manifest"
+
+	git "github.com/libgit2/git2go"
+)
+
+// FetchManifest gets the default manifest file from a Gitiles server.
+func FetchManifest(service *gitiles.Service, repo, branch string) (*manifest.Manifest, error) {
+	project := service.NewRepoService(repo)
+
+	// When checking this out, it's called "manifest.xml". Go figure.
+	c, err := project.GetBlob(branch, "default.xml")
+	if err != nil {
+		return nil, err
+	}
+	mf, err := manifest.Parse(c)
+	if err != nil {
+		return nil, err
+	}
+
+	return mf, nil
+}
+
+// DerefManifest uses the Gitiles JSON interface to fill in
+// Project.Revision and Project.CloneURL in the given manifest.
+func DerefManifest(service *gitiles.Service, mf *manifest.Manifest) error {
+	// Collect all branch names we might care about, so we can
+	// request data from all branches in one JSON call.  Normally,
+	// all projects use the same branch, but individual projects
+	// may specify a special branch.
+	branchSet := map[string]struct{}{}
+
+	var todoProjects []int
+	for i, p := range mf.Project {
+		rev := mf.ProjectRevision(&p)
+
+		// According to the repo doc, the revision should be a branch,
+		// either like 'refs/heads/master' or 'master'. We abuse this field by
+		// also allowing commit SHA1s.
+		if _, err := git.NewOid(rev); err == nil {
+			// Already a SHA1, don't change.
+			continue
+		}
+
+		branchSet[rev] = struct{}{}
+		todoProjects = append(todoProjects, i)
+	}
+
+	var branches []string
+	for k := range branchSet {
+		branches = append(branches, k)
+	}
+
+	repos, err := service.List(branches)
+	if err != nil {
+		return err
+	}
+	for _, i := range todoProjects {
+		p := &mf.Project[i]
+
+		proj, ok := repos[p.Name]
+		if !ok {
+			return fmt.Errorf("server list doesn't mention repo %s", p.Name)
+		}
+
+		p.CloneURL = proj.CloneURL
+
+		branch := mf.ProjectRevision(p)
+		commit, ok := proj.Branches[branch]
+		if !ok {
+			return fmt.Errorf("branch %q for repo %s not returned", branch, p.Name)
+		}
+
+		p.Revision = commit
+	}
+	return nil
+}