1
# Copyright (C) 2010-2018 Jelmer Vernooij <jelmer@jelmer.uk>
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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 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 (
60
read_packed_refs_with_peeled,
66
transport as _mod_transport,
68
from ...errors import (
69
AlreadyControlDirError,
78
class TransportRefsContainer(RefsContainer):
79
"""Refs container that reads refs from a transport."""
81
def __init__(self, transport, worktree_transport=None):
82
self.transport = transport
83
if worktree_transport is None:
84
worktree_transport = transport
85
self.worktree_transport = worktree_transport
86
self._packed_refs = None
87
self._peeled_refs = None
90
return "%s(%r)" % (self.__class__.__name__, self.transport)
92
def _ensure_dir_exists(self, path):
93
for n in range(path.count("/")):
94
dirname = "/".join(path.split("/")[:n+1])
96
self.transport.mkdir(dirname)
100
def subkeys(self, base):
101
"""Refs present in this container under a base.
103
:param base: The base to return refs under.
104
:return: A set of valid refs in this container under the base; the base
105
prefix is stripped from the ref names returned.
108
base_len = len(base) + 1
109
for refname in self.allkeys():
110
if refname.startswith(base):
111
keys.add(refname[base_len:])
117
self.worktree_transport.get_bytes("HEAD")
123
iter_files = list(self.transport.clone("refs").iter_files_recursive())
124
for filename in iter_files:
125
refname = "refs/%s" % urllib.unquote(filename)
126
if check_ref_format(refname):
128
except (TransportNotPossible, NoSuchFile):
130
keys.update(self.get_packed_refs())
133
def get_packed_refs(self):
134
"""Get contents of the packed-refs file.
136
:return: Dictionary mapping ref names to SHA1s
138
:note: Will return an empty dictionary when no packed-refs file is
141
# TODO: invalidate the cache on repacking
142
if self._packed_refs is None:
143
# set both to empty because we want _peeled_refs to be
144
# None if and only if _packed_refs is also None.
145
self._packed_refs = {}
146
self._peeled_refs = {}
148
f = self.transport.get("packed-refs")
152
first_line = iter(f).next().rstrip()
153
if (first_line.startswith("# pack-refs") and " peeled" in
155
for sha, name, peeled in read_packed_refs_with_peeled(f):
156
self._packed_refs[name] = sha
158
self._peeled_refs[name] = peeled
161
for sha, name in read_packed_refs(f):
162
self._packed_refs[name] = sha
165
return self._packed_refs
167
def get_peeled(self, name):
168
"""Return the cached peeled value of a ref, if available.
170
:param name: Name of the ref to peel
171
:return: The peeled value of the ref. If the ref is known not point to a
172
tag, this will be the SHA the ref refers to. If the ref may point to
173
a tag, but no cached information is available, None is returned.
175
self.get_packed_refs()
176
if self._peeled_refs is None or name not in self._packed_refs:
177
# No cache: no peeled refs were read, or this ref is loose
179
if name in self._peeled_refs:
180
return self._peeled_refs[name]
185
def read_loose_ref(self, name):
186
"""Read a reference file and return its contents.
188
If the reference file a symbolic reference, only read the first line of
189
the file. Otherwise, only read the first 40 bytes.
191
:param name: the refname to read, relative to refpath
192
:return: The contents of the ref file, or None if the file does not
194
:raises IOError: if any other error occurs
197
transport = self.worktree_transport
199
transport = self.transport
201
f = transport.get(name)
204
f = StringIO(f.read())
206
header = f.read(len(SYMREF))
208
# Read only the first line
209
return header + iter(f).next().rstrip("\r\n")
211
# Read only the first 40 bytes
212
return header + f.read(40-len(SYMREF))
216
def _remove_packed_ref(self, name):
217
if self._packed_refs is None:
219
# reread cached refs from disk, while holding the lock
221
self._packed_refs = None
222
self.get_packed_refs()
224
if name not in self._packed_refs:
227
del self._packed_refs[name]
228
if name in self._peeled_refs:
229
del self._peeled_refs[name]
230
f = self.transport.open_write_stream("packed-refs")
232
write_packed_refs(f, self._packed_refs, self._peeled_refs)
236
def set_symbolic_ref(self, name, other):
237
"""Make a ref point at another ref.
239
:param name: Name of the ref to set
240
:param other: Name of the ref to point at
242
self._check_refname(name)
243
self._check_refname(other)
245
transport = self.transport
246
self._ensure_dir_exists(name)
248
transport = self.worktree_transport
249
transport.put_bytes(name, SYMREF + other + '\n')
251
def set_if_equals(self, name, old_ref, new_ref):
252
"""Set a refname to new_ref only if it currently equals old_ref.
254
This method follows all symbolic references, and can be used to perform
255
an atomic compare-and-swap operation.
257
:param name: The refname to set.
258
:param old_ref: The old sha the refname must refer to, or None to set
260
:param new_ref: The new sha the refname will refer to.
261
:return: True if the set was successful, False otherwise.
264
realnames, _ = self.follow(name)
265
realname = realnames[-1]
266
except (KeyError, IndexError):
268
if realname == b'HEAD':
269
transport = self.worktree_transport
271
transport = self.transport
272
self._ensure_dir_exists(realname)
273
transport.put_bytes(realname, new_ref+"\n")
276
def add_if_new(self, name, ref):
277
"""Add a new reference only if it does not already exist.
279
This method follows symrefs, and only ensures that the last ref in the
280
chain does not exist.
282
:param name: The refname to set.
283
:param ref: The new sha the refname will refer to.
284
:return: True if the add was successful, False otherwise.
287
realnames, contents = self.follow(name)
288
if contents is not None:
290
realname = realnames[-1]
291
except (KeyError, IndexError):
293
self._check_refname(realname)
294
if realname == b'HEAD':
295
transport = self.worktree_transport
297
transport = self.transport
298
self._ensure_dir_exists(realname)
299
transport.put_bytes(realname, ref+"\n")
302
def remove_if_equals(self, name, old_ref):
303
"""Remove a refname only if it currently equals old_ref.
305
This method does not follow symbolic references. It can be used to
306
perform an atomic compare-and-delete operation.
308
:param name: The refname to delete.
309
:param old_ref: The old sha the refname must refer to, or None to delete
311
:return: True if the delete was successful, False otherwise.
313
self._check_refname(name)
316
transport = self.worktree_transport
318
transport = self.transport
320
transport.delete(name)
323
self._remove_packed_ref(name)
326
def get(self, name, default=None):
332
def lock_ref(self, name):
334
transport = self.worktree_transport
336
transport = self.transport
337
self._ensure_dir_exists(name)
338
lockname = name + ".lock"
340
return transport.lock_write(lockname)
341
except TransportNotPossible:
342
# better than not locking at all, I guess?
343
if transport.has(lockname):
344
raise LockError(lockname + " exists")
345
transport.put_bytes(lockname, "Locked by brz-git")
346
from ...lock import LogicalLockResult
347
return LogicalLockResult(lambda: transport.delete(lockname))
350
class TransportRepo(BaseRepo):
352
def __init__(self, transport, bare, refs_text=None):
353
self.transport = transport
356
with transport.get(CONTROLDIR) as f:
357
path = read_gitfile(f)
358
except (ReadError, NoSuchFile):
360
self._controltransport = self.transport
362
self._controltransport = self.transport.clone('.git')
364
self._controltransport = self.transport.clone(path)
365
commondir = self.get_named_file(COMMONDIR)
366
if commondir is not None:
368
commondir = os.path.join(
370
commondir.read().rstrip(b"\r\n").decode(
371
sys.getfilesystemencoding()))
372
self._commontransport = \
373
_mod_transport.get_transport_from_path(commondir)
375
self._commontransport = self._controltransport
376
object_store = TransportObjectStore(
377
self._commontransport.clone(OBJECTDIR))
378
if refs_text is not None:
379
refs_container = InfoRefsContainer(StringIO(refs_text))
381
head = TransportRefsContainer(self._commontransport).read_loose_ref("HEAD")
385
refs_container._refs["HEAD"] = head
387
refs_container = TransportRefsContainer(
388
self._commontransport, self._controltransport)
389
super(TransportRepo, self).__init__(object_store,
392
def controldir(self):
393
return self._controltransport.local_abspath('.')
397
return self.transport.local_abspath('.')
399
def _determine_file_mode(self):
400
# Be consistent with bzr
401
if sys.platform == 'win32':
405
def get_named_file(self, path):
406
"""Get a file from the control dir with a specific name.
408
Although the filename should be interpreted as a filename relative to
409
the control dir in a disk-baked Repo, the object returned need not be
410
pointing to a file in that location.
412
:param path: The path to the file, relative to the control dir.
413
:return: An open file object, or None if the file does not exist.
416
return self._controltransport.get(path.lstrip('/'))
420
def _put_named_file(self, relpath, contents):
421
self._controltransport.put_bytes(relpath, contents)
423
def index_path(self):
424
"""Return the path to the index file."""
425
return self._controltransport.local_abspath(INDEX_FILENAME)
427
def open_index(self):
428
"""Open the index for this repository."""
429
from dulwich.index import Index
430
if not self.has_index():
431
raise NoIndexPresent()
432
return Index(self.index_path())
435
"""Check if an index is present."""
436
# Bare repos must never have index files; non-bare repos may have a
437
# missing index file, which is treated as empty.
440
def get_config(self):
441
from dulwich.config import ConfigFile
443
return ConfigFile.from_file(self._controltransport.get('config'))
447
def get_config_stack(self):
448
from dulwich.config import StackedConfig
450
p = self.get_config()
456
backends.extend(StackedConfig.default_backends())
457
return StackedConfig(backends, writable=writable)
460
return "<%s for %r>" % (self.__class__.__name__, self.transport)
463
def init(cls, transport, bare=False):
466
transport.mkdir(".git")
468
raise AlreadyControlDirError(transport.base)
469
control_transport = transport.clone(".git")
471
control_transport = transport
472
for d in BASE_DIRECTORIES:
474
control_transport.mkdir("/".join(d))
478
control_transport.mkdir(OBJECTDIR)
480
raise AlreadyControlDirError(transport.base)
481
TransportObjectStore.init(control_transport.clone(OBJECTDIR))
482
ret = cls(transport, bare)
483
ret.refs.set_symbolic_ref("HEAD", "refs/heads/master")
484
ret._init_files(bare)
488
class TransportObjectStore(PackBasedObjectStore):
489
"""Git-style object store that exists on disk."""
491
def __init__(self, transport):
492
"""Open an object store.
494
:param transport: Transport to open data from
496
super(TransportObjectStore, self).__init__()
497
self.transport = transport
498
self.pack_transport = self.transport.clone(PACKDIR)
499
self._alternates = None
501
def __eq__(self, other):
502
if not isinstance(other, TransportObjectStore):
504
return self.transport == other.transport
507
return "%s(%r)" % (self.__class__.__name__, self.transport)
510
def alternates(self):
511
if self._alternates is not None:
512
return self._alternates
513
self._alternates = []
514
for path in self._read_alternate_paths():
516
t = _mod_transport.get_transport_from_path(path)
517
self._alternates.append(self.__class__(t))
518
return self._alternates
520
def _read_alternate_paths(self):
522
f = self.transport.get("info/alternates")
527
for l in f.read().splitlines():
539
# FIXME: Never invalidates.
540
if not self._pack_cache:
541
self._update_pack_cache()
542
return self._pack_cache.values()
544
def _update_pack_cache(self):
545
for pack in self._load_packs():
546
self._pack_cache[pack._basename] = pack
548
def _pack_names(self):
550
f = self.transport.get('info/packs')
552
return self.pack_transport.list_dir(".")
555
for line in f.read().splitlines():
558
(kind, name) = line.split(" ", 1)
564
def _remove_pack(self, pack):
565
self.pack_transport.delete(os.path.basename(pack.index.path))
566
self.pack_transport.delete(pack.data.filename)
568
def _load_packs(self):
570
for name in self._pack_names():
571
if name.startswith("pack-") and name.endswith(".pack"):
573
size = self.pack_transport.stat(name).st_size
574
except TransportNotPossible:
575
# FIXME: This reads the whole pack file at once
576
f = self.pack_transport.get(name)
578
pd = PackData(name, StringIO(contents), size=len(contents))
580
pd = PackData(name, self.pack_transport.get(name),
582
idxname = name.replace(".pack", ".idx")
583
idx = load_pack_index_file(idxname, self.pack_transport.get(idxname))
584
pack = Pack.from_objects(pd, idx)
585
pack._basename = idxname[:-4]
589
def _iter_loose_objects(self):
590
for base in self.transport.list_dir('.'):
593
for rest in self.transport.list_dir(base):
596
def _split_loose_object(self, sha):
597
return (sha[:2], sha[2:])
599
def _remove_loose_object(self, sha):
600
path = '%s/%s' % self._split_loose_object(sha)
601
self.transport.delete(path)
603
def _get_loose_object(self, sha):
604
path = '%s/%s' % self._split_loose_object(sha)
606
return ShaFile.from_file(self.transport.get(path))
610
def add_object(self, obj):
611
"""Add a single object to this object store.
613
:param obj: Object to add
615
(dir, file) = self._split_loose_object(obj.id)
617
self.transport.mkdir(dir)
620
path = "%s/%s" % (dir, file)
621
if self.transport.has(path):
622
return # Already there, no need to write again
623
self.transport.put_bytes(path, obj.as_legacy_object())
625
def move_in_pack(self, f):
626
"""Move a specific file containing a pack into the pack directory.
628
:note: The file should be on the same file system as the
631
:param path: Path to the pack file.
634
p = PackData("", f, len(f.getvalue()))
635
entries = p.sorted_entries()
636
basename = "pack-%s" % iter_sha1(entry[0] for entry in entries)
637
p._filename = basename + ".pack"
639
self.pack_transport.put_file(basename + ".pack", f)
640
idxfile = self.pack_transport.open_write_stream(basename + ".idx")
642
write_pack_index_v2(idxfile, entries, p.get_stored_checksum())
645
idxfile = self.pack_transport.get(basename + ".idx")
646
idx = load_pack_index_file(basename+".idx", idxfile)
647
final_pack = Pack.from_objects(p, idx)
648
final_pack._basename = basename
649
self._add_known_pack(basename, final_pack)
652
def add_thin_pack(self):
653
"""Add a new thin pack to this object store.
655
Thin packs are packs that contain deltas with parents that exist
658
from cStringIO import StringIO
661
if len(f.getvalue()) > 0:
662
return self.move_in_thin_pack(f)
667
def move_in_thin_pack(self, f):
668
"""Move a specific file containing a pack into the pack directory.
670
:note: The file should be on the same file system as the
673
:param path: Path to the pack file.
676
data = PackData.from_file(self.get_raw, f, len(f.getvalue()))
677
idx = MemoryPackIndex(data.sorted_entries(), data.get_stored_checksum())
678
p = Pack.from_objects(data, idx)
680
pack_sha = idx.objects_sha1()
682
datafile = self.pack_transport.open_write_stream(
683
"pack-%s.pack" % pack_sha)
685
entries, data_sum = write_pack_data(datafile, p.pack_tuples())
689
idxfile = self.pack_transport.open_write_stream(
690
"pack-%s.idx" % pack_sha)
692
write_pack_index_v2(idxfile, data.sorted_entries(), data_sum)
695
basename = "pack-%s" % pack_sha
696
final_pack = Pack(basename)
697
self._add_known_pack(basename, final_pack)
701
"""Add a new pack to this object store.
703
:return: Fileobject to write to and a commit function to
704
call when the pack is finished.
706
from cStringIO import StringIO
709
if len(f.getvalue()) > 0:
710
return self.move_in_pack(f)
715
return f, commit, abort
718
def init(cls, transport):
720
transport.mkdir('info')
724
transport.mkdir(PACKDIR)
727
return cls(transport)