Compare commits

...

18 Commits

Author SHA1 Message Date
c325dc35f6 sync: Fetch after applying bundle and retry after errors
After a $GIT_URL/clone.bundle has been applied to the new local
repository, perform an incremental fetch using `git fetch` to ensure
the local repository is up-to-date. This allows the hosting server
to offer stale /clone.bundle files to bootstrap a new client.

If a single git fetch fails, it may succeed again after a short
delay.  Transient failures are typical in environments where the
remote Git server happens to have limits on how many requests it
can serve at once (the anonymous git daemon, or an HTTP server).
Wait a randomized delay between 30 and 45 seconds and retry the
failed project once.  This delay gives the site time to recover
from a transient traffic spike, and the randomization makes it less
likely that a spike occurs again from all of the same clients.

Change-Id: I97fb0fcb33630fb78ac1a21d1a4a3e2268ab60c0
Signed-off-by: Shawn O. Pearce <sop@google.com>
2011-10-03 08:30:24 -07:00
f322b9abb4 sync: Support downloading bundle to initialize repository
An HTTP (or HTTPS) based remote server may now offer a 'clone.bundle'
file in each repository's Git directory. Over an http:// or https://
remote repo will first ask for '$URL/clone.bundle', and if present
download this to bootstrap the local client, rather than relying
on the native Git transport to initialize the new repository.

Bundles may be hosted elsewhere. The client automatically follows a
HTTP 302 redirect to acquire the bundle file. This allows servers
to direct clients to cached copies residing on content delivery
networks, where the bundle may be closer to the end-user.

Bundle downloads are resumeable from where they last left off,
allowing clients to initialize large repositories even when the
connection gets interrupted.

If a bundle does not exist for a repository (a HTTP 404 response
code is returned for '$URL/clone.bundle'), the native Git transport
is used instead. If the client is performing a shallow sync, the
bundle transport is not used, as there is no way to embed shallow
data into the bundle.

Change-Id: I05dad17792fd6fd20635a0f71589566e557cc743
Signed-off-by: Shawn O. Pearce <sop@google.com>
2011-09-28 10:07:36 -07:00
db728cd866 Allow remote url to be relative to manifst url 2011-09-28 10:07:01 -07:00
c4657969eb sync: Update default -j flag from manifest
If the manifest is updated and the default sync-j attribute
was modified, honor it during this sync session if the user
has not supplied a -j flag on the command line.

Change-Id: I127ee5c779e2bbbb40b30bddc10ec1fa704b3bf3
Signed-off-by: Shawn O. Pearce <sop@google.com>
2011-09-26 09:08:44 -07:00
7b947de1ee Ignore missing ~/.netrc
Change-Id: Ifa6065d57a6cb11ad57ddd44bc88d9690fe234ab
Signed-off-by: Shawn O. Pearce <sop@google.com>
2011-09-23 11:50:31 -07:00
6392c87945 sync: Allow -j to have a default in manifest
This permits manifest authors to suggest a number of parallel
fetch operations against a remote server. For example, Gerrit
Code Review servers support queuing of requests and processes
them in first-in, first-out order. Running concurrent fetches
can utilize multiple CPUs on the Gerrit server, but will also
decrease overall operation latency by having the request put
into the queue ready to execute as soon as a CPU is free.

Change-Id: I3d3904acb6f63516bae4b071c510ad57a2afab18
Signed-off-by: Shawn O. Pearce <sop@google.com>
2011-09-22 18:08:27 -07:00
97d2b2f7a0 sync: Limit -j to file descriptors
Each worker thread requires at least 3 file descriptors to run the
forked 'git fetch' child to operate against the local repository.
Mac OS X has the RLIMIT_NOFILE set to 256 by default, which means
a sync -j128 often fails when the workers run out of pipes within
the Python parent process.

Change-Id: I2cdb14621b899424b079daf7969bc8c16b85b903
Signed-off-by: Shawn O. Pearce <sop@google.com>
2011-09-22 18:08:26 -07:00
3a0e782790 Add global option --time to track execution
This prints a simple line after a command ends, providing
information about how long it executed for using real wall
clock time. Its mostly useful for looking at sync times.

Change-Id: Ie0997df0a0f90150270835d94b58a01a10bc3956
Signed-off-by: Shawn O. Pearce <sop@google.com>
2011-09-22 18:08:18 -07:00
490d09a314 Support units in progress messages
This allows our progress meter to be used for bytes transferred, by
setting the units to KB or MB to let the user know the size.

Change-Id: Ie8653d4a40d79439026c18bd51915845b2c5bba9
Signed-off-by: Shawn O. Pearce <sop@google.com>
2011-09-19 14:52:57 -07:00
13111b4e97 Add support for url.*.insteadof
Teach repo how to resolve URLs using the url.insteadof feature
that C Git natively uses during clone, fetch or push. This will
later allow repo to resolve a URL before accessing it directly.
We do not want to pre-resolve things and store the resolved URL
into individual projects, as this makes it impossible for the
user to undo the insteadof mapping at a later date.

Change-Id: I0f62e811197c53fbc8a8be424e3cabf4ed07b4cb
Signed-off-by: Shawn O. Pearce <sop@google.com>
2011-09-19 14:52:57 -07:00
bd0312a484 Support ~/.netrc for HTTP Basic authentication
If repo tries to access a URL over HTTP and the user needs to
authenticate, offer a match from ~/.netrc. This matches behavior
with the Git command line client.

Change-Id: I803f3c5d562177ea0330941350cff3cc1e1bef08
Signed-off-by: Shawn O. Pearce <sop@google.com>
2011-09-19 14:52:32 -07:00
334851e4b6 Enhance HTTP support
Setting REPO_CURL_VERBOSE=1 in the environment will register a debug
level HTTPHandler on the urllib2 library, showing HTTP requests and
responses on the stderr channel of repo.

