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 (
58
read_packed_refs_with_peeled,
64
transport as _mod_transport,
66
from ...errors import (
67
AlreadyControlDirError,
74
class TransportRefsContainer(RefsContainer):
75
"""Refs container that reads refs from a transport."""
77
def __init__(self, transport, worktree_transport=None):
78
self.transport = transport
79
if worktree_transport is None:
80
worktree_transport = transport
81
self.worktree_transport = worktree_transport
82
self._packed_refs = None
83
self._peeled_refs = None
86
return "%s(%r)" % (self.__class__.__name__, self.transport)
88
def _ensure_dir_exists(self, path):
89
for n in range(path.count("/")):
90
dirname = "/".join(path.split("/")[:n+1])
92
self.transport.mkdir(dirname)
96
def subkeys(self, base):
99
iter_files = self.transport.clone(base).iter_files_recursive()
100
keys.update(("%s/%s" % (base, urllib.unquote(refname))).strip("/") for
101
refname in iter_files if check_ref_format("%s/%s" % (base, refname)))
102
except (TransportNotPossible, NoSuchFile):
104
for key in self.get_packed_refs():
105
if key.startswith(base):
106
keys.add(key[len(base):].strip("/"))
112
self.worktree_transport.get_bytes("HEAD")
118
iter_files = list(self.transport.clone("refs").iter_files_recursive())
119
for filename in iter_files:
120
refname = "refs/%s" % urllib.unquote(filename)
121
if check_ref_format(refname):
123
except (TransportNotPossible, NoSuchFile):
125
keys.update(self.get_packed_refs())
128
def get_packed_refs(self):
129
"""Get contents of the packed-refs file.
131
:return: Dictionary mapping ref names to SHA1s
133
:note: Will return an empty dictionary when no packed-refs file is
136
# TODO: invalidate the cache on repacking
137
if self._packed_refs is None:
138
# set both to empty because we want _peeled_refs to be
139
# None if and only if _packed_refs is also None.
140
self._packed_refs = {}
141
self._peeled_refs = {}
143
f = self.transport.get("packed-refs")
147
first_line = iter(f).next().rstrip()
148
if (first_line.startswith("# pack-refs") and " peeled" in
150
for sha, name, peeled in read_packed_refs_with_peeled(f):
151
self._packed_refs[name] = sha
153
self._peeled_refs[name] = peeled
156
for sha, name in read_packed_refs(f):
157
self._packed_refs[name] = sha
160
return self._packed_refs
162
def get_peeled(self, name):
163
"""Return the cached peeled value of a ref, if available.
165
:param name: Name of the ref to peel
166
:return: The peeled value of the ref. If the ref is known not point to a
167
tag, this will be the SHA the ref refers to. If the ref may point to
168
a tag, but no cached information is available, None is returned.
170
self.get_packed_refs()
171
if self._peeled_refs is None or name not in self._packed_refs:
172
# No cache: no peeled refs were read, or this ref is loose
174
if name in self._peeled_refs:
175
return self._peeled_refs[name]
180
def read_loose_ref(self, name):
181
"""Read a reference file and return its contents.
183
If the reference file a symbolic reference, only read the first line of
184
the file. Otherwise, only read the first 40 bytes.
186
:param name: the refname to read, relative to refpath
187
:return: The contents of the ref file, or None if the file does not
189
:raises IOError: if any other error occurs
192
transport = self.worktree_transport
194
transport = self.transport
196
f = transport.get(name)
199
f = StringIO(f.read())
201
header = f.read(len(SYMREF))
203
# Read only the first line
204
return header + iter(f).next().rstrip("\r\n")
206
# Read only the first 40 bytes
207
return header + f.read(40-len(SYMREF))
211
def _remove_packed_ref(self, name):
212
if self._packed_refs is None:
214
# reread cached refs from disk, while holding the lock
216
self._packed_refs = None
217
self.get_packed_refs()
219
if name not in self._packed_refs:
222
del self._packed_refs[name]
223
if name in self._peeled_refs:
224
del self._peeled_refs[name]
225
f = self.transport.open_write_stream("packed-refs")
227
write_packed_refs(f, self._packed_refs, self._peeled_refs)
231
def set_symbolic_ref(self, name, other):
232
"""Make a ref point at another ref.
234
:param name: Name of the ref to set
235
:param other: Name of the ref to point at
237
self._check_refname(name)
238
self._check_refname(other)
240
transport = self.transport
241
self._ensure_dir_exists(name)
243
transport = self.worktree_transport
244
transport.put_bytes(name, SYMREF + other + '\n')
246
def set_if_equals(self, name, old_ref, new_ref):
247
"""Set a refname to new_ref only if it currently equals old_ref.
249
This method follows all symbolic references, and can be used to perform
250
an atomic compare-and-swap operation.
252
:param name: The refname to set.
253
:param old_ref: The old sha the refname must refer to, or None to set
255
:param new_ref: The new sha the refname will refer to.
256
:return: True if the set was successful, False otherwise.
259
realnames, _ = self.follow(name)
260
realname = realnames[-1]
261
except (KeyError, IndexError):
263
if realname == b'HEAD':
264
transport = self.worktree_transport
266
transport = self.transport
267
self._ensure_dir_exists(realname)
268
transport.put_bytes(realname, new_ref+"\n")
271
def add_if_new(self, name, ref):
272
"""Add a new reference only if it does not already exist.
274
This method follows symrefs, and only ensures that the last ref in the
275
chain does not exist.
277
:param name: The refname to set.
278
:param ref: The new sha the refname will refer to.
279
:return: True if the add was successful, False otherwise.
282
realnames, contents = self.follow(name)
283
if contents is not None:
285
realname = realnames[-1]
286
except (KeyError, IndexError):
288
self._check_refname(realname)
289
if realname == b'HEAD':
290
transport = self.worktree_transport
292
transport = self.transport
293
self._ensure_dir_exists(realname)
294
transport.put_bytes(realname, ref+"\n")
297
def remove_if_equals(self, name, old_ref):
298
"""Remove a refname only if it currently equals old_ref.
300
This method does not follow symbolic references. It can be used to
301
perform an atomic compare-and-delete operation.
303
:param name: The refname to delete.
304
:param old_ref: The old sha the refname must refer to, or None to delete
306
:return: True if the delete was successful, False otherwise.
308
self._check_refname(name)
311
transport = self.worktree_transport
313
transport = self.transport
315
transport.delete(name)
318
self._remove_packed_ref(name)
321
def get(self, name, default=None):
328
class TransportRepo(BaseRepo):
330
def __init__(self, transport, bare, refs_text=None):
331
self.transport = transport
334
self._controltransport = self.transport
336
self._controltransport = self.transport.clone('.git')
337
commondir = self.get_named_file(COMMONDIR)
338
if commondir is not None:
340
commondir = os.path.join(
342
commondir.read().rstrip(b"\r\n").decode(
343
sys.getfilesystemencoding()))
344
self._commontransport = \
345
_mod_transport.get_transport_from_path(commondir)
347
self._commontransport = self._controltransport
348
object_store = TransportObjectStore(
349
self._commontransport.clone(OBJECTDIR))
350
if refs_text is not None:
351
refs_container = InfoRefsContainer(StringIO(refs_text))
353
head = TransportRefsContainer(self._commontransport).read_loose_ref("HEAD")
357
refs_container._refs["HEAD"] = head
359
refs_container = TransportRefsContainer(
360
self._commontransport, self._controltransport)
361
super(TransportRepo, self).__init__(object_store,
364
def controldir(self):
365
return self._controltransport.local_abspath('.')
369
return self.transport.local_abspath('.')
371
def _determine_file_mode(self):
372
# Be consistent with bzr
373
if sys.platform == 'win32':
377
def get_named_file(self, path):
378
"""Get a file from the control dir with a specific name.
380
Although the filename should be interpreted as a filename relative to
381
the control dir in a disk-baked Repo, the object returned need not be
382
pointing to a file in that location.
384
:param path: The path to the file, relative to the control dir.
385
:return: An open file object, or None if the file does not exist.
388
return self._controltransport.get(path.lstrip('/'))
392
def _put_named_file(self, relpath, contents):
393
self._controltransport.put_bytes(relpath, contents)
395
def index_path(self):
396
"""Return the path to the index file."""
397
return self._controltransport.local_abspath(INDEX_FILENAME)
399
def open_index(self):
400
"""Open the index for this repository."""
401
from dulwich.index import Index
402
if not self.has_index():
403
raise NoIndexPresent()
404
return Index(self.index_path())
407
"""Check if an index is present."""
408
# Bare repos must never have index files; non-bare repos may have a
409
# missing index file, which is treated as empty.
412
def get_config(self):
413
from dulwich.config import ConfigFile
415
return ConfigFile.from_file(self._controltransport.get('config'))
419
def get_config_stack(self):
420
from dulwich.config import StackedConfig
422
p = self.get_config()
428
backends.extend(StackedConfig.default_backends())
429
return StackedConfig(backends, writable=writable)
432
return "<%s for %r>" % (self.__class__.__name__, self.transport)
435
def init(cls, transport, bare=False):
438
transport.mkdir(".git")
440
raise AlreadyControlDirError(transport.base)
441
control_transport = transport.clone(".git")
443
control_transport = transport
444
for d in BASE_DIRECTORIES:
446
control_transport.mkdir("/".join(d))
450
control_transport.mkdir(OBJECTDIR)
452
raise AlreadyControlDirError(transport.base)
453
TransportObjectStore.init(control_transport.clone(OBJECTDIR))
454
ret = cls(transport, bare)
455
ret.refs.set_symbolic_ref("HEAD", "refs/heads/master")
456
ret._init_files(bare)
460
class TransportObjectStore(PackBasedObjectStore):
461
"""Git-style object store that exists on disk."""
463
def __init__(self, transport):
464
"""Open an object store.
466
:param transport: Transport to open data from
468
super(TransportObjectStore, self).__init__()
469
self.transport = transport
470
self.pack_transport = self.transport.clone(PACKDIR)
471
self._alternates = None
473
def __eq__(self, other):
474
if not isinstance(other, TransportObjectStore):
476
return self.transport == other.transport
479
return "%s(%r)" % (self.__class__.__name__, self.transport)
482
def alternates(self):
483
if self._alternates is not None:
484
return self._alternates
485
self._alternates = []
486
for path in self._read_alternate_paths():
488
t = _mod_transport.get_transport_from_path(path)
489
self._alternates.append(self.__class__(t))
490
return self._alternates
492
def _read_alternate_paths(self):
494
f = self.transport.get("info/alternates")
499
for l in f.read().splitlines():
511
# FIXME: Never invalidates.
512
if not self._pack_cache:
513
self._update_pack_cache()
514
return self._pack_cache.values()
516
def _update_pack_cache(self):
517
for pack in self._load_packs():
518
self._pack_cache[pack._basename] = pack
520
def _pack_names(self):
522
f = self.transport.get('info/packs')
524
return self.pack_transport.list_dir(".")
527
for line in f.read().splitlines():
530
(kind, name) = line.split(" ", 1)
536
def _remove_pack(self, pack):
537
self.pack_transport.delete(os.path.basename(pack.index.path))
538
self.pack_transport.delete(pack.data.filename)
540
def _load_packs(self):
542
for name in self._pack_names():
543
if name.startswith("pack-") and name.endswith(".pack"):
545
size = self.pack_transport.stat(name).st_size
546
except TransportNotPossible:
547
# FIXME: This reads the whole pack file at once
548
f = self.pack_transport.get(name)
550
pd = PackData(name, StringIO(contents), size=len(contents))
552
pd = PackData(name, self.pack_transport.get(name),
554
idxname = name.replace(".pack", ".idx")
555
idx = load_pack_index_file(idxname, self.pack_transport.get(idxname))
556
pack = Pack.from_objects(pd, idx)
557
pack._basename = idxname[:-4]
561
def _iter_loose_objects(self):
562
for base in self.transport.list_dir('.'):
565
for rest in self.transport.list_dir(base):
568
def _split_loose_object(self, sha):
569
return (sha[:2], sha[2:])
571
def _remove_loose_object(self, sha):
572
path = '%s/%s' % self._split_loose_object(sha)
573
self.transport.delete(path)
575
def _get_loose_object(self, sha):
576
path = '%s/%s' % self._split_loose_object(sha)
578
return ShaFile.from_file(self.transport.get(path))
582
def add_object(self, obj):
583
"""Add a single object to this object store.
585
:param obj: Object to add
587
(dir, file) = self._split_loose_object(obj.id)
589
self.transport.mkdir(dir)
592
path = "%s/%s" % (dir, file)
593
if self.transport.has(path):
594
return # Already there, no need to write again
595
self.transport.put_bytes(path, obj.as_legacy_object())
597
def move_in_pack(self, f):
598
"""Move a specific file containing a pack into the pack directory.
600
:note: The file should be on the same file system as the
603
:param path: Path to the pack file.
606
p = PackData("", f, len(f.getvalue()))
607
entries = p.sorted_entries()
608
basename = "pack-%s" % iter_sha1(entry[0] for entry in entries)
609
p._filename = basename + ".pack"
611
self.pack_transport.put_file(basename + ".pack", f)
612
idxfile = self.pack_transport.open_write_stream(basename + ".idx")
614
write_pack_index_v2(idxfile, entries, p.get_stored_checksum())
617
idxfile = self.pack_transport.get(basename + ".idx")
618
idx = load_pack_index_file(basename+".idx", idxfile)
619
final_pack = Pack.from_objects(p, idx)
620
final_pack._basename = basename
621
self._add_known_pack(basename, final_pack)
624
def add_thin_pack(self):
625
"""Add a new thin pack to this object store.
627
Thin packs are packs that contain deltas with parents that exist
630
from cStringIO import StringIO
633
if len(f.getvalue()) > 0:
634
return self.move_in_thin_pack(f)
639
def move_in_thin_pack(self, f):
640
"""Move a specific file containing a pack into the pack directory.
642
:note: The file should be on the same file system as the
645
:param path: Path to the pack file.
648
data = PackData.from_file(self.get_raw, f, len(f.getvalue()))
649
idx = MemoryPackIndex(data.sorted_entries(), data.get_stored_checksum())
650
p = Pack.from_objects(data, idx)
652
pack_sha = idx.objects_sha1()
654
datafile = self.pack_transport.open_write_stream(
655
"pack-%s.pack" % pack_sha)
657
entries, data_sum = write_pack_data(datafile, p.pack_tuples())
661
idxfile = self.pack_transport.open_write_stream(
662
"pack-%s.idx" % pack_sha)
664
write_pack_index_v2(idxfile, data.sorted_entries(), data_sum)
667
basename = "pack-%s" % pack_sha
668
final_pack = Pack(basename)
669
self._add_known_pack(basename, final_pack)
673
"""Add a new pack to this object store.
675
:return: Fileobject to write to and a commit function to
676
call when the pack is finished.
678
from cStringIO import StringIO
681
if len(f.getvalue()) > 0:
682
return self.move_in_pack(f)
687
return f, commit, abort
690
def init(cls, transport):
692
transport.mkdir('info')
696
transport.mkdir(PACKDIR)
699
return cls(transport)