1
# Copyright (C) 2010-2012 Jelmer Vernooij <jelmer@samba.org>
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17
"""A Git repository implementation that uses a Bazaar transport."""
19
from __future__ import absolute_import
21
from cStringIO import StringIO
27
from dulwich.errors import (
31
from dulwich.objects import (
34
from dulwich.object_store import (
38
from dulwich.pack import (
47
from dulwich.repo import (
56
read_packed_refs_with_peeled,
62
transport as _mod_transport,
64
from ...errors import (
71
class TransportRefsContainer(RefsContainer):
72
"""Refs container that reads refs from a transport."""
74
def __init__(self, transport):
75
self.transport = transport
76
self._packed_refs = None
77
self._peeled_refs = None
80
return "%s(%r)" % (self.__class__.__name__, self.transport)
82
def _ensure_dir_exists(self, path):
83
for n in range(path.count("/")):
84
dirname = "/".join(path.split("/")[:n+1])
86
self.transport.mkdir(dirname)
90
def subkeys(self, base):
93
iter_files = self.transport.clone(base).iter_files_recursive()
94
keys.update(("%s/%s" % (base, urllib.unquote(refname))).strip("/") for
95
refname in iter_files if check_ref_format("%s/%s" % (base, refname)))
96
except (TransportNotPossible, NoSuchFile):
98
for key in self.get_packed_refs():
99
if key.startswith(base):
100
keys.add(key[len(base):].strip("/"))
106
self.transport.get_bytes("HEAD")
112
iter_files = list(self.transport.clone("refs").iter_files_recursive())
113
for filename in iter_files:
114
refname = "refs/%s" % urllib.unquote(filename)
115
if check_ref_format(refname):
117
except (TransportNotPossible, NoSuchFile):
119
keys.update(self.get_packed_refs())
122
def get_packed_refs(self):
123
"""Get contents of the packed-refs file.
125
:return: Dictionary mapping ref names to SHA1s
127
:note: Will return an empty dictionary when no packed-refs file is
130
# TODO: invalidate the cache on repacking
131
if self._packed_refs is None:
132
# set both to empty because we want _peeled_refs to be
133
# None if and only if _packed_refs is also None.
134
self._packed_refs = {}
135
self._peeled_refs = {}
137
f = self.transport.get("packed-refs")
141
first_line = iter(f).next().rstrip()
142
if (first_line.startswith("# pack-refs") and " peeled" in
144
for sha, name, peeled in read_packed_refs_with_peeled(f):
145
self._packed_refs[name] = sha
147
self._peeled_refs[name] = peeled
150
for sha, name in read_packed_refs(f):
151
self._packed_refs[name] = sha
154
return self._packed_refs
156
def get_peeled(self, name):
157
"""Return the cached peeled value of a ref, if available.
159
:param name: Name of the ref to peel
160
:return: The peeled value of the ref. If the ref is known not point to a
161
tag, this will be the SHA the ref refers to. If the ref may point to
162
a tag, but no cached information is available, None is returned.
164
self.get_packed_refs()
165
if self._peeled_refs is None or name not in self._packed_refs:
166
# No cache: no peeled refs were read, or this ref is loose
168
if name in self._peeled_refs:
169
return self._peeled_refs[name]
174
def read_loose_ref(self, name):
175
"""Read a reference file and return its contents.
177
If the reference file a symbolic reference, only read the first line of
178
the file. Otherwise, only read the first 40 bytes.
180
:param name: the refname to read, relative to refpath
181
:return: The contents of the ref file, or None if the file does not
183
:raises IOError: if any other error occurs
186
f = self.transport.get(name)
189
f = StringIO(f.read())
191
header = f.read(len(SYMREF))
193
# Read only the first line
194
return header + iter(f).next().rstrip("\r\n")
196
# Read only the first 40 bytes
197
return header + f.read(40-len(SYMREF))
201
def _remove_packed_ref(self, name):
202
if self._packed_refs is None:
204
# reread cached refs from disk, while holding the lock
206
self._packed_refs = None
207
self.get_packed_refs()
209
if name not in self._packed_refs:
212
del self._packed_refs[name]
213
if name in self._peeled_refs:
214
del self._peeled_refs[name]
215
f = self.transport.open_write_stream("packed-refs")
217
write_packed_refs(f, self._packed_refs, self._peeled_refs)
221
def set_symbolic_ref(self, name, other):
222
"""Make a ref point at another ref.
224
:param name: Name of the ref to set
225
:param other: Name of the ref to point at
227
self._check_refname(name)
228
self._check_refname(other)
229
self._ensure_dir_exists(name)
230
self.transport.put_bytes(name, SYMREF + other + '\n')
232
def set_if_equals(self, name, old_ref, new_ref):
233
"""Set a refname to new_ref only if it currently equals old_ref.
235
This method follows all symbolic references, and can be used to perform
236
an atomic compare-and-swap operation.
238
:param name: The refname to set.
239
:param old_ref: The old sha the refname must refer to, or None to set
241
:param new_ref: The new sha the refname will refer to.
242
:return: True if the set was successful, False otherwise.
245
realnames, _ = self.follow(name)
246
realname = realnames[-1]
247
except (KeyError, IndexError):
249
self._ensure_dir_exists(realname)
250
self.transport.put_bytes(realname, new_ref+"\n")
253
def add_if_new(self, name, ref):
254
"""Add a new reference only if it does not already exist.
256
This method follows symrefs, and only ensures that the last ref in the
257
chain does not exist.
259
:param name: The refname to set.
260
:param ref: The new sha the refname will refer to.
261
:return: True if the add was successful, False otherwise.
264
realnames, contents = self.follow(name)
265
if contents is not None:
267
realname = realnames[-1]
268
except (KeyError, IndexError):
270
self._check_refname(realname)
271
self._ensure_dir_exists(realname)
272
self.transport.put_bytes(realname, ref+"\n")
275
def remove_if_equals(self, name, old_ref):
276
"""Remove a refname only if it currently equals old_ref.
278
This method does not follow symbolic references. It can be used to
279
perform an atomic compare-and-delete operation.
281
:param name: The refname to delete.
282
:param old_ref: The old sha the refname must refer to, or None to delete
284
:return: True if the delete was successful, False otherwise.
286
self._check_refname(name)
289
self.transport.delete(name)
292
self._remove_packed_ref(name)
295
def get(self, name, default=None):
302
class TransportRepo(BaseRepo):
304
def __init__(self, transport, bare, refs_text=None):
305
self.transport = transport
308
self._controltransport = self.transport
310
self._controltransport = self.transport.clone('.git')
311
object_store = TransportObjectStore(
312
self._controltransport.clone(OBJECTDIR))
313
if refs_text is not None:
314
from dulwich.repo import InfoRefsContainer # dulwich >= 0.8.2
315
refs_container = InfoRefsContainer(StringIO(refs_text))
317
head = TransportRefsContainer(self._controltransport).read_loose_ref("HEAD")
321
refs_container._refs["HEAD"] = head
323
refs_container = TransportRefsContainer(self._controltransport)
324
super(TransportRepo, self).__init__(object_store,
327
def controldir(self):
328
return self._controltransport.local_abspath('.')
332
return self.transport.local_abspath('.')
334
def _determine_file_mode(self):
335
# Be consistent with bzr
336
if sys.platform == 'win32':
340
def get_named_file(self, path):
341
"""Get a file from the control dir with a specific name.
343
Although the filename should be interpreted as a filename relative to
344
the control dir in a disk-baked Repo, the object returned need not be
345
pointing to a file in that location.
347
:param path: The path to the file, relative to the control dir.
348
:return: An open file object, or None if the file does not exist.
351
return self._controltransport.get(path.lstrip('/'))
355
def _put_named_file(self, relpath, contents):
356
self._controltransport.put_bytes(relpath, contents)
358
def index_path(self):
359
"""Return the path to the index file."""
360
return self._controltransport.local_abspath(INDEX_FILENAME)
362
def open_index(self):
363
"""Open the index for this repository."""
364
from dulwich.index import Index
365
if not self.has_index():
366
raise NoIndexPresent()
367
return Index(self.index_path())
370
"""Check if an index is present."""
371
# Bare repos must never have index files; non-bare repos may have a
372
# missing index file, which is treated as empty.
375
def get_config(self):
376
from dulwich.config import ConfigFile
378
return ConfigFile.from_file(self._controltransport.get('config'))
382
def get_config_stack(self):
383
from dulwich.config import StackedConfig
385
p = self.get_config()
391
backends.extend(StackedConfig.default_backends())
392
return StackedConfig(backends, writable=writable)
395
return "<%s for %r>" % (self.__class__.__name__, self.transport)
398
def init(cls, transport, bare=False):
400
transport.mkdir(".git")
401
control_transport = transport.clone(".git")
403
control_transport = transport
404
for d in BASE_DIRECTORIES:
405
control_transport.mkdir("/".join(d))
406
control_transport.mkdir(OBJECTDIR)
407
TransportObjectStore.init(control_transport.clone(OBJECTDIR))
408
ret = cls(transport, bare)
409
ret.refs.set_symbolic_ref("HEAD", "refs/heads/master")
410
ret._init_files(bare)
414
class TransportObjectStore(PackBasedObjectStore):
415
"""Git-style object store that exists on disk."""
417
def __init__(self, transport):
418
"""Open an object store.
420
:param transport: Transport to open data from
422
super(TransportObjectStore, self).__init__()
423
self.transport = transport
424
self.pack_transport = self.transport.clone(PACKDIR)
425
self._alternates = None
428
return "%s(%r)" % (self.__class__.__name__, self.transport)
431
def alternates(self):
432
if self._alternates is not None:
433
return self._alternates
434
self._alternates = []
435
for path in self._read_alternate_paths():
437
t = _mod_transport.get_transport_from_path(path)
438
self._alternates.append(self.__class__(t))
439
return self._alternates
441
def _read_alternate_paths(self):
443
f = self.transport.get("info/alternates")
448
for l in f.read().splitlines():
460
# FIXME: Never invalidates.
461
if not self._pack_cache:
462
self._update_pack_cache()
463
return self._pack_cache.values()
465
def _update_pack_cache(self):
466
for pack in self._load_packs():
467
self._pack_cache[pack._basename] = pack
469
def _pack_names(self):
471
f = self.transport.get('info/packs')
473
return self.pack_transport.list_dir(".")
476
for line in f.read().splitlines():
479
(kind, name) = line.split(" ", 1)
485
def _load_packs(self):
487
for name in self._pack_names():
488
if name.startswith("pack-") and name.endswith(".pack"):
490
size = self.pack_transport.stat(name).st_size
491
except TransportNotPossible:
492
# FIXME: This reads the whole pack file at once
493
f = self.pack_transport.get(name)
495
pd = PackData(name, StringIO(contents), size=len(contents))
497
pd = PackData(name, self.pack_transport.get(name),
499
idxname = name.replace(".pack", ".idx")
500
idx = load_pack_index_file(idxname, self.pack_transport.get(idxname))
501
pack = Pack.from_objects(pd, idx)
502
pack._basename = idxname[:-5]
506
def _iter_loose_objects(self):
507
for base in self.transport.list_dir('.'):
510
for rest in self.transport.list_dir(base):
513
def _split_loose_object(self, sha):
514
return (sha[:2], sha[2:])
516
def _remove_loose_object(self, sha):
517
path = '%s/%s' % self._split_loose_object(sha)
518
self.transport.delete(path)
520
def _get_loose_object(self, sha):
521
path = '%s/%s' % self._split_loose_object(sha)
523
return ShaFile.from_file(self.transport.get(path))
527
def add_object(self, obj):
528
"""Add a single object to this object store.
530
:param obj: Object to add
532
(dir, file) = self._split_loose_object(obj.id)
534
self.transport.mkdir(dir)
537
path = "%s/%s" % (dir, file)
538
if self.transport.has(path):
539
return # Already there, no need to write again
540
self.transport.put_bytes(path, obj.as_legacy_object())
542
def move_in_pack(self, f):
543
"""Move a specific file containing a pack into the pack directory.
545
:note: The file should be on the same file system as the
548
:param path: Path to the pack file.
551
p = PackData(None, f, len(f.getvalue()))
552
entries = p.sorted_entries()
553
basename = "pack-%s" % iter_sha1(entry[0] for entry in entries)
555
self.pack_transport.put_file(basename + ".pack", f)
556
idxfile = self.pack_transport.open_write_stream(basename + ".idx")
558
write_pack_index_v2(idxfile, entries, p.get_stored_checksum())
561
idxfile = self.pack_transport.get(basename + ".idx")
562
idx = load_pack_index_file(basename+".idx", idxfile)
563
final_pack = Pack.from_objects(p, idx)
564
self._add_known_pack(basename, final_pack)
567
def add_thin_pack(self):
568
"""Add a new thin pack to this object store.
570
Thin packs are packs that contain deltas with parents that exist
573
from cStringIO import StringIO
576
if len(f.getvalue()) > 0:
577
return self.move_in_thin_pack(f)
582
def move_in_thin_pack(self, f):
583
"""Move a specific file containing a pack into the pack directory.
585
:note: The file should be on the same file system as the
588
:param path: Path to the pack file.
591
data = PackData.from_file(self.get_raw, f, len(f.getvalue()))
592
idx = MemoryPackIndex(data.sorted_entries(), data.get_stored_checksum())
593
p = Pack.from_objects(data, idx)
595
pack_sha = idx.objects_sha1()
597
datafile = self.pack_transport.open_write_stream(
598
"pack-%s.pack" % pack_sha)
600
entries, data_sum = write_pack_data(datafile, p.pack_tuples())
604
idxfile = self.pack_transport.open_write_stream(
605
"pack-%s.idx" % pack_sha)
607
write_pack_index_v2(idxfile, data.sorted_entries(), data_sum)
610
basename = "pack-%s" % pack_sha
611
final_pack = Pack(basename)
612
self._add_known_pack(basename, final_pack)
616
"""Add a new pack to this object store.
618
:return: Fileobject to write to and a commit function to
619
call when the pack is finished.
621
from cStringIO import StringIO
624
if len(f.getvalue()) > 0:
625
return self.move_in_pack(f)
630
return f, commit, abort
633
def init(cls, transport):
634
transport.mkdir('info')
635
transport.mkdir(PACKDIR)
636
return cls(transport)