/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar
0.200.1 by Robert Collins
Commit initial content.
1
# Copyright (C) 2006 Canonical Ltd
2
# Authors: Robert Collins <robert.collins@canonical.com>
3
#
4
# This program is free software; you can redistribute it and/or modify
5
# it under the terms of the GNU General Public License as published by
6
# the Free Software Foundation; either version 2 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU General Public License for more details.
13
#
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, write to the Free Software
16
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
17
18
19
"""A GIT branch and repository format implementation for bzr."""
20
21
0.200.2 by Aaron Bentley
Get viz working, with new -r support
22
from StringIO import StringIO
23
0.200.1 by Robert Collins
Commit initial content.
24
import stgit
25
import stgit.git as git
26
0.200.12 by Aaron Bentley
Get checkout of git repos working (though a bit crackful)
27
from bzrlib import config, iterablefile, graph, osutils, urlutils
0.200.1 by Robert Collins
Commit initial content.
28
from bzrlib.decorators import *
29
import bzrlib.branch
30
import bzrlib.bzrdir
31
import bzrlib.errors as errors
32
import bzrlib.repository
33
from bzrlib.revision import Revision
34
35
0.200.4 by Aaron Bentley
Supply a BranchConfig instead of a Transport
36
class GitBranchConfig(config.BranchConfig):
37
    """BranchConfig that uses locations.conf in place of branch.conf""" 
38
39
    def __init__(self, branch):
40
        config.BranchConfig.__init__(self, branch)
41
        # do not provide a BranchDataConfig
42
        self.option_sources = self.option_sources[0], self.option_sources[2]
43
44
    def set_user_option(self, name, value, local=False):
45
        """Force local to True"""
46
        config.BranchConfig.set_user_option(self, name, value, local=True)
0.200.2 by Aaron Bentley
Get viz working, with new -r support
47
48
0.200.1 by Robert Collins
Commit initial content.
49
def gitrevid_from_bzr(revision_id):
0.200.11 by Aaron Bentley
get_revision_graph doesn't require a head revision
50
    if revision_id is None:
51
        return None
0.200.1 by Robert Collins
Commit initial content.
52
    return revision_id[4:]
53
54
55
def bzrrevid_from_git(revision_id):
56
    return "git:" + revision_id
57
58
59
class GitLock(object):
60
    """A lock that thunks through to Git."""
61
62
    def lock_write(self):
63
        pass
64
65
    def lock_read(self):
66
        pass
67
68
    def unlock(self):
69
        pass
70
71
72
class GitLockableFiles(bzrlib.lockable_files.LockableFiles):
73
    """Git specific lockable files abstraction."""
74
75
    def __init__(self, lock):
76
        self._lock = lock
77
        self._transaction = None
78
        self._lock_mode = None
79
        self._lock_count = 0
80
81
82
class GitDir(bzrlib.bzrdir.BzrDir):
83
    """An adapter to the '.git' dir used by git."""
84
85
    def __init__(self, transport, lockfiles, format):
86
        self._format = format
87
        self.root_transport = transport
88
        self.transport = transport.clone('.git')
89
        self._lockfiles = lockfiles
90
91
    def get_branch_transport(self, branch_format):
92
        if branch_format is None:
93
            return self.transport
94
        if isinstance(branch_format, GitBzrDirFormat):
95
            return self.transport
96
        raise errors.IncompatibleFormat(branch_format, self._format)
97
98
    get_repository_transport = get_branch_transport
99
    get_workingtree_transport = get_branch_transport
100
101
    def is_supported(self):
102
        return True
103
104
    def open_branch(self, ignored=None):
105
        """'crate' a branch for this dir."""
106
        return GitBranch(self, self._lockfiles)
107
108
    def open_repository(self, shared=False):
109
        """'open' a repository for this dir."""
110
        return GitRepository(self._gitrepo, self, self._lockfiles)
111
0.200.3 by Aaron Bentley
Avoid ugly errors opening working trees
112
    def open_workingtree(self):
113
        loc = urlutils.unescape_for_display(self.root_transport.base, 'ascii')
114
        raise errors.NoWorkingTree(loc)
