/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to transportgit.py

  • Committer: Jelmer Vernooij
  • Date: 2010-06-28 22:30:34 UTC
  • mto: (0.200.953 trunk)
  • mto: This revision was merged to the branch mainline in revision 6960.
  • Revision ID: jelmer@samba.org-20100628223034-vylrgdyakmqoupl6
use transport repo objects even for local access.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2010 Jelmer Vernooij <jelmer@samba.org>
 
2
#
 
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.
 
7
#
 
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.
 
12
#
 
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
 
16
 
 
17
"""A Git repository implementation that uses a Bazaar transport."""
 
18
 
 
19
from cStringIO import StringIO
 
20
 
 
21
from dulwich.errors import (
 
22
    NotGitRepository,
 
23
    NoIndexPresent,
 
24
    )
 
25
from dulwich.objects import (
 
26
    ShaFile,
 
27
    )
 
28
from dulwich.object_store import (
 
29
    PackBasedObjectStore,
 
30
    PACKDIR,
 
31
    )
 
32
from dulwich.pack import (
 
33
    PackData,
 
34
    Pack,
 
35
    iter_sha1,
 
36
    load_pack_index_file,
 
37
    write_pack_index_v2,
 
38
    )
 
39
from dulwich.repo import (
 
40
    BaseRepo,
 
41
    RefsContainer,
 
42
    INDEX_FILENAME,
 
43
    OBJECTDIR,
 
44
    REFSDIR,
 
45
    SYMREF,
 
46
    check_ref_format,
 
47
    read_packed_refs_with_peeled,
 
48
    read_packed_refs,
 
49
    write_packed_refs,
 
50
    )
 
51
 
 
52
from bzrlib.errors import (
 
53
    FileExists,
 
54
    NoSuchFile,
 
55
    TransportNotPossible,
 
56
    )
 
57
 
 
58
 
 
59
class TransportRefsContainer(RefsContainer):
 
60
    """Refs container that reads refs from a transport."""
 
61
 
 
62
    def __init__(self, transport):
 
63
        self.transport = transport
 
64
        self._packed_refs = None
 
65
        self._peeled_refs = None
 
66
 
 
67
    def __repr__(self):
 
68
        return "%s(%r)" % (self.__class__.__name__, self.transport)
 
69
 
 
70
    def subkeys(self, base):
 
71
        keys = set()
 
72
        path = self.refpath(base)
 
73
        try:
 
74
            iter_files = self.transport.clone(base).iter_files_recursive()
 
75
        except TransportNotPossible:
 
76
            pass
 
77
        else:
 
78
            for refname in iter_files:
 
79
                # check_ref_format requires at least one /, so we prepend the
 
80
                # base before calling it.
 
81
                if check_ref_format("%s/%s" % (base, refname)):
 
82
                    keys.add(("%s/%s" % refname).strip("/"))
 
83
        for key in self.get_packed_refs():
 
84
            if key.startswith(base):
 
85
                keys.add(key[len(base):].strip("/"))
 
86
        return keys
 
87
 
 
88
    def allkeys(self):
 
89
        keys = set()
 
90
        if self.transport.has("HEAD"):
 
91
            keys.add("HEAD")
 
92
        path = ""
 
93
        try:
 
94
            iter_files = self.transport.clone("refs").iter_files_recursive()
 
95
        except TransportNotPossible:
 
96
            pass
 
97
        else:
 
98
            for refname in iter_files:
 
99
                if check_ref_format(refname):
 
100
                    keys.add(refname)
 
101
        keys.update(self.get_packed_refs())
 
102
        return keys
 
103
 
 
104
    def get_packed_refs(self):
 
105
        """Get contents of the packed-refs file.
 
106
 
 
107
        :return: Dictionary mapping ref names to SHA1s
 
108
 
 
109
        :note: Will return an empty dictionary when no packed-refs file is
 
110
            present.
 
111
        """
 
112
        # TODO: invalidate the cache on repacking
 
113
        if self._packed_refs is None:
 
114
            # set both to empty because we want _peeled_refs to be
 
115
            # None if and only if _packed_refs is also None.
 
116
            self._packed_refs = {}
 
117
            self._peeled_refs = {}
 
118
            try:
 
119
                f = self.transport.get("packed-refs")
 
120
            except NoSuchFile:
 
121
                return {}
 
122
            try:
 
123
                first_line = iter(f).next().rstrip()
 
