/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-02-12 00:39:11 UTC
  • mto: (0.200.718 trunk)
  • mto: This revision was merged to the branch mainline in revision 6960.
  • Revision ID: jelmer@samba.org-20100212003911-hz63ctlfgsvrzrjh
Cope with has_index not existing.

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
 
 
20
from dulwich.errors import (
 
21
    NotGitRepository,
 
22
    )
 
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
    )
 
33
from dulwich.repo import (
 
34
    BaseRepo,
 
35
    RefsContainer,
 
36
    OBJECTDIR,
 
37
    REFSDIR,
 
38
    BASE_DIRECTORIES,
 
39
    SYMREF,
 
40
    check_ref_format,
 
41
    read_packed_refs_with_peeled,
 
42
    read_packed_refs,
 
43
    write_packed_refs,
 
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)
 
70
        super(TransportRepo, self).__init__(object_store, refs)
 
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."""
 
92
        from dulwich.index import Index
 
93
        return Index(self._controltransport.local_abspath('index'))
 
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
 
 
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)