Add a 'smart sync' option to repo sync

This option allows the user to specify a manifest server to use when
syncing. This manifest server will provide a manifest pegging each
project to a known green build. This allows developers to work on a
known good tree that is known to build and pass tests, preventing
failed builds to hamper productivity.

The manifest used is not "sticky" so as to allow subsequent
'repo sync' calls to sync to the tip of the tree.

Change-Id: Id0a24ece20f5a88034ad364b416a1dd2e394226d
diff --git a/docs/manifest-format.txt b/docs/manifest-format.txt
index da0e69f..211344ee 100644
--- a/docs/manifest-format.txt
+++ b/docs/manifest-format.txt
@@ -22,6 +22,7 @@
   <!DOCTYPE manifest [
     <!ELEMENT manifest (remote*,
                         default?,
+                        manifest-server?,
                         remove-project*,
                         project*)>
   
@@ -33,6 +34,9 @@
     <!ELEMENT default (EMPTY)>
     <!ATTLIST default remote   IDREF #IMPLIED>
     <!ATTLIST default revision CDATA #IMPLIED>
+
+    <!ELEMENT manifest-server (EMPTY)>
+    <!ATTLIST url              CDATA #REQUIRED>
   
     <!ELEMENT project (EMPTY)>
     <!ATTLIST project name     CDATA #REQUIRED>
@@ -89,6 +93,27 @@
 revision attribute will use this revision.
 
 
+Element manifest-server
+-----------------------
+
+At most one manifest-server may be specified. The url attribute
+is used to specify the URL of a manifest server, which is an
+XML RPC service that will return a manifest in which each project
+is pegged to a known good revision for the current branch and
+target.
+
+The manifest server should implement:
+
+  GetApprovedManifest(branch, target)
+
+The target to use is defined by environment variables TARGET_PRODUCT
+and TARGET_BUILD_VARIANT. These variables are used to create a string
+of the form $TARGET_PRODUCT-$TARGET_BUILD_VARIANT, e.g. passion-userdebug.
+If one of those variables or both are not present, the program will call
+GetApprovedManifest without the target paramater and the manifest server
+should choose a reasonable default target.
+
+
 Element project
 ---------------
 
diff --git a/manifest_xml.py b/manifest_xml.py
index 7d02f9d..d0c9deb 100644
--- a/manifest_xml.py
+++ b/manifest_xml.py
@@ -65,8 +65,8 @@
 
     self._Unload()
 
-  def Link(self, name):
-    """Update the repo metadata to use a different manifest.
+  def Override(self, name):
+    """Use a different manifest, just for the current instantiation.
     """
     path = os.path.join(self.manifestProject.worktree, name)
     if not os.path.isfile(path):
@@ -80,6 +80,11 @@
     finally:
       self.manifestFile = old
 
+  def Link(self, name):
+    """Update the repo metadata to use a different manifest.
+    """
+    self.Override(name)
+
     try:
       if os.path.exists(self.manifestFile):
         os.remove(self.manifestFile)
@@ -123,6 +128,12 @@
       root.appendChild(e)
       root.appendChild(doc.createTextNode(''))
 
+    if self._manifest_server:
+      e = doc.createElement('manifest-server')
+      e.setAttribute('url', self._manifest_server)
+      root.appendChild(e)
+      root.appendChild(doc.createTextNode(''))
+
     sort_projects = list(self.projects.keys())
     sort_projects.sort()
 
@@ -169,6 +180,11 @@
     return self._default
 
   @property
+  def manifest_server(self):
+    self._Load()
+    return self._manifest_server
+
+  @property
   def IsMirror(self):
     return self.manifestProject.config.GetBoolean('repo.mirror')
 
@@ -178,6 +194,7 @@
     self._remotes = {}
     self._default = None
     self.branch = None
+    self._manifest_server = None
 
   def _Load(self):
     if not self._loaded:
@@ -247,6 +264,15 @@
       self._default = _Default()
 
     for node in config.childNodes:
+      if node.nodeName == 'manifest-server':
+        url = self._reqatt(node, 'url')
+        if self._manifest_server is not None:
+            raise ManifestParseError, \
+                'duplicate manifest-server in %s' % \
+                (self.manifestFile)
+        self._manifest_server = url
+
+    for node in config.childNodes:
       if node.nodeName == 'project':
         project = self._ParseProject(node)
         if self._projects.get(project.name):
@@ -315,7 +341,7 @@
   def _ParseProject(self, node):
     """
     reads a <project> element from the manifest file
-    """ 
+    """
     name = self._reqatt(node, 'name')
 
     remote = self._get_remote(node)
diff --git a/subcmds/sync.py b/subcmds/sync.py
index ceb81ea..deff171 100644
--- a/subcmds/sync.py
+++ b/subcmds/sync.py
@@ -17,9 +17,11 @@
 import os
 import re
 import shutil
+import socket
 import subprocess
 import sys
 import time
+import xmlrpclib
 
 from git_command import GIT
 from project import HEAD
@@ -57,6 +59,10 @@
 if the project is currently on a topic branch, but the manifest
 revision is temporarily needed.
 
+The -s/--smart-sync option can be used to sync to a known good
+build as specified by the manifest-server element in the current
+manifest.
+
 SSH Connections
 ---------------
 
@@ -97,6 +103,9 @@
     p.add_option('-d','--detach',
                  dest='detach_head', action='store_true',
                  help='detach projects back to manifest revision')
+    p.add_option('-s', '--smart-sync',
+                 dest='smart_sync', action='store_true',
+                 help='smart sync using manifest from a known good build')
 
     g = p.add_option_group('repo Version options')
     g.add_option('--no-repo-verify',
@@ -182,6 +191,49 @@
       print >>sys.stderr, 'error: cannot combine -n and -l'
       sys.exit(1)
 
+    if opt.smart_sync:
+      if not self.manifest.manifest_server:
+        print >>sys.stderr, \
+            'error: cannot smart sync: no manifest server defined in manifest'
+        sys.exit(1)
+      try:
+        server = xmlrpclib.Server(self.manifest.manifest_server)
+        p = self.manifest.manifestProject
+        b = p.GetBranch(p.CurrentBranch)
+        branch = b.merge
+
+        env = dict(os.environ)
+        if (env.has_key('TARGET_PRODUCT') and
+            env.has_key('TARGET_BUILD_VARIANT')):
+          target = '%s-%s' % (env['TARGET_PRODUCT'],
+                              env['TARGET_BUILD_VARIANT'])
+          [success, manifest_str] = server.GetApprovedManifest(branch, target)
+        else:
+          [success, manifest_str] = server.GetApprovedManifest(branch)
+
+        if success:
+          manifest_name = "smart_sync_override.xml"
+          manifest_path = os.path.join(self.manifest.manifestProject.worktree,
+                                       manifest_name)
+          try:
+            f = open(manifest_path, 'w')
+            try:
+              f.write(manifest_str)
+              self.manifest.Override(manifest_name)
+            finally:
+              f.close()
+          except IOError:
+            print >>sys.stderr, 'error: cannot write manifest to %s' % \
+                manifest_path
+            sys.exit(1)
+        else:
+          print >>sys.stderr, 'error: %s' % manifest_str
+          sys.exit(1)
+      except socket.error:
+        print >>sys.stderr, 'error: cannot connect to manifest server %s' % (
+            self.manifest.manifest_server)
+        sys.exit(1)
+
     rp = self.manifest.repoProject
     rp.PreSync()