124
                if (first_line.startswith("# pack-refs") and " peeled" in
 
125
                        first_line):
 
126
                    for sha, name, peeled in read_packed_refs_with_peeled(f):
 
127
                        self._packed_refs[name] = sha
 
128
                        if peeled:
 
129
                            self._peeled_refs[name] = peeled
 
130
                else:
 
131
                    f.seek(0)
 
132
                    for sha, name in read_packed_refs(f):
 
133
                        self._packed_refs[name] = sha
 
134
            finally:
 
135
                f.close()
 
136
        return self._packed_refs
 
137
 
 
138
    def get_peeled(self, name):
 
139
        """Return the cached peeled value of a ref, if available.
 
140
 
 
141
        :param name: Name of the ref to peel
 
142
        :return: The peeled value of the ref. If the ref is known not point to a
 
143
            tag, this will be the SHA the ref refers to. If the ref may point to
 
144
            a tag, but no cached information is available, None is returned.
 
145
        """
 
146
        self.get_packed_refs()
 
147
        if self._peeled_refs is None or name not in self._packed_refs:
 
148
            # No cache: no peeled refs were read, or this ref is loose
 
149
            return None
 
150
        if name in self._peeled_refs:
 
151
            return self._peeled_refs[name]
 
152
        else:
 
153
            # Known not peelable
 
154
            return self[name]
 
155
 
 
156
    def read_loose_ref(self, name):
 
157
        """Read a reference file and return its contents.
 
158
 
 
159
        If the reference file a symbolic reference, only read the first line of
 
160
        the file. Otherwise, only read the first 40 bytes.
 
161
 
 
162
        :param name: the refname to read, relative to refpath
 
163
        :return: The contents of the ref file, or None if the file does not
 
164
            exist.
 
165
        :raises IOError: if any other error occurs
 
166
        """
 
167
        try:
 
168
            f = self.transport.get(name)
 
169
        except NoSuchFile:
 
170
            return None
 
171
        try:
 
172
            header = f.read(len(SYMREF))
 
173
            if header == SYMREF:
 
174
                # Read only the first line
 
175
                return header + iter(f).next().rstrip("\r\n")
 
176
            else:
 
177
                # Read only the first 40 bytes
 
178
                return header + f.read(40-len(SYMREF))
 
179
        finally:
 
180
            f.close()
 
181
 
 
182
    def _remove_packed_ref(self, name):
 
183
        if self._packed_refs is None:
 
184
            return
 
185
        # reread cached refs from disk, while holding the lock
 
186
 
 
187
        self._packed_refs = None
 
188
        self.get_packed_refs()
 
189
 
 
190
        if name not in self._packed_refs:
 
191
            return
 
192
 
 
193
        del self._packed_refs[name]
 
194
        if name in self._peeled_refs:
 
195
            del self._peeled_refs[name]
 
196
        f = StringIO()
 
197
        write_packed_refs(f, self._packed_refs, self._peeled_refs)
 
198
        f.seek(0)
 
199
        self.transport.put_file("packed-refs", f)
 
200
 
 
201
    def set_symbolic_ref(self, name, other):
 
202
        """Make a ref point at another ref.
 
203
 
 
204
        :param name: Name of the ref to set
 
205
        :param other: Name of the ref to point at
 
206
        """
 
207
        self._check_refname(name)
 
208
        self._check_refname(other)
 
209
        self.transport.put_bytes(name, SYMREF + other + '\n')
 
210
 
 
211
    def set_if_equals(self, name, old_ref, new_ref):
 
212
        """Set a refname to new_ref only if it currently equals old_ref.
 
213
 
 
214
        This method follows all symbolic references, and can be used to perform
 
215
        an atomic compare-and-swap operation.
 
216
 
 
217
        :param name: The refname to set.
 
218
        :param old_ref: The old sha the refname must refer to, or None to set
 
219
            unconditionally.
 
220
        :param new_ref: The new sha the refname will refer to.
 
221
        :return: True if the set was successful, False otherwise.
 
222
        """
 
223
        try:
 
224
            realname, _ = self._follow(name)
 
225
        except KeyError:
 
226
            realname = name
 
227
        self.transport.put_bytes_non_atomic(realname, new_ref+"\n",
 
228
                create_parent_dir=True)
 
229
        return True
 
230
 
 
231
    def add_if_new(self, name, ref):
 
