Change project.revision to revisionExpr and revisionId

The revisionExpr field now holds an expression from the manifest,
such as "refs/heads/master", while revisionId holds the current
commit-ish SHA-1 of the revisionExpr.  Currently that is only
filled in if the manifest points directly to a SHA-1.

Signed-off-by: Shawn O. Pearce <sop@google.com>
diff --git a/manifest_xml.py b/manifest_xml.py
index 5c5617a..51e653e 100644
--- a/manifest_xml.py
+++ b/manifest_xml.py
@@ -27,7 +27,7 @@
 class _Default(object):
   """Project defaults within the manifest."""
 
-  revision = None
+  revisionExpr = None
   remote = None
 
 class _XmlRemote(object):
@@ -116,9 +116,9 @@
     if d.remote:
       have_default = True
       e.setAttribute('remote', d.remote.name)
-    if d.revision:
+    if d.revisionExpr:
       have_default = True
-      e.setAttribute('revision', d.revision)
+      e.setAttribute('revision', d.revisionExpr)
     if have_default:
       root.appendChild(e)
       root.appendChild(doc.createTextNode(''))
@@ -138,12 +138,12 @@
       if peg_rev:
         if self.IsMirror:
           e.setAttribute('revision',
-                         p.bare_git.rev_parse(p.revision + '^0'))
+                         p.bare_git.rev_parse(p.revisionExpr + '^0'))
         else:
           e.setAttribute('revision',
                          p.work_git.rev_parse(HEAD + '^0'))
-      elif not d.revision or p.revision != d.revision:
-        e.setAttribute('revision', p.revision)
+      elif not d.revisionExpr or p.revisionExpr != d.revisionExpr:
+        e.setAttribute('revision', p.revisionExpr)
 
       for c in p.copyfiles:
         ce = doc.createElement('copyfile')
@@ -286,7 +286,8 @@
                         gitdir = gitdir,
                         worktree = None,
                         relpath = None,
-                        revision = m.revision)
+                        revisionExpr = m.revisionExpr,
+                        revisionId = None)
       self._projects[project.name] = project
 
   def _ParseRemote(self, node):
@@ -306,9 +307,9 @@
     """
     d = _Default()
     d.remote = self._get_remote(node)
-    d.revision = node.getAttribute('revision')
-    if d.revision == '':
-      d.revision = None
+    d.revisionExpr = node.getAttribute('revision')
+    if d.revisionExpr == '':
+      d.revisionExpr = None
     return d
 
   def _ParseProject(self, node):
@@ -325,10 +326,10 @@
             "no remote for project %s within %s" % \
             (name, self.manifestFile)
 
-    revision = node.getAttribute('revision')
-    if not revision:
-      revision = self._default.revision
-    if not revision:
+    revisionExpr = node.getAttribute('revision')
+    if not revisionExpr:
+      revisionExpr = self._default.revisionExpr
+    if not revisionExpr:
       raise ManifestParseError, \
             "no revision for project %s within %s" % \
             (name, self.manifestFile)
@@ -355,7 +356,8 @@
                       gitdir = gitdir,
                       worktree = worktree,
                       relpath = path,
-                      revision = revision)
+                      revisionExpr = revisionExpr,
+                      revisionId = None)
 
     for n in node.childNodes:
       if n.nodeName == 'copyfile':
diff --git a/project.py b/project.py
index cab4715..e094b5d 100644
--- a/project.py
+++ b/project.py
@@ -228,14 +228,23 @@
                gitdir,
                worktree,
                relpath,
-               revision):
+               revisionExpr,
+               revisionId):
     self.manifest = manifest
     self.name = name
     self.remote = remote
     self.gitdir = gitdir
     self.worktree = worktree
     self.relpath = relpath
-    self.revision = revision
+    self.revisionExpr = revisionExpr
+
+    if   revisionId is None \
+     and revisionExpr \
+     and IsId(revisionExpr):
+      self.revisionId = revisionExpr
+    else:
+      self.revisionId = revisionId
+
     self.snapshots = {}
     self.copyfiles = []
     self.config = GitConfig.ForRepository(
@@ -605,6 +614,23 @@
     for file in self.copyfiles:
       file._Copy()
 
+  def GetRevisionId(self, all=None):
+    if self.revisionId:
+      return self.revisionId
+
+    rem = self.GetRemote(self.remote.name)
+    rev = rem.ToLocal(self.revisionExpr)
+
+    if all is not None and rev in all:
+      return all[rev]
+
+    try:
+      return self.bare_git.rev_parse('--verify', '%s^0' % rev)
+    except GitError:
+      raise ManifestInvalidRevisionError(
+        'revision %s in %s not found' % (self.revisionExpr,
+                                         self.name))
+
   def Sync_LocalHalf(self, syncbuf):
     """Perform only the local IO portion of the sync process.
        Network access is not required.