115
0.200.1 by Robert Collins
Commit initial content.
116
117
class GitBzrDirFormat(bzrlib.bzrdir.BzrDirFormat):
118
    """The .git directory control format."""
119
120
    @classmethod
121
    def _known_formats(self):
122
        return set([GitBzrDirFormat()])
123
124
    def open(self, transport, _create=False, _found=None):
125
        """Open this directory.
126
        
127
        :param _create: create the git dir on the fly. private to GitDirFormat.
128
        """
129
        # we dont grok readonly - git isn't integrated with transport.
130
        url = transport.base
131
        if url.startswith('readonly+'):
132
            url = url[len('readonly+'):]
133
        if url.startswith('file://'):
134
            url = url[len('file://'):]
135
        url = url.encode('utf8')
0.200.9 by Aaron Bentley
Don't open a GitBzrDir if no .git directory
136
        if not transport.has('.git'):
137
            raise errors.NotBranchError(path=transport.base)
0.200.1 by Robert Collins
Commit initial content.
138
        lockfiles = GitLockableFiles(GitLock())
139
        return GitDir(transport, lockfiles, self)
140
141
    @classmethod
142
    def probe_transport(klass, transport):
143
        """Our format is present if the transport ends in '.not/'."""
144
        # little ugly, but works
145
        format = klass() 
146
        # try a manual probe first, its a little faster perhaps ?
147
        if transport.has('.git'):
148
            return format
149
        # delegate to the main opening code. This pays a double rtt cost at the
150
        # moment, so perhaps we want probe_transport to return the opened thing
151
        # rather than an openener ? or we could return a curried thing with the
152
        # dir to open already instantiated ? Needs more thought.
153
        try:
154
            format.open(transport)
155
            return format
156
        except Exception, e:
157
            raise errors.NotBranchError(path=transport.base)
158
        raise errors.NotBranchError(path=transport.base)
159
160
161
bzrlib.bzrdir.BzrDirFormat.register_control_format(GitBzrDirFormat)
162
163
164
class GitBranch(bzrlib.branch.Branch):
165
    """An adapter to git repositories for bzr Branch objects."""
166
167
    def __init__(self, gitdir, lockfiles):
168
        self.bzrdir = gitdir
169
        self.control_files = lockfiles
170
        self.repository = GitRepository(gitdir, lockfiles)
171
        self.base = gitdir.root_transport.base
0.200.7 by Aaron Bentley
open_branch fails in non-branches
172
        if '.git' not in gitdir.root_transport.list_dir('.'):
173
            raise errors.NotBranchError(self.base)
0.200.1 by Robert Collins
Commit initial content.
174
175
    def lock_write(self):
176
        self.control_files.lock_write()
177
178
    @needs_read_lock
179
    def last_revision(self):
180
        # perhaps should escape this ?
0.200.6 by Aaron Bentley
Cache revisions from graph_ancestry_with_ghosts
181
        return bzrrevid_from_git(self.repository.git.get_head())
0.200.1 by Robert Collins
Commit initial content.
182
0.200.4 by Aaron Bentley
Supply a BranchConfig instead of a Transport
183
    @needs_read_lock
0.200.2 by Aaron Bentley
Get viz working, with new -r support
184
    def revision_history(self):
0.200.6 by Aaron Bentley
Cache revisions from graph_ancestry_with_ghosts
185
        node = self.last_revision()
0.200.10 by Aaron Bentley
Fix bugs in log/revision_graph API
186
        ancestors = self.repository.get_revision_graph(node)
0.200.6 by Aaron Bentley
Cache revisions from graph_ancestry_with_ghosts
187
        history = []
188
        while node is not None:
189
            history.append(node)
190
            if len(ancestors[node]) > 0:
191
                node = ancestors[node][0]
192
            else:
193
                node = None
0.200.2 by Aaron Bentley
Get viz working, with new -r support
194
        return list(reversed(history))
195
0.200.4 by Aaron Bentley
Supply a BranchConfig instead of a Transport
196
    def get_config(self):
197
        return GitBranchConfig(self)
198
0.200.1 by Robert Collins
Commit initial content.
199
    def lock_read(self):
200
        self.control_files.lock_read()
