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.')