summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorShawn O. Pearce <sop@google.com>2011-09-19 14:50:58 -0700
committerShawn O. Pearce <sop@google.com>2011-09-28 10:07:36 -0700
commitf322b9abb4cadc67b991baf6ba1b9f2fbd5d7812 (patch)
treece75a04fed2e84457800325d158de13645cef67e
parentdb728cd866d4950779620993e12e76f09eb6e2ee (diff)
downloadgit-repo-f322b9abb4cadc67b991baf6ba1b9f2fbd5d7812.tar.gz
sync: Support downloading bundle to initialize repositoryv1.7.7
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>
-rw-r--r--error.py9
-rw-r--r--git_config.py6
-rwxr-xr-xmain.py4
-rw-r--r--project.py145
-rwxr-xr-xrepo103
-rw-r--r--subcmds/init.py7
6 files changed, 250 insertions, 24 deletions
diff --git a/error.py b/error.py
index 52381581..812585cd 100644
--- a/error.py
+++ b/error.py
@@ -57,6 +57,15 @@ class UploadError(Exception):
57 def __str__(self): 57 def __str__(self):
58 return self.reason 58 return self.reason
59 59
60class DownloadError(Exception):
61 """Cannot download a repository.
62 """
63 def __init__(self, reason):
64 self.reason = reason
65
66 def __str__(self):
67 return self.reason
68
60class NoSuchProjectError(Exception): 69class NoSuchProjectError(Exception):
61 """A specified project does not exist in the work tree. 70 """A specified project does not exist in the work tree.
62 """ 71 """
diff --git a/git_config.py b/git_config.py
index e4f4a0ab..bcd6e8d6 100644
--- a/git_config.py
+++ b/git_config.py
@@ -491,6 +491,12 @@ def close_ssh():
491URI_SCP = re.compile(r'^([^@:]*@?[^:/]{1,}):') 491URI_SCP = re.compile(r'^([^@:]*@?[^:/]{1,}):')
492URI_ALL = re.compile(r'^([a-z][a-z+]*)://([^@/]*@?[^/]*)/') 492URI_ALL = re.compile(r'^([a-z][a-z+]*)://([^@/]*@?[^/]*)/')
493 493
494def GetSchemeFromUrl(url):
495 m = URI_ALL.match(url)
496 if m:
497 return m.group(1)
498 return None
499
494def _preconnect(url): 500def _preconnect(url):
495 m = URI_ALL.match(url) 501 m = URI_ALL.match(url)
496 if m: 502 if m:
diff --git a/main.py b/main.py
index c5c71c36..8ffdfcce 100755
--- a/main.py
+++ b/main.py
@@ -37,6 +37,7 @@ from command import InteractiveCommand
37from command import MirrorSafeCommand 37from command import MirrorSafeCommand
38from command import PagedCommand 38from command import PagedCommand
39from editor import Editor 39from editor import Editor
40from error import DownloadError
40from error import ManifestInvalidRevisionError 41from error import ManifestInvalidRevisionError
41from error import NoSuchProjectError 42from error import NoSuchProjectError
42from error import RepoChangedException 43from error import RepoChangedException
@@ -143,6 +144,9 @@ class _Repo(object):
143 else: 144 else:
144 print >>sys.stderr, 'real\t%dh%dm%.3fs' \ 145 print >>sys.stderr, 'real\t%dh%dm%.3fs' \
145 % (hours, minutes, seconds) 146 % (hours, minutes, seconds)
147 except DownloadError, e:
148 print >>sys.stderr, 'error: %s' % str(e)
149 sys.exit(1)
146 except ManifestInvalidRevisionError, e: 150 except ManifestInvalidRevisionError, e:
147 print >>sys.stderr, 'error: %s' % str(e) 151 print >>sys.stderr, 'error: %s' % str(e)
148 sys.exit(1) 152 sys.exit(1)
diff --git a/project.py b/project.py
index 3efc4452..5adfe82e 100644
--- a/project.py
+++ b/project.py
@@ -24,9 +24,11 @@ import urllib2
24 24
25from color import Coloring 25from color import Coloring
26from git_command import GitCommand 26from git_command import GitCommand
27from git_config import GitConfig, IsId 27from git_config import GitConfig, IsId, GetSchemeFromUrl
28from error import DownloadError
28from error import GitError, HookError, ImportError, UploadError 29from error import GitError, HookError, ImportError, UploadError
29from error import ManifestInvalidRevisionError 30from error import ManifestInvalidRevisionError
31from progress import Progress
30 32
31from git_refs import GitRefs, HEAD, R_HEADS, R_TAGS, R_PUB, R_M 33from git_refs import GitRefs, HEAD, R_HEADS, R_TAGS, R_PUB, R_M
32 34
@@ -884,15 +886,13 @@ class Project(object):
884 886
885## Sync ## 887## Sync ##
886 888
887 def Sync_NetworkHalf(self, quiet=False): 889 def Sync_NetworkHalf(self, quiet=False, is_new=None):
888 """Perform only the network IO portion of the sync process. 890 """Perform only the network IO portion of the sync process.
889 Local working directory/branch state is not affected. 891 Local working directory/branch state is not affected.
890 """ 892 """
891 is_new = not self.Exists 893 if is_new is None:
894 is_new = not self.Exists
892 if is_new: 895 if is_new:
893 if not quiet:
894 print >>sys.stderr
895 print >>sys.stderr, 'Initializing project %s ...' % self.name
896 self._InitGitDir() 896 self._InitGitDir()
897 897
898 self._InitRemote() 898 self._InitRemote()
@@ -1312,9 +1312,16 @@ class Project(object):
1312 name = self.remote.name 1312 name = self.remote.name
1313 1313
1314 ssh_proxy = False 1314 ssh_proxy = False
1315 if self.GetRemote(name).PreConnectFetch(): 1315 remote = self.GetRemote(name)
1316 if remote.PreConnectFetch():
1316 ssh_proxy = True 1317 ssh_proxy = True
1317 1318
1319 bundle_dst = os.path.join(self.gitdir, 'clone.bundle')
1320 bundle_tmp = os.path.join(self.gitdir, 'clone.bundle.tmp')
1321 use_bundle = False
1322 if os.path.exists(bundle_dst) or os.path.exists(bundle_tmp):
1323 use_bundle = True
1324
1318 if initial: 1325 if initial:
1319 alt = os.path.join(self.gitdir, 'objects/info/alternates') 1326 alt = os.path.join(self.gitdir, 'objects/info/alternates')
1320 try: 1327 try:
@@ -1329,6 +1336,8 @@ class Project(object):
1329 ref_dir = None 1336 ref_dir = None
1330 1337
1331 if ref_dir and 'objects' == os.path.basename(ref_dir): 1338 if ref_dir and 'objects' == os.path.basename(ref_dir):
1339 if use_bundle:
1340 use_bundle = False
1332 ref_dir = os.path.dirname(ref_dir) 1341 ref_dir = os.path.dirname(ref_dir)
1333 packed_refs = os.path.join(self.gitdir, 'packed-refs') 1342 packed_refs = os.path.join(self.gitdir, 'packed-refs')
1334 remote = self.GetRemote(name) 1343 remote = self.GetRemote(name)
@@ -1368,6 +1377,7 @@ class Project(object):
1368 1377
1369 else: 1378 else:
1370 ref_dir = None 1379 ref_dir = None
1380 use_bundle = True
1371 1381
1372 cmd = ['fetch'] 1382 cmd = ['fetch']
1373 1383
@@ -1376,15 +1386,37 @@ class Project(object):
1376 depth = self.manifest.manifestProject.config.GetString('repo.depth') 1386 depth = self.manifest.manifestProject.config.GetString('repo.depth')
1377 if depth and initial: 1387 if depth and initial:
1378 cmd.append('--depth=%s' % depth) 1388 cmd.append('--depth=%s' % depth)
1389 use_bundle = False
1379 1390
1380 if quiet: 1391 if quiet:
1381 cmd.append('--quiet') 1392 cmd.append('--quiet')
1382 if not self.worktree: 1393 if not self.worktree:
1383 cmd.append('--update-head-ok') 1394 cmd.append('--update-head-ok')
1384 cmd.append(name) 1395
1385 if tag is not None: 1396 if use_bundle and not os.path.exists(bundle_dst):
1386 cmd.append('tag') 1397 bundle_url = remote.url + '/clone.bundle'
1387 cmd.append(tag) 1398 bundle_url = GitConfig.ForUser().UrlInsteadOf(bundle_url)
1399 if GetSchemeFromUrl(bundle_url) in ('http', 'https'):
1400 use_bundle = self._FetchBundle(
1401 bundle_url,
1402 bundle_tmp,
1403 bundle_dst,
1404 quiet=quiet)
1405 else:
1406 use_bundle = False
1407
1408 if use_bundle:
1409 if not quiet:
1410 cmd.append('--quiet')
1411 cmd.append(bundle_dst)
1412 for f in remote.fetch:
1413 cmd.append(str(f))
1414 cmd.append('refs/tags/*:refs/tags/*')
1415 else:
1416 cmd.append(name)
1417 if tag is not None:
1418 cmd.append('tag')
1419 cmd.append(tag)
1388 1420
1389 ok = GitCommand(self, 1421 ok = GitCommand(self,
1390 cmd, 1422 cmd,
@@ -1399,8 +1431,99 @@ class Project(object):
1399 os.remove(packed_refs) 1431 os.remove(packed_refs)
1400 self.bare_git.pack_refs('--all', '--prune') 1432 self.bare_git.pack_refs('--all', '--prune')
1401 1433
1434 if os.path.exists(bundle_dst):
1435 os.remove(bundle_dst)
1436 if os.path.exists(bundle_tmp):
1437 os.remove(bundle_tmp)
1438
1402 return ok 1439 return ok
1403 1440
1441 def _FetchBundle(self, srcUrl, tmpPath, dstPath, quiet=False):
1442 keep = True
1443 done = False
1444 dest = open(tmpPath, 'a+b')
1445 try:
1446 dest.seek(0, os.SEEK_END)
1447 pos = dest.tell()
1448
1449 req = urllib2.Request(srcUrl)
1450 if pos > 0:
1451 req.add_header('Range', 'bytes=%d-' % pos)
1452
1453 try:
1454 r = urllib2.urlopen(req)
1455 except urllib2.HTTPError, e:
1456 if e.code == 404:
1457 keep = False
1458 return False
1459 elif e.info()['content-type'] == 'text/plain':
1460 try:
1461 msg = e.read()
1462 if len(msg) > 0 and msg[-1] == '\n':
1463 msg = msg[0:-1]
1464 msg = ' (%s)' % msg
1465 except:
1466 msg = ''
1467 else:
1468 try:
1469 from BaseHTTPServer import BaseHTTPRequestHandler
1470 res = BaseHTTPRequestHandler.responses[e.code]
1471 msg = ' (%s: %s)' % (res[0], res[1])
1472 except:
1473 msg = ''
1474 raise DownloadError('HTTP %s%s' % (e.code, msg))
1475 except urllib2.URLError, e:
1476 raise DownloadError('%s (%s)' % (e.reason, req.get_host()))
1477
1478 p = None
1479 try:
1480 size = r.headers['content-length']
1481 unit = 1 << 10
1482
1483 if size and not quiet:
1484 if size > 1024 * 1.3:
1485 unit = 1 << 20
1486 desc = 'MB'
1487 else:
1488 desc = 'KB'
1489 p = Progress(
1490 'Downloading %s' % self.relpath,
1491 int(size) / unit,
1492 units=desc)
1493 if pos > 0:
1494 p.update(pos / unit)
1495
1496 s = 0
1497 while True:
1498 d = r.read(8192)
1499 if d == '':
1500 done = True
1501 return True
1502 dest.write(d)
1503 if p:
1504 s += len(d)
1505 if s >= unit:
1506 p.update(s / unit)
1507 s = s % unit
1508 if p:
1509 if s >= unit:
1510 p.update(s / unit)
1511 else:
1512 p.update(1)
1513 finally:
1514 r.close()
1515 if p:
1516 p.end()
1517 finally:
1518 dest.close()
1519
1520 if os.path.exists(dstPath):
1521 os.remove(dstPath)
1522 if done:
1523 os.rename(tmpPath, dstPath)
1524 elif not keep:
1525 os.remove(tmpPath)
1526
1404 def _Checkout(self, rev, quiet=False): 1527 def _Checkout(self, rev, quiet=False):
1405 cmd = ['checkout'] 1528 cmd = ['checkout']
1406 if quiet: 1529 if quiet:
diff --git a/repo b/repo
index 1468fad3..0e779833 100755
--- a/repo
+++ b/repo
@@ -28,7 +28,7 @@ if __name__ == '__main__':
28del magic 28del magic
29 29
30# increment this whenever we make important changes to this script 30# increment this whenever we make important changes to this script
31VERSION = (1, 12) 31VERSION = (1, 13)
32 32
33# increment this if the MAINTAINER_KEYS block is modified 33# increment this if the MAINTAINER_KEYS block is modified
34KEYRING_VERSION = (1,0) 34KEYRING_VERSION = (1,0)
@@ -91,6 +91,7 @@ import re
91import readline 91import readline
92import subprocess 92import subprocess
93import sys 93import sys
94import urllib2
94 95
95home_dot_repo = os.path.expanduser('~/.repoconfig') 96home_dot_repo = os.path.expanduser('~/.repoconfig')
96gpg_dir = os.path.join(home_dot_repo, 'gnupg') 97gpg_dir = os.path.join(home_dot_repo, 'gnupg')
@@ -187,10 +188,6 @@ def _Init(args):
187 else: 188 else:
188 can_verify = True 189 can_verify = True
189 190
190 if not opt.quiet:
191 print >>sys.stderr, 'Getting repo ...'
192 print >>sys.stderr, ' from %s' % url
193
194 dst = os.path.abspath(os.path.join(repodir, S_repo)) 191 dst = os.path.abspath(os.path.join(repodir, S_repo))
195 _Clone(url, dst, opt.quiet) 192 _Clone(url, dst, opt.quiet)
196 193
@@ -300,15 +297,42 @@ def _SetConfig(local, name, value):
300 raise CloneFailure() 297 raise CloneFailure()
301 298
302 299
303def _Fetch(local, quiet, *args): 300def _InitHttp():
301 handlers = []
302
303 mgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
304 try:
305 import netrc
306 n = netrc.netrc()
307 for host in n.hosts:
308 p = n.hosts[host]
309 mgr.add_password(None, 'http://%s/' % host, p[0], p[2])
310 mgr.add_password(None, 'https://%s/' % host, p[0], p[2])
311 except:
312 pass
313 handlers.append(urllib2.HTTPBasicAuthHandler(mgr))
314
315 if 'http_proxy' in os.environ:
316 url = os.environ['http_proxy']
317 handlers.append(urllib2.ProxyHandler({'http': url, 'https': url}))
318 if 'REPO_CURL_VERBOSE' in os.environ:
319 handlers.append(urllib2.HTTPHandler(debuglevel=1))
320 handlers.append(urllib2.HTTPSHandler(debuglevel=1))
321 urllib2.install_opener(urllib2.build_opener(*handlers))
322
323def _Fetch(url, local, src, quiet):
324 if not quiet:
325 print >>sys.stderr, 'Get %s' % url
326
304 cmd = [GIT, 'fetch'] 327 cmd = [GIT, 'fetch']
305 if quiet: 328 if quiet:
306 cmd.append('--quiet') 329 cmd.append('--quiet')
307 err = subprocess.PIPE 330 err = subprocess.PIPE
308 else: 331 else:
309 err = None 332 err = None
310 cmd.extend(args) 333 cmd.append(src)
311 cmd.append('origin') 334 cmd.append('+refs/heads/*:refs/remotes/origin/*')
335 cmd.append('refs/tags/*:refs/tags/*')
312 336
313 proc = subprocess.Popen(cmd, cwd = local, stderr = err) 337 proc = subprocess.Popen(cmd, cwd = local, stderr = err)
314 if err: 338 if err:
@@ -317,6 +341,62 @@ def _Fetch(local, quiet, *args):
317 if proc.wait() != 0: 341 if proc.wait() != 0:
318 raise CloneFailure() 342 raise CloneFailure()
319 343
344def _DownloadBundle(url, local, quiet):
345 if not url.endswith('/'):
346 url += '/'
347 url += 'clone.bundle'
348
349 proc = subprocess.Popen(
350 [GIT, 'config', '--get-regexp', 'url.*.insteadof'],
351 cwd = local,
352 stdout = subprocess.PIPE)
353 for line in proc.stdout:
354 m = re.compile(r'^url\.(.*)\.insteadof (.*)$').match(line)
355 if m:
356 new_url = m.group(1)
357 old_url = m.group(2)
358 if url.startswith(old_url):
359 url = new_url + url[len(old_url):]
360 break
361 proc.stdout.close()
362 proc.wait()
363
364 if not url.startswith('http:') and not url.startswith('https:'):
365 return False
366
367 dest = open(os.path.join(local, '.git', 'clone.bundle'), 'w+b')
368 try:
369 try:
370 r = urllib2.urlopen(url)
371 except urllib2.HTTPError, e:
372 if e.code == 404:
373 return False
374 print >>sys.stderr, 'fatal: Cannot get %s' % url
375 print >>sys.stderr, 'fatal: HTTP error %s' % e.code
376 raise CloneFailure()
377 except urllib2.URLError, e:
378 print >>sys.stderr, 'fatal: Cannot get %s' % url
379 print >>sys.stderr, 'fatal: error %s' % e.reason
380 raise CloneFailure()
381 try:
382 if not quiet:
383 print >>sys.stderr, 'Get %s' % url
384 while True:
385 buf = r.read(8192)
386 if buf == '':
387 return True
388 dest.write(buf)
389 finally:
390 r.close()
391 finally:
392 dest.close()
393
394def _ImportBundle(local):
395 path = os.path.join(local, '.git', 'clone.bundle')
396 try:
397 _Fetch(local, local, path, True)
398 finally:
399 os.remove(path)
320 400
321def _Clone(url, local, quiet): 401def _Clone(url, local, quiet):
322 """Clones a git repository to a new subdirectory of repodir 402 """Clones a git repository to a new subdirectory of repodir
@@ -344,11 +424,14 @@ def _Clone(url, local, quiet):
344 print >>sys.stderr, 'fatal: could not create %s' % local 424 print >>sys.stderr, 'fatal: could not create %s' % local
345 raise CloneFailure() 425 raise CloneFailure()
346 426
427 _InitHttp()
347 _SetConfig(local, 'remote.origin.url', url) 428 _SetConfig(local, 'remote.origin.url', url)
348 _SetConfig(local, 'remote.origin.fetch', 429 _SetConfig(local, 'remote.origin.fetch',
349 '+refs/heads/*:refs/remotes/origin/*') 430 '+refs/heads/*:refs/remotes/origin/*')
350 _Fetch(local, quiet) 431 if _DownloadBundle(url, local, quiet):
351 _Fetch(local, quiet, '--tags') 432 _ImportBundle(local)
433 else:
434 _Fetch(url, local, 'origin', quiet)
352 435
353 436
354def _Verify(cwd, branch, quiet): 437def _Verify(cwd, branch, quiet):
diff --git a/subcmds/init.py b/subcmds/init.py
index c35cc82c..9214aed5 100644
--- a/subcmds/init.py
+++ b/subcmds/init.py
@@ -21,6 +21,7 @@ from color import Coloring
21from command import InteractiveCommand, MirrorSafeCommand 21from command import InteractiveCommand, MirrorSafeCommand
22from error import ManifestParseError 22from error import ManifestParseError
23from project import SyncBuffer 23from project import SyncBuffer
24from git_config import GitConfig
24from git_command import git_require, MIN_GIT_VERSION 25from git_command import git_require, MIN_GIT_VERSION
25 26
26class Init(InteractiveCommand, MirrorSafeCommand): 27class Init(InteractiveCommand, MirrorSafeCommand):
@@ -108,8 +109,8 @@ to update the working directory files.
108 sys.exit(1) 109 sys.exit(1)
109 110
110 if not opt.quiet: 111 if not opt.quiet:
111 print >>sys.stderr, 'Getting manifest ...' 112 print >>sys.stderr, 'Get %s' \
112 print >>sys.stderr, ' from %s' % opt.manifest_url 113 % GitConfig.ForUser().UrlInsteadOf(opt.manifest_url)
113 m._InitGitDir() 114 m._InitGitDir()
114 115
115 if opt.manifest_branch: 116 if opt.manifest_branch:
@@ -138,7 +139,7 @@ to update the working directory files.
138 print >>sys.stderr, 'fatal: --mirror not supported on existing client' 139 print >>sys.stderr, 'fatal: --mirror not supported on existing client'
139 sys.exit(1) 140 sys.exit(1)
140 141
141 if not m.Sync_NetworkHalf(): 142 if not m.Sync_NetworkHalf(is_new=is_new):
142 r = m.GetRemote(m.remote.name) 143 r = m.GetRemote(m.remote.name)
143 print >>sys.stderr, 'fatal: cannot obtain manifest %s' % r.url 144 print >>sys.stderr, 'fatal: cannot obtain manifest %s' % r.url
144 145