232
        """Add a new reference only if it does not already exist.
 
233
 
 
234
        This method follows symrefs, and only ensures that the last ref in the
 
235
        chain does not exist.
 
236
 
 
237
        :param name: The refname to set.
 
238
        :param ref: The new sha the refname will refer to.
 
239
        :return: True if the add was successful, False otherwise.
 
240
        """
 
241
        try:
 
242
            realname, contents = self._follow(name)
 
243
            if contents is not None:
 
244
                return False
 
245
        except KeyError:
 
246
            realname = name
 
247
        self._check_refname(realname)
 
248
        self.transport.put_bytes_non_atomic(realname, ref+"\n",
 
249
                create_parent_dir=True)
 
250
        return True
 
251
 
 
252
    def remove_if_equals(self, name, old_ref):
 
253
        """Remove a refname only if it currently equals old_ref.
 
254
 
 
255
        This method does not follow symbolic references. It can be used to
 
256
        perform an atomic compare-and-delete operation.
 
257
 
 
258
        :param name: The refname to delete.
 
259
        :param old_ref: The old sha the refname must refer to, or None to delete
 
260
            unconditionally.
 
261
        :return: True if the delete was successful, False otherwise.
 
262
        """
 
263
        self._check_refname(name)
 
264
        # may only be packed
 
265
        try:
 
266
            self.transport.remove(name)
 
267
        except NoSuchFile:
 
268
            pass
 
269
        self._remove_packed_ref(name)
 
270
        return True
 
271
 
 
272
 
 
273
class TransportRepo(BaseRepo):
 
274
 
 
275
    def __init__(self, transport):
 
276
        self.transport = transport
 
277
        try:
 
278
            if self.transport.has(".git/%s" % OBJECTDIR):
 
279
                self.bare = False
 
280
                self._controltransport = self.transport.clone('.git')
 
281
            elif self.transport.has(OBJECTDIR) or self.transport.has(REFSDIR):
 
282
                self.bare = True
 
283
                self._controltransport = self.transport
 
284
            else:
 
285
                raise NotGitRepository(self.transport)
 
286
        except NoSuchFile:
 
287
            raise NotGitRepository(self.transport)
 
288
        object_store = TransportObjectStore(
 
289
            self._controltransport.clone(OBJECTDIR))
 
290
        super(TransportRepo, self).__init__(object_store, 
 
291
                TransportRefsContainer(self._controltransport))
 
292
 
 
293
    def get_named_file(self, path):
 
294
        """Get a file from the control dir with a specific name.
 
295
 
 
296
        Although the filename should be interpreted as a filename relative to
 
297
        the control dir in a disk-baked Repo, the object returned need not be
 
298
        pointing to a file in that location.
 
299
 
 
300
        :param path: The path to the file, relative to the control dir.
 
301
        :return: An open file object, or None if the file does not exist.
 
302
        """
 
303
        try:
 
304
            return self._controltransport.get(path.lstrip('/'))
 
305
        except NoSuchFile:
 
306
            return None
 
307
 
 
308
    def index_path(self):
 
309
        """Return the path to the index file."""
 
310
        return self._controltransport.local_abspath(INDEX_FILENAME)
 
311
 
 
312
    def open_index(self):
 
313
        """Open the index for this repository."""
 
314
        from dulwich.index import Index
 
315
        if not self.has_index():
 
316
            raise NoIndexPresent()
 
317
        return Index(self.index_path())
 
318
 
 
319
    def has_index(self):
 
320
        return self._controltransport.has(INDEX_FILENAME)
 
321
 
 
322
    def __repr__(self):
 
323
        return "<TransportRepo for %r>" % self.transport
 
324
 
 
325
 
 
326
class TransportObjectStore(PackBasedObjectStore):
 
327
    """Git-style object store that exists on disk."""
 
328
 
 
329
    def __init__(self, transport):
 
330
        """Open an object store.
 
331
 
 
332
        :param transport: Transport to open data from
 
333
        """
 
334
        super(TransportObjectStore, self).__init__()
 
335
        self.transport = transport
 
336
        self.pack_transport = self.transport.clone(PACKDIR)
 
337
    
 
338
    def _pack_cache_stale(self):
 
339
        return False # FIXME
 
340
 
 
341
    def _pack_names(self):
 
342
        try:
 
343
            f = self.transport.get('info/packs')
 
344
        except NoSuchFile:
 
345
            return self.pack_transport.list_dir(".")
 
346
        else:
 
347
            ret = []
 
348
            for line in f.readlines():
 
349
                line = line.rstrip("\n")
 