201
202
    def unlock(self):
203
        self.control_files.unlock()
204
0.200.4 by Aaron Bentley
Supply a BranchConfig instead of a Transport
205
    def get_push_location(self):
206
        """See Branch.get_push_location."""
207
        push_loc = self.get_config().get_user_option('push_location')
208
        return push_loc
209
210
    def set_push_location(self, location):
211
        """See Branch.set_push_location."""
212
        self.get_config().set_user_option('push_location', location, 
213
                                          local=True)
214
0.200.1 by Robert Collins
Commit initial content.
215
216
class GitRepository(bzrlib.repository.Repository):
217
    """An adapter to git repositories for bzr."""
218
219
    def __init__(self, gitdir, lockfiles):
220
        self.bzrdir = gitdir
221
        self.control_files = lockfiles
0.200.5 by Aaron Bentley
Switch to using GitModel
222
        gitdirectory = urlutils.local_path_from_url(gitdir.transport.base)
223
        self.git = GitModel(gitdirectory)
0.200.6 by Aaron Bentley
Cache revisions from graph_ancestry_with_ghosts
224
        self._revision_cache = {}
225
226
    def _ancestor_revisions(self, revision_ids):
0.200.11 by Aaron Bentley
get_revision_graph doesn't require a head revision
227
        if revision_ids is not None:
228
            git_revisions = [gitrevid_from_bzr(r) for r in revision_ids]
229
        else:
230
            git_revisions = None
0.200.6 by Aaron Bentley
Cache revisions from graph_ancestry_with_ghosts
231
        for lines in self.git.ancestor_lines(git_revisions):
232
            yield self.parse_rev(lines)
233
0.200.12 by Aaron Bentley
Get checkout of git repos working (though a bit crackful)
234
    def is_shared(self):
235
        return True
236
0.200.10 by Aaron Bentley
Fix bugs in log/revision_graph API
237
    def get_revision_graph(self, revision_id=None):
0.200.11 by Aaron Bentley
get_revision_graph doesn't require a head revision
238
        if revision_id is None:
239
            revisions = None
240
        else:
241
            revisions = [revision_id]
242
        return self.get_revision_graph_with_ghosts(revisions).get_ancestors()
0.200.10 by Aaron Bentley
Fix bugs in log/revision_graph API
243
0.200.6 by Aaron Bentley
Cache revisions from graph_ancestry_with_ghosts
244
    def get_revision_graph_with_ghosts(self, revision_ids=None):
245
        result = graph.Graph()
246
        for revision in self._ancestor_revisions(revision_ids):
247
            result.add_node(revision.revision_id, revision.parent_ids)
248
            self._revision_cache[revision.revision_id] = revision
249
        return result
0.200.1 by Robert Collins
Commit initial content.
250
251
    def get_revision(self, revision_id):
0.200.6 by Aaron Bentley
Cache revisions from graph_ancestry_with_ghosts
252
        if revision_id in self._revision_cache:
253
            return self._revision_cache[revision_id]
0.200.5 by Aaron Bentley
Switch to using GitModel
254
        raw = self.git.rev_list([gitrevid_from_bzr(revision_id)], max_count=1,
255
                                header=True)
0.200.6 by Aaron Bentley
Cache revisions from graph_ancestry_with_ghosts
256
        return self.parse_rev(raw)
257
0.200.12 by Aaron Bentley
Get checkout of git repos working (though a bit crackful)
258
    def has_revision(self, revision_id):
259
        try:
260
            self.get_revision(revision_id)
261
        except NoSuchRevision:
262
            return False
263
        else:
264
            return True
265
0.200.8 by Aaron Bentley
Work on log
266
    def get_revisions(self, revisions):
267
        return [self.get_revision(r) for r in revisions]
268
0.200.6 by Aaron Bentley
Cache revisions from graph_ancestry_with_ghosts
269
    def parse_rev(self, raw):
0.200.1 by Robert Collins
Commit initial content.
270
        # first field is the rev itself.
271
        # then its 'field value'
272
        # until the EOF??
273
        parents = []
274
        log = []
275
        in_log = False
276
        committer = None
