[partial] Update partial cache logic for git 2.24.

This makes use of multiple promisor remotes, and improves cache checks
for the presence of a revision.

It also relaxes some cache checks, we were previously ignoring tag
presense in the cache, this restricts references to ignore to only HEAD.

Bug: 34965

Change-Id: I949b53b81c6b83bcba00c85d3e55243df66ebbc4
diff --git a/gitutil/git.go b/gitutil/git.go
index 07729a7..a65612e 100644
--- a/gitutil/git.go
+++ b/gitutil/git.go
@@ -6,7 +6,6 @@
 
 import (
 	"bytes"
-	"encoding/hex"
 	"fmt"
 	"io"
 	"os"
@@ -137,6 +136,28 @@
 	return g.run("remote", "add", name, path)
 }
 
+// AddOrReplacePartialRemote adds a new partial remote with given name and path.
+// If the name already exists, it replaces the named remote with new path.
+func (g *Git) AddOrReplacePartialRemote(name, path string) error {
+	configStr := fmt.Sprintf("remote.%s.url", name)
+	if err := g.Config(configStr, path); err != nil {
+		return err
+	}
+	configStr = fmt.Sprintf("remote.%s.partialCloneFilter", name)
+	if err := g.Config(configStr, "blob:none"); err != nil {
+		return err
+	}
+	configStr = fmt.Sprintf("remote.%s.promisor", name)
+	if err := g.Config(configStr, "true"); err != nil {
+		return err
+	}
+	configStr = fmt.Sprintf("remote.%s.fetch", name)
+	if err := g.Config(configStr, "+refs/heads/*:refs/remotes/origin/*"); err != nil {
+		return err
+	}
+	return nil
+}
+
 // AddOrReplaceRemote adds a new remote with given name and path. If the name
 // already exists, it replaces the named remote with new path.
 func (g *Git) AddOrReplaceRemote(name, path string) error {
@@ -217,8 +238,8 @@
 	return branches, nil
 }
 
