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."""
20
from dulwich.errors import (
23
from dulwich.objects import (
26
from dulwich.object_store import (
30
from dulwich.pack import (
33
from dulwich.repo import (
41
read_packed_refs_with_peeled,
49
from bzrlib.errors import (
54
class TransportRepo(BaseRepo):
56
def __init__(self, transport):
57
self.transport = transport
58
if self.transport.has(urlutils.join(".git", OBJECTDIR)):
60
self._controltransport = self.transport.clone('.git')
61
elif (self.transport.has(OBJECTDIR) and
62
self.transport.has(REFSDIR)):
64
self._controltransport = self.transport
66
raise NotGitRepository(self.transport)
67
object_store = TransportObjectStore(
68
self._controltransport.clone(OBJECTDIR))
69
refs = TransportRefsContainer(self._controltransport)
70
super(TransportRepo, self).__init__(object_store, refs)
72
def get_named_file(self, path):
73
"""Get a file from the control dir with a specific name.
75
Although the filename should be interpreted as a filename relative to
76
the control dir in a disk-baked Repo, the object returned need not be
77
pointing to a file in that location.
79
:param path: The path to the file, relative to the control dir.
80
:return: An open file object, or None if the file does not exist.
83
return self._controltransport.get(path.lstrip('/'))
87
def put_named_file(self, path, contents):
88
self._controltransport.put_bytes(path.lstrip('/'), contents)
91
"""Open the index for this repository."""
92
from dulwich.index import Index
93
return Index(self._controltransport.local_abspath('index'))
96
return "<TransportRepo for %r>" % self.transport
99
def init(cls, transport, mkdir=True):
100
transport.mkdir('.git')
101
controltransport = transport.clone('.git')
102
cls.init_bare(controltransport)
103
return cls(controltransport)
106
def init_bare(cls, transport, mkdir=True):
107
for d in BASE_DIRECTORIES:
108
transport.mkdir(urlutils.join(*d))
110
ret.refs.set_ref("HEAD", "refs/heads/master")
111
ret.put_named_file('description', "Unnamed repository")
112
ret.put_named_file('config', """[core]
113
repositoryformatversion = 0
116
logallrefupdates = true
118
ret.put_named_file('info/excludes', '')
124
class TransportObjectStore(PackBasedObjectStore):
125
"""Git-style object store that exists on disk."""
127
def __init__(self, transport):
128
"""Open an object store.
130
:param transport: Transport to open data from
132
super(TransportObjectStore, self).__init__()
133
self.transport = transport
134
self.pack_transport = self.transport.clone(PACKDIR)
136
def _load_packs(self):
138
for name in self.pack_transport.list_dir('.'):
139
# TODO: verify that idx exists first
140
if name.startswith("pack-") and name.endswith(".pack"):
141
# TODO: if stat fails, just use None - after all
142
# the st_mtime is just used for sorting
143
pack_files.append((self.pack_transport.stat(name).st_mtime, name))
144
pack_files.sort(reverse=True)
145
suffix_len = len(".pack")
146
return [Pack(self.pack_transport.get(f)[:-suffix_len]) for _, f in pack_files]
148
def _iter_loose_objects(self):
149
for base in self.transport.list_dir('.'):
152
for rest in self.transport.list_dir(base):
155
def _split_loose_object(self, sha):
156
return (sha[:2], sha[2:])
158
def _get_loose_object(self, sha):
159
path = '%s/%s' % self._split_loose_object(sha)
161
return ShaFile.from_file(self.transport.get(path))
165
def add_object(self, obj):
166
"""Add a single object to this object store.
168
:param obj: Object to add
170
(dir, file) = self._split_loose_object(obj.id)
171
self.transport.mkdir(dir)
172
path = "%s/%s" % (dir, file)
173
if self.transport.has(path):
174
return # Already there, no need to write again
175
self.transport.put_bytes(path, obj.as_legacy_object())
178
"""Add a new pack to this object store.
180
:return: Fileobject to write to and a commit function to
181
call when the pack is finished.
183
fd, path = tempfile.mkstemp(dir=self.pack_dir, suffix=".pack")
184
f = os.fdopen(fd, 'wb')
188
if os.path.getsize(path) > 0:
189
self.move_in_pack(path)
193
class TransportRefsContainer(RefsContainer):
194
"""Refs container that reads refs from a transport."""
196
def __init__(self, transport):
197
self.transport = transport
198
self._packed_refs = None
199
self._peeled_refs = {}
202
return "%s(%r)" % (self.__class__.__name__, self.transport)
204
def subkeys(self, base):
206
path = self.refpath(base)
207
for root, dirs, files in os.walk(path):
208
dir = root[len(path):].strip("/")
209
for filename in files:
210
refname = ("%s/%s" % (dir, filename)).strip("/")
211
# check_ref_format requires at least one /, so we prepend the
212
# base before calling it.
213
if check_ref_format("%s/%s" % (base, refname)):
215
for key in self.get_packed_refs():
216
if key.startswith(base):
217
keys.add(key[len(base):].strip("/"))
222
if self.transport.has(self.refpath("HEAD")):
224
path = self.refpath("")
225
for root, dirs, files in os.walk(self.refpath("refs")):
226
dir = root[len(path):].strip("/")
227
for filename in files:
228
refname = ("%s/%s" % (dir, filename)).strip("/")
229
if check_ref_format(refname):
231
keys.update(self.get_packed_refs())
234
def get_packed_refs(self):
235
"""Get contents of the packed-refs file.
237
:return: Dictionary mapping ref names to SHA1s
239
:note: Will return an empty dictionary when no packed-refs file is
242
# TODO: invalidate the cache on repacking
243
if self._packed_refs is None:
244
self._packed_refs = {}
245
path = os.path.join(self.path, 'packed-refs')
247
f = GitFile(path, 'rb')
249
if e.errno == errno.ENOENT:
253
first_line = iter(f).next().rstrip()
254
if (first_line.startswith("# pack-refs") and " peeled" in
256
for sha, name, peeled in read_packed_refs_with_peeled(f):
257
self._packed_refs[name] = sha
259
self._peeled_refs[name] = peeled
262
for sha, name in read_packed_refs(f):
263
self._packed_refs[name] = sha
266
return self._packed_refs
268
def read_loose_ref(self, name):
270
f = self.transport.get(name)
272
header = f.read(len(SYMREF))
274
# Read only the first line
275
return header + iter(f).next().rstrip("\n")
277
# Read only the first 40 bytes
278
return header + f.read(40-len(SYMREF))
284
def _remove_packed_ref(self, name):
285
if self._packed_refs is None:
287
filename = os.path.join(self.path, 'packed-refs')
288
# reread cached refs from disk, while holding the lock
289
f = GitFile(filename, 'wb')
291
self._packed_refs = None
292
self.get_packed_refs()
294
if name not in self._packed_refs:
297
del self._packed_refs[name]
298
if name in self._peeled_refs:
299
del self._peeled_refs[name]
300
write_packed_refs(f, self._packed_refs, self._peeled_refs)
305
def set_if_equals(self, name, old_ref, new_ref):
306
"""Set a refname to new_ref only if it currently equals old_ref.
308
This method follows all symbolic references, and can be used to perform
309
an atomic compare-and-swap operation.
311
:param name: The refname to set.
312
:param old_ref: The old sha the refname must refer to, or None to set
314
:param new_ref: The new sha the refname will refer to.
315
:return: True if the set was successful, False otherwise.
318
realname, _ = self._follow(name)
321
dir_transport = self.transport.clone(urlutils.dirname(realname))
322
dir_transport.create_prefix()
323
f = GitFile(filename, 'wb')
325
if old_ref is not None:
327
# read again while holding the lock
328
orig_ref = self.read_loose_ref(realname)
330
orig_ref = self.get_packed_refs().get(realname, None)
331
if orig_ref != old_ref:
334
except (OSError, IOError):
338
f.write(new_ref+"\n")
339
except (OSError, IOError):
346
def add_if_new(self, name, ref):
347
"""Add a new reference only if it does not already exist."""
348
self._check_refname(name)
349
ensure_dir_exists(urlutils.dirname(filename))
350
f = GitFile(filename, 'wb')
352
if self.transport.has(name) or name in self.get_packed_refs():
357
except (OSError, IOError):
364
def __setitem__(self, name, ref):
365
"""Set a reference name to point to the given SHA1.
367
This method follows all symbolic references.
369
:note: This method unconditionally overwrites the contents of a reference
370
on disk. To update atomically only if the reference has not changed
371
on disk, use set_if_equals().
373
self.set_if_equals(name, None, ref)
375
def remove_if_equals(self, name, old_ref):
376
"""Remove a refname only if it currently equals old_ref.
378
This method does not follow symbolic references. It can be used to
379
perform an atomic compare-and-delete operation.
381
:param name: The refname to delete.
382
:param old_ref: The old sha the refname must refer to, or None to delete
384
:return: True if the delete was successful, False otherwise.
386
self._check_refname(name)
387
filename = self.refpath(name)
388
ensure_dir_exists(os.path.dirname(filename))
389
f = GitFile(filename, 'wb')
391
if old_ref is not None:
392
orig_ref = self.read_loose_ref(name)
394
orig_ref = self.get_packed_refs().get(name, None)
395
if orig_ref != old_ref:
398
if os.path.exists(filename):
400
self._remove_packed_ref(name)
402
# never write, we just wanted the lock
406
def __delitem__(self, name):
409
This method does not follow symbolic references.
410
:note: This method unconditionally deletes the contents of a reference
411
on disk. To delete atomically only if the reference has not changed
412
on disk, use set_if_equals().
414
self.remove_if_equals(name, None)