During any HTTP or HTTPS request created inside of the repo process,
a custom User-Agent header is now defined:

  User-Agent: git-repo/1.7.5 (Linux) git/1.7.7 Python/2.6.5

Change-Id: Ia5026fb1e1500659bd2af27416d85e205048bf26
Signed-off-by: Shawn O. Pearce <sop@google.com>
2011-09-19 14:51:47 -07:00
014d060989 Honor http_proxy variable globally
If the http_proxy environment variable was set, honor it during
the entire repo session for any Python created HTTP connections.

Change-Id: Ib4ae833cb2cdd47ab0126949f6b399d2c142887d
Signed-off-by: Shawn O. Pearce <sop@google.com>
2011-09-11 13:11:04 -07:00
44da16e8a0 Change default REPO_URL to code.google.com
Change-Id: If7700daf96fb8f3ee449e5774017272ef31b4b44
2011-09-05 14:16:49 -07:00
65e0f35fda Add commit-msg hook also for manifest project
The manifest project has - by design - not a review URL associated
with it. It is actually not even a 'project' in repo's sense.

This will prevent the commit-msg hook from being added, which is
not necessarily wanted as the project is managed in gerrit.

This commit will enable the commit-msg hook, which in turn will
add the Change-Id-line to every new commit in it. This simplifies
replacing patch sets (by git push ... refs/for/...).

Change-Id: I42d0f6fd79e6282d9d47074a3819e68d968999a7
Signed-off-by: Victor Boivie <victor.boivie@sonyericsson.com>
2011-07-20 07:34:23 -07:00
08c880db18 Smart tag support
This is an evolution of 'smart-sync' that adds a new option, -t,
that allows you to specify a tag/label to use instead of the
"latest good build" on the current manifest branch which -s does.

Signed-off-by: Victor Boivie <victor.boivie@sonyericsson.com>
Change-Id: I8c20fd91104a6aafa0271d4d33f6c4850aade17e
2011-07-20 07:13:48 -07:00
a101f1c167 Honor 'http_proxy' environment variable
'repo upload' makes http request using urllib2 python library.
Unfortunately this library does not work (by default) in case
if the user behind a proxy.

This change adds proxy handler in case if 'http_proxy' environment
variable is set.

Change-Id: Ic4176ad733fc21bd5b59661b3eacc2f0a7c3c1ff
2011-07-20 07:11:00 -07:00
49cd59bc86 Add --depth option to main repo wrapper.
See related repo change:
  https://review.source.android.com/#change,22722

Change-Id: I9bdd86971c94604477b91cdf47d6fac2c0bc186e
2011-06-14 16:59:19 -07:00
11 changed files with 492 additions and 93 deletions

View File

@ -38,6 +38,7 @@ following DTD:
<!ELEMENT default (EMPTY)> <!ELEMENT default (EMPTY)>
<!ATTLIST default remote IDREF #IMPLIED> <!ATTLIST default remote IDREF #IMPLIED>
<!ATTLIST default revision CDATA #IMPLIED> <!ATTLIST default revision CDATA #IMPLIED>
<!ATTLIST default sync-j CDATA #IMPLIED>
<!ELEMENT manifest-server (EMPTY)> <!ELEMENT manifest-server (EMPTY)>
<!ATTLIST url CDATA #REQUIRED> <!ATTLIST url CDATA #REQUIRED>

View File

@ -57,6 +57,15 @@ class UploadError(Exception):
def __str__(self): def __str__(self):
return self.reason return self.reason
class DownloadError(Exception):
"""Cannot download a repository.
"""
def __init__(self, reason):
self.reason = reason
def __str__(self):
return self.reason
class NoSuchProjectError(Exception): class NoSuchProjectError(Exception):
"""A specified project does not exist in the work tree. """A specified project does not exist in the work tree.
""" """

View File

@ -72,6 +72,8 @@ def terminate_ssh_clients():
pass pass
_ssh_clients = [] _ssh_clients = []
_git_version = None
class _GitCall(object): class _GitCall(object):
def version(self): def version(self):
p = GitCommand(None, ['--version'], capture_stdout=True) p = GitCommand(None, ['--version'], capture_stdout=True)
@ -79,6 +81,21 @@ class _GitCall(object):
return p.stdout return p.stdout
return None return None
def version_tuple(self):
global _git_version
if _git_version is None:
ver_str = git.version()
if ver_str.startswith('git version '):
_git_version = tuple(
map(lambda x: int(x),
ver_str[len('git version '):].strip().split('.')[0:3]
))
else:
print >>sys.stderr, 'fatal: "%s" unsupported' % ver_str
sys.exit(1)
return _git_version
def __getattr__(self, name): def __getattr__(self, name):
name = name.replace('_','-') name = name.replace('_','-')
def fun(*cmdv): def fun(*cmdv):
@ -88,23 +105,9 @@ class _GitCall(object):
return fun return fun
git = _GitCall() git = _GitCall()
_git_version = None
def git_require(min_version, fail=False): def git_require(min_version, fail=False):
global _git_version git_version = git.version_tuple()
if min_version <= git_version:
if _git_version is None:
ver_str = git.version()
if ver_str.startswith('git version '):
_git_version = tuple(
map(lambda x: int(x),
ver_str[len('git version '):].strip().split('.')[0:3]
))
else:
print >>sys.stderr, 'fatal: "%s" unsupported' % ver_str
sys.exit(1)
if min_version <= _git_version:
return True return True
if fail: if fail:
need = '.'.join(map(lambda x: str(x), min_version)) need = '.'.join(map(lambda x: str(x), min_version))

View File

