1
# Copyright (C) 2010 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 cStringIO import StringIO
23
from dulwich.errors import (
27
from dulwich.objects import (
30
from dulwich.object_store import (
34
from dulwich.pack import (
43
from dulwich.repo import (
52
read_packed_refs_with_peeled,
58
transport as _mod_transport,
60
from bzrlib.errors import (
67
class TransportRefsContainer(RefsContainer):
68
"""Refs container that reads refs from a transport."""
70
def __init__(self, transport):
71
self.transport = transport
72
self._packed_refs = None
73
self._peeled_refs = None
76
return "%s(%r)" % (self.__class__.__name__, self.transport)
78
def _ensure_dir_exists(self, path):
79
for n in range(path.count("/")):
80
dirname = "/".join(path.split("/")[:n+1])
82
self.transport.mkdir(dirname)
86
def subkeys(self, base):
89
iter_files = self.transport.clone(base).iter_files_recursive()
90
keys.update(("%s/%s" % (base, refname)).strip("/") for
91
refname in iter_files if check_ref_format("%s/%s" % (base, refname)))
92
except (TransportNotPossible, NoSuchFile):
94
for key in self.get_packed_refs():
95
if key.startswith(base):
96
keys.add(key[len(base):].strip("/"))
101
if self.transport.has("HEAD"):
104
iter_files = list(self.transport.clone("refs").iter_files_recursive())
105
for filename in iter_files:
106
refname = "refs/%s" % filename
107
if check_ref_format(refname):
109
except (TransportNotPossible, NoSuchFile):
111
keys.update(self.get_packed_refs())
114
def get_packed_refs(self):
115
"""Get contents of the packed-refs file.
117
:return: Dictionary mapping ref names to SHA1s
119
:note: Will return an empty dictionary when no packed-refs file is
122
# TODO: invalidate the cache on repacking
123
if self._packed_refs is None:
124
# set both to empty because we want _peeled_refs to be
125
# None if and only if _packed_refs is also None.
126
self._packed_refs = {}
127
self._peeled_refs = {}
129
f = self.transport.get("packed-refs")
133
first_line = iter(f).next().rstrip()
134
if (first_line.startswith("# pack-refs") and " peeled" in
136
for sha, name, peeled in read_packed_refs_with_peeled(f):
137
self._packed_refs[name] = sha
139
self._peeled_refs[name] = peeled
142
for sha, name in read_packed_refs(f):
143
self._packed_refs[name] = sha
146
return self._packed_refs
148
def get_peeled(self, name):
149
"""Return the cached peeled value of a ref, if available.
151
:param name: Name of the ref to peel
152
:return: The peeled value of the ref. If the ref is known not point to a
153
tag, this will be the SHA the ref refers to. If the ref may point to
154
a tag, but no cached information is available, None is returned.
156
self.get_packed_refs()
157
if self._peeled_refs is None or name not in self._packed_refs:
158
# No cache: no peeled refs were read, or this ref is loose
160
if name in self._peeled_refs:
161
return self._peeled_refs[name]
166
def read_loose_ref(self, name):
167
"""Read a reference file and return its contents.
169
If the reference file a symbolic reference, only read the first line of
170
the file. Otherwise, only read the first 40 bytes.
172
:param name: the refname to read, relative to refpath
173
:return: The contents of the ref file, or None if the file does not
175
:raises IOError: if any other error occurs
178
f = self.transport.get(name)
182
header = f.read(len(SYMREF))
184
# Read only the first line
185
return header + iter(f).next().rstrip("\r\n")
187
# Read only the first 40 bytes
188
return header + f.read(40-len(SYMREF))
192
def _remove_packed_ref(self, name):
193
if self._packed_refs is None:
195
# reread cached refs from disk, while holding the lock
197
self._packed_refs = None
198
self.get_packed_refs()
200
if name not in self._packed_refs:
203
del self._packed_refs[name]
204
if name in self._peeled_refs:
205
del self._peeled_refs[name]
206
f = self.transport.open_write_stream("packed-refs")
208
write_packed_refs(f, self._packed_refs, self._peeled_refs)
212
def set_symbolic_ref(self, name, other):
213
"""Make a ref point at another ref.
215
:param name: Name of the ref to set
216
:param other: Name of the ref to point at
218
self._check_refname(name)
219
self._check_refname(other)
220
self._ensure_dir_exists(name)
221
self.transport.put_bytes(name, SYMREF + other + '\n')
223
def set_if_equals(self, name, old_ref, new_ref):
224
"""Set a refname to new_ref only if it currently equals old_ref.
226
This method follows all symbolic references, and can be used to perform
227
an atomic compare-and-swap operation.
229
:param name: The refname to set.
230
:param old_ref: The old sha the refname must refer to, or None to set
232
:param new_ref: The new sha the refname will refer to.
233
:return: True if the set was successful, False otherwise.
236
realname, _ = self._follow(name)
239
self._ensure_dir_exists(realname)
240
self.transport.put_bytes(realname, new_ref+"\n")
243
def add_if_new(self, name, ref):
244
"""Add a new reference only if it does not already exist.
246
This method follows symrefs, and only ensures that the last ref in the
247
chain does not exist.
249
:param name: The refname to set.
250
:param ref: The new sha the refname will refer to.
251
:return: True if the add was successful, False otherwise.
254
realname, contents = self._follow(name)
255
if contents is not None:
259
self._check_refname(realname)
260
self._ensure_dir_exists(realname)
261
self.transport.put_bytes(realname, ref+"\n")
264
def remove_if_equals(self, name, old_ref):
265
"""Remove a refname only if it currently equals old_ref.
267
This method does not follow symbolic references. It can be used to
268
perform an atomic compare-and-delete operation.
270
:param name: The refname to delete.
271
:param old_ref: The old sha the refname must refer to, or None to delete
273
:return: True if the delete was successful, False otherwise.
275
self._check_refname(name)
278
self.transport.delete(name)
281
self._remove_packed_ref(name)
285
class TransportRepo(BaseRepo):
287
def __init__(self, transport):
288
self.transport = transport
290
if self.transport.has(".git/%s" % OBJECTDIR):
292
self._controltransport = self.transport.clone('.git')
293
elif self.transport.has_any(["info/refs", OBJECTDIR, REFSDIR]):
295
self._controltransport = self.transport
297
raise NotGitRepository(self.transport)
299
raise NotGitRepository(self.transport)
300
object_store = TransportObjectStore(
301
self._controltransport.clone(OBJECTDIR))
302
super(TransportRepo, self).__init__(object_store,
303
TransportRefsContainer(self._controltransport))
305
def get_named_file(self, path):
306
"""Get a file from the control dir with a specific name.
308
Although the filename should be interpreted as a filename relative to
309
the control dir in a disk-baked Repo, the object returned need not be
310
pointing to a file in that location.
312
:param path: The path to the file, relative to the control dir.
313
:return: An open file object, or None if the file does not exist.
316
return self._controltransport.get(path.lstrip('/'))
320
def _put_named_file(self, relpath, contents):
321
self._controltransport.put_bytes(relpath, contents)
323
def index_path(self):
324
"""Return the path to the index file."""
325
return self._controltransport.local_abspath(INDEX_FILENAME)
327
def open_index(self):
328
"""Open the index for this repository."""
329
from dulwich.index import Index
330
if not self.has_index():
331
raise NoIndexPresent()
332
return Index(self.index_path())
335
"""Check if an index is present."""
336
# Bare repos must never have index files; non-bare repos may have a
337
# missing index file, which is treated as empty.
341
return "<%s for %r>" % (self.__class__.__name__, self.transport)
344
def init(cls, transport, bare=False):
346
transport.mkdir(".git")
347
control_transport = transport.clone(".git")
349
control_transport = transport
350
for d in BASE_DIRECTORIES:
351
control_transport.mkdir("/".join(d))
352
control_transport.mkdir(OBJECTDIR)
353
TransportObjectStore.init(control_transport.clone(OBJECTDIR))
355
ret.refs.set_symbolic_ref("HEAD", "refs/heads/master")
356
ret._init_files(bare)
360
class TransportObjectStore(PackBasedObjectStore):
361
"""Git-style object store that exists on disk."""
363
def __init__(self, transport):
364
"""Open an object store.
366
:param transport: Transport to open data from
368
super(TransportObjectStore, self).__init__()
369
self.transport = transport
370
self.pack_transport = self.transport.clone(PACKDIR)
371
self._alternates = None
374
return "%s(%r)" % (self.__class__.__name__, self.transport)
376
def _pack_cache_stale(self):
380
def alternates(self):
381
if self._alternates is not None:
382
return self._alternates
383
self._alternates = []
384
for path in self._read_alternate_paths():
386
t = _mod_transport.get_transport_from_path(path)
387
self._alternates.append(self.__class__(t))
388
return self._alternates
390
def _read_alternate_paths(self):
392
f = self.transport.get("info/alternates")
397
for l in f.readlines():
408
def _pack_names(self):
410
f = self.transport.get('info/packs')
412
return self.pack_transport.list_dir(".")
415
for line in f.readlines():
416
line = line.rstrip("\n")
419
(kind, name) = line.split(" ", 1)
425
def _load_packs(self):
427
for name in self._pack_names():
428
if name.startswith("pack-") and name.endswith(".pack"):
430
size = self.pack_transport.stat(name).st_size
431
except TransportNotPossible:
432
# FIXME: This reads the whole pack file at once
433
f = self.pack_transport.get(name)
435
pd = PackData(name, StringIO(contents), size=len(contents))
437
pd = PackData(name, self.pack_transport.get(name),
439
idxname = name.replace(".pack", ".idx")
440
idx = load_pack_index_file(idxname, self.pack_transport.get(idxname))
441
pack = Pack.from_objects(pd, idx)
445
def _iter_loose_objects(self):
446
for base in self.transport.list_dir('.'):
449
for rest in self.transport.list_dir(base):
452
def _split_loose_object(self, sha):
453
return (sha[:2], sha[2:])
455
def _remove_loose_object(self, sha):
456
path = '%s/%s' % self._split_loose_object(sha)
457
self.transport.delete(path)
459
def _get_loose_object(self, sha):
460
path = '%s/%s' % self._split_loose_object(sha)
462
return ShaFile.from_file(self.transport.get(path))
466
def add_object(self, obj):
467
"""Add a single object to this object store.
469
:param obj: Object to add
471
(dir, file) = self._split_loose_object(obj.id)
473
self.transport.mkdir(dir)
476
path = "%s/%s" % (dir, file)
477
if self.transport.has(path):
478
return # Already there, no need to write again
479
self.transport.put_bytes(path, obj.as_legacy_object())
481
def move_in_pack(self, f):
482
"""Move a specific file containing a pack into the pack directory.
484
:note: The file should be on the same file system as the
487
:param path: Path to the pack file.
490
p = PackData(None, f, len(f.getvalue()))
491
entries = p.sorted_entries()
492
basename = "pack-%s" % iter_sha1(entry[0] for entry in entries)
494
self.pack_transport.put_file(basename + ".pack", f)
495
idxfile = self.pack_transport.open_write_stream(basename + ".idx")
497
write_pack_index_v2(idxfile, entries, p.get_stored_checksum())
500
idxfile = self.pack_transport.get(basename + ".idx")
501
idx = load_pack_index_file(basename+".idx", idxfile)
502
final_pack = Pack.from_objects(p, idx)
503
self._add_known_pack(final_pack)
506
def add_thin_pack(self):
507
"""Add a new thin pack to this object store.
509
Thin packs are packs that contain deltas with parents that exist
512
from cStringIO import StringIO
515
if len(f.getvalue()) > 0:
516
return self.move_in_thin_pack(f)
521
def move_in_thin_pack(self, f):
522
"""Move a specific file containing a pack into the pack directory.
524
:note: The file should be on the same file system as the
527
:param path: Path to the pack file.
530
data = PackData.from_file(self.get_raw, f, len(f.getvalue()))
531
idx = MemoryPackIndex(data.sorted_entries(), data.get_stored_checksum())
532
p = Pack.from_objects(data, idx)
534
pack_sha = idx.objects_sha1()
536
datafile = self.pack_transport.open_write_stream(
537
"pack-%s.pack" % pack_sha)
539
entries, data_sum = write_pack_data(datafile, p.pack_tuples())
543
idxfile = self.pack_transport.open_write_stream(
544
"pack-%s.idx" % pack_sha)
546
write_pack_index_v2(idxfile, data.sorted_entries(), data_sum)
549
final_pack = Pack("pack-%s" % pack_sha)
550
self._add_known_pack(final_pack)
554
"""Add a new pack to this object store.
556
:return: Fileobject to write to and a commit function to
557
call when the pack is finished.
559
from cStringIO import StringIO
562
if len(f.getvalue()) > 0:
563
return self.move_in_pack(f)
569
def init(cls, transport):
570
transport.mkdir('info')
571
transport.mkdir(PACKDIR)
572
return cls(transport)