350
                if not line:
 
351
                    continue
 
352
                (kind, name) = line.split(" ", 1)
 
353
                if kind != "P":
 
354
                    continue
 
355
                ret.append(name)
 
356
            return ret
 
357
 
 
358
    def _load_packs(self):
 
359
        ret = []
 
360
        for name in self._pack_names():
 
361
            if name.startswith("pack-") and name.endswith(".pack"):
 
362
                try:
 
363
                    size = self.pack_transport.stat(name).st_size
 
364
                except TransportNotPossible:
 
365
                    def pd():
 
366
                        # FIXME: This reads the whole pack file at once
 
367
                        f = self.pack_transport.get(name)
 
368
                        contents = f.read()
 
369
                        return PackData(name, StringIO(contents), size=len(contents))
 
370
                else:
 
371
                    pd = lambda: PackData(name, self.pack_transport.get(name),
 
372
                            size=size)
 
373
                idxname = name.replace(".pack", ".idx")
 
374
                idx = lambda: load_pack_index_file(idxname, self.pack_transport.get(idxname))
 
375
                pack = Pack.from_lazy_objects(pd, idx)
 
376
                ret.append(pack)
 
377
        return ret
 
378
 
 
379
    def _iter_loose_objects(self):
 
380
        for base in self.transport.list_dir('.'):
 
381
            if len(base) != 2:
 
382
                continue
 
383
            for rest in self.transport.list_dir(base):
 
384
                yield base+rest
 
385
 
 
386
    def _split_loose_object(self, sha):
 
387
        return (sha[:2], sha[2:])
 
388
 
 
389
    def _remove_loose_object(self, sha):
 
390
        path = '%s/%s' % self._split_loose_object(sha)
 
391
        self.transport.remove(path)
 
392
 
 
393
    def _get_loose_object(self, sha):
 
394
        path = '%s/%s' % self._split_loose_object(sha)
 
395
        try:
 
396
            return ShaFile.from_file(self.transport.get(path))
 
397
        except NoSuchFile:
 
398
            return None
 
399
 
 
400
    def add_object(self, obj):
 
401
        """Add a single object to this object store.
 
402
 
 
403
        :param obj: Object to add
 
404
        """
 
405
        (dir, file) = self._split_loose_object(obj.id)
 
406
        try:
 
407
            self.transport.mkdir(dir)
 
408
        except FileExists:
 
409
            pass
 
410
        path = "%s/%s" % (dir, file)
 
411
        if self.transport.has(path):
 
412
            return # Already there, no need to write again
 
413
        self.transport.put_bytes(path, obj.as_legacy_object())
 
414
 
 
415
    def move_in_pack(self, f):
 
416
        """Move a specific file containing a pack into the pack directory.
 
417
 
 
418
        :note: The file should be on the same file system as the
 
419
            packs directory.
 
420
 
 
421
        :param path: Path to the pack file.
 
422
        """
 
423
        f.seek(0)
 
424
        p = PackData(None, f, len(f.getvalue()))
 
425
        entries = p.sorted_entries()
 
426
        basename = "pack-%s" % iter_sha1(entry[0] for entry in entries)
 
427
        f.seek(0)
 
428
        self.pack_transport.put_file(basename + ".pack", f)
 
429
        idxfile = StringIO()
 
430
        write_pack_index_v2(idxfile, entries, p.get_stored_checksum())
 
431
        idxfile.seek(0)
 
432
        self.pack_transport.put_file(basename + ".idx", idxfile)
 
433
        idxfile.seek(0)
 
434
        idx = load_pack_index_file(basename+".idx", idxfile)
 
435
        final_pack = Pack.from_objects(p, idx)
 
436
        self._add_known_pack(final_pack)
 
437
        return final_pack
 
438
 
 
439
    def add_pack(self):
 
440
        """Add a new pack to this object store. 
 
441
 
 
442
        :return: Fileobject to write to and a commit function to 
 
443
            call when the pack is finished.
 
444
        """
 
445
        from cStringIO import StringIO
 
446
        f = StringIO()
 
447
        def commit():
 
448
            if len(f.getvalue()) > 0:
 
449
                return self.move_in_pack(f)
 
450
            else:
 
451
                return None
 
452
        return f, commit
 
453
 
 
454
    @classmethod
 
455
    def init(cls, transport):
 
456
        transport.mkdir('info')
 
457
        transport.mkdir(PACKDIR)
 
458
        return cls(transport)