@@ -613,19 +639,7 @@
     all = self.bare_ref.all
     self.CleanPublishedCache(all)
 
-    rem = self.GetRemote(self.remote.name)
-    rev = rem.ToLocal(self.revision)
-    if rev in all:
-      revid = all[rev]
-    elif IsId(rev):
-      revid = rev
-    else:
-      try:
-        revid = self.bare_git.rev_parse('--verify', '%s^0' % rev)
-      except GitError:
-        raise ManifestInvalidRevisionError(
-          'revision %s in %s not found' % (self.revision, self.name))
-
+    revid = self.GetRevisionId(all)
     head = self.work_git.GetHead()
     if head.startswith(R_HEADS):
       branch = head[len(R_HEADS):]
@@ -649,11 +663,11 @@
         #
         return
 
-      lost = self._revlist(not_rev(rev), HEAD)
+      lost = self._revlist(not_rev(revid), HEAD)
       if lost:
         syncbuf.info(self, "discarding %d commits", len(lost))
       try:
-        self._Checkout(rev, quiet=True)
+        self._Checkout(revid, quiet=True)
       except GitError, e:
         syncbuf.fail(self, e)
         return
@@ -666,9 +680,8 @@
       return
 
     branch = self.GetBranch(branch)
-    merge = branch.LocalMerge
 
-    if not merge:
+    if not branch.LocalMerge:
       # The current branch has no tracking configuration.
       # Jump off it to a deatched HEAD.
       #
@@ -676,17 +689,17 @@
                    "leaving %s; does not track upstream",
                    branch.name)
       try:
-        self._Checkout(rev, quiet=True)
+        self._Checkout(revid, quiet=True)
       except GitError, e:
         syncbuf.fail(self, e)
         return
       self._CopyFiles()
       return
 
-    upstream_gain = self._revlist(not_rev(HEAD), rev)
+    upstream_gain = self._revlist(not_rev(HEAD), revid)
     pub = self.WasPublished(branch.name, all)
     if pub:
-      not_merged = self._revlist(not_rev(rev), pub)
+      not_merged = self._revlist(not_rev(revid), pub)
       if not_merged:
         if upstream_gain:
           # The user has published this branch and some of those
@@ -703,24 +716,15 @@
         # strict subset.  We can fast-forward safely.
         #
         def _doff():
-          self._FastForward(rev)
+          self._FastForward(revid)
           self._CopyFiles()
         syncbuf.later1(self, _doff)
         return
 
-    # If the upstream switched on us, warn the user.
-    #
-    if merge != rev:
-      syncbuf.info(self, "manifest switched %s...%s", merge, rev)
-
     # Examine the local commits not in the remote.  Find the
     # last one attributed to this user, if any.
     #
-    local_changes = self._revlist(
-      not_rev(merge),
-      HEAD,
-      format='%H %ce')
-
+    local_changes = self._revlist(not_rev(revid), HEAD, format='%H %ce')
     last_mine = None
     cnt_mine = 0
     for commit in local_changes:
@@ -738,6 +742,19 @@
       syncbuf.fail(self, _DirtyError())
       return
 
+    # If the upstream switched on us, warn the user.
+    #
+    if branch.merge != self.revisionExpr:
+      if branch.merge and self.revisionExpr:
+        syncbuf.info(self,
+                     'manifest switched %s...%s',
+                     branch.merge,
+                     self.revisionExpr)
+      elif branch.merge:
+        syncbuf.info(self,
+                     'manifest no longer tracks %s',
+                     branch.merge)
+
     if cnt_mine < len(local_changes):
       # Upstream rebased.  Not everything in HEAD
       # was created by this user.
@@ -746,25 +763,25 @@
                    "discarding %d commits removed from upstream",
                    len(local_changes) - cnt_mine)
 
-    branch.remote = rem
-    branch.merge = self.revision
+    branch.remote = self.GetRemote(self.remote.name)
+    branch.merge = self.revisionExpr
     branch.Save()
 
     if cnt_mine > 0:
       def _dorebase():