@ -198,6 +198,15 @@ class GitConfig(object):
except KeyError: except KeyError:
return False return False
def UrlInsteadOf(self, url):
"""Resolve any url.*.insteadof references.
"""
for new_url in self.GetSubSections('url'):
old_url = self.GetString('url.%s.insteadof' % new_url)
if old_url is not None and url.startswith(old_url):
return new_url + url[len(old_url):]
return url
@property @property
def _sections(self): def _sections(self):
d = self._section_dict d = self._section_dict
@ -482,6 +491,12 @@ def close_ssh():
URI_SCP = re.compile(r'^([^@:]*@?[^:/]{1,}):') URI_SCP = re.compile(r'^([^@:]*@?[^:/]{1,}):')
URI_ALL = re.compile(r'^([a-z][a-z+]*)://([^@/]*@?[^/]*)/') URI_ALL = re.compile(r'^([a-z][a-z+]*)://([^@/]*@?[^/]*)/')
def GetSchemeFromUrl(url):
m = URI_ALL.match(url)
if m:
return m.group(1)
return None
def _preconnect(url): def _preconnect(url):
m = URI_ALL.match(url) m = URI_ALL.match(url)
if m: if m:

101
main.py
View File

@ -22,17 +22,22 @@ if __name__ == '__main__':
del sys.argv[-1] del sys.argv[-1]
del magic del magic
import netrc
import optparse import optparse
import os import os
import re import re
import sys import sys
import time
import urllib2
from trace import SetTrace from trace import SetTrace
from git_command import git, GitCommand
from git_config import init_ssh, close_ssh from git_config import init_ssh, close_ssh
from command import InteractiveCommand from command import InteractiveCommand
from command import MirrorSafeCommand from command import MirrorSafeCommand
from command import PagedCommand from command import PagedCommand
from editor import Editor from editor import Editor
from error import DownloadError
from error import ManifestInvalidRevisionError from error import ManifestInvalidRevisionError
from error import NoSuchProjectError from error import NoSuchProjectError
from error import RepoChangedException from error import RepoChangedException
@ -53,6 +58,9 @@ global_options.add_option('--no-pager',
global_options.add_option('--trace', global_options.add_option('--trace',
dest='trace', action='store_true', dest='trace', action='store_true',
help='trace git command execution') help='trace git command execution')
global_options.add_option('--time',
dest='time', action='store_true',
help='time repo command execution')
global_options.add_option('--version', global_options.add_option('--version',
dest='show_version', action='store_true', dest='show_version', action='store_true',
help='display this version of repo') help='display this version of repo')
@ -122,7 +130,23 @@ class _Repo(object):
RunPager(config) RunPager(config)
try: try:
cmd.Execute(copts, cargs) start = time.time()
try:
cmd.Execute(copts, cargs)
finally:
elapsed = time.time() - start
hours, remainder = divmod(elapsed, 3600)
minutes, seconds = divmod(remainder, 60)
if gopts.time:
if hours == 0:
print >>sys.stderr, 'real\t%dm%.3fs' \
% (minutes, seconds)
else:
print >>sys.stderr, 'real\t%dh%dm%.3fs' \
% (hours, minutes, seconds)
except DownloadError, e:
print >>sys.stderr, 'error: %s' % str(e)
sys.exit(1)
except ManifestInvalidRevisionError, e: except ManifestInvalidRevisionError, e:
print >>sys.stderr, 'error: %s' % str(e) print >>sys.stderr, 'error: %s' % str(e)
sys.exit(1) sys.exit(1)
@ -133,6 +157,9 @@ class _Repo(object):
print >>sys.stderr, 'error: no project in current directory' print >>sys.stderr, 'error: no project in current directory'
sys.exit(1) sys.exit(1)
def _MyRepoPath():
return os.path.dirname(__file__)
def _MyWrapperPath(): def _MyWrapperPath():
return os.path.join(os.path.dirname(__file__), 'repo') return os.path.join(os.path.dirname(__file__), 'repo')
@ -199,6 +226,77 @@ def _PruneOptions(argv, opt):
continue continue
i += 1 i += 1
_user_agent = None
def _UserAgent():
global _user_agent
if _user_agent is None:
py_version = sys.version_info
os_name = sys.platform
if os_name == 'linux2':
os_name = 'Linux'
elif os_name == 'win32':
os_name = 'Win32'
elif os_name == 'cygwin':
os_name = 'Cygwin'
elif os_name == 'darwin':
os_name = 'Darwin'
p = GitCommand(
None, ['describe', 'HEAD'],
cwd = _MyRepoPath(),
capture_stdout = True)
if p.Wait() == 0:
repo_version = p.stdout
if len(repo_version) > 0 and repo_version[-1] == '\n':
repo_version = repo_version[0:-1]
if len(repo_version) > 0 and repo_version[0] == 'v':
repo_version = repo_version[1:]
else:
repo_version = 'unknown'
_user_agent = 'git-repo/%s (%s) git/%s Python/%d.%d.%d' % (
repo_version,
os_name,
'.'.join(map(lambda d: str(d), git.version_tuple())),
py_version[0], py_version[1], py_version[2])
return _user_agent
class _UserAgentHandler(urllib2.BaseHandler):
def http_request(self, req):
req.add_header('User-Agent', _UserAgent())
return req
def https_request(self, req):
req.add_header('User-Agent', _UserAgent())
return req
def init_http():
handlers = [_UserAgentHandler()]
mgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
try:
n = netrc.netrc()
for host in n.hosts:
p = n.hosts[host]
mgr.add_password(None, 'http://%s/' % host, p[0], p[2])
mgr.add_password(None, 'https://%s/' % host, p[0], p[2])
except netrc.NetrcParseError:
pass
except IOError:
pass
handlers.append(urllib2.HTTPBasicAuthHandler(mgr))
if 'http_proxy' in os.environ:
url = os.environ['http_proxy']
handlers.append(urllib2.ProxyHandler({'http': url, 'https': url}))
if 'REPO_CURL_VERBOSE' in os.environ:
handlers.append(urllib2.HTTPHandler(debuglevel=1))
handlers.append(urllib2.HTTPSHandler(debuglevel=1))
urllib2.install_opener(urllib2.build_opener(*handlers))
def _Main(argv): def _Main(argv):
opt = optparse.OptionParser(usage="repo wrapperinfo -- ...") opt = optparse.OptionParser(usage="repo wrapperinfo -- ...")
opt.add_option("--repo-dir", dest="repodir", opt.add_option("--repo-dir", dest="repodir",
@ -217,6 +315,7 @@ def _Main(argv):
try: try:
try: try:
init_ssh() init_ssh()
init_http()
repo._Run(argv) repo._Run(argv)
finally: finally:
close_ssh() close_ssh()

View File

@ -14,7 +14,9 @@
# limitations under the License. # limitations under the License.
import os import os
import re
import sys import sys
import urlparse
import xml.dom.minidom import xml.dom.minidom
from git_config import GitConfig, IsId from git_config import GitConfig, IsId
@ -24,26 +26,36 @@ from error import ManifestParseError
MANIFEST_FILE_NAME = 'manifest.xml' MANIFEST_FILE_NAME = 'manifest.xml'
LOCAL_MANIFEST_NAME = 'local_manifest.xml' LOCAL_MANIFEST_NAME = 'local_manifest.xml'
urlparse.uses_relative.extend(['ssh', 'git'])
urlparse.uses_netloc.extend(['ssh', 'git'])
class _Default(object): class _Default(object):
"""Project defaults within the manifest.""" """Project defaults within the manifest."""
revisionExpr = None revisionExpr = None
remote = None remote = None
sync_j = 1
class _XmlRemote(object): class _XmlRemote(object):
def __init__(self, def __init__(self,
name, name,
fetch=None, fetch=None,
manifestUrl=None,
review=None): review=None):
self.name = name self.name = name
self.fetchUrl = fetch self.fetchUrl = fetch
self.manifestUrl = manifestUrl
self.reviewUrl = review self.reviewUrl = review
def ToRemoteSpec(self, projectName): def ToRemoteSpec(self, projectName):
url = self.fetchUrl url = self.fetchUrl.rstrip('/') + '/' + projectName + '.git'
while url.endswith('/'): manifestUrl = self.manifestUrl.rstrip('/')
url = url[:-1] # urljoin will get confused if there is no scheme in the base url
url += '/%s.git' % projectName # ie, if manifestUrl is of the form <hostname:port>
if manifestUrl.find(':') != manifestUrl.find('/') - 1:
manifestUrl = 'gopher://' + manifestUrl
url = urlparse.urljoin(manifestUrl, url)
url = re.sub(r'^gopher://', '', url)
return RemoteSpec(self.name, url, self.reviewUrl) return RemoteSpec(self.name, url, self.reviewUrl)
class XmlManifest(object): class XmlManifest(object):
@ -133,6 +145,9 @@ class XmlManifest(object):
if d.revisionExpr: if d.revisionExpr:
have_default = True have_default = True
e.setAttribute('revision', d.revisionExpr) e.setAttribute('revision', d.revisionExpr)
if d.sync_j > 1:
have_default = True
e.setAttribute('sync-j', '%d' % d.sync_j)
if have_default: if have_default:
root.appendChild(e) root.appendChild(e)
root.appendChild(doc.createTextNode('')) root.appendChild(doc.createTextNode(''))
@ -362,7 +377,8 @@ class XmlManifest(object):
if name is None: if name is None:
s = m_url.rindex('/') + 1 s = m_url.rindex('/') + 1
remote = _XmlRemote('origin', m_url[:s]) manifestUrl = self.manifestProject.config.GetString('remote.origin.url')
remote = _XmlRemote('origin', m_url[:s], manifestUrl)
name = m_url[s:] name = m_url[s:]
if name.endswith('.git'): if name.endswith('.git'):
@ -390,7 +406,8 @@ class XmlManifest(object):
review = node.getAttribute('review') review = node.getAttribute('review')
if review == '': if review == '':
review = None review = None
return _XmlRemote(name, fetch, review) manifestUrl = self.manifestProject.config.GetString('remote.origin.url')
return _XmlRemote(name, fetch, manifestUrl, review)
def _ParseDefault(self, node): def _ParseDefault(self, node):
""" """
@ -401,6 +418,11 @@ class XmlManifest(object):
d.revisionExpr = node.getAttribute('revision') d.revisionExpr = node.getAttribute('revision')
if d.revisionExpr == '': if d.revisionExpr == '':
d.revisionExpr = None d.revisionExpr = None
sync_j = node.getAttribute('sync-j')
if sync_j == '' or sync_j is None:
d.sync_j = 1
else:
d.sync_j = int(sync_j)
return d return d
def _ParseNotice(self, node): def _ParseNotice(self, node):

View File

@ -21,13 +21,14 @@ from trace import IsTrace
_NOT_TTY = not os.isatty(2) _NOT_TTY = not os.isatty(2)
class Progress(object): class Progress(object):
def __init__(self, title, total=0): def __init__(self, title, total=0, units=''):
self._title = title self._title = title
self._total = total self._total = total
self._done = 0 self._done = 0
self._lastp = -1 self._lastp = -1
self._start = time() self._start = time()
self._show = False self._show = False
self._units = units
def update(self, inc=1): def update(self, inc=1):
self._done += inc self._done += inc
@ -51,11 +52,11 @@ class Progress(object):
if self._lastp != p: if self._lastp != p:
self._lastp = p self._lastp = p
sys.stderr.write('\r%s: %3d%% (%d/%d) ' % ( sys.stderr.write('\r%s: %3d%% (%d%s/%d%s) ' % (
self._title, self._title,
p, p,
self._done, self._done, self._units,
self._total)) self._total, self._units))
sys.stderr.flush() sys.stderr.flush()
def end(self): def end(self):
@ -69,9 +70,9 @@ class Progress(object):
sys.stderr.flush() sys.stderr.flush()
else: else:
p = (100 * self._done) / self._total p = (100 * self._done) / self._total
sys.stderr.write('\r%s: %3d%% (%d/%d), done. \n' % ( sys.stderr.write('\r%s: %3d%% (%d%s/%d%s), done. \n' % (
self._title, self._title,
p, p,
self._done, self._done, self._units,
self._total)) self._total, self._units))
sys.stderr.flush() sys.stderr.flush()

View File

@ -16,17 +16,21 @@ import traceback
import errno import errno
import filecmp import filecmp
import os import os
import random
import re import re
import shutil import shutil
import stat import stat
import sys import sys
import time
import urllib2 import urllib2
from color import Coloring from color import Coloring
from git_command import GitCommand from git_command import GitCommand
from git_config import GitConfig, IsId from git_config import GitConfig, IsId, GetSchemeFromUrl
from error import DownloadError
from error import GitError, HookError, ImportError, UploadError from error import GitError, HookError, ImportError, UploadError
from error import ManifestInvalidRevisionError from error import ManifestInvalidRevisionError
from progress import Progress
from git_refs import GitRefs, HEAD, R_HEADS, R_TAGS, R_PUB, R_M from git_refs import GitRefs, HEAD, R_HEADS, R_TAGS, R_PUB, R_M
@ -884,19 +888,33 @@ class Project(object):
## Sync ## ## Sync ##
def Sync_NetworkHalf(self, quiet=False): def Sync_NetworkHalf(self, quiet=False, is_new=None):
"""Perform only the network IO portion of the sync process. """Perform only the network IO portion of the sync process.
Local working directory/branch state is not affected. Local working directory/branch state is not affected.
""" """
is_new = not self.Exists if is_new is None:
is_new = not self.Exists
if is_new: if is_new:
if not quiet:
print >>sys.stderr
print >>sys.stderr, 'Initializing project %s ...' % self.name
self._InitGitDir() self._InitGitDir()
self._InitRemote() self._InitRemote()
if not self._RemoteFetch(initial=is_new, quiet=quiet):
if is_new:
alt = os.path.join(self.gitdir, 'objects/info/alternates')
try:
fd = open(alt, 'rb')
try:
alt_dir = fd.readline().rstrip()
finally:
fd.close()
except IOError:
alt_dir = None
else:
alt_dir = None
if alt_dir is None and self._ApplyCloneBundle(initial=is_new, quiet=quiet):
is_new = False
if not self._RemoteFetch(initial=is_new, quiet=quiet, alt_dir=alt_dir):
return False return False
#Check that the requested ref was found after fetch #Check that the requested ref was found after fetch
@ -1307,29 +1325,19 @@ class Project(object):
def _RemoteFetch(self, name=None, tag=None, def _RemoteFetch(self, name=None, tag=None,
initial=False, initial=False,
quiet=False): quiet=False,
alt_dir=None):
if not name: if not name:
name = self.remote.name name = self.remote.name
ssh_proxy = False ssh_proxy = False
if self.GetRemote(name).PreConnectFetch(): remote = self.GetRemote(name)
if remote.PreConnectFetch():
ssh_proxy = True ssh_proxy = True
if initial: if initial:
alt = os.path.join(self.gitdir, 'objects/info/alternates') if alt_dir and 'objects' == os.path.basename(alt_dir):
try: ref_dir = os.path.dirname(alt_dir)
fd = open(alt, 'rb')
try:
ref_dir = fd.readline()
if ref_dir and ref_dir.endswith('\n'):
ref_dir = ref_dir[:-1]
finally:
fd.close()
except IOError, e:
ref_dir = None
if ref_dir and 'objects' == os.path.basename(ref_dir):
ref_dir = os.path.dirname(ref_dir)
packed_refs = os.path.join(self.gitdir, 'packed-refs') packed_refs = os.path.join(self.gitdir, 'packed-refs')
remote = self.GetRemote(name) remote = self.GetRemote(name)
@ -1365,9 +1373,8 @@ class Project(object):
old_packed += line old_packed += line
_lwrite(packed_refs, tmp_packed) _lwrite(packed_refs, tmp_packed)
else: else:
ref_dir = None alt_dir = None
cmd = ['fetch'] cmd = ['fetch']
@ -1386,21 +1393,149 @@ class Project(object):
cmd.append('tag') cmd.append('tag')
cmd.append(tag) cmd.append(tag)
ok = GitCommand(self, ok = False
cmd, for i in range(2):
bare = True, if GitCommand(self, cmd, bare=True, ssh_proxy=ssh_proxy).Wait() == 0:
ssh_proxy = ssh_proxy).Wait() == 0 ok = True
break
time.sleep(random.randint(30, 45))
if initial: if initial:
if ref_dir: if alt_dir:
if old_packed != '': if old_packed != '':
_lwrite(packed_refs, old_packed) _lwrite(packed_refs, old_packed)
else: else:
os.remove(packed_refs) os.remove(packed_refs)
self.bare_git.pack_refs('--all', '--prune') self.bare_git.pack_refs('--all', '--prune')
return ok return ok
def _ApplyCloneBundle(self, initial=False, quiet=False):
if initial and self.manifest.manifestProject.config.GetString('repo.depth'):
return False
remote = self.GetRemote(self.remote.name)
bundle_url = remote.url + '/clone.bundle'
bundle_url = GitConfig.ForUser().UrlInsteadOf(bundle_url)
if GetSchemeFromUrl(bundle_url) not in ('http', 'https'):
return False
bundle_dst = os.path.join(self.gitdir, 'clone.bundle')
bundle_tmp = os.path.join(self.gitdir, 'clone.bundle.tmp')
exist_dst = os.path.exists(bundle_dst)
exist_tmp = os.path.exists(bundle_tmp)
if not initial and not exist_dst and not exist_tmp:
return False
if not exist_dst:
exist_dst = self._FetchBundle(bundle_url, bundle_tmp, bundle_dst, quiet)
if not exist_dst:
return False
cmd = ['fetch']
if quiet:
cmd.append('--quiet')
if not self.worktree:
cmd.append('--update-head-ok')
cmd.append(bundle_dst)
for f in remote.fetch:
cmd.append(str(f))
cmd.append('refs/tags/*:refs/tags/*')
ok = GitCommand(self, cmd, bare=True).Wait() == 0
if os.path.exists(bundle_dst):
os.remove(bundle_dst)
if os.path.exists(bundle_tmp):
os.remove(bundle_tmp)
return ok
def _FetchBundle(self, srcUrl, tmpPath, dstPath, quiet):
keep = True
done = False
dest = open(tmpPath, 'a+b')
try:
dest.seek(0, os.SEEK_END)
pos = dest.tell()
req = urllib2.Request(srcUrl)
if pos > 0:
req.add_header('Range', 'bytes=%d-' % pos)
try:
r = urllib2.urlopen(req)
except urllib2.HTTPError, e:
if e.code == 404:
keep = False
return False
elif e.info()['content-type'] == 'text/plain':
try:
msg = e.read()
if len(msg) > 0 and msg[-1] == '\n':
msg = msg[0:-1]
msg = ' (%s)' % msg
except:
msg = ''
else:
try:
from BaseHTTPServer import BaseHTTPRequestHandler
res = BaseHTTPRequestHandler.responses[e.code]
msg = ' (%s: %s)' % (res[0], res[1])
except:
msg = ''
raise DownloadError('HTTP %s%s' % (e.code, msg))
except urllib2.URLError, e:
raise DownloadError('%s (%s)' % (e.reason, req.get_host()))
p = None
try:
size = r.headers['content-length']
unit = 1 << 10
if size and not quiet:
if size > 1024 * 1.3:
unit = 1 << 20
desc = 'MB'
else:
desc = 'KB'
p = Progress(
'Downloading %s' % self.relpath,
int(size) / unit,
units=desc)
if pos > 0:
p.update(pos / unit)
s = 0
while True:
d = r.read(8192)
if d == '':
done = True
return True
dest.write(d)
if p:
s += len(d)
if s >= unit:
p.update(s / unit)
s = s % unit
if p:
if s >= unit:
p.update(s / unit)
else:
p.update(1)
finally:
r.close()
if p:
p.end()
finally:
dest.close()
if os.path.exists(dstPath):
os.remove(dstPath)
if done:
os.rename(tmpPath, dstPath)
elif not keep:
os.remove(tmpPath)
def _Checkout(self, rev, quiet=False): def _Checkout(self, rev, quiet=False):
cmd = ['checkout'] cmd = ['checkout']
if quiet: if quiet:
@ -1484,10 +1619,13 @@ class Project(object):
for stock_hook in _ProjectHooks(): for stock_hook in _ProjectHooks():
name = os.path.basename(stock_hook) name = os.path.basename(stock_hook)
if name in ('commit-msg',) and not self.remote.review: if name in ('commit-msg',) and not self.remote.review \
and not self is self.manifest.manifestProject:
# Don't install a Gerrit Code Review hook if this # Don't install a Gerrit Code Review hook if this
# project does not appear to use it for reviews. # project does not appear to use it for reviews.
# #
# Since the manifest project is one of those, but also
# managed through gerrit, it's excluded
continue continue
dst = os.path.join(hooks, name) dst = os.path.join(hooks, name)

110
repo
View File

@ -2,7 +2,7 @@
## repo default configuration ## repo default configuration
## ##
REPO_URL='git://android.git.kernel.org/tools/repo.git' REPO_URL='https://code.google.com/p/git-repo/'
REPO_REV='stable' REPO_REV='stable'
# Copyright (C) 2008 Google Inc. # Copyright (C) 2008 Google Inc.
@ -28,7 +28,7 @@ if __name__ == '__main__':
del magic del magic
# increment this whenever we make important changes to this script # increment this whenever we make important changes to this script
VERSION = (1, 10) VERSION = (1, 13)
# increment this if the MAINTAINER_KEYS block is modified # increment this if the MAINTAINER_KEYS block is modified
KEYRING_VERSION = (1,0) KEYRING_VERSION = (1,0)
@ -91,6 +91,7 @@ import re
import readline import readline
import subprocess import subprocess
import sys import sys
import urllib2
home_dot_repo = os.path.expanduser('~/.repoconfig') home_dot_repo = os.path.expanduser('~/.repoconfig')
gpg_dir = os.path.join(home_dot_repo, 'gnupg') gpg_dir = os.path.join(home_dot_repo, 'gnupg')
@ -121,6 +122,10 @@ group.add_option('--mirror',
group.add_option('--reference', group.add_option('--reference',
dest='reference', dest='reference',
help='location of mirror directory', metavar='DIR') help='location of mirror directory', metavar='DIR')
group.add_option('--depth', type='int', default=None,
dest='depth',
help='create a shallow clone with given depth; see git clone')
# Tool # Tool
group = init_optparse.add_option_group('repo Version options') group = init_optparse.add_option_group('repo Version options')
@ -183,10 +188,6 @@ def _Init(args):
else: else:
can_verify = True can_verify = True
if not opt.quiet:
print >>sys.stderr, 'Getting repo ...'
print >>sys.stderr, ' from %s' % url
dst = os.path.abspath(os.path.join(repodir, S_repo)) dst = os.path.abspath(os.path.join(repodir, S_repo))
_Clone(url, dst, opt.quiet) _Clone(url, dst, opt.quiet)
@ -296,15 +297,42 @@ def _SetConfig(local, name, value):
raise CloneFailure() raise CloneFailure()
def _Fetch(local, quiet, *args): def _InitHttp():
handlers = []
mgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
try:
import netrc
n = netrc.netrc()
for host in n.hosts:
p = n.hosts[host]
mgr.add_password(None, 'http://%s/' % host, p[0], p[2])
mgr.add_password(None, 'https://%s/' % host, p[0], p[2])
except:
pass
handlers.append(urllib2.HTTPBasicAuthHandler(mgr))
if 'http_proxy' in os.environ:
url = os.environ['http_proxy']
handlers.append(urllib2.ProxyHandler({'http': url, 'https': url}))
if 'REPO_CURL_VERBOSE' in os.environ:
handlers.append(urllib2.HTTPHandler(debuglevel=1))
handlers.append(urllib2.HTTPSHandler(debuglevel=1))
urllib2.install_opener(urllib2.build_opener(*handlers))
def _Fetch(url, local, src, quiet):
if not quiet:
print >>sys.stderr, 'Get %s' % url
cmd = [GIT, 'fetch'] cmd = [GIT, 'fetch']
if quiet: if quiet:
cmd.append('--quiet') cmd.append('--quiet')
err = subprocess.PIPE err = subprocess.PIPE
else: else:
err = None err = None
cmd.extend(args) cmd.append(src)
cmd.append('origin') cmd.append('+refs/heads/*:refs/remotes/origin/*')
cmd.append('refs/tags/*:refs/tags/*')
proc = subprocess.Popen(cmd, cwd = local, stderr = err) proc = subprocess.Popen(cmd, cwd = local, stderr = err)
if err: if err:
@ -313,6 +341,62 @@ def _Fetch(local, quiet, *args):
if proc.wait() != 0: if proc.wait() != 0:
raise CloneFailure() raise CloneFailure()
def _DownloadBundle(url, local, quiet):
if not url.endswith('/'):
url += '/'
url += 'clone.bundle'
proc = subprocess.Popen(
[GIT, 'config', '--get-regexp', 'url.*.insteadof'],
cwd = local,
stdout = subprocess.PIPE)
for line in proc.stdout:
m = re.compile(r'^url\.(.*)\.insteadof (.*)$').match(line)
if m:
new_url = m.group(1)
old_url = m.group(2)
if url.startswith(old_url):
url = new_url + url[len(old_url):]
break
proc.stdout.close()
proc.wait()
if not url.startswith('http:') and not url.startswith('https:'):
return False
dest = open(os.path.join(local, '.git', 'clone.bundle'), 'w+b')
try:
try:
r = urllib2.urlopen(url)
except urllib2.HTTPError, e:
if e.code == 404:
return False
print >>sys.stderr, 'fatal: Cannot get %s' % url
print >>sys.stderr, 'fatal: HTTP error %s' % e.code
raise CloneFailure()
except urllib2.URLError, e:
print >>sys.stderr, 'fatal: Cannot get %s' % url
print >>sys.stderr, 'fatal: error %s' % e.reason
raise CloneFailure()
try:
if not quiet:
print >>sys.stderr, 'Get %s' % url
while True:
buf = r.read(8192)
if buf == '':
return True
dest.write(buf)
finally:
r.close()
finally:
dest.close()
def _ImportBundle(local):
path = os.path.join(local, '.git', 'clone.bundle')
try:
_Fetch(local, local, path, True)
finally:
os.remove(path)
def _Clone(url, local, quiet): def _Clone(url, local, quiet):
"""Clones a git repository to a new subdirectory of repodir """Clones a git repository to a new subdirectory of repodir
@ -340,11 +424,14 @@ def _Clone(url, local, quiet):
print >>sys.stderr, 'fatal: could not create %s' % local print >>sys.stderr, 'fatal: could not create %s' % local
raise CloneFailure() raise CloneFailure()
_InitHttp()
_SetConfig(local, 'remote.origin.url', url) _SetConfig(local, 'remote.origin.url', url)
_SetConfig(local, 'remote.origin.fetch', _SetConfig(local, 'remote.origin.fetch',
'+refs/heads/*:refs/remotes/origin/*') '+refs/heads/*:refs/remotes/origin/*')
_Fetch(local, quiet) if _DownloadBundle(url, local, quiet):
_Fetch(local, quiet, '--tags') _ImportBundle(local)
else:
_Fetch(url, local, 'origin', quiet)
def _Verify(cwd, branch, quiet): def _Verify(cwd, branch, quiet):
@ -596,4 +683,3 @@ def main(orig_args):
if __name__ == '__main__': if __name__ == '__main__':
main(sys.argv[1:]) main(sys.argv[1:])

View File

@ -21,6 +21,7 @@ from color import Coloring
from command import InteractiveCommand, MirrorSafeCommand from command import InteractiveCommand, MirrorSafeCommand
from error import ManifestParseError from error import ManifestParseError
from project import SyncBuffer from project import SyncBuffer
from git_config import GitConfig
from git_command import git_require, MIN_GIT_VERSION from git_command import git_require, MIN_GIT_VERSION
class Init(InteractiveCommand, MirrorSafeCommand): class Init(InteractiveCommand, MirrorSafeCommand):
@ -108,8 +109,8 @@ to update the working directory files.
sys.exit(1) sys.exit(1)
if not opt.quiet: if not opt.quiet:
print >>sys.stderr, 'Getting manifest ...' print >>sys.stderr, 'Get %s' \
print >>sys.stderr, ' from %s' % opt.manifest_url % GitConfig.ForUser().UrlInsteadOf(opt.manifest_url)
m._InitGitDir() m._InitGitDir()
if opt.manifest_branch: if opt.manifest_branch:
@ -138,7 +139,7 @@ to update the working directory files.
print >>sys.stderr, 'fatal: --mirror not supported on existing client' print >>sys.stderr, 'fatal: --mirror not supported on existing client'
sys.exit(1) sys.exit(1)
if not m.Sync_NetworkHalf(): if not m.Sync_NetworkHalf(is_new=is_new):
r = m.GetRemote(m.remote.name) r = m.GetRemote(m.remote.name)
print >>sys.stderr, 'fatal: cannot obtain manifest %s' % r.url print >>sys.stderr, 'fatal: cannot obtain manifest %s' % r.url

View File

@ -28,6 +28,14 @@ try:
except ImportError: except ImportError:
import dummy_threading as _threading import dummy_threading as _threading
try:
import resource
def _rlimit_nofile():
return resource.getrlimit(resource.RLIMIT_NOFILE)
except ImportError:
def _rlimit_nofile():
return (256, 256)
from git_command import GIT from git_command import GIT
from git_refs import R_HEADS from git_refs import R_HEADS
from project import HEAD from project import HEAD
@ -72,7 +80,8 @@ revision is temporarily needed.
The -s/--smart-sync option can be used to sync to a known good 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 build as specified by the manifest-server element in the current
manifest. manifest. The -t/--smart-tag option is similar and allows you to
specify a custom tag/label.
The -f/--force-broken option can be used to proceed with syncing The -f/--force-broken option can be used to proceed with syncing
other projects if a project sync fails. other projects if a project sync fails.
@ -108,6 +117,8 @@ later is required to fix a server side protocol bug.
""" """
def _Options(self, p, show_smart=True): def _Options(self, p, show_smart=True):
self.jobs = self.manifest.default.sync_j
p.add_option('-f', '--force-broken', p.add_option('-f', '--force-broken',
dest='force_broken', action='store_true', dest='force_broken', action='store_true',
help="continue sync even if a project fails to sync") help="continue sync even if a project fails to sync")
@ -125,11 +136,14 @@ later is required to fix a server side protocol bug.
help='be more quiet') help='be more quiet')
p.add_option('-j','--jobs', p.add_option('-j','--jobs',
dest='jobs', action='store', type='int', dest='jobs', action='store', type='int',
help="number of projects to fetch simultaneously") help="projects to fetch simultaneously (default %d)" % self.jobs)
if show_smart: if show_smart:
p.add_option('-s', '--smart-sync', p.add_option('-s', '--smart-sync',
dest='smart_sync', action='store_true', dest='smart_sync', action='store_true',
help='smart sync using manifest from a known good build') help='smart sync using manifest from a known good build')
p.add_option('-t', '--smart-tag',
dest='smart_tag', action='store',
help='smart sync using manifest from a known tag')
g = p.add_option_group('repo Version options') g = p.add_option_group('repo Version options')
g.add_option('--no-repo-verify', g.add_option('--no-repo-verify',
@ -308,6 +322,10 @@ uncommitted changes are present' % project.relpath
def Execute(self, opt, args): def Execute(self, opt, args):
if opt.jobs: if opt.jobs:
self.jobs = opt.jobs self.jobs = opt.jobs
if self.jobs > 1:
soft_limit, _ = _rlimit_nofile()
self.jobs = min(self.jobs, (soft_limit - 5) / 3)
if opt.network_only and opt.detach_head: if opt.network_only and opt.detach_head:
print >>sys.stderr, 'error: cannot combine -n and -d' print >>sys.stderr, 'error: cannot combine -n and -d'
sys.exit(1) sys.exit(1)
@ -315,27 +333,31 @@ uncommitted changes are present' % project.relpath
print >>sys.stderr, 'error: cannot combine -n and -l' print >>sys.stderr, 'error: cannot combine -n and -l'
sys.exit(1) sys.exit(1)
if opt.smart_sync: if opt.smart_sync or opt.smart_tag:
if not self.manifest.manifest_server: if not self.manifest.manifest_server:
print >>sys.stderr, \ print >>sys.stderr, \
'error: cannot smart sync: no manifest server defined in manifest' 'error: cannot smart sync: no manifest server defined in manifest'
sys.exit(1) sys.exit(1)
try: try:
server = xmlrpclib.Server(self.manifest.manifest_server) server = xmlrpclib.Server(self.manifest.manifest_server)
p = self.manifest.manifestProject if opt.smart_sync:
b = p.GetBranch(p.CurrentBranch) p = self.manifest.manifestProject
branch = b.merge b = p.GetBranch(p.CurrentBranch)
if branch.startswith(R_HEADS): branch = b.merge
branch = branch[len(R_HEADS):] if branch.startswith(R_HEADS):
branch = branch[len(R_HEADS):]
env = os.environ.copy() env = os.environ.copy()
if (env.has_key('TARGET_PRODUCT') and if (env.has_key('TARGET_PRODUCT') and
env.has_key('TARGET_BUILD_VARIANT')): env.has_key('TARGET_BUILD_VARIANT')):
target = '%s-%s' % (env['TARGET_PRODUCT'], target = '%s-%s' % (env['TARGET_PRODUCT'],
env['TARGET_BUILD_VARIANT']) env['TARGET_BUILD_VARIANT'])
[success, manifest_str] = server.GetApprovedManifest(branch, target) [success, manifest_str] = server.GetApprovedManifest(branch, target)
else:
[success, manifest_str] = server.GetApprovedManifest(branch)
else: else:
[success, manifest_str] = server.GetApprovedManifest(branch) assert(opt.smart_tag)
[success, manifest_str] = server.GetManifest(opt.smart_tag)
if success: if success:
manifest_name = "smart_sync_override.xml" manifest_name = "smart_sync_override.xml"
@ -378,6 +400,8 @@ uncommitted changes are present' % project.relpath
if not syncbuf.Finish(): if not syncbuf.Finish():
sys.exit(1) sys.exit(1)
self.manifest._Unload() self.manifest._Unload()
if opt.jobs is None:
self.jobs = self.manifest.default.sync_j
all = self.GetProjects(args, missing_ok=True) all = self.GetProjects(args, missing_ok=True)
if not opt.local_only: if not opt.local_only: