manifest: record the original revision when in -r mode.

Currently when doing a sync against a revision locked manifest,
sync has no option but to fall back to sync'ing the entire refs space;
it doesn't know which ref to ask for that contains the sha1 it wants.

This sucks if we're in -c mode; thus when we generate a revision
locked manifest, record the originating branch- and try syncing that
branch first.  If the sha1 is found within that branch, this saves
us having to pull down the rest of the repo- a potentially heavy
saving.

If that branch doesn't have the desired sha1, we fallback to sync'ing
everything.

Change-Id: I99a5e44fa1d792dfcada76956a2363187df94cf1
diff --git a/manifest_xml.py b/manifest_xml.py
index 8e9efd1..be18547 100644
--- a/manifest_xml.py
+++ b/manifest_xml.py
@@ -123,7 +123,7 @@
     if r.reviewUrl is not None:
       e.setAttribute('review', r.reviewUrl)
 
-  def Save(self, fd, peg_rev=False):
+  def Save(self, fd, peg_rev=False, peg_rev_upstream=True):
     """Write the current manifest out to the given file descriptor.
     """
     mp = self.manifestProject
@@ -197,11 +197,15 @@
         e.setAttribute('remote', p.remote.name)
       if peg_rev:
         if self.IsMirror:
-          e.setAttribute('revision',
-                         p.bare_git.rev_parse(p.revisionExpr + '^0'))
+          value = p.bare_git.rev_parse(p.revisionExpr + '^0')
         else:
-          e.setAttribute('revision',
-                         p.work_git.rev_parse(HEAD + '^0'))
+          value = p.work_git.rev_parse(HEAD + '^0')
+        e.setAttribute('revision', value)
+        if peg_rev_upstream and value != p.revisionExpr:
+          # Only save the origin if the origin is not a sha1, and the default
+          # isn't our value, and the if the default doesn't already have that
+          # covered.
+          e.setAttribute('upstream', p.revisionExpr)
       elif not d.revisionExpr or p.revisionExpr != d.revisionExpr:
         e.setAttribute('revision', p.revisionExpr)
 
@@ -573,6 +577,8 @@
     else:
       sync_c = sync_c.lower() in ("yes", "true", "1")
 
+    upstream = node.getAttribute('upstream')
+
     groups = ''
     if node.hasAttribute('groups'):
       groups = node.getAttribute('groups')
@@ -599,7 +605,8 @@
                       revisionId = None,
                       rebase = rebase,
                       groups = groups,
-                      sync_c = sync_c)
+                      sync_c = sync_c,
+                      upstream = upstream)
 
     for n in node.childNodes:
       if n.nodeName == 'copyfile':
diff --git a/project.py b/project.py
index 4621013..04c43bb 100644
--- a/project.py
+++ b/project.py
@@ -484,7 +484,8 @@
                revisionId,
                rebase = True,
                groups = None,
-               sync_c = False):
+               sync_c = False,
+               upstream = None):
     self.manifest = manifest
     self.name = name
     self.remote = remote
@@ -506,6 +507,7 @@
     self.rebase = rebase
     self.groups = groups
     self.sync_c = sync_c
+    self.upstream = upstream
 
     self.snapshots = {}
     self.copyfiles = []
@@ -1373,6 +1375,16 @@
     is_sha1 = False
     tag_name = None
 
+    def CheckForSha1():
+        try:
+          # if revision (sha or tag) is not present then following function
+          # throws an error.
+          self.bare_git.rev_parse('--verify', '%s^0' % self.revisionExpr)
+          return True
+        except GitError:
+          # There is no such persistent revision. We have to fetch it.
+          return False
+
     if current_branch_only:
       if ID_RE.match(self.revisionExpr) is not None:
         is_sha1 = True
@@ -1381,14 +1393,10 @@
         tag_name = self.revisionExpr[len(R_TAGS):]
 
       if is_sha1 or tag_name is not None:
-        try:
-          # if revision (sha or tag) is not present then following function
-          # throws an error.
-          self.bare_git.rev_parse('--verify', '%s^0' % self.revisionExpr)
+        if CheckForSha1():
           return True
-        except GitError:
-          # There is no such persistent revision. We have to fetch it.
-          pass
+      if is_sha1 and (not self.upstream or ID_RE.match(self.upstream)):
+        current_branch_only = False
 
     if not name:
       name = self.remote.name
@@ -1453,7 +1461,7 @@
       cmd.append('--update-head-ok')
     cmd.append(name)
 
-    if not current_branch_only or is_sha1:
+    if not current_branch_only:
       # Fetch whole repo
       cmd.append('--tags')
       cmd.append((u'+refs/heads/*:') + remote.ToLocal('refs/heads/*'))
@@ -1462,15 +1470,23 @@
       cmd.append(tag_name)
     else:
       branch = self.revisionExpr
+      if is_sha1:
+        branch = self.upstream
       if branch.startswith(R_HEADS):
         branch = branch[len(R_HEADS):]
       cmd.append((u'+refs/heads/%s:' % branch) + remote.ToLocal('refs/heads/%s' % branch))
 
     ok = False
     for i in range(2):
-      if GitCommand(self, cmd, bare=True, ssh_proxy=ssh_proxy).Wait() == 0:
+      ret = GitCommand(self, cmd, bare=True, ssh_proxy=ssh_proxy).Wait()
+      if ret == 0:
         ok = True
         break
+      elif current_branch_only and is_sha1 and ret == 128:
+        # Exit code 128 means "couldn't find the ref you asked for"; if we're in sha1
+        # mode, we just tried sync'ing from the upstream field; it doesn't exist, thus
+        # abort the optimization attempt and do a full sync.
+        break
       time.sleep(random.randint(30, 45))
 
     if initial:
@@ -1480,6 +1496,15 @@
         else:
           os.remove(packed_refs)
       self.bare_git.pack_refs('--all', '--prune')
+
+    if is_sha1 and current_branch_only and self.upstream:
+      # We just synced the upstream given branch; verify we
+      # got what we wanted, else trigger a second run of all
+      # refs.
+      if not CheckForSha1():
+        return self._RemoteFetch(name=name, current_branch_only=False,
+                                 initial=False, quiet=quiet, alt_dir=alt_dir)
+
     return ok
 
   def _ApplyCloneBundle(self, initial=False, quiet=False):
diff --git a/subcmds/manifest.py b/subcmds/manifest.py
index cd19653..4388765 100644
--- a/subcmds/manifest.py
+++ b/subcmds/manifest.py
@@ -48,6 +48,11 @@
     p.add_option('-r', '--revision-as-HEAD',
                  dest='peg_rev', action='store_true',
                  help='Save revisions as current HEAD')
+    p.add_option('--suppress-upstream-revision', dest='peg_rev_upstream',
+                 default=True, action='store_false',
+                 help='If in -r mode, do not write the upstream field.  '
+                 'Only of use if the branch names for a sha1 manifest are '
+                 'sensitive.')
     p.add_option('-o', '--output-file',
                  dest='output_file',
                  default='-',
@@ -60,7 +65,8 @@
     else:
       fd = open(opt.output_file, 'w')
     self.manifest.Save(fd,
-                       peg_rev = opt.peg_rev)
+                       peg_rev = opt.peg_rev,
+                       peg_rev_upstream = opt.peg_rev_upstream)
     fd.close()
     if opt.output_file != '-':
       print >>sys.stderr, 'Saved manifest to %s' % opt.output_file