-        self._Rebase(upstream = '%s^1' % last_mine, onto = rev)
+        self._Rebase(upstream = '%s^1' % last_mine, onto = revid)
         self._CopyFiles()
       syncbuf.later2(self, _dorebase)
     elif local_changes:
       try:
-        self._ResetHard(rev)
+        self._ResetHard(revid)
         self._CopyFiles()
       except GitError, e:
         syncbuf.fail(self, e)
         return
     else:
       def _doff():
-        self._FastForward(rev)
+        self._FastForward(revid)
         self._CopyFiles()
       syncbuf.later1(self, _doff)
 
@@ -786,7 +803,7 @@
     if GitCommand(self, cmd, bare=True).Wait() != 0:
       return None
     return DownloadedChange(self,
-                            remote.ToLocal(self.revision),
+                            self.GetRevisionId(),
                             change_id,
                             patch_id,
                             self.bare_git.rev_parse('FETCH_HEAD'))
@@ -810,15 +827,8 @@
 
     branch = self.GetBranch(name)
     branch.remote = self.GetRemote(self.remote.name)
-    branch.merge = self.revision
-
-    rev = branch.LocalMerge
-    if rev in all:
-      revid = all[rev]
-    elif IsId(rev):
-      revid = rev
-    else:
-      revid = None
+    branch.merge = self.revisionExpr
+    revid = self.GetRevisionId(all)
 
     if head.startswith(R_HEADS):
       try:
@@ -839,7 +849,7 @@
       return True
 
     if GitCommand(self,
-                  ['checkout', '-b', branch.name, rev],
+                  ['checkout', '-b', branch.name, revid],
                   capture_stdout = True,
                   capture_stderr = True).Wait() == 0:
       branch.Save()
@@ -900,19 +910,12 @@
       #
       head = all[head]
 
-      rev = self.GetRemote(self.remote.name).ToLocal(self.revision)
-      if rev in all:
-        revid = all[rev]
-      elif IsId(rev):
-        revid = rev
-      else:
-        revid = None
-
-      if revid and head == revid:
+      revid = self.GetRevisionId(all)
+      if head == revid:
         _lwrite(os.path.join(self.worktree, '.git', HEAD),
                 '%s\n' % revid)
       else:
-        self._Checkout(rev, quiet=True)
+        self._Checkout(revid, quiet=True)
 
     return GitCommand(self,
                       ['branch', '-D', name],
@@ -931,7 +934,7 @@
         if cb is None or name != cb:
           kill.append(name)
 
-    rev = self.GetRemote(self.remote.name).ToLocal(self.revision)
+    rev = self.GetRevisionId(left)
     if cb is not None \
        and not self._revlist(HEAD + '...' + rev) \
        and not self.IsDirty(consider_untracked = False):
@@ -1081,24 +1084,25 @@
 
   def _InitMRef(self):
     if self.manifest.branch:
-      msg = 'manifest set to %s' % self.revision
-      ref = R_M + self.manifest.branch
-      cur = self.bare_ref.symref(ref)
-
-      if IsId(self.revision):
-        if cur != '' or self.bare_ref.get(ref) != self.revision:
-          dst = self.revision + '^0'
-          self.bare_git.UpdateRef(ref, dst, message = msg, detach = True)
-      else:
-        remote = self.GetRemote(self.remote.name)
-        dst = remote.ToLocal(self.revision)
-        if cur != dst:
-          self.bare_git.symbolic_ref('-m', msg, ref, dst)
+      self._InitAnyMRef(R_M + self.manifest.branch)
 
   def _InitMirrorHead(self):
-    dst = self.GetRemote(self.remote.name).ToLocal(self.revision)
-    msg = 'manifest set to %s' % self.revision
-    self.bare_git.SetHead(dst, message=msg)
+    self._InitAnyMRef(self, HEAD)
+
+  def _InitAnyMRef(self, ref):
+    cur = self.bare_ref.symref(ref)
+
+    if self.revisionId:
+      if cur != '' or self.bare_ref.get(ref) != self.revisionId:
+        msg = 'manifest set to %s' % self.revisionId
+        dst = self.revisionId + '^0'
+        self.bare_git.UpdateRef(ref, dst, message = msg, detach = True)
+    else:
+      remote = self.GetRemote(self.remote.name)
+      dst = remote.ToLocal(self.revisionExpr)
+      if cur != dst:
+        msg = 'manifest set to %s' % self.revisionExpr
+        self.bare_git.symbolic_ref('-m', msg, ref, dst)
 
   def _InitWorkTree(self):
     dotgit = os.path.join(self.worktree, '.git')
@@ -1125,14 +1129,11 @@
           else:
             raise
 
-      rev = self.GetRemote(self.remote.name).ToLocal(self.revision)
-      rev = self.bare_git.rev_parse('%s^0' % rev)
-
-      _lwrite(os.path.join(dotgit, HEAD), '%s\n' % rev)
+      _lwrite(os.path.join(dotgit, HEAD), '%s\n' % self.GetRevisionId())
 
       cmd = ['read-tree', '--reset', '-u']
       cmd.append('-v')
-      cmd.append('HEAD')
+      cmd.append(HEAD)
       if GitCommand(self, cmd).Wait() != 0:
         raise GitError("cannot initialize work tree")
       self._CopyFiles()
@@ -1433,7 +1434,8 @@
                      worktree = worktree,
                      remote = RemoteSpec('origin'),
                      relpath = '.repo/%s' % name,
-                     revision = 'refs/heads/master')
+                     revisionExpr = 'refs/heads/master',
+                     revisionId = None)
 
   def PreSync(self):
     if self.Exists:
@@ -1441,7 +1443,8 @@
       if cb:
         base = self.GetBranch(cb).merge
         if base:
-          self.revision = base
+          self.revisionExpr = base
+          self.revisionId = None
 
   @property
   def LastFetch(self):
@@ -1455,16 +1458,11 @@
   def HasChanges(self):
     """Has the remote received new commits not yet checked out?
     """
-    if not self.remote or not self.revision:
+    if not self.remote or not self.revisionExpr:
       return False
 
     all = self.bare_ref.all
-    rev = self.GetRemote(self.remote.name).ToLocal(self.revision)
-    if rev in all:
-      revid = all[rev]
-    else:
-      revid = rev
-
+    revid = self.GetRevisionId(all)
     head = self.work_git.GetHead()
     if head.startswith(R_HEADS):
       try:
@@ -1474,6 +1472,6 @@
 
     if revid == head:
       return False
-    elif self._revlist(not_rev(HEAD), rev):
+    elif self._revlist(not_rev(HEAD), revid):
       return True
     return False
diff --git a/subcmds/forall.py b/subcmds/forall.py
index 8d701a6..b66313d 100644
--- a/subcmds/forall.py
+++ b/subcmds/forall.py
@@ -160,10 +160,8 @@
       setenv('REPO_PROJECT', project.name)
       setenv('REPO_PATH', project.relpath)
       setenv('REPO_REMOTE', project.remote.name)
-      setenv('REPO_LREV', project\
-        .GetRemote(project.remote.name)\
-        .ToLocal(project.revision))
-      setenv('REPO_RREV', project.revision)
+      setenv('REPO_LREV', project.GetRevisionId())
+      setenv('REPO_RREV', project.revisionExpr)
 
       if mirror:
         setenv('GIT_DIR', project.gitdir)
diff --git a/subcmds/init.py b/subcmds/init.py
index 0d0fcd0..5ba4d79 100644
--- a/subcmds/init.py
+++ b/subcmds/init.py
@@ -113,12 +113,12 @@
       m._InitGitDir()
 
       if opt.manifest_branch:
-        m.revision = opt.manifest_branch
+        m.revisionExpr = opt.manifest_branch
       else:
-        m.revision = 'refs/heads/master'
+        m.revisionExpr = 'refs/heads/master'
     else:
       if opt.manifest_branch:
-        m.revision = opt.manifest_branch
+        m.revisionExpr = opt.manifest_branch
       else:
         m.PreSync()
 
diff --git a/subcmds/sync.py b/subcmds/sync.py
index c61c3bb..9c9a3b7 100644
--- a/subcmds/sync.py
+++ b/subcmds/sync.py
@@ -207,17 +207,14 @@
 warning: Cannot automatically authenticate repo."""
     return True
 
-  remote = project.GetRemote(project.remote.name)
-  ref = remote.ToLocal(project.revision)
-
   try:
-    cur = project.bare_git.describe(ref)
+    cur = project.bare_git.describe(project.GetRevisionId())
   except GitError:
     cur = None
 
   if not cur \
      or re.compile(r'^.*-[0-9]{1,}-g[0-9a-f]{1,}$').match(cur):
-    rev = project.revision
+    rev = project.revisionExpr
     if rev.startswith(R_HEADS):
       rev = rev[len(R_HEADS):]