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 io import BytesIO
27
from dulwich.errors import (
31
from dulwich.file import (
35
from dulwich.objects import (
38
from dulwich.object_store import (
42
from dulwich.pack import (
51
from dulwich.repo import (
64
read_packed_refs_with_peeled,
70
transport as _mod_transport,
72
from ...errors import (
73
AlreadyControlDirError,
84
from ...lock import LogicalLockResult
87
class TransportRefsContainer(RefsContainer):
88
"""Refs container that reads refs from a transport."""
90
def __init__(self, transport, worktree_transport=None):
91
self.transport = transport
92
if worktree_transport is None:
93
worktree_transport = transport
94
self.worktree_transport = worktree_transport
95
self._packed_refs = None
96
self._peeled_refs = None
99
return "%s(%r)" % (self.__class__.__name__, self.transport)
101
def _ensure_dir_exists(self, path):
102
for n in range(path.count("/")):
103
dirname = "/".join(path.split("/")[:n+1])
105
self.transport.mkdir(dirname)
109
def subkeys(self, base):
110
"""Refs present in this container under a base.
112
:param base: The base to return refs under.
113
:return: A set of valid refs in this container under the base; the base
114
prefix is stripped from the ref names returned.
117
base_len = len(base) + 1
118
for refname in self.allkeys():
119
if refname.startswith(base):
120
keys.add(refname[base_len:])
126
self.worktree_transport.get_bytes("HEAD")
132
iter_files = list(self.transport.clone("refs").iter_files_recursive())
133
for filename in iter_files:
134
refname = "refs/%s" % urllib.unquote(filename)
135
if check_ref_format(refname):
137
except (TransportNotPossible, NoSuchFile):
139
keys.update(self.get_packed_refs())
142
def get_packed_refs(self):
143
"""Get contents of the packed-refs file.
145
:return: Dictionary mapping ref names to SHA1s
147
:note: Will return an empty dictionary when no packed-refs file is
150
# TODO: invalidate the cache on repacking
151
if self._packed_refs is None:
152
# set both to empty because we want _peeled_refs to be
153
# None if and only if _packed_refs is also None.
154
self._packed_refs = {}
155
self._peeled_refs = {}
157
f = self.transport.get("packed-refs")
161
first_line = next(iter(f)).rstrip()
162
if (first_line.startswith("# pack-refs") and " peeled" in
164
for sha, name, peeled in read_packed_refs_with_peeled(f):
165
self._packed_refs[name] = sha
167
self._peeled_refs[name] = peeled
170
for sha, name in read_packed_refs(f):
171
self._packed_refs[name] = sha
174
return self._packed_refs
176
def get_peeled(self, name):
177
"""Return the cached peeled value of a ref, if available.
179
:param name: Name of the ref to peel
180
:return: The peeled value of the ref. If the ref is known not point to a
181
tag, this will be the SHA the ref refers to. If the ref may point to
182
a tag, but no cached information is available, None is returned.
184
self.get_packed_refs()
185
if self._peeled_refs is None or name not in self._packed_refs:
186
# No cache: no peeled refs were read, or this ref is loose
188
if name in self._peeled_refs:
189
return self._peeled_refs[name]
194
def read_loose_ref(self, name):
195
"""Read a reference file and return its contents.
197
If the reference file a symbolic reference, only read the first line of
198
the file. Otherwise, only read the first 40 bytes.
200
:param name: the refname to read, relative to refpath
201
:return: The contents of the ref file, or None if the file does not
203
:raises IOError: if any other error occurs
206
transport = self.worktree_transport
208
transport = self.transport
210
f = transport.get(name)
213
f = BytesIO(f.read())
215
header = f.read(len(SYMREF))
217
# Read only the first line
218
return header + next(iter(f)).rstrip(b"\r\n")
220
# Read only the first 40 bytes
221
return header + f.read(40-len(SYMREF))
225
def _remove_packed_ref(self, name):
226
if self._packed_refs is None:
228
# reread cached refs from disk, while holding the lock
230
self._packed_refs = None
231
self.get_packed_refs()
233
if name not in self._packed_refs:
236
del self._packed_refs[name]
237
if name in self._peeled_refs:
238
del self._peeled_refs[name]
239
f = self.transport.open_write_stream("packed-refs")
241
write_packed_refs(f, self._packed_refs, self._peeled_refs)
245
def set_symbolic_ref(self, name, other):
246
"""Make a ref point at another ref.
248
:param name: Name of the ref to set
249
:param other: Name of the ref to point at
251
self._check_refname(name)
252
self._check_refname(other)
254
transport = self.transport
255
self._ensure_dir_exists(name)
257
transport = self.worktree_transport
258
transport.put_bytes(name, SYMREF + other + b'\n')
260
def set_if_equals(self, name, old_ref, new_ref):
261
"""Set a refname to new_ref only if it currently equals old_ref.
263
This method follows all symbolic references, and can be used to perform
264
an atomic compare-and-swap operation.
266
:param name: The refname to set.
267
:param old_ref: The old sha the refname must refer to, or None to set
269
:param new_ref: The new sha the refname will refer to.
270
:return: True if the set was successful, False otherwise.
273
realnames, _ = self.follow(name)
274
realname = realnames[-1]
275
except (KeyError, IndexError):
277
if realname == b'HEAD':
278
transport = self.worktree_transport
280
transport = self.transport
281
self._ensure_dir_exists(realname)
282
transport.put_bytes(realname, new_ref+"\n")
285
def add_if_new(self, name, ref):
286
"""Add a new reference only if it does not already exist.
288
This method follows symrefs, and only ensures that the last ref in the
289
chain does not exist.
291
:param name: The refname to set.
292
:param ref: The new sha the refname will refer to.
293
:return: True if the add was successful, False otherwise.
296
realnames, contents = self.follow(name)
297
if contents is not None:
299
realname = realnames[-1]
300
except (KeyError, IndexError):
302
self._check_refname(realname)
303
if realname == b'HEAD':
304
transport = self.worktree_transport
306
transport = self.transport
307
self._ensure_dir_exists(realname)
308
transport.put_bytes(realname, ref+"\n")
311
def remove_if_equals(self, name, old_ref):
312
"""Remove a refname only if it currently equals old_ref.
314
This method does not follow symbolic references. It can be used to
315
perform an atomic compare-and-delete operation.
317
:param name: The refname to delete.
318
:param old_ref: The old sha the refname must refer to, or None to delete
320
:return: True if the delete was successful, False otherwise.
322
self._check_refname(name)
325
transport = self.worktree_transport
327
transport = self.transport
329
transport.delete(name)
332
self._remove_packed_ref(name)
335
def get(self, name, default=None):
341
def unlock_ref(self, name):
343
transport = self.worktree_transport
345
transport = self.transport
346
lockname = name + ".lock"
348
self.transport.delete(lockname)
352
def lock_ref(self, name):
354
transport = self.worktree_transport
356
transport = self.transport
357
self._ensure_dir_exists(name)
358
lockname = name + ".lock"
360
local_path = self.transport.local_abspath(name)
362
# This is racy, but what can we do?
363
if self.transport.has(lockname):
364
raise LockContention(name)
365
lock_result = self.transport.put_bytes(lockname, b'Locked by brz-git')
366
return LogicalLockResult(lambda: self.transport.delete(lockname))
369
gf = GitFile(local_path, 'wb')
370
except FileLocked as e:
371
raise LockContention(name, e)
375
self.transport.delete(lockname)
377
raise LockBroken(lockname)
378
# GitFile.abort doesn't care if the lock has already disappeared
380
return LogicalLockResult(unlock)
383
class TransportRepo(BaseRepo):
385
def __init__(self, transport, bare, refs_text=None):
386
self.transport = transport
389
with transport.get(CONTROLDIR) as f:
390
path = read_gitfile(f)
391
except (ReadError, NoSuchFile):
393
self._controltransport = self.transport
395
self._controltransport = self.transport.clone('.git')
397
self._controltransport = self.transport.clone(path)
398
commondir = self.get_named_file(COMMONDIR)
399
if commondir is not None:
401
commondir = os.path.join(
403
commondir.read().rstrip(b"\r\n").decode(
404
sys.getfilesystemencoding()))
405
self._commontransport = \
406
_mod_transport.get_transport_from_path(commondir)
408
self._commontransport = self._controltransport
409
object_store = TransportObjectStore(
410
self._commontransport.clone(OBJECTDIR))
411
if refs_text is not None:
412
refs_container = InfoRefsContainer(BytesIO(refs_text))
414
head = TransportRefsContainer(self._commontransport).read_loose_ref("HEAD")
418
refs_container._refs["HEAD"] = head
420
refs_container = TransportRefsContainer(
421
self._commontransport, self._controltransport)
422
super(TransportRepo, self).__init__(object_store,
425
def controldir(self):
426
return self._controltransport.local_abspath('.')
429
return self._commontransport.local_abspath('.')
433
return self.transport.local_abspath('.')
435
def _determine_file_mode(self):
436
# Be consistent with bzr
437
if sys.platform == 'win32':
441
def get_named_file(self, path):
442
"""Get a file from the control dir with a specific name.
444
Although the filename should be interpreted as a filename relative to
445
the control dir in a disk-baked Repo, the object returned need not be
446
pointing to a file in that location.
448
:param path: The path to the file, relative to the control dir.
449
:return: An open file object, or None if the file does not exist.
452
return self._controltransport.get(path.lstrip('/'))
456
def _put_named_file(self, relpath, contents):
457
self._controltransport.put_bytes(relpath, contents)
459
def index_path(self):
460
"""Return the path to the index file."""
461
return self._controltransport.local_abspath(INDEX_FILENAME)
463
def open_index(self):
464
"""Open the index for this repository."""
465
from dulwich.index import Index
466
if not self.has_index():
467
raise NoIndexPresent()
468
return Index(self.index_path())
471
"""Check if an index is present."""
472
# Bare repos must never have index files; non-bare repos may have a
473
# missing index file, which is treated as empty.
476
def get_config(self):
477
from dulwich.config import ConfigFile
479
return ConfigFile.from_file(self._controltransport.get('config'))
483
def get_config_stack(self):
484
from dulwich.config import StackedConfig
486
p = self.get_config()
492
backends.extend(StackedConfig.default_backends())
493
return StackedConfig(backends, writable=writable)
496
return "<%s for %r>" % (self.__class__.__name__, self.transport)
499
def init(cls, transport, bare=False):
502
transport.mkdir(".git")
504
raise AlreadyControlDirError(transport.base)
505
control_transport = transport.clone(".git")
507
control_transport = transport
508
for d in BASE_DIRECTORIES:
510
control_transport.mkdir("/".join(d))
514
control_transport.mkdir(OBJECTDIR)
516
raise AlreadyControlDirError(transport.base)
517
TransportObjectStore.init(control_transport.clone(OBJECTDIR))
518
ret = cls(transport, bare)
519
ret.refs.set_symbolic_ref(b"HEAD", b"refs/heads/master")
520
ret._init_files(bare)
524
class TransportObjectStore(PackBasedObjectStore):
525
"""Git-style object store that exists on disk."""
527
def __init__(self, transport):
528
"""Open an object store.
530
:param transport: Transport to open data from
532
super(TransportObjectStore, self).__init__()
533
self.transport = transport
534
self.pack_transport = self.transport.clone(PACKDIR)
535
self._alternates = None
537
def __eq__(self, other):
538
if not isinstance(other, TransportObjectStore):
540
return self.transport == other.transport
543
return "%s(%r)" % (self.__class__.__name__, self.transport)
546
def alternates(self):
547
if self._alternates is not None:
548
return self._alternates
549
self._alternates = []
550
for path in self._read_alternate_paths():
552
t = _mod_transport.get_transport_from_path(path)
553
self._alternates.append(self.__class__(t))
554
return self._alternates
556
def _read_alternate_paths(self):
558
f = self.transport.get("info/alternates")
563
for l in f.read().splitlines():
575
# FIXME: Never invalidates.
576
if not self._pack_cache:
577
self._update_pack_cache()
578
return self._pack_cache.values()
580
def _update_pack_cache(self):
581
for pack in self._load_packs():
582
self._pack_cache[pack._basename] = pack
584
def _pack_names(self):
586
f = self.transport.get('info/packs')
588
return self.pack_transport.list_dir(".")
591
for line in f.read().splitlines():
594
(kind, name) = line.split(" ", 1)
600
def _remove_pack(self, pack):
601
self.pack_transport.delete(os.path.basename(pack.index.path))
602
self.pack_transport.delete(pack.data.filename)
604
def _load_packs(self):
606
for name in self._pack_names():
607
if name.startswith("pack-") and name.endswith(".pack"):
609
size = self.pack_transport.stat(name).st_size
610
except TransportNotPossible:
611
# FIXME: This reads the whole pack file at once
612
f = self.pack_transport.get(name)
614
pd = PackData(name, BytesIO(contents), size=len(contents))
616
pd = PackData(name, self.pack_transport.get(name),
618
idxname = name.replace(".pack", ".idx")
619
idx = load_pack_index_file(idxname, self.pack_transport.get(idxname))
620
pack = Pack.from_objects(pd, idx)
621
pack._basename = idxname[:-4]
625
def _iter_loose_objects(self):
626
for base in self.transport.list_dir('.'):
629
for rest in self.transport.list_dir(base):
632
def _split_loose_object(self, sha):
633
return (sha[:2], sha[2:])
635
def _remove_loose_object(self, sha):
636
path = '%s/%s' % self._split_loose_object(sha)
637
self.transport.delete(path)
639
def _get_loose_object(self, sha):
640
path = '%s/%s' % self._split_loose_object(sha)
642
return ShaFile.from_file(self.transport.get(path))
646
def add_object(self, obj):
647
"""Add a single object to this object store.
649
:param obj: Object to add
651
(dir, file) = self._split_loose_object(obj.id)
653
self.transport.mkdir(dir)
656
path = "%s/%s" % (dir, file)
657
if self.transport.has(path):
658
return # Already there, no need to write again
659
self.transport.put_bytes(path, obj.as_legacy_object())
661
def move_in_pack(self, f):
662
"""Move a specific file containing a pack into the pack directory.
664
:note: The file should be on the same file system as the
667
:param path: Path to the pack file.
670
p = PackData("", f, len(f.getvalue()))
671
entries = p.sorted_entries()
672
basename = "pack-%s" % iter_sha1(entry[0] for entry in entries)
673
p._filename = basename + ".pack"
675
self.pack_transport.put_file(basename + ".pack", f)
676
idxfile = self.pack_transport.open_write_stream(basename + ".idx")
678
write_pack_index_v2(idxfile, entries, p.get_stored_checksum())
681
idxfile = self.pack_transport.get(basename + ".idx")
682
idx = load_pack_index_file(basename+".idx", idxfile)
683
final_pack = Pack.from_objects(p, idx)
684
final_pack._basename = basename
685
self._add_known_pack(basename, final_pack)
688
def move_in_thin_pack(self, f):
689
"""Move a specific file containing a pack into the pack directory.
691
:note: The file should be on the same file system as the
694
:param path: Path to the pack file.
697
p = Pack('', resolve_ext_ref=self.get_raw)
698
p._data = PackData.from_file(f, len(f.getvalue()))
700
p._idx_load = lambda: MemoryPackIndex(p.data.sorted_entries(), p.data.get_stored_checksum())
702
pack_sha = p.index.objects_sha1()
704
datafile = self.pack_transport.open_write_stream(
705
"pack-%s.pack" % pack_sha)
707
entries, data_sum = write_pack_objects(datafile, p.pack_tuples())
710
entries = sorted([(k, v[0], v[1]) for (k, v) in entries.items()])
711
idxfile = self.pack_transport.open_write_stream(
712
"pack-%s.idx" % pack_sha)
714
write_pack_index_v2(idxfile, entries, data_sum)
717
# TODO(jelmer): Just add new pack to the cache
718
self._flush_pack_cache()
721
"""Add a new pack to this object store.
723
:return: Fileobject to write to and a commit function to
724
call when the pack is finished.
728
if len(f.getvalue()) > 0:
729
return self.move_in_pack(f)
734
return f, commit, abort
737
def init(cls, transport):
739
transport.mkdir('info')
743
transport.mkdir(PACKDIR)
746
return cls(transport)