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
26
from dulwich.errors import (
29
from dulwich.file import (
33
from dulwich.objects import (
36
from dulwich.object_store import (
40
from dulwich.pack import (
49
from dulwich.repo import (
60
read_packed_refs_with_peeled,
67
transport as _mod_transport,
70
from ..errors import (
71
AlreadyControlDirError,
81
from ..lock import LogicalLockResult
84
class TransportRefsContainer(RefsContainer):
85
"""Refs container that reads refs from a transport."""
87
def __init__(self, transport, worktree_transport=None):
88
self.transport = transport
89
if worktree_transport is None:
90
worktree_transport = transport
91
self.worktree_transport = worktree_transport
92
self._packed_refs = None
93
self._peeled_refs = None
96
return "%s(%r)" % (self.__class__.__name__, self.transport)
98
def _ensure_dir_exists(self, path):
99
for n in range(path.count("/")):
100
dirname = "/".join(path.split("/")[:n+1])
102
self.transport.mkdir(dirname)
106
def subkeys(self, base):
107
"""Refs present in this container under a base.
109
:param base: The base to return refs under.
110
:return: A set of valid refs in this container under the base; the base
111
prefix is stripped from the ref names returned.
114
base_len = len(base) + 1
115
for refname in self.allkeys():
116
if refname.startswith(base):
117
keys.add(refname[base_len:])
123
self.worktree_transport.get_bytes("HEAD")
129
iter_files = list(self.transport.clone("refs").iter_files_recursive())
130
for filename in iter_files:
131
unquoted_filename = urlutils.unquote_to_bytes(filename)
132
refname = osutils.pathjoin(b"refs", unquoted_filename)
133
if check_ref_format(refname):
135
except (TransportNotPossible, NoSuchFile):
137
keys.update(self.get_packed_refs())
140
def get_packed_refs(self):
141
"""Get contents of the packed-refs file.
143
:return: Dictionary mapping ref names to SHA1s
145
:note: Will return an empty dictionary when no packed-refs file is
148
# TODO: invalidate the cache on repacking
149
if self._packed_refs is None:
150
# set both to empty because we want _peeled_refs to be
151
# None if and only if _packed_refs is also None.
152
self._packed_refs = {}
153
self._peeled_refs = {}
155
f = self.transport.get("packed-refs")
159
first_line = next(iter(f)).rstrip()
160
if (first_line.startswith(b"# pack-refs") and b" peeled" in
162
for sha, name, peeled in read_packed_refs_with_peeled(f):
163
self._packed_refs[name] = sha
165
self._peeled_refs[name] = peeled
168
for sha, name in read_packed_refs(f):
169
self._packed_refs[name] = sha
172
return self._packed_refs
174
def get_peeled(self, name):
175
"""Return the cached peeled value of a ref, if available.
177
:param name: Name of the ref to peel
178
:return: The peeled value of the ref. If the ref is known not point to a
179
tag, this will be the SHA the ref refers to. If the ref may point to
180
a tag, but no cached information is available, None is returned.
182
self.get_packed_refs()
183
if self._peeled_refs is None or name not in self._packed_refs:
184
# No cache: no peeled refs were read, or this ref is loose
186
if name in self._peeled_refs:
187
return self._peeled_refs[name]
192
def read_loose_ref(self, name):
193
"""Read a reference file and return its contents.
195
If the reference file a symbolic reference, only read the first line of
196
the file. Otherwise, only read the first 40 bytes.
198
:param name: the refname to read, relative to refpath
199
:return: The contents of the ref file, or None if the file does not
201
:raises IOError: if any other error occurs
204
transport = self.worktree_transport
206
transport = self.transport
208
f = transport.get(urlutils.quote_from_bytes(name))
212
header = f.read(len(SYMREF))
214
# Read only the first line
215
return header + next(iter(f)).rstrip(b"\r\n")
217
# Read only the first 40 bytes
218
return header + f.read(40-len(SYMREF))
220
def _remove_packed_ref(self, name):
221
if self._packed_refs is None:
223
# reread cached refs from disk, while holding the lock
225
self._packed_refs = None
226
self.get_packed_refs()
228
if name not in self._packed_refs:
231
del self._packed_refs[name]
232
if name in self._peeled_refs:
233
del self._peeled_refs[name]
234
f = self.transport.open_write_stream("packed-refs")
236
write_packed_refs(f, self._packed_refs, self._peeled_refs)
240
def set_symbolic_ref(self, name, other):
241
"""Make a ref point at another ref.
243
:param name: Name of the ref to set
244
:param other: Name of the ref to point at
246
self._check_refname(name)
247
self._check_refname(other)
249
transport = self.transport
250
self._ensure_dir_exists(urlutils.quote_from_bytes(name))
252
transport = self.worktree_transport
253
transport.put_bytes(urlutils.quote_from_bytes(name), SYMREF + other + b'\n')
255
def set_if_equals(self, name, old_ref, new_ref):
256
"""Set a refname to new_ref only if it currently equals old_ref.
258
This method follows all symbolic references, and can be used to perform
259
an atomic compare-and-swap operation.
261
:param name: The refname to set.
262
:param old_ref: The old sha the refname must refer to, or None to set
264
:param new_ref: The new sha the refname will refer to.
265
:return: True if the set was successful, False otherwise.
268
realnames, _ = self.follow(name)
269
realname = realnames[-1]
270
except (KeyError, IndexError):
272
if realname == b'HEAD':
273
transport = self.worktree_transport
275
transport = self.transport
276
self._ensure_dir_exists(urlutils.quote_from_bytes(realname))
277
transport.put_bytes(urlutils.quote_from_bytes(realname), new_ref+b"\n")
280
def add_if_new(self, name, ref):
281
"""Add a new reference only if it does not already exist.
283
This method follows symrefs, and only ensures that the last ref in the
284
chain does not exist.
286
:param name: The refname to set.
287
:param ref: The new sha the refname will refer to.
288
:return: True if the add was successful, False otherwise.
291
realnames, contents = self.follow(name)
292
if contents is not None:
294
realname = realnames[-1]
295
except (KeyError, IndexError):
297
self._check_refname(realname)
298
if realname == b'HEAD':
299
transport = self.worktree_transport
301
transport = self.transport
302
self._ensure_dir_exists(urlutils.quote_from_bytes(realname))
303
transport.put_bytes(urlutils.quote_from_bytes(realname), ref+b"\n")
306
def remove_if_equals(self, name, old_ref):
307
"""Remove a refname only if it currently equals old_ref.
309
This method does not follow symbolic references. It can be used to
310
perform an atomic compare-and-delete operation.
312
:param name: The refname to delete.
313
:param old_ref: The old sha the refname must refer to, or None to delete
315
:return: True if the delete was successful, False otherwise.
317
self._check_refname(name)
320
transport = self.worktree_transport
322
transport = self.transport
324
transport.delete(urlutils.quote_from_bytes(name))
327
self._remove_packed_ref(name)
330
def get(self, name, default=None):
336
def unlock_ref(self, name):
338
transport = self.worktree_transport
340
transport = self.transport
341
lockname = name + b".lock"
343
self.transport.delete(urlutils.quote_from_bytes(lockname))
347
def lock_ref(self, name):
349
transport = self.worktree_transport
351
transport = self.transport
352
self._ensure_dir_exists(urlutils.quote_from_bytes(name))
353
lockname = urlutils.quote_from_bytes(name + b".lock")
355
local_path = self.transport.local_abspath(urlutils.quote_from_bytes(name))
357
# This is racy, but what can we do?
358
if self.transport.has(lockname):
359
raise LockContention(name)
360
lock_result = self.transport.put_bytes(lockname, b'Locked by brz-git')
361
return LogicalLockResult(lambda: self.transport.delete(lockname))
364
gf = GitFile(local_path, 'wb')
365
except FileLocked as e:
366
raise LockContention(name, e)
370
self.transport.delete(lockname)
372
raise LockBroken(lockname)
373
# GitFile.abort doesn't care if the lock has already disappeared
375
return LogicalLockResult(unlock)
378
# TODO(jelmer): Use upstream read_gitfile; unfortunately that expects strings
379
# rather than bytes..
381
"""Read a ``.git`` file.
383
The first line of the file should start with "gitdir: "
385
:param f: File-like object to read from
389
if not cs.startswith(b"gitdir: "):
390
raise ValueError("Expected file to start with 'gitdir: '")
391
return cs[len(b"gitdir: "):].rstrip(b"\n")
394
class TransportRepo(BaseRepo):
396
def __init__(self, transport, bare, refs_text=None):
397
self.transport = transport
400
with transport.get(CONTROLDIR) as f:
401
path = read_gitfile(f)
402
except (ReadError, NoSuchFile):
404
self._controltransport = self.transport
406
self._controltransport = self.transport.clone('.git')
408
self._controltransport = self.transport.clone(urlutils.quote_from_bytes(path))
409
commondir = self.get_named_file(COMMONDIR)
410
if commondir is not None:
412
commondir = os.path.join(
414
commondir.read().rstrip(b"\r\n").decode(
415
sys.getfilesystemencoding()))
416
self._commontransport = \
417
_mod_transport.get_transport_from_path(commondir)
419
self._commontransport = self._controltransport
420
object_store = TransportObjectStore(
421
self._commontransport.clone(OBJECTDIR))
422
if refs_text is not None:
423
refs_container = InfoRefsContainer(BytesIO(refs_text))
425
head = TransportRefsContainer(self._commontransport).read_loose_ref("HEAD")
429
refs_container._refs["HEAD"] = head
431
refs_container = TransportRefsContainer(
432
self._commontransport, self._controltransport)
433
super(TransportRepo, self).__init__(object_store,
436
def controldir(self):
437
return self._controltransport.local_abspath('.')
440
return self._commontransport.local_abspath('.')
444
return self.transport.local_abspath('.')
446
def _determine_file_mode(self):
447
# Be consistent with bzr
448
if sys.platform == 'win32':
452
def get_named_file(self, path):
453
"""Get a file from the control dir with a specific name.
455
Although the filename should be interpreted as a filename relative to
456
the control dir in a disk-baked Repo, the object returned need not be
457
pointing to a file in that location.
459
:param path: The path to the file, relative to the control dir.
460
:return: An open file object, or None if the file does not exist.
463
return self._controltransport.get(path.lstrip('/'))
467
def _put_named_file(self, relpath, contents):
468
self._controltransport.put_bytes(relpath, contents)
470
def index_path(self):
471
"""Return the path to the index file."""
472
return self._controltransport.local_abspath(INDEX_FILENAME)
474
def open_index(self):
475
"""Open the index for this repository."""
476
from dulwich.index import Index
477
if not self.has_index():
478
raise NoIndexPresent()
479
return Index(self.index_path())
482
"""Check if an index is present."""
483
# Bare repos must never have index files; non-bare repos may have a
484
# missing index file, which is treated as empty.
487
def get_config(self):
488
from dulwich.config import ConfigFile
490
with self._controltransport.get('config') as f:
491
return ConfigFile.from_file(f)
495
def get_config_stack(self):
496
from dulwich.config import StackedConfig
498
p = self.get_config()
504
backends.extend(StackedConfig.default_backends())
505
return StackedConfig(backends, writable=writable)
508
return "<%s for %r>" % (self.__class__.__name__, self.transport)
511
def init(cls, transport, bare=False):
514
transport.mkdir(".git")
516
raise AlreadyControlDirError(transport.base)
517
control_transport = transport.clone(".git")
519
control_transport = transport
520
for d in BASE_DIRECTORIES:
522
control_transport.mkdir("/".join(d))
526
control_transport.mkdir(OBJECTDIR)
528
raise AlreadyControlDirError(transport.base)
529
TransportObjectStore.init(control_transport.clone(OBJECTDIR))
530
ret = cls(transport, bare)
531
ret.refs.set_symbolic_ref(b"HEAD", b"refs/heads/master")
532
ret._init_files(bare)
536
class TransportObjectStore(PackBasedObjectStore):
537
"""Git-style object store that exists on disk."""
539
def __init__(self, transport):
540
"""Open an object store.
542
:param transport: Transport to open data from
544
super(TransportObjectStore, self).__init__()
545
self.transport = transport
546
self.pack_transport = self.transport.clone(PACKDIR)
547
self._alternates = None
549
def __eq__(self, other):
550
if not isinstance(other, TransportObjectStore):
552
return self.transport == other.transport
555
return "%s(%r)" % (self.__class__.__name__, self.transport)
558
def alternates(self):
559
if self._alternates is not None:
560
return self._alternates
561
self._alternates = []
562
for path in self._read_alternate_paths():
564
t = _mod_transport.get_transport_from_path(path)
565
self._alternates.append(self.__class__(t))
566
return self._alternates
568
def _read_alternate_paths(self):
570
f = self.transport.get("info/alternates")
575
for l in f.read().splitlines():
585
# FIXME: Never invalidates.
586
if not self._pack_cache:
587
self._update_pack_cache()
588
return self._pack_cache.values()
590
def _update_pack_cache(self):
591
for pack in self._load_packs():
592
self._pack_cache[pack._basename] = pack
594
def _pack_names(self):
596
return self.pack_transport.list_dir(".")
597
except TransportNotPossible:
599
f = self.transport.get('info/packs')
601
# Hmm, warn about running 'git update-server-info' ?
604
# TODO(jelmer): Move to top-level after dulwich
605
# 0.19.7 is released.
606
from dulwich.object_store import read_packs_file
608
return read_packs_file(f)
612
def _remove_pack(self, pack):
613
self.pack_transport.delete(os.path.basename(pack.index.path))
614
self.pack_transport.delete(pack.data.filename)
616
def _load_packs(self):
618
for name in self._pack_names():
619
if name.startswith("pack-") and name.endswith(".pack"):
621
size = self.pack_transport.stat(name).st_size
622
except TransportNotPossible:
623
f = self.pack_transport.get(name)
624
pd = PackData(name, f, size=len(contents))
626
pd = PackData(name, self.pack_transport.get(name),
628
idxname = name.replace(".pack", ".idx")
629
idx = load_pack_index_file(idxname, self.pack_transport.get(idxname))
630
pack = Pack.from_objects(pd, idx)
631
pack._basename = idxname[:-4]
635
def _iter_loose_objects(self):
636
for base in self.transport.list_dir('.'):
639
for rest in self.transport.list_dir(base):
640
yield (base+rest).encode(sys.getfilesystemencoding())
642
def _split_loose_object(self, sha):
643
return (sha[:2], sha[2:])
645
def _remove_loose_object(self, sha):
646
path = osutils.joinpath(self._split_loose_object(sha))
647
self.transport.delete(urlutils.quote_from_bytes(path))
649
def _get_loose_object(self, sha):
650
path = osutils.joinpath(self._split_loose_object(sha))
652
with self.transport.get(urlutils.quote_from_bytes(path)) as f:
653
return ShaFile.from_file(f)
657
def add_object(self, obj):
658
"""Add a single object to this object store.
660
:param obj: Object to add
662
(dir, file) = self._split_loose_object(obj.id)
664
self.transport.mkdir(urlutils.quote_from_bytes(dir))
667
path = urlutils.quote_from_bytes(osutils.pathjoin(dir, file))
668
if self.transport.has(path):
669
return # Already there, no need to write again
670
self.transport.put_bytes(path, obj.as_legacy_object())
672
def move_in_pack(self, f):
673
"""Move a specific file containing a pack into the pack directory.
675
:note: The file should be on the same file system as the
678
:param path: Path to the pack file.
681
p = PackData("", f, len(f.getvalue()))
682
entries = p.sorted_entries()
683
basename = "pack-%s" % iter_sha1(entry[0] for entry in entries).decode('ascii')
684
p._filename = basename + ".pack"
686
self.pack_transport.put_file(basename + ".pack", f)
687
idxfile = self.pack_transport.open_write_stream(basename + ".idx")
689
write_pack_index_v2(idxfile, entries, p.get_stored_checksum())
692
idxfile = self.pack_transport.get(basename + ".idx")
693
idx = load_pack_index_file(basename+".idx", idxfile)
694
final_pack = Pack.from_objects(p, idx)
695
final_pack._basename = basename
696
self._add_known_pack(basename, final_pack)
699
def move_in_thin_pack(self, f):
700
"""Move a specific file containing a pack into the pack directory.
702
:note: The file should be on the same file system as the
705
:param path: Path to the pack file.
708
p = Pack('', resolve_ext_ref=self.get_raw)
709
p._data = PackData.from_file(f, len(f.getvalue()))
711
p._idx_load = lambda: MemoryPackIndex(p.data.sorted_entries(), p.data.get_stored_checksum())
713
pack_sha = p.index.objects_sha1()
715
datafile = self.pack_transport.open_write_stream(
716
"pack-%s.pack" % pack_sha.decode('ascii'))
718
entries, data_sum = write_pack_objects(datafile, p.pack_tuples())
721
entries = sorted([(k, v[0], v[1]) for (k, v) in entries.items()])
722
idxfile = self.pack_transport.open_write_stream(
723
"pack-%s.idx" % pack_sha.decode('ascii'))
725
write_pack_index_v2(idxfile, entries, data_sum)
728
# TODO(jelmer): Just add new pack to the cache
729
self._flush_pack_cache()
732
"""Add a new pack to this object store.
734
:return: Fileobject to write to and a commit function to
735
call when the pack is finished.
739
if len(f.getvalue()) > 0:
740
return self.move_in_pack(f)
745
return f, commit, abort
748
def init(cls, transport):
750
transport.mkdir('info')
754
transport.mkdir(PACKDIR)
757
return cls(transport)