-// IsRevAvailable runs cat-file on a commit hash is available locally.
-func (g *Git) IsRevAvailable(rev string) bool {
+// IsRevAvailable checks if a commit hash is available locally.
+func (g *Git) IsRevAvailable(jirix *jiri.X, rev string) bool {
 	// TODO: (haowei@)(11517) We are having issues with corrupted
 	// cache data on mac builders. Return a non-nil error
 	// to force the mac builders fetch from remote to avoid
@@ -226,11 +247,26 @@
 	if runtime.GOOS == "darwin" {
 		return false
 	}
-	// test if rev is a legit sha1 hash string
-	if _, err := hex.DecodeString(rev); len(rev) != 40 || err != nil {
+	// If it wants HEAD, always fetch.
+	if rev == "HEAD" {
 		return false
 	}
-
+	// Ensure the revision is present.
+	if jirix.Partial {
+		currentRevision, err := g.CurrentRevision()
+		if err != nil {
+			jirix.Logger.Errorf("could not get current revision\n")
+			return false
+		}
+		expectedRevision, err := g.CurrentRevisionForRef(rev)
+		if err != nil {
+			jirix.Logger.Errorf("could not get revision\n")
+			return false
+		}
+		if currentRevision != expectedRevision {
+			return false
+		}
+	}
 	if err := g.run("cat-file", "-e", rev); err != nil {
 		return false
 	}
@@ -656,6 +692,7 @@
 	updateShallow := false
 	depth := 0
 	fetchTag := ""
+	updateHeadOk := false
 	for _, opt := range opts {
 		switch typedOpt := opt.(type) {
 		case TagsOpt:
@@ -670,6 +707,8 @@
 			updateShallow = bool(typedOpt)
 		case FetchTagOpt:
 			fetchTag = string(typedOpt)
+		case UpdateHeadOkOpt:
+			updateHeadOk = bool(typedOpt)
 		}
 	}
 	args := []string{}
@@ -689,6 +728,9 @@
 	if all {
 		args = append(args, "--all")
 	}
+	if updateHeadOk {
+		args = append(args, "--update-head-ok")
+	}
 	if remote != "" {
 		args = append(args, remote)
 	}
diff --git a/gitutil/options.go b/gitutil/options.go
index 902a494..9462573 100644
--- a/gitutil/options.go
+++ b/gitutil/options.go
@@ -126,3 +126,7 @@
 type RebaseMerges bool
 
 func (RebaseMerges) rebaseOpt() {}
+
+type UpdateHeadOkOpt bool
+
+func (UpdateHeadOkOpt) fetchOpt() {}
diff --git a/project/loader.go b/project/loader.go
index b40a954..9938954 100644
--- a/project/loader.go
+++ b/project/loader.go
@@ -324,7 +324,6 @@
 		return fmtError(err)
 	}
 	remoteUrl := rewriteRemote(jirix, p.Remote)
-	r := remoteUrl
 	task := jirix.Logger.AddTaskMsg("Creating manifest: %s", remote.Name)
 	defer task.Done()
 	if cacheDirPath != "" {
@@ -335,21 +334,25 @@
 		if err := updateOrCreateCache(jirix, cacheDirPath, remoteUrl, remote.RemoteBranch, remote.Revision, 0); err != nil {
 			return err
 		}
-		r = cacheDirPath
 	}
 	opts := []gitutil.CloneOpt{gitutil.ReferenceOpt(cacheDirPath), gitutil.NoCheckoutOpt(true)}
 	if jirix.Partial {
 		opts = append(opts, gitutil.OmitBlobsOpt(true))
 	}
-	if err := clone(jirix, r, path, opts...); err != nil {
+	if err := clone(jirix, remoteUrl, path, opts...); err != nil {
 		return err
 	}
-	scm := gitutil.New(jirix, gitutil.RootDirOpt(path))
-	defer func() {
-		if err := scm.AddOrReplaceRemote("origin", remoteUrl); err != nil {
-			jirix.Logger.Errorf("failed to set remote back to %v for project %+v", remoteUrl, p)
+	if jirix.Partial && cacheDirPath != "" {
+		// Set Cache Remote
+		scm := gitutil.New(jirix, gitutil.RootDirOpt(p.Path))
+		if err := scm.Config("extensions.partialClone", "origin"); err != nil {
+			return err
 		}
-	}()
+		if err := scm.AddOrReplacePartialRemote("cache", cacheDirPath); err != nil {
+			return err
+		}
+	}
+
 	p.Revision = remote.Revision
 	p.RemoteBranch = remote.RemoteBranch
 	if err := checkoutHeadRevision(jirix, p, false); err != nil {
diff --git a/project/operations.go b/project/operations.go
index 69284b8..26b71d7 100644
--- a/project/operations.go
+++ b/project/operations.go
@@ -7,6 +7,7 @@
 import (
 	"fmt"
 	"io"
+	"io/ioutil"
 	"os"
 	"path"
 	"path/filepath"
@@ -88,31 +89,54 @@
 func (op createOperation) checkoutProject(jirix *jiri.X, cache string) error {
 	var err error
 	remote := rewriteRemote(jirix, op.project.Remote)
+	scm := gitutil.New(jirix, gitutil.RootDirOpt(op.project.Path))
 	// Hack to make fuchsia.git happen
 	if op.destination == jirix.Root {
-		scm := gitutil.New(jirix, gitutil.RootDirOpt(op.project.Path))
 		if err = scm.Init(op.destination); err != nil {
 			return err
 		}
 		if err = scm.AddOrReplaceRemote("origin", remote); err != nil {
 			return err
 		}
-		// We must specify a refspec here in order for patch to be able to set
-		// upstream to 'origin/master'.
-		if op.project.HistoryDepth > 0 && cache != "" {
-			if err = scm.FetchRefspec(cache, "+refs/heads/*:refs/remotes/origin/*", gitutil.DepthOpt(op.project.HistoryDepth)); err != nil {
+		// This appears to be set to 0 via some quirk of `git init`.
+		if err := scm.Config("core.repositoryformatversion", "1"); err != nil {
+			return err
+		}
+		if jirix.Partial {
+			if err := scm.Config("extensions.partialClone", "origin"); err != nil {
 				return err
 			}
-		} else if cache != "" {
-			if err = scm.FetchRefspec(cache, "+refs/heads/*:refs/remotes/origin/*"); err != nil {
-				return err
-			}
-		} else {
-			if err = scm.FetchRefspec(remote, "+refs/heads/*:refs/remotes/origin/*"); err != nil {
+			if err := scm.AddOrReplacePartialRemote("origin", remote); err != nil {
 				return err
 			}
 		}
+		// We must specify a refspec here in order for patch to be able to set
+		// upstream to 'origin/master'.
+		if err := scm.Config("remote.origin.fetch", "+refs/heads/*:refs/remotes/origin/*"); err != nil {
+			return err
+		}
+		if cache != "" {
+			objPath := "objects"
+			if jirix.Partial {
+				objPath = ".git/objects"
+			}
+			if err := ioutil.WriteFile(filepath.Join(op.destination, ".git/objects/info/alternates"), []byte(filepath.Join(cache, objPath) + "\n"), 0644); err != nil {
+				return err
+			}
+		}
+		if err = fetchAll(jirix, op.project); err != nil {
+			return err
+		}
 	} else {
+		r := remote
+		if cache != "" {
+			r = cache
+			defer func() {
+				if err := scm.AddOrReplaceRemote("origin", remote); err != nil {
+						jirix.Logger.Errorf("failed to set remote back to %v for project %+v", remote, op.project)
+				}
+			}()
+		}
 		opts := []gitutil.CloneOpt{gitutil.NoCheckoutOpt(true)}
 		if op.project.HistoryDepth > 0 {
 			opts = append(opts, gitutil.DepthOpt(op.project.HistoryDepth))
@@ -123,18 +147,18 @@
 		if jirix.Partial {
 			opts = append(opts, gitutil.OmitBlobsOpt(true))
 		}
-		if cache != "" {
-			if err = clone(jirix, cache, op.destination, opts...); err != nil {
-				return err
-			}
-			scm := gitutil.New(jirix, gitutil.RootDirOpt(op.project.Path))
-			if err = scm.AddOrReplaceRemote("origin", remote); err != nil {
-				return err
-			}
-		} else {
-			if err = clone(jirix, remote, op.destination, opts...); err != nil {
-				return err
-			}
+		if err = clone(jirix, r, op.destination, opts...); err != nil {
+			return err
+		}
+	}
+
+	if jirix.Partial && cache != "" {
+		// Set Cache Remote
+		if err := scm.Config("extensions.partialClone", "origin"); err != nil {
+			return err
+		}
+		if err := scm.AddOrReplacePartialRemote("cache", cache); err != nil {
+			return err
 		}
 	}
 
@@ -149,13 +173,6 @@
 	if err := writeMetadata(jirix, op.project, op.project.Path); err != nil {
 		return err
 	}
-	scm := gitutil.New(jirix, gitutil.RootDirOpt(op.project.Path))
-
-	// Reset remote to point to correct location so that shared cache does not cause problem.
-	if err := scm.SetRemoteUrl("origin", remote); err != nil {
-		return err
-	}
-
 	// Delete inital branch(es)
 	if branches, _, err := scm.GetBranches(); err != nil {
 		jirix.Logger.Warningf("not able to get branches for newly created project %s(%s)\n\n", op.project.Name, op.project.Path)
diff --git a/project/project.go b/project/project.go
index 45076bc..145e9fc 100644
--- a/project/project.go
+++ b/project/project.go
@@ -1967,27 +1967,34 @@
 			jirix.Logger.Warningf("set remote.origin.fetch failed under git cache directory %q due to error: %v", dir, err)
 			return errCacheCorruption
 		}
-		// Cache already present, update it
-		// TODO : update this after implementing FetchAll using g
-		if scm.IsRevAvailable(revision) {
-			jirix.Logger.Infof("%s(%s) cache up-to-date; skipping\n", remote, dir)
-			return nil
+		if jirix.Partial {
+			if err := scm.AddOrReplacePartialRemote("origin", remote); err != nil {
+				return err
+			}
 		}
 		msg := fmt.Sprintf("Updating cache: %q", dir)
 		task := jirix.Logger.AddTaskMsg(msg)
 		defer task.Done()
 		t := jirix.Logger.TrackTime(msg)
 		defer t.Done()
+		// Cache already present, update it
+		// TODO : update this after implementing FetchAll using g
+		if scm.IsRevAvailable(jirix, revision) {
+			jirix.Logger.Infof("%s(%s) cache up-to-date; skipping\n", remote, dir)
+			return nil
+		}
 		// We need to explicitly specify the ref for fetch to update in case
 		// the cache was created with a previous version and uses "refs/*"
 		if err := retry.Function(jirix, func() error {
-			git := gitutil.New(jirix, gitutil.RootDirOpt(dir))
-			if err := git.FetchRefspec("origin", refspec,
-				gitutil.DepthOpt(depth), gitutil.PruneOpt(true), gitutil.UpdateShallowOpt(true)); err != nil {
+			// Use --update-head-ok here to force fetch to update the current branch.
+			// This is used in the case of a partial clone having a working tree
+			// checked out in the cache.
+			if err := scm.FetchRefspec("origin", refspec,
+				gitutil.DepthOpt(depth), gitutil.PruneOpt(true), gitutil.UpdateShallowOpt(true), gitutil.UpdateHeadOkOpt(true)); err != nil {
 				return err
 			}
 			if jirix.Partial {
-				if err := git.CheckoutBranch(revision, gitutil.DetachOpt(true), gitutil.ForceOpt(true)); err != nil {
+				if err := scm.CheckoutBranch(revision, gitutil.DetachOpt(true), gitutil.ForceOpt(true)); err != nil {
 					return err
 				}
 			}
@@ -2036,17 +2043,19 @@
 		defer t.Done()
 		// Try use clone.bundle to speed up the initialization of git cache.
 		os.MkdirAll(dir, 0755)
-		if err := createCacheThroughBundle(); err != nil {
-			jirix.Logger.Debugf("create git cache for %q through clone.bundle failed due to error: %v", remote, err)
-			os.RemoveAll(dir)
-		} else {
-			jirix.Logger.Debugf("git cache for %q created through clone.bundle", remote)
-			return nil
+		if !jirix.Partial {
+			if err := createCacheThroughBundle(); err != nil {
+				jirix.Logger.Debugf("create git cache for %q through clone.bundle failed due to error: %v", remote, err)
+				os.RemoveAll(dir)
+			} else {
+				jirix.Logger.Debugf("git cache for %q created through clone.bundle", remote)
+				return nil
+			}
 		}
 
 		opts := []gitutil.CloneOpt{gitutil.DepthOpt(depth)}
 		if jirix.Partial {
-			opts = append(opts, gitutil.OmitBlobsOpt(true))
+			opts = append(opts, gitutil.NoCheckoutOpt(true), gitutil.OmitBlobsOpt(true))
 		} else {
 			opts = append(opts, gitutil.BareOpt(true))
 		}
@@ -2065,6 +2074,9 @@
 		if err := git.Config("remote.origin.fetch", refspec); err != nil {
 			return err
 		}
+		if err := git.Config("uploadpack.allowFilter", "true"); err != nil {
+			return err
+		}
 		return nil
 	}