/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

Special-case NULL_REVISION when looking for Git shas.

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