/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar
0.246.1 by Jelmer Vernooij
Add TransportRepo class.
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
20
from dulwich.errors import (
21
    NotGitRepository,
22
    )
0.246.4 by Jelmer Vernooij
more work on transportgit.
23
from dulwich.objects import (
24
    ShaFile,
25
    )
26
from dulwich.object_store import (
27
    PackBasedObjectStore,
28
    PACKDIR,
29
    )
30
from dulwich.pack import (
31
    Pack,
32
    )
0.246.1 by Jelmer Vernooij
Add TransportRepo class.
33
from dulwich.repo import (
34
    BaseRepo,
0.246.4 by Jelmer Vernooij
more work on transportgit.
35
    RefsContainer,
0.246.1 by Jelmer Vernooij
Add TransportRepo class.
36
    OBJECTDIR,
37
    REFSDIR,
0.246.4 by Jelmer Vernooij
more work on transportgit.
38
    BASE_DIRECTORIES,
39
    SYMREF,
40
    check_ref_format,
41
    read_packed_refs_with_peeled,
42
    read_packed_refs,
43
    write_packed_refs,
0.246.1 by Jelmer Vernooij
Add TransportRepo class.
44
    )
45
46
from bzrlib import (
47
    urlutils,
48
    )
49
from bzrlib.errors import (
50
    NoSuchFile,
51
    )
52
53
54
class TransportRepo(BaseRepo):
55
56
    def __init__(self, transport):
57
        self.transport = transport
58
        if self.transport.has(urlutils.join(".git", OBJECTDIR)):
59
            self.bare = False
60
            self._controltransport = self.transport.clone('.git')
61
        elif (self.transport.has(OBJECTDIR) and
62
              self.transport.has(REFSDIR)):
63
            self.bare = True
64
            self._controltransport = self.transport
65
        else:
66
            raise NotGitRepository(self.transport)
67
        object_store = TransportObjectStore(
68
            self._controltransport.clone(OBJECTDIR))
69
        refs = TransportRefsContainer(self._controltransport)
0.246.4 by Jelmer Vernooij
more work on transportgit.
70
        super(TransportRepo, self).__init__(object_store, refs)
0.246.1 by Jelmer Vernooij
Add TransportRepo class.
71
72
    def get_named_file(self, path):
73
        """Get a file from the control dir with a specific name.
74
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.
78
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.
81
        """
82
        try:
83
            return self._controltransport.get(path.lstrip('/'))
84
        except NoSuchFile:
85
            return None
86
87
    def put_named_file(self, path, contents):
88
        self._controltransport.put_bytes(path.lstrip('/'), contents)
89
90
    def open_index(self):
91
        """Open the index for this repository."""
0.246.4 by Jelmer Vernooij
more work on transportgit.
92
        from dulwich.index import Index
93
        return Index(self._controltransport.local_abspath('index'))
0.246.1 by Jelmer Vernooij
Add TransportRepo class.
94
95
    def __repr__(self):
96
        return "<TransportRepo for %r>" % self.transport
97
98
    @classmethod
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)
104
105
    @classmethod
106
    def init_bare(cls, transport, mkdir=True):
107
        for d in BASE_DIRECTORIES:
108
            transport.mkdir(urlutils.join(*d))
109
        ret = cls(transport)
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
114
    filemode = true
115
    bare = false
116
    logallrefupdates = true
117
""")
118
        ret.put_named_file('info/excludes', '')
119
        return ret
120
121
    create = init_bare
122
0.246.4 by Jelmer Vernooij
more work on transportgit.
123
124
class TransportObjectStore(PackBasedObjectStore):
125
    """Git-style object store that exists on disk."""
126
127
    def __init__(self, transport):
128
        """Open an object store.
129
130
        :param transport: Transport to open data from
131
        """
132
        super(TransportObjectStore, self).__init__()
133
        self.transport = transport
134
        self.pack_transport = self.transport.clone(PACKDIR)
135
136
    def _load_packs(self):
137
        pack_files = []
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]
147
148
    def _iter_loose_objects(self):
149
        for base in self.transport.list_dir('.'):
150
            if len(base) != 2:
151
                continue
152
            for rest in self.transport.list_dir(base):
153
                yield base+rest
154
155
    def _split_loose_object(self, sha):
156
        return (sha[:2], sha[2:])
157
158
    def _get_loose_object(self, sha):
159
        path = '%s/%s' % self._split_loose_object(sha)
160
        try:
161
            return ShaFile.from_file(self.transport.get(path))
162
        except NoSuchFile:
163
            return None
164
165
    def add_object(self, obj):
166
        """Add a single object to this object store.
167
168
        :param obj: Object to add
169
        """
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())
176
177
    def add_pack(self):
178
        """Add a new pack to this object store. 
179
180
        :return: Fileobject to write to and a commit function to 
181
            call when the pack is finished.
182
        """
183
        fd, path = tempfile.mkstemp(dir=self.pack_dir, suffix=".pack")
184
        f = os.fdopen(fd, 'wb')
185
        def commit():
186
            os.fsync(fd)
187
            f.close()
188
            if os.path.getsize(path) > 0:
189
                self.move_in_pack(path)
190
        return f, commit
191
192
193
class TransportRefsContainer(RefsContainer):
194
    """Refs container that reads refs from a transport."""
195
196
    def __init__(self, transport):
197
        self.transport = transport
198
        self._packed_refs = None
199
        self._peeled_refs = {}
200
201
    def __repr__(self):
202
        return "%s(%r)" % (self.__class__.__name__, self.transport)
203
204
    def subkeys(self, base):
205
        keys = set()
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)):
214
                    keys.add(refname)
215
        for key in self.get_packed_refs():
216
            if key.startswith(base):
217
                keys.add(key[len(base):].strip("/"))
218
        return keys
219
220
    def allkeys(self):
221
        keys = set()
222
        if self.transport.has(self.refpath("HEAD")):
223
            keys.add("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):
230
                    keys.add(refname)
231
        keys.update(self.get_packed_refs())
232
        return keys
233
234
    def get_packed_refs(self):
235
        """Get contents of the packed-refs file.
236
237
        :return: Dictionary mapping ref names to SHA1s
238
239
        :note: Will return an empty dictionary when no packed-refs file is
240
            present.
241
        """
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')
246
            try:
247
                f = GitFile(path, 'rb')
248
            except IOError, e:
249
                if e.errno == errno.ENOENT:
250
                    return {}
251
                raise
252
            try:
253
                first_line = iter(f).next().rstrip()
254
                if (first_line.startswith("# pack-refs") and " peeled" in
255
                        first_line):
256
                    for sha, name, peeled in read_packed_refs_with_peeled(f):
257
                        self._packed_refs[name] = sha
258
                        if peeled:
259
                            self._peeled_refs[name] = peeled
260
                else:
261
                    f.seek(0)
262
                    for sha, name in read_packed_refs(f):
263
                        self._packed_refs[name] = sha
264
            finally:
265
                f.close()
266
        return self._packed_refs
267
268
    def read_loose_ref(self, name):
269
        try:
270
            f = self.transport.get(name)
271
            try:
272
                header = f.read(len(SYMREF))
273
                if header == SYMREF:
274
                    # Read only the first line
275
                    return header + iter(f).next().rstrip("\n")
276
                else:
277
                    # Read only the first 40 bytes
278
                    return header + f.read(40-len(SYMREF))
279
            finally:
280
                f.close()
281
        except NoSuchFile:
282
            return None
283
284
    def _remove_packed_ref(self, name):
285
        if self._packed_refs is None:
286
            return
287
        filename = os.path.join(self.path, 'packed-refs')
288
        # reread cached refs from disk, while holding the lock
289
        f = GitFile(filename, 'wb')
290
        try:
291
            self._packed_refs = None
292
            self.get_packed_refs()
293
294
            if name not in self._packed_refs:
295
                return
296
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)
301
            f.close()
302
        finally:
303
            f.abort()
304
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.
307
308
        This method follows all symbolic references, and can be used to perform
309
        an atomic compare-and-swap operation.
310
311
        :param name: The refname to set.
312
        :param old_ref: The old sha the refname must refer to, or None to set
313
            unconditionally.
314
        :param new_ref: The new sha the refname will refer to.
315
        :return: True if the set was successful, False otherwise.
316
        """
317
        try:
318
            realname, _ = self._follow(name)
319
        except KeyError:
320
            realname = name
321
        dir_transport = self.transport.clone(urlutils.dirname(realname))
322
        dir_transport.create_prefix()
323
        f = GitFile(filename, 'wb')
324
        try:
325
            if old_ref is not None:
326
                try:
327
                    # read again while holding the lock
328
                    orig_ref = self.read_loose_ref(realname)
329
                    if orig_ref is None:
330
                        orig_ref = self.get_packed_refs().get(realname, None)
331
                    if orig_ref != old_ref:
332
                        f.abort()
333
                        return False
334
                except (OSError, IOError):
335
                    f.abort()
336
                    raise
337
            try:
338
                f.write(new_ref+"\n")
339
            except (OSError, IOError):
340
                f.abort()
341
                raise
342
        finally:
343
            f.close()
344
        return True
345
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')
351
        try:
352
            if self.transport.has(name) or name in self.get_packed_refs():
353
                f.abort()
354
                return False
355
            try:
356
                f.write(ref+"\n")
357
            except (OSError, IOError):
358
                f.abort()
359
                raise
360
        finally:
361
            f.close()
362
        return True
363
364
    def __setitem__(self, name, ref):
365
        """Set a reference name to point to the given SHA1.
366
367
        This method follows all symbolic references.
368
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().
372
        """
373
        self.set_if_equals(name, None, ref)
374
375
    def remove_if_equals(self, name, old_ref):
376
        """Remove a refname only if it currently equals old_ref.
377
378
        This method does not follow symbolic references. It can be used to
379
        perform an atomic compare-and-delete operation.
380
381
        :param name: The refname to delete.
382
        :param old_ref: The old sha the refname must refer to, or None to delete
383
            unconditionally.
384
        :return: True if the delete was successful, False otherwise.
385
        """
386
        self._check_refname(name)
387
        filename = self.refpath(name)
388
        ensure_dir_exists(os.path.dirname(filename))
389
        f = GitFile(filename, 'wb')
390
        try:
391
            if old_ref is not None:
392
                orig_ref = self.read_loose_ref(name)
393
                if orig_ref is None:
394
                    orig_ref = self.get_packed_refs().get(name, None)
395
                if orig_ref != old_ref:
396
                    return False
397
            # may only be packed
398
            if os.path.exists(filename):
399
                os.remove(filename)
400
            self._remove_packed_ref(name)
401
        finally:
402
            # never write, we just wanted the lock
403
            f.abort()
404
        return True
405
406
    def __delitem__(self, name):
407
        """Remove a refname.
408
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().
413
        """
414
        self.remove_if_equals(name, None)