Merge "repo: Support multiple branches for the same project."
diff --git a/docs/manifest-format.txt b/docs/manifest-format.txt
index dcc90d0..e48b75f 100644
--- a/docs/manifest-format.txt
+++ b/docs/manifest-format.txt
@@ -27,15 +27,15 @@
                         remove-project*,
                         project*,
                         repo-hooks?)>
-  
+
     <!ELEMENT notice (#PCDATA)>
-  
+
     <!ELEMENT remote (EMPTY)>
     <!ATTLIST remote name         ID    #REQUIRED>
     <!ATTLIST remote alias        CDATA #IMPLIED>
     <!ATTLIST remote fetch        CDATA #REQUIRED>
     <!ATTLIST remote review       CDATA #IMPLIED>
-  
+
     <!ELEMENT default (EMPTY)>
     <!ATTLIST default remote      IDREF #IMPLIED>
     <!ATTLIST default revision    CDATA #IMPLIED>
@@ -46,8 +46,8 @@
 
     <!ELEMENT manifest-server (EMPTY)>
     <!ATTLIST url              CDATA #REQUIRED>
-  
-    <!ELEMENT project (annotation?,
+
+    <!ELEMENT project (annotation*,
                        project*)>
     <!ATTLIST project name        CDATA #REQUIRED>
     <!ATTLIST project path        CDATA #IMPLIED>
@@ -65,7 +65,7 @@
     <!ATTLIST annotation name  CDATA #REQUIRED>
     <!ATTLIST annotation value CDATA #REQUIRED>
     <!ATTLIST annotation keep  CDATA "true">
-  
+
     <!ELEMENT remove-project (EMPTY)>
     <!ATTLIST remove-project name  CDATA #REQUIRED>
 
diff --git a/git_command.py b/git_command.py
index d347dd6..51f5e3c 100644
--- a/git_command.py
+++ b/git_command.py
@@ -86,7 +86,7 @@
     global _git_version
 
     if _git_version is None:
-      ver_str = git.version()
+      ver_str = git.version().decode('utf-8')
       if ver_str.startswith('git version '):
         _git_version = tuple(
           map(int,
diff --git a/git_config.py b/git_config.py
index a294a0b..f6093a2 100644
--- a/git_config.py
+++ b/git_config.py
@@ -304,8 +304,8 @@
     d = self._do('--null', '--list')
     if d is None:
       return c
-    for line in d.rstrip('\0').split('\0'):  # pylint: disable=W1401
-                                             # Backslash is not anomalous
+    for line in d.decode('utf-8').rstrip('\0').split('\0'):  # pylint: disable=W1401
+                                                             # Backslash is not anomalous
       if '\n' in line:
         key, val = line.split('\n', 1)
       else:
diff --git a/git_refs.py b/git_refs.py
index 4dd6876..3c26606 100644
--- a/git_refs.py
+++ b/git_refs.py
@@ -100,7 +100,7 @@
   def _ReadPackedRefs(self):
     path = os.path.join(self._gitdir, 'packed-refs')
     try:
-      fd = open(path, 'rb')
+      fd = open(path, 'r')
       mtime = os.path.getmtime(path)
     except IOError:
       return
diff --git a/hooks/commit-msg b/hooks/commit-msg
index b37dfaa..5ca2b11 100755
--- a/hooks/commit-msg
+++ b/hooks/commit-msg
@@ -1,5 +1,5 @@
 #!/bin/sh
-# From Gerrit Code Review 2.5.2
+# From Gerrit Code Review 2.6
 #
 # Part of Gerrit Code Review (http://code.google.com/p/gerrit/)
 #
@@ -154,7 +154,7 @@
 		if (unprinted) {
 			print "Change-Id: I'"$id"'"
 		}
-	}' "$MSG" > $T && mv $T "$MSG" || rm -f $T
+	}' "$MSG" > "$T" && mv "$T" "$MSG" || rm -f "$T"
 }
 _gen_ChangeIdInput() {
 	echo "tree `git write-tree`"
diff --git a/manifest_xml.py b/manifest_xml.py
index 647e89f..c5f3bcc 100644
--- a/manifest_xml.py
+++ b/manifest_xml.py
@@ -51,6 +51,12 @@
   sync_c = False
   sync_s = False
 
+  def __eq__(self, other):
+    return self.__dict__ == other.__dict__
+
+  def __ne__(self, other):
+    return self.__dict__ != other.__dict__
+
 class _XmlRemote(object):
   def __init__(self,
                name,
@@ -92,7 +98,7 @@
     url = self.resolvedFetchUrl.rstrip('/') + '/' + projectName
     remoteName = self.name
     if self.remoteAlias:
-        remoteName = self.remoteAlias
+      remoteName = self.remoteAlias
     return RemoteSpec(remoteName, url, self.reviewUrl)
 
 class XmlManifest(object):
@@ -228,7 +234,9 @@
       e.setAttribute('name', name)
       if relpath != name:
         e.setAttribute('path', relpath)
-      remoteName = d.remote.remoteAlias or d.remote.name
+      remoteName = None
+      if d.remote:
+        remoteName = d.remote.remoteAlias or d.remote.name
       if not d.remote or p.remote.name != remoteName:
         e.setAttribute('remote', p.remote.name)
       if peg_rev:
@@ -325,6 +333,10 @@
   def IsMirror(self):
     return self.manifestProject.config.GetBoolean('repo.mirror')
 
+  @property
+  def IsArchive(self):
+    return self.manifestProject.config.GetBoolean('repo.archive')
+
   def _Unload(self):
     self._loaded = False
     self._projects = {}
@@ -432,11 +444,13 @@
 
     for node in itertools.chain(*node_list):
       if node.nodeName == 'default':
-        if self._default is not None:
-          raise ManifestParseError(
-              'duplicate default in %s' %
-              (self.manifestFile))
-        self._default = self._ParseDefault(node)
+        new_default = self._ParseDefault(node)
+        if self._default is None:
+          self._default = new_default
+        elif new_default != self._default:
+          raise ManifestParseError('duplicate default in %s' %
+                                   (self.manifestFile))
+
     if self._default is None:
       self._default = _Default()
 
diff --git a/project.py b/project.py
index f9f1f75..46f3b8f 100644
--- a/project.py
+++ b/project.py
@@ -23,6 +23,7 @@
 import stat
 import subprocess
 import sys
+import tarfile
 import tempfile
 import time
 
@@ -82,7 +83,7 @@
   """
   global _project_hook_list
   if _project_hook_list is None:
-    d = os.path.abspath(os.path.dirname(__file__))
+    d = os.path.realpath(os.path.abspath(os.path.dirname(__file__)))
     d = os.path.join(d , 'hooks')
     _project_hook_list = [os.path.join(d, x) for x in os.listdir(d)]
   return _project_hook_list
@@ -986,15 +987,62 @@
 
 ## Sync ##
 
+  def _ExtractArchive(self, tarpath, path=None):
+    """Extract the given tar on its current location
+
+    Args:
+        - tarpath: The path to the actual tar file
+
+    """
+    try:
+      with tarfile.open(tarpath, 'r') as tar:
+        tar.extractall(path=path)
+        return True
+    except (IOError, tarfile.TarError) as e:
+      print("error: Cannot extract archive %s: "
+            "%s" % (tarpath, str(e)), file=sys.stderr)
+    return False
+
   def Sync_NetworkHalf(self,
       quiet=False,
       is_new=None,
       current_branch_only=False,
       clone_bundle=True,
-      no_tags=False):
+      no_tags=False,
+      archive=False):
     """Perform only the network IO portion of the sync process.
        Local working directory/branch state is not affected.
     """
+    if archive and not isinstance(self, MetaProject):
+      if self.remote.url.startswith(('http://', 'https://')):
+        print("error: %s: Cannot fetch archives from http/https "
+              "remotes." % self.name, file=sys.stderr)
+        return False
+
+      name = self.relpath.replace('\\', '/')
+      name = name.replace('/', '_')
+      tarpath = '%s.tar' % name
+      topdir = self.manifest.topdir
+
+      try:
+        self._FetchArchive(tarpath, cwd=topdir)
+      except GitError as e:
+        print('error: %s' % str(e), file=sys.stderr)
+        return False
+
+      # From now on, we only need absolute tarpath
+      tarpath = os.path.join(topdir, tarpath)
+
+      if not self._ExtractArchive(tarpath, path=topdir):
+        return False
+      try:
+        os.remove(tarpath)
+      except OSError as e:
+        print("warn: Cannot remove archive %s: "
+              "%s" % (tarpath, str(e)), file=sys.stderr)
+      self._CopyFiles()
+      return True
+
     if is_new is None:
       is_new = not self.Exists
     if is_new:
@@ -1169,7 +1217,7 @@
     last_mine = None
     cnt_mine = 0
     for commit in local_changes:
-      commit_id, committer_email = commit.split(' ', 1)
+      commit_id, committer_email = commit.decode('utf-8').split(' ', 1)
       if committer_email == self.UserEmail:
         last_mine = commit_id
         cnt_mine += 1
@@ -1580,6 +1628,19 @@
 
 ## Direct Git Commands ##
 
+  def _FetchArchive(self, tarpath, cwd=None):
+    cmd = ['archive', '-v', '-o', tarpath]
+    cmd.append('--remote=%s' % self.remote.url)
+    cmd.append('--prefix=%s/' % self.relpath)
+    cmd.append(self.revisionExpr)
+
+    command = GitCommand(self, cmd, cwd=cwd,
+                         capture_stdout=True,
+                         capture_stderr=True)
+
+    if command.Wait() != 0:
+      raise GitError('git archive %s: %s' % (self.name, command.stderr))
+
   def _RemoteFetch(self, name=None,
                    current_branch_only=False,
                    initial=False,
@@ -1847,11 +1908,11 @@
             cookiefile = line[len(prefix):]
             break
         if p.wait():
-          line = iter(p.stderr).next()
-          if ' -print_config' in line:
+          err_msg = p.stderr.read()
+          if ' -print_config' in err_msg:
             pass  # Persistent proxy doesn't support -print_config.
           else:
-            print(line + p.stderr.read(), file=sys.stderr)
+            print(err_msg, file=sys.stderr)
         if cookiefile:
           return cookiefile
       except OSError as e:
@@ -1971,7 +2032,7 @@
       self._InitHooks()
 
   def _InitHooks(self):
-    hooks = self._gitdir_path('hooks')
+    hooks = os.path.realpath(self._gitdir_path('hooks'))
     if not os.path.exists(hooks):
       os.makedirs(hooks)
     for stock_hook in _ProjectHooks():
diff --git a/repo b/repo
index eeb8920..56d784f 100755
--- a/repo
+++ b/repo
@@ -110,6 +110,7 @@
 MIN_PYTHON_VERSION = (2, 6)     # minimum supported python version
 
 
+import errno
 import optparse
 import os
 import re
@@ -138,10 +139,9 @@
 # Python version check
 ver = sys.version_info
 if ver[0] == 3:
-  _print('error: Python 3 support is not fully implemented in repo yet.\n'
+  _print('warning: Python 3 support is currently experimental. YMMV.\n'
          'Please use Python 2.6 - 2.7 instead.',
          file=sys.stderr)
-  sys.exit(1)
 if (ver[0], ver[1]) < MIN_PYTHON_VERSION:
   _print('error: Python version %s unsupported.\n'
          'Please use Python 2.6 - 2.7 instead.'
@@ -181,6 +181,10 @@
 group.add_option('--depth', type='int', default=None,
                  dest='depth',
                  help='create a shallow clone with given depth; see git clone')
+group.add_option('--archive',
+                 dest='archive', action='store_true',
+                 help='checkout an archive instead of a git repository for '
+                      'each project. See git archive.')
 group.add_option('-g', '--groups',
                  dest='groups', default='default',
                  help='restrict manifest projects to ones with specified '
@@ -240,10 +244,10 @@
     _print("fatal: invalid branch name '%s'" % branch, file=sys.stderr)
     raise CloneFailure()
 
-  if not os.path.isdir(repodir):
-    try:
-      os.mkdir(repodir)
-    except OSError as e:
+  try:
+    os.mkdir(repodir)
+  except OSError as e:
+    if e.errno != errno.EEXIST:
       _print('fatal: cannot make %s directory: %s'
              % (repodir, e.strerror), file=sys.stderr)
       # Don't raise CloneFailure; that would delete the
@@ -322,18 +326,18 @@
 
 
 def SetupGnuPG(quiet):
-  if not os.path.isdir(home_dot_repo):
-    try:
-      os.mkdir(home_dot_repo)
-    except OSError as e:
+  try:
+    os.mkdir(home_dot_repo)
+  except OSError as e:
+    if e.errno != errno.EEXIST:
       _print('fatal: cannot make %s directory: %s'
              % (home_dot_repo, e.strerror), file=sys.stderr)
       sys.exit(1)
 
-  if not os.path.isdir(gpg_dir):
-    try:
-      os.mkdir(gpg_dir, stat.S_IRWXU)
-    except OSError as e:
+  try:
+    os.mkdir(gpg_dir, stat.S_IRWXU)
+  except OSError as e:
+    if e.errno != errno.EEXIST:
       _print('fatal: cannot make %s directory: %s' % (gpg_dir, e.strerror),
              file=sys.stderr)
       sys.exit(1)
@@ -739,7 +743,7 @@
     repo_main = my_main
 
   ver_str = '.'.join(map(str, VERSION))
-  me = [repo_main,
+  me = [sys.executable, repo_main,
         '--repo-dir=%s' % rel_repo_dir,
         '--wrapper-version=%s' % ver_str,
         '--wrapper-path=%s' % wrapper_path,
@@ -747,7 +751,7 @@
   me.extend(orig_args)
   me.extend(extra_args)
   try:
-    os.execv(repo_main, me)
+    os.execv(sys.executable, me)
   except OSError as e:
     _print("fatal: unable to start %s" % repo_main, file=sys.stderr)
     _print("fatal: %s" % e, file=sys.stderr)
diff --git a/subcmds/branches.py b/subcmds/branches.py
index c2e7c4b..f714c1e 100644
--- a/subcmds/branches.py
+++ b/subcmds/branches.py
@@ -139,7 +139,7 @@
       if in_cnt < project_cnt:
         fmt = out.write
         paths = []
-        if in_cnt < project_cnt - in_cnt: 
+        if in_cnt < project_cnt - in_cnt:
           in_type = 'in'
           for b in i.projects:
             paths.append(b.project.relpath)
diff --git a/subcmds/init.py b/subcmds/init.py
index a44fb7a..b1fcb69 100644
--- a/subcmds/init.py
+++ b/subcmds/init.py
@@ -99,6 +99,10 @@
     g.add_option('--depth', type='int', default=None,
                  dest='depth',
                  help='create a shallow clone with given depth; see git clone')
+    g.add_option('--archive',
+                 dest='archive', action='store_true',
+                 help='checkout an archive instead of a git repository for '
+                      'each project. See git archive.')
     g.add_option('-g', '--groups',
                  dest='groups', default='default',
                  help='restrict manifest projects to ones with specified '
@@ -198,6 +202,16 @@
     if opt.reference:
       m.config.SetString('repo.reference', opt.reference)
 
+    if opt.archive:
+      if is_new:
+        m.config.SetString('repo.archive', 'true')
+      else:
+        print('fatal: --archive is only supported when initializing a new '
+              'workspace.', file=sys.stderr)
+        print('Either delete the .repo folder in this workspace, or initialize '
+              'in another location.', file=sys.stderr)
+        sys.exit(1)
+
     if opt.mirror:
       if is_new:
         m.config.SetString('repo.mirror', 'true')
@@ -366,6 +380,13 @@
     if opt.reference:
       opt.reference = os.path.expanduser(opt.reference)
 
+    # Check this here, else manifest will be tagged "not new" and init won't be
+    # possible anymore without removing the .repo/manifests directory.
+    if opt.archive and opt.mirror:
+      print('fatal: --mirror and --archive cannot be used together.',
+            file=sys.stderr)
+      sys.exit(1)
+
     self._SyncManifest(opt)
     self._LinkManifest(opt.manifest_name)
 
diff --git a/subcmds/sync.py b/subcmds/sync.py
index d1a0641..5e7385d 100644
--- a/subcmds/sync.py
+++ b/subcmds/sync.py
@@ -272,7 +272,7 @@
           quiet=opt.quiet,
           current_branch_only=opt.current_branch_only,
           clone_bundle=not opt.no_clone_bundle,
-          no_tags=opt.no_tags)
+          no_tags=opt.no_tags, archive=self.manifest.IsArchive)
         self._fetch_times.Set(project, time.time() - start)
 
         # Lock around all the rest of the code, since printing, updating a set
@@ -315,7 +315,8 @@
             quiet=opt.quiet,
             current_branch_only=opt.current_branch_only,
             clone_bundle=not opt.no_clone_bundle,
-            no_tags=opt.no_tags):
+            no_tags=opt.no_tags,
+            archive=self.manifest.IsArchive):
           fetched.add(project.gitdir)
         else:
           print('error: Cannot fetch %s' % project.name, file=sys.stderr)
@@ -363,7 +364,9 @@
     pm.end()
     self._fetch_times.Save()
 
-    self._GCProjects(projects)
+    if not self.manifest.IsArchive:
+      self._GCProjects(projects)
+
     return fetched
 
   def _GCProjects(self, projects):
@@ -671,7 +674,7 @@
         previously_missing_set = missing_set
         fetched.update(self._Fetch(missing, opt))
 
-    if self.manifest.IsMirror:
+    if self.manifest.IsMirror or self.manifest.IsArchive:
       # bail out now, we have no working tree
       return
 
@@ -791,7 +794,7 @@
   def _Load(self):
     if self._times is None:
       try:
-        f = open(self._path)
+        f = open(self._path, 'rb')
       except IOError:
         self._times = {}
         return self._times
diff --git a/subcmds/upload.py b/subcmds/upload.py
index 9ad55d7..5621240 100644
--- a/subcmds/upload.py
+++ b/subcmds/upload.py
@@ -349,8 +349,9 @@
 
         # Make sure our local branch is not setup to track a different remote branch
         merge_branch = self._GetMergeBranch(branch.project)
-        full_dest = 'refs/heads/%s' % destination
-        if not opt.dest_branch and merge_branch and merge_branch != full_dest:
+        if destination:
+          full_dest = 'refs/heads/%s' % destination
+          if not opt.dest_branch and merge_branch and merge_branch != full_dest:
             print('merge branch %s does not match destination branch %s'
                   % (merge_branch, full_dest))
             print('skipping upload.')