0.200.6 by Aaron Bentley
Cache revisions from graph_ancestry_with_ghosts
277
        revision_id = bzrrevid_from_git(raw[0][:-1])
0.200.1 by Robert Collins
Commit initial content.
278
        for field in raw[1:]:
279
            #if field.startswith('author '):
280
            #    committer = field[7:]
281
            if field.startswith('parent '):
282
                parents.append(bzrrevid_from_git(field.split()[1]))
283
            elif field.startswith('committer '):
284
                commit_fields = field.split()
285
                if committer is None:
286
                    committer = ' '.join(commit_fields[1:-3])
287
                timestamp = commit_fields[-2]
288
                timezone = commit_fields[-1]
0.200.12 by Aaron Bentley
Get checkout of git repos working (though a bit crackful)
289
            elif field.startswith('tree '):
290
                tree_id = field.split()[1]
0.200.1 by Robert Collins
Commit initial content.
291
            elif in_log:
0.200.6 by Aaron Bentley
Cache revisions from graph_ancestry_with_ghosts
292
                log.append(field[4:])
0.200.1 by Robert Collins
Commit initial content.
293
            elif field == '\n':
294
                in_log = True
295
296
        log = ''.join(log)
297
        result = Revision(revision_id)
298
        result.parent_ids = parents
299
        result.message = log
300
        result.inventory_sha1 = ""
301
        result.timezone = timezone and int(timezone)
302
        result.timestamp = float(timestamp)
303
        result.committer = committer 
0.200.12 by Aaron Bentley
Get checkout of git repos working (though a bit crackful)
304
        result.properties['git-tree-id'] = tree_id
0.200.1 by Robert Collins
Commit initial content.
305
        return result
0.200.5 by Aaron Bentley
Switch to using GitModel
306
0.200.12 by Aaron Bentley
Get checkout of git repos working (though a bit crackful)
307
    def revision_tree(self, revision_id):
308
        return GitRevisionTree(self, revision_id)
309
310
    def get_inventory(self, revision_id):
311
        revision = self.get_revision(revision_id)
312
        inventory = GitInventory(revision_id)
313
        tree_id = revision.properties['git-tree-id']
314
        type_map = {'blob': 'file', 'tree': 'directory' }
315
        def get_inventory(tree_id, prefix):
316
            for perms, type, obj_id, name in self.git.get_inventory(tree_id):
317
                full_path = prefix + name
318
                if type == 'blob':
319
                    text_sha1 = obj_id
320
                else:
321
                    text_sha1 = None
322
                executable = (perms[-3] in ('1', '3', '5', '7'))
323
                entry = GitEntry(full_path, type_map[type], revision_id,
324
                                 text_sha1, executable)
325
                inventory.entries[full_path] = entry
326
                if type == 'tree':
327
                    get_inventory(obj_id, full_path+'/')
328
        get_inventory(tree_id, '')
329
        return inventory
330
331
332
class GitRevisionTree(object):
333
334
    def __init__(self, repository, revision_id):
335
        self.repository = repository
336
        self.revision_id = revision_id
337
        self.inventory = repository.get_inventory(revision_id)
338
339
    def get_file(self, file_id):
340
        obj_id = self.inventory[file_id].text_sha1
341
        lines = self.repository.git.cat_file('blob', obj_id)
342
        return iterablefile.IterableFile(lines)
343
344
    def is_executable(self, file_id):
345
        return self.inventory[file_id].executable
346
347
348
class GitInventory(object):
349
350
    def __init__(self, revision_id):
351
        self.entries = {}
352
        self.root = GitEntry('', 'directory', revision_id)
353
        self.entries[''] = self.root
354
355
    def __getitem__(self, key):
356
        return self.entries[key]
357
358
    def iter_entries(self):
359
        return iter(sorted(self.entries.items()))
360
361
    def iter_entries_by_dir(self):
362
        return self.iter_entries()
363
364
    def __len__(self):
365
        return len(self.entries)
366
367
368
class GitEntry(object):
369
370
    def __init__(self, path, kind, revision, text_sha1=None, executable=False,
371
                 text_size=None):
372
        self.path = path
373
        self.file_id = path
374
        self.kind = kind
375
        self.executable = executable
376
        self.name = osutils.basename(path)
377
        if path == '':
378
            self.parent_id = None
379
        else:
380
            self.parent_id = osutils.dirname(path)
381
        self.revision = revision
382
        self.symlink_target = None
383
        self.text_sha1 = text_sha1
384
        self.text_size = None
385
386
    def __repr__(self):
387
        return "GitEntry(%r, %r, %r, %r)" % (self.path, self.kind, 
388
                                             self.revision, self.parent_id)
389
390
0.200.5 by Aaron Bentley
Switch to using GitModel
391
class GitModel(object):
392
    """API that follows GIT model closely"""
393
394
    def __init__(self, git_dir):
395
        self.git_dir = git_dir
396
397
    def git_command(self, command, args):
0.200.10 by Aaron Bentley
Fix bugs in log/revision_graph API
398
        args = ' '.join("'%s'" % arg for arg in args)
0.200.5 by Aaron Bentley
Switch to using GitModel
399
        return 'git --git-dir=%s %s %s' % (self.git_dir, command, args) 
400
401
    def git_lines(self, command, args):
402
        return stgit.git._output_lines(self.git_command(command, args))
403
0.200.6 by Aaron Bentley
Cache revisions from graph_ancestry_with_ghosts
404
    def git_line(self, command, args):
405
        return stgit.git._output_one_line(self.git_command(command, args))
406
0.200.12 by Aaron Bentley
Get checkout of git repos working (though a bit crackful)
407
    def cat_file(self, type, object_id, pretty=False):
408
        args = []
409
        if pretty:
410
            args.append('-p')
411
        else:
412
            args.append(type)
413
        args.append(object_id)
414
        return self.git_lines('cat-file', args)
415
0.200.5 by Aaron Bentley
Switch to using GitModel
416
    def rev_list(self, heads, max_count=None, header=False):
417
        args = []
418
        if max_count is not None:
419
            args.append('--max-count=%d' % max_count)
420
        if header is not False:
421
            args.append('--header')
0.200.11 by Aaron Bentley
get_revision_graph doesn't require a head revision
422
        if heads is None:
423
            args.append('--all')
424
        else:
425
            args.extend(heads)
0.200.5 by Aaron Bentley
Switch to using GitModel
426
        return self.git_lines('rev-list', args)
0.200.6 by Aaron Bentley
Cache revisions from graph_ancestry_with_ghosts
427
428
    def rev_parse(self, git_id):
429
        args = ['--verify', git_id]
430
        return self.git_line('rev-parse', args)
431
432
    def get_head(self):
433
        return self.rev_parse('HEAD')
434
435
    def ancestor_lines(self, revisions):
436
        revision_lines = []
437
        for line in self.rev_list(revisions, header=True):
438
            if line.startswith('\x00'):
439
                yield revision_lines
0.200.10 by Aaron Bentley
Fix bugs in log/revision_graph API
440
                revision_lines = [line[1:].decode('latin-1')]
0.200.6 by Aaron Bentley
Cache revisions from graph_ancestry_with_ghosts
441
            else:
0.200.10 by Aaron Bentley
Fix bugs in log/revision_graph API
442
                revision_lines.append(line.decode('latin-1'))
0.200.6 by Aaron Bentley
Cache revisions from graph_ancestry_with_ghosts
443
        assert revision_lines == ['']
0.200.12 by Aaron Bentley
Get checkout of git repos working (though a bit crackful)
444
445
    def get_inventory(self, tree_id):
446
        for line in self.cat_file('tree', tree_id, True):
447
            sections = line.split(' ', 2)
448
            obj_id, name = sections[2].split('\t', 1)
449
            name = name.rstrip('\n')
450
            if name.startswith('"'):
451
                name = name[1:-1].decode('string_escape').decode('utf-8')
452
            yield (sections[0], sections[1], obj_id, name)
0.201.1 by Jelmer Vernooij
Add very small initial testsuite.
453
454
def test_suite():
455
    from unittest import TestSuite, TestLoader
456
    import tests
457
458
    suite = TestSuite()
459
460
    suite.addTest(tests.test_suite())
461
462
    return suite