/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.16 by Jelmer Vernooij
Fix bzr-git for use with current bzr.dev.
27
from bzrlib import config, iterablefile, deprecated_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."""
0.200.13 by Robert Collins
Workaround breakage with bazaar.launchpad.net.
110
        return GitRepository(self, self._lockfiles)
0.200.1 by Robert Collins
Commit initial content.
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+'):]
0.200.13 by Robert Collins
Workaround breakage with bazaar.launchpad.net.
133
        path = urlutils.local_path_from_url(url)
0.200.9 by Aaron Bentley
Don't open a GitBzrDir if no .git directory
134
        if not transport.has('.git'):
135
            raise errors.NotBranchError(path=transport.base)
0.200.1 by Robert Collins
Commit initial content.
136
        lockfiles = GitLockableFiles(GitLock())
137
        return GitDir(transport, lockfiles, self)
138
139
    @classmethod
140
    def probe_transport(klass, transport):
141
        """Our format is present if the transport ends in '.not/'."""
142
        # little ugly, but works
0.200.13 by Robert Collins
Workaround breakage with bazaar.launchpad.net.
143
        format = klass()
0.200.1 by Robert Collins
Commit initial content.
144
        # delegate to the main opening code. This pays a double rtt cost at the
145
        # moment, so perhaps we want probe_transport to return the opened thing
146
        # rather than an openener ? or we could return a curried thing with the
147
        # dir to open already instantiated ? Needs more thought.
148
        try:
149
            format.open(transport)
150
            return format
151
        except Exception, e:
152
            raise errors.NotBranchError(path=transport.base)
153
        raise errors.NotBranchError(path=transport.base)
154
155
156
bzrlib.bzrdir.BzrDirFormat.register_control_format(GitBzrDirFormat)
157
158
0.200.16 by Jelmer Vernooij
Fix bzr-git for use with current bzr.dev.
159
class GitBranchFormat(bzrlib.branch.BranchFormat):
0.200.17 by Jelmer Vernooij
Fix formatting, implement Repository.revision_trees().
160
0.200.16 by Jelmer Vernooij
Fix bzr-git for use with current bzr.dev.
161
    def get_branch_description(self):
162
        return 'Git Branch'
163
164
0.200.1 by Robert Collins
Commit initial content.
165
class GitBranch(bzrlib.branch.Branch):
166
    """An adapter to git repositories for bzr Branch objects."""
167
168
    def __init__(self, gitdir, lockfiles):
169
        self.bzrdir = gitdir
170
        self.control_files = lockfiles
171
        self.repository = GitRepository(gitdir, lockfiles)
172
        self.base = gitdir.root_transport.base
0.200.7 by Aaron Bentley
open_branch fails in non-branches
173
        if '.git' not in gitdir.root_transport.list_dir('.'):
174
            raise errors.NotBranchError(self.base)
0.200.16 by Jelmer Vernooij
Fix bzr-git for use with current bzr.dev.
175
        self._format = GitBranchFormat()
0.200.1 by Robert Collins
Commit initial content.
176
177
    def lock_write(self):
178
        self.control_files.lock_write()
179
180
    @needs_read_lock
181
    def last_revision(self):
182
        # perhaps should escape this ?
0.200.6 by Aaron Bentley
Cache revisions from graph_ancestry_with_ghosts
183
        return bzrrevid_from_git(self.repository.git.get_head())
0.200.1 by Robert Collins
Commit initial content.
184
0.200.4 by Aaron Bentley
Supply a BranchConfig instead of a Transport
185
    @needs_read_lock
0.200.2 by Aaron Bentley
Get viz working, with new -r support
186
    def revision_history(self):
0.200.6 by Aaron Bentley
Cache revisions from graph_ancestry_with_ghosts
187
        node = self.last_revision()
0.200.10 by Aaron Bentley
Fix bugs in log/revision_graph API
188
        ancestors = self.repository.get_revision_graph(node)
0.200.6 by Aaron Bentley
Cache revisions from graph_ancestry_with_ghosts
189
        history = []
190
        while node is not None:
191
            history.append(node)
192
            if len(ancestors[node]) > 0:
193
                node = ancestors[node][0]
194
            else:
195
                node = None
0.200.2 by Aaron Bentley
Get viz working, with new -r support
196
        return list(reversed(history))
197
0.200.4 by Aaron Bentley
Supply a BranchConfig instead of a Transport
198
    def get_config(self):
199
        return GitBranchConfig(self)
200
0.200.1 by Robert Collins
Commit initial content.
201
    def lock_read(self):
202
        self.control_files.lock_read()
203
204
    def unlock(self):
205
        self.control_files.unlock()
206
0.200.4 by Aaron Bentley
Supply a BranchConfig instead of a Transport
207
    def get_push_location(self):
208
        """See Branch.get_push_location."""
209
        push_loc = self.get_config().get_user_option('push_location')
210
        return push_loc
211
212
    def set_push_location(self, location):
213
        """See Branch.set_push_location."""
214
        self.get_config().set_user_option('push_location', location, 
215
                                          local=True)
216
0.200.1 by Robert Collins
Commit initial content.
217
218
class GitRepository(bzrlib.repository.Repository):
219
    """An adapter to git repositories for bzr."""
220
221
    def __init__(self, gitdir, lockfiles):
222
        self.bzrdir = gitdir
223
        self.control_files = lockfiles
0.200.5 by Aaron Bentley
Switch to using GitModel
224
        gitdirectory = urlutils.local_path_from_url(gitdir.transport.base)
225
        self.git = GitModel(gitdirectory)
0.200.6 by Aaron Bentley
Cache revisions from graph_ancestry_with_ghosts
226
        self._revision_cache = {}
227
228
    def _ancestor_revisions(self, revision_ids):
0.200.11 by Aaron Bentley
get_revision_graph doesn't require a head revision
229
        if revision_ids is not None:
230
            git_revisions = [gitrevid_from_bzr(r) for r in revision_ids]
231
        else:
232
            git_revisions = None
0.200.6 by Aaron Bentley
Cache revisions from graph_ancestry_with_ghosts
233
        for lines in self.git.ancestor_lines(git_revisions):
234
            yield self.parse_rev(lines)
235
0.200.12 by Aaron Bentley
Get checkout of git repos working (though a bit crackful)
236
    def is_shared(self):
237
        return True
238
0.200.10 by Aaron Bentley
Fix bugs in log/revision_graph API
239
    def get_revision_graph(self, revision_id=None):
0.200.11 by Aaron Bentley
get_revision_graph doesn't require a head revision
240
        if revision_id is None:
241
            revisions = None
242
        else:
243
            revisions = [revision_id]
244
        return self.get_revision_graph_with_ghosts(revisions).get_ancestors()
0.200.10 by Aaron Bentley
Fix bugs in log/revision_graph API
245
0.200.6 by Aaron Bentley
Cache revisions from graph_ancestry_with_ghosts
246
    def get_revision_graph_with_ghosts(self, revision_ids=None):
0.200.16 by Jelmer Vernooij
Fix bzr-git for use with current bzr.dev.
247
        result = deprecated_graph.Graph()
0.200.6 by Aaron Bentley
Cache revisions from graph_ancestry_with_ghosts
248
        for revision in self._ancestor_revisions(revision_ids):
249
            result.add_node(revision.revision_id, revision.parent_ids)
250
            self._revision_cache[revision.revision_id] = revision
251
        return result
0.200.1 by Robert Collins
Commit initial content.
252
253
    def get_revision(self, revision_id):
0.200.6 by Aaron Bentley
Cache revisions from graph_ancestry_with_ghosts
254
        if revision_id in self._revision_cache:
255
            return self._revision_cache[revision_id]
0.200.5 by Aaron Bentley
Switch to using GitModel
256
        raw = self.git.rev_list([gitrevid_from_bzr(revision_id)], max_count=1,
257
                                header=True)
0.200.6 by Aaron Bentley
Cache revisions from graph_ancestry_with_ghosts
258
        return self.parse_rev(raw)
259
0.200.12 by Aaron Bentley
Get checkout of git repos working (though a bit crackful)
260
    def has_revision(self, revision_id):
261
        try:
262
            self.get_revision(revision_id)
263
        except NoSuchRevision:
264
            return False
265
        else:
266
            return True
267
0.200.8 by Aaron Bentley
Work on log
268
    def get_revisions(self, revisions):
269
        return [self.get_revision(r) for r in revisions]
270
0.200.6 by Aaron Bentley
Cache revisions from graph_ancestry_with_ghosts
271
    def parse_rev(self, raw):
0.200.1 by Robert Collins
Commit initial content.
272
        # first field is the rev itself.
273
        # then its 'field value'
274
        # until the EOF??
275
        parents = []
276
        log = []
277
        in_log = False
278
        committer = None
0.200.6 by Aaron Bentley
Cache revisions from graph_ancestry_with_ghosts
279
        revision_id = bzrrevid_from_git(raw[0][:-1])
0.200.1 by Robert Collins
Commit initial content.
280
        for field in raw[1:]:
281
            #if field.startswith('author '):
282
            #    committer = field[7:]
283
            if field.startswith('parent '):
284
                parents.append(bzrrevid_from_git(field.split()[1]))
285
            elif field.startswith('committer '):
286
                commit_fields = field.split()
287
                if committer is None:
288
                    committer = ' '.join(commit_fields[1:-3])
289
                timestamp = commit_fields[-2]
290
                timezone = commit_fields[-1]
0.200.12 by Aaron Bentley
Get checkout of git repos working (though a bit crackful)
291
            elif field.startswith('tree '):
292
                tree_id = field.split()[1]
0.200.1 by Robert Collins
Commit initial content.
293
            elif in_log:
0.200.6 by Aaron Bentley
Cache revisions from graph_ancestry_with_ghosts
294
                log.append(field[4:])
0.200.1 by Robert Collins
Commit initial content.
295
            elif field == '\n':
296
                in_log = True
297
298
        log = ''.join(log)
299
        result = Revision(revision_id)
300
        result.parent_ids = parents
301
        result.message = log
302
        result.inventory_sha1 = ""
303
        result.timezone = timezone and int(timezone)
304
        result.timestamp = float(timestamp)
305
        result.committer = committer 
0.200.12 by Aaron Bentley
Get checkout of git repos working (though a bit crackful)
306
        result.properties['git-tree-id'] = tree_id
0.200.1 by Robert Collins
Commit initial content.
307
        return result
0.200.5 by Aaron Bentley
Switch to using GitModel
308
0.200.17 by Jelmer Vernooij
Fix formatting, implement Repository.revision_trees().
309
    def revision_trees(self, revids):
310
        for revid in revids:
311
            yield self.revision_tree(revid)
312
0.200.12 by Aaron Bentley
Get checkout of git repos working (though a bit crackful)
313
    def revision_tree(self, revision_id):
314
        return GitRevisionTree(self, revision_id)
315
316
    def get_inventory(self, revision_id):
317
        revision = self.get_revision(revision_id)
318
        inventory = GitInventory(revision_id)
319
        tree_id = revision.properties['git-tree-id']
320
        type_map = {'blob': 'file', 'tree': 'directory' }
321
        def get_inventory(tree_id, prefix):
322
            for perms, type, obj_id, name in self.git.get_inventory(tree_id):
323
                full_path = prefix + name
324
                if type == 'blob':
325
                    text_sha1 = obj_id
326
                else:
327
                    text_sha1 = None
328
                executable = (perms[-3] in ('1', '3', '5', '7'))
329
                entry = GitEntry(full_path, type_map[type], revision_id,
330
                                 text_sha1, executable)
331
                inventory.entries[full_path] = entry
332
                if type == 'tree':
333
                    get_inventory(obj_id, full_path+'/')
334
        get_inventory(tree_id, '')
335
        return inventory
336
337
338
class GitRevisionTree(object):
339
340
    def __init__(self, repository, revision_id):
341
        self.repository = repository
342
        self.revision_id = revision_id
343
        self.inventory = repository.get_inventory(revision_id)
344
345
    def get_file(self, file_id):
346
        obj_id = self.inventory[file_id].text_sha1
347
        lines = self.repository.git.cat_file('blob', obj_id)
348
        return iterablefile.IterableFile(lines)
349
350
    def is_executable(self, file_id):
351
        return self.inventory[file_id].executable
352
353
354
class GitInventory(object):
355
356
    def __init__(self, revision_id):
357
        self.entries = {}
358
        self.root = GitEntry('', 'directory', revision_id)
359
        self.entries[''] = self.root
360
361
    def __getitem__(self, key):
362
        return self.entries[key]
363
364
    def iter_entries(self):
365
        return iter(sorted(self.entries.items()))
366
367
    def iter_entries_by_dir(self):
368
        return self.iter_entries()
369
370
    def __len__(self):
371
        return len(self.entries)
372
373
374
class GitEntry(object):
375
376
    def __init__(self, path, kind, revision, text_sha1=None, executable=False,
377
                 text_size=None):
378
        self.path = path
379
        self.file_id = path
380
        self.kind = kind
381
        self.executable = executable
382
        self.name = osutils.basename(path)
383
        if path == '':
384
            self.parent_id = None
385
        else:
386
            self.parent_id = osutils.dirname(path)
387
        self.revision = revision
388
        self.symlink_target = None
389
        self.text_sha1 = text_sha1
390
        self.text_size = None
391
392
    def __repr__(self):
393
        return "GitEntry(%r, %r, %r, %r)" % (self.path, self.kind, 
394
                                             self.revision, self.parent_id)
395
396
0.200.5 by Aaron Bentley
Switch to using GitModel
397
class GitModel(object):
398
    """API that follows GIT model closely"""
399
400
    def __init__(self, git_dir):
401
        self.git_dir = git_dir
402
403
    def git_command(self, command, args):
0.200.10 by Aaron Bentley
Fix bugs in log/revision_graph API
404
        args = ' '.join("'%s'" % arg for arg in args)
0.200.5 by Aaron Bentley
Switch to using GitModel
405
        return 'git --git-dir=%s %s %s' % (self.git_dir, command, args) 
406
407
    def git_lines(self, command, args):
408
        return stgit.git._output_lines(self.git_command(command, args))
409
0.200.6 by Aaron Bentley
Cache revisions from graph_ancestry_with_ghosts
410
    def git_line(self, command, args):
411
        return stgit.git._output_one_line(self.git_command(command, args))
412
0.200.12 by Aaron Bentley
Get checkout of git repos working (though a bit crackful)
413
    def cat_file(self, type, object_id, pretty=False):
414
        args = []
415
        if pretty:
416
            args.append('-p')
417
        else:
418
            args.append(type)
419
        args.append(object_id)
420
        return self.git_lines('cat-file', args)
421
0.200.5 by Aaron Bentley
Switch to using GitModel
422
    def rev_list(self, heads, max_count=None, header=False):
423
        args = []
424
        if max_count is not None:
425
            args.append('--max-count=%d' % max_count)
426
        if header is not False:
427
            args.append('--header')
0.200.11 by Aaron Bentley
get_revision_graph doesn't require a head revision
428
        if heads is None:
429
            args.append('--all')
430
        else:
431
            args.extend(heads)
0.200.5 by Aaron Bentley
Switch to using GitModel
432
        return self.git_lines('rev-list', args)
0.200.6 by Aaron Bentley
Cache revisions from graph_ancestry_with_ghosts
433
434
    def rev_parse(self, git_id):
435
        args = ['--verify', git_id]
436
        return self.git_line('rev-parse', args)
437
438
    def get_head(self):
439
        return self.rev_parse('HEAD')
440
441
    def ancestor_lines(self, revisions):
442
        revision_lines = []
443
        for line in self.rev_list(revisions, header=True):
444
            if line.startswith('\x00'):
445
                yield revision_lines
0.200.10 by Aaron Bentley
Fix bugs in log/revision_graph API
446
                revision_lines = [line[1:].decode('latin-1')]
0.200.6 by Aaron Bentley
Cache revisions from graph_ancestry_with_ghosts
447
            else:
0.200.10 by Aaron Bentley
Fix bugs in log/revision_graph API
448
                revision_lines.append(line.decode('latin-1'))
0.200.6 by Aaron Bentley
Cache revisions from graph_ancestry_with_ghosts
449
        assert revision_lines == ['']
0.200.12 by Aaron Bentley
Get checkout of git repos working (though a bit crackful)
450
451
    def get_inventory(self, tree_id):
452
        for line in self.cat_file('tree', tree_id, True):
453
            sections = line.split(' ', 2)
454
            obj_id, name = sections[2].split('\t', 1)
455
            name = name.rstrip('\n')
456
            if name.startswith('"'):
457
                name = name[1:-1].decode('string_escape').decode('utf-8')
458
            yield (sections[0], sections[1], obj_id, name)
0.201.1 by Jelmer Vernooij
Add very small initial testsuite.
459
460
def test_suite():
461
    from unittest import TestSuite, TestLoader
462
    import tests
463
464
    suite = TestSuite()
465
466
    suite.addTest(tests.test_suite())
467
468
    return suite