/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 mapping.py

More work on roundtrip push support.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
# Copyright (C) 2007 Canonical Ltd
 
2
# Copyright (C) 2008-2010 Jelmer Vernooij <jelmer@samba.org>
 
3
# Copyright (C) 2008 John Carr
2
4
#
3
5
# This program is free software; you can redistribute it and/or modify
4
6
# it under the terms of the GNU General Public License as published by
16
18
 
17
19
"""Converters, etc for going between Bazaar and Git ids."""
18
20
 
19
 
from bzrlib import errors, foreign
 
21
import base64
 
22
import stat
 
23
 
 
24
from bzrlib import (
 
25
    bencode,
 
26
    errors,
 
27
    foreign,
 
28
    trace,
 
29
    )
 
30
from bzrlib.inventory import (
 
31
    ROOT_ID,
 
32
    )
 
33
from bzrlib.foreign import (
 
34
    ForeignVcs,
 
35
    VcsMappingRegistry,
 
36
    ForeignRevision,
 
37
    )
 
38
from bzrlib.revision import (
 
39
    NULL_REVISION,
 
40
    )
 
41
from bzrlib.plugins.git.hg import (
 
42
    format_hg_metadata,
 
43
    extract_hg_metadata,
 
44
    )
 
45
from bzrlib.plugins.git.roundtrip import (
 
46
    extract_bzr_metadata,
 
47
    inject_bzr_metadata,
 
48
    BzrGitRevisionMetadata,
 
49
    deserialize_fileid_map,
 
50
    serialize_fileid_map,
 
51
    )
 
52
 
 
53
DEFAULT_FILE_MODE = stat.S_IFREG | 0644
 
54
 
 
55
 
 
56
def escape_file_id(file_id):
 
57
    return file_id.replace('_', '__').replace(' ', '_s')
 
58
 
 
59
 
 
60
def unescape_file_id(file_id):
 
61
    ret = []
 
62
    i = 0
 
63
    while i < len(file_id):
 
64
        if file_id[i] != '_':
 
65
            ret.append(file_id[i])
 
66
        else:
 
67
            if file_id[i+1] == '_':
 
68
                ret.append("_")
 
69
            elif file_id[i+1] == 's':
 
70
                ret.append(" ")
 
71
            else:
 
72
                raise AssertionError("unknown escape character %s" %
 
73
                    file_id[i+1])
 
74
            i += 1
 
75
        i += 1
 
76
    return "".join(ret)
 
77
 
 
78
 
 
79
def fix_person_identifier(text):
 
80
    if "<" in text and ">" in text:
 
81
        return text
 
82
    return "%s <%s>" % (text, text)
 
83
 
 
84
 
 
85
def warn_escaped(commit, num_escaped):
 
86
    trace.warning("Escaped %d XML-invalid characters in %s. Will be unable "
 
87
                  "to regenerate the SHA map.", num_escaped, commit)
 
88
 
 
89
 
 
90
def warn_unusual_mode(commit, path, mode):
 
91
    trace.mutter("Unusual file mode %o for %s in %s. Storing as revision "
 
92
                 "property. ", mode, path, commit)
20
93
 
21
94
 
22
95
class BzrGitMapping(foreign.VcsMapping):
23
96
    """Class that maps between Git and Bazaar semantics."""
24
97
    experimental = False
25
98
 
26
 
    def revision_id_foreign_to_bzr(self, git_rev_id):
 
99
    BZR_FILE_IDS_FILE = None
 
100
 
 
101
    BZR_DUMMY_FILE = None
 
102
 
 
103
    def __init__(self):
 
104
        super(BzrGitMapping, self).__init__(foreign_git)
 
105
 
 
106
    def __eq__(self, other):
 
107
        return (type(self) == type(other) and
 
108
                self.revid_prefix == other.revid_prefix)
 
109
 
 
110
    @classmethod
 
111
    def revision_id_foreign_to_bzr(cls, git_rev_id):
27
112
        """Convert a git revision id handle to a Bazaar revision id."""
28
 
        return "%s:%s" % (self.revid_prefix, git_rev_id)
 
113
        from dulwich.protocol import ZERO_SHA
 
114
        if git_rev_id == ZERO_SHA:
 
115
            return NULL_REVISION
 
116
        return "%s:%s" % (cls.revid_prefix, git_rev_id)
29
117
 
30
 
    def revision_id_bzr_to_foreign(self, bzr_rev_id):
 
118
    @classmethod
 
119
    def revision_id_bzr_to_foreign(cls, bzr_rev_id):
31
120
        """Convert a Bazaar revision id to a git revision id handle."""
32
 
        if not bzr_rev_id.startswith("%s:" % self.revid_prefix):
33
 
            raise errors.InvalidRevisionId(bzr_rev_id, self)
34
 
        return bzr_rev_id[len(self.revid_prefix)+1:]
35
 
 
36
 
    def show_foreign_revid(self, foreign_revid):
37
 
        return { "git commit": foreign_revid }
38
 
 
39
 
 
40
 
class BzrGitMappingExperimental(BzrGitMapping):
 
121
        if not bzr_rev_id.startswith("%s:" % cls.revid_prefix):
 
122
            raise errors.InvalidRevisionId(bzr_rev_id, cls)
 
123
        return bzr_rev_id[len(cls.revid_prefix)+1:], cls()
 
124
 
 
125
    def generate_file_id(self, path):
 
126
        # Git paths are just bytestrings
 
127
        # We must just hope they are valid UTF-8..
 
128
        if path == "":
 
129
            return ROOT_ID
 
130
        if type(path) is unicode:
 
131
            path = path.encode("utf-8")
 
132
        return escape_file_id(path)
 
133
 
 
134
    def is_control_file(self, path):
 
135
        return path in (self.BZR_FILE_IDS_FILE, self.BZR_DUMMY_FILE)
 
136
 
 
137
    def parse_file_id(self, file_id):
 
138
        if file_id == ROOT_ID:
 
139
            return ""
 
140
        return unescape_file_id(file_id)
 
141
 
 
142
    def revid_as_refname(self, revid):
 
143
        import urllib
 
144
        return "refs/bzr/%s" % urllib.quote(revid)
 
145
 
 
146
    def import_unusual_file_modes(self, rev, unusual_file_modes):
 
147
        if unusual_file_modes:
 
148
            ret = [(path, unusual_file_modes[path])
 
149
                   for path in sorted(unusual_file_modes.keys())]
 
150
            rev.properties['file-modes'] = bencode.bencode(ret)
 
151
 
 
152
    def export_unusual_file_modes(self, rev):
 
153
        try:
 
154
            file_modes = rev.properties['file-modes']
 
155
        except KeyError:
 
156
            return {}
 
157
        else:
 
158
            return dict([(self.generate_file_id(path), mode) for (path, mode) in bencode.bdecode(file_modes.encode("utf-8"))])
 
159
 
 
160
    def _generate_git_svn_metadata(self, rev, encoding):
 
161
        try:
 
162
            git_svn_id = rev.properties["git-svn-id"]
 
163
        except KeyError:
 
164
            return ""
 
165
        else:
 
166
            return "\ngit-svn-id: %s\n" % git_svn_id.encode(encoding)
 
167
 
 
168
    def _generate_hg_message_tail(self, rev):
 
169
        extra = {}
 
170
        renames = []
 
171
        branch = 'default'
 
172
        for name in rev.properties:
 
173
            if name == 'hg:extra:branch':
 
174
                branch = rev.properties['hg:extra:branch']
 
175
            elif name.startswith('hg:extra'):
 
176
                extra[name[len('hg:extra:'):]] = base64.b64decode(
 
177
                    rev.properties[name])
 
178
            elif name == 'hg:renames':
 
179
                renames = bencode.bdecode(base64.b64decode(
 
180
                    rev.properties['hg:renames']))
 
181
            # TODO: Export other properties as 'bzr:' extras?
 
182
        ret = format_hg_metadata(renames, branch, extra)
 
183
        assert isinstance(ret, str)
 
184
        return ret
 
185
 
 
186
    def _extract_git_svn_metadata(self, rev, message):
 
187
        lines = message.split("\n")
 
188
        if not (lines[-1] == "" and len(lines) >= 2 and lines[-2].startswith("git-svn-id:")):
 
189
            return message
 
190
        git_svn_id = lines[-2].split(": ", 1)[1]
 
191
        rev.properties['git-svn-id'] = git_svn_id
 
192
        (url, rev, uuid) = parse_git_svn_id(git_svn_id)
 
193
        # FIXME: Convert this to converted-from property somehow..
 
194
        ret = "\n".join(lines[:-2])
 
195
        assert isinstance(ret, str)
 
196
        return ret
 
197
 
 
198
    def _extract_hg_metadata(self, rev, message):
 
199
        (message, renames, branch, extra) = extract_hg_metadata(message)
 
200
        if branch is not None:
 
201
            rev.properties['hg:extra:branch'] = branch
 
202
        for name, value in extra.iteritems():
 
203
            rev.properties['hg:extra:' + name] = base64.b64encode(value)
 
204
        if renames:
 
205
            rev.properties['hg:renames'] = base64.b64encode(bencode.bencode(
 
206
                [(new, old) for (old, new) in renames.iteritems()]))
 
207
        return message
 
208
 
 
209
    def _extract_bzr_metadata(self, rev, message):
 
210
        (message, metadata) = extract_bzr_metadata(message)
 
211
        return message, metadata
 
212
 
 
213
    def _decode_commit_message(self, rev, message, encoding):
 
214
        return message.decode(encoding), BzrGitRevisionMetadata()
 
215
 
 
216
    def _encode_commit_message(self, rev, message, encoding):
 
217
        return message.encode(encoding)
 
218
 
 
219
    def export_fileid_map(self, fileid_map):
 
220
        """Export a file id map to a fileid map.
 
221
 
 
222
        :param fileid_map: File id map, mapping paths to file ids
 
223
        :return: A Git blob object
 
224
        """
 
225
        from dulwich.objects import Blob
 
226
        b = Blob()
 
227
        b.set_raw_chunks(serialize_fileid_map(fileid_map))
 
228
        return b
 
229
 
 
230
    def export_commit(self, rev, tree_sha, parent_lookup, roundtrip,
 
231
                      verifiers):
 
232
        """Turn a Bazaar revision in to a Git commit
 
233
 
 
234
        :param tree_sha: Tree sha for the commit
 
235
        :param parent_lookup: Function for looking up the GIT sha equiv of a
 
236
            bzr revision
 
237
        :param roundtrip: Whether to store roundtripping information.
 
238
        :param verifiers: Verifiers info
 
239
        :return dulwich.objects.Commit represent the revision:
 
240
        """
 
241
        from dulwich.objects import Commit
 
242
        commit = Commit()
 
243
        commit.tree = tree_sha
 
244
        if roundtrip:
 
245
            metadata = BzrGitRevisionMetadata()
 
246
            metadata.verifiers = verifiers
 
247
        else:
 
248
            metadata = None
 
249
        parents = []
 
250
        for p in rev.parent_ids:
 
251
            try:
 
252
                git_p = parent_lookup(p)
 
253
            except KeyError:
 
254
                git_p = None
 
255
                if metadata is not None:
 
256
                    metadata.explicit_parent_ids = rev.parent_ids
 
257
            if git_p is not None:
 
258
                assert len(git_p) == 40, "unexpected length for %r" % git_p
 
259
                parents.append(git_p)
 
260
        commit.parents = parents
 
261
        try:
 
262
            encoding = rev.properties['git-explicit-encoding']
 
263
        except KeyError:
 
264
            encoding = rev.properties.get('git-implicit-encoding', 'utf-8')
 
265
        commit.encoding = rev.properties.get('git-explicit-encoding')
 
266
        commit.committer = fix_person_identifier(rev.committer.encode(
 
267
            encoding))
 
268
        commit.author = fix_person_identifier(
 
269
            rev.get_apparent_authors()[0].encode(encoding))
 
270
        commit.commit_time = long(rev.timestamp)
 
271
        if 'author-timestamp' in rev.properties:
 
272
            commit.author_time = long(rev.properties['author-timestamp'])
 
273
        else:
 
274
            commit.author_time = commit.commit_time
 
275
        commit._commit_timezone_neg_utc = "commit-timezone-neg-utc" in rev.properties
 
276
        commit.commit_timezone = rev.timezone
 
277
        commit._author_timezone_neg_utc = "author-timezone-neg-utc" in rev.properties
 
278
        if 'author-timezone' in rev.properties:
 
279
            commit.author_timezone = int(rev.properties['author-timezone'])
 
280
        else:
 
281
            commit.author_timezone = commit.commit_timezone
 
282
        commit.message = self._encode_commit_message(rev, rev.message, 
 
283
            encoding)
 
284
        assert type(commit.message) == str
 
285
        if metadata is not None:
 
286
            try:
 
287
                mapping_registry.parse_revision_id(rev.revision_id)
 
288
            except errors.InvalidRevisionId:
 
289
                metadata.revision_id = rev.revision_id
 
290
            mapping_properties = set(
 
291
                ['author', 'author-timezone', 'author-timezone-neg-utc',
 
292
                 'commit-timezone-neg-utc', 'git-implicit-encoding',
 
293
                 'git-explicit-encoding', 'author-timestamp', 'file-modes'])
 
294
            for k, v in rev.properties.iteritems():
 
295
                if not k in mapping_properties:
 
296
                    metadata.properties[k] = v
 
297
        if self.roundtripping:
 
298
            commit.message = inject_bzr_metadata(commit.message, metadata, 
 
299
                                                 encoding)
 
300
        assert type(commit.message) == str
 
301
        return commit
 
302
 
 
303
    def import_fileid_map(self, blob):
 
304
        """Convert a git file id map blob.
 
305
 
 
306
        :param blob: Git blob object with fileid map
 
307
        :return: Dictionary mapping paths to file ids
 
308
        """
 
309
        return deserialize_fileid_map(blob.data)
 
310
 
 
311
    def import_commit(self, commit, lookup_parent_revid):
 
312
        """Convert a git commit to a bzr revision.
 
313
 
 
314
        :return: a `bzrlib.revision.Revision` object, foreign revid and a
 
315
            testament sha1
 
316
        """
 
317
        if commit is None:
 
318
            raise AssertionError("Commit object can't be None")
 
319
        rev = ForeignRevision(commit.id, self,
 
320
                self.revision_id_foreign_to_bzr(commit.id))
 
321
        rev.parent_ids = tuple([lookup_parent_revid(p) for p in commit.parents])
 
322
        rev.git_metadata = None
 
323
        def decode_using_encoding(rev, commit, encoding):
 
324
            rev.committer = str(commit.committer).decode(encoding)
 
325
            if commit.committer != commit.author:
 
326
                rev.properties['author'] = str(commit.author).decode(encoding)
 
327
            rev.message, rev.git_metadata = self._decode_commit_message(
 
328
                rev, commit.message, encoding)
 
329
        if commit.encoding is not None:
 
330
            rev.properties['git-explicit-encoding'] = commit.encoding
 
331
            decode_using_encoding(rev, commit, commit.encoding)
 
332
        else:
 
333
            for encoding in ('utf-8', 'latin1'):
 
334
                try:
 
335
                    decode_using_encoding(rev, commit, encoding)
 
336
                except UnicodeDecodeError:
 
337
                    pass
 
338
                else:
 
339
                    if encoding != 'utf-8':
 
340
                        rev.properties['git-implicit-encoding'] = encoding
 
341
                    break
 
342
        if commit.commit_time != commit.author_time:
 
343
            rev.properties['author-timestamp'] = str(commit.author_time)
 
344
        if commit.commit_timezone != commit.author_timezone:
 
345
            rev.properties['author-timezone'] = "%d" % commit.author_timezone
 
346
        if commit._author_timezone_neg_utc:
 
347
            rev.properties['author-timezone-neg-utc'] = ""
 
348
        if commit._commit_timezone_neg_utc:
 
349
            rev.properties['commit-timezone-neg-utc'] = ""
 
350
        rev.timestamp = commit.commit_time
 
351
        rev.timezone = commit.commit_timezone
 
352
        if rev.git_metadata is not None:
 
353
            md = rev.git_metadata
 
354
            roundtrip_revid = md.revision_id
 
355
            if md.explicit_parent_ids:
 
356
                rev.parent_ids = md.explicit_parent_ids
 
357
            rev.properties.update(md.properties)
 
358
            verifiers = md.verifiers
 
359
        else:
 
360
            roundtrip_revid = None
 
361
            verifiers = {}
 
362
        return rev, roundtrip_revid, verifiers
 
363
 
 
364
    def get_fileid_map(self, lookup_object, tree_sha):
 
365
        """Obtain a fileid map for a particular tree.
 
366
 
 
367
        :param lookup_object: Function for looking up an object
 
368
        :param tree_sha: SHA of the root tree
 
369
        :return: GitFileIdMap instance
 
370
        """
 
371
        try:
 
372
            file_id_map_sha = lookup_object(tree_sha)[self.BZR_FILE_IDS_FILE][1]
 
373
        except KeyError:
 
374
            file_ids = {}
 
375
        else:
 
376
            file_ids = self.import_fileid_map(lookup_object(file_id_map_sha))
 
377
        return GitFileIdMap(file_ids, self)
 
378
 
 
379
 
 
380
class BzrGitMappingv1(BzrGitMapping):
 
381
    revid_prefix = 'git-v1'
 
382
    experimental = False
 
383
 
 
384
    def __str__(self):
 
385
        return self.revid_prefix
 
386
 
 
387
 
 
388
class BzrGitMappingExperimental(BzrGitMappingv1):
41
389
    revid_prefix = 'git-experimental'
42
390
    experimental = True
43
 
 
44
 
 
45
 
default_mapping = BzrGitMappingExperimental()
 
391
    roundtripping = True
 
392
 
 
393
    BZR_FILE_IDS_FILE = '.bzrfileids'
 
394
 
 
395
    BZR_DUMMY_FILE = '.bzrdummy'
 
396
 
 
397
    def _decode_commit_message(self, rev, message, encoding):
 
398
        message = self._extract_hg_metadata(rev, message)
 
399
        message = self._extract_git_svn_metadata(rev, message)
 
400
        message, metadata = self._extract_bzr_metadata(rev, message)
 
401
        return message.decode(encoding), metadata
 
402
 
 
403
    def _encode_commit_message(self, rev, message, encoding):
 
404
        ret = message.encode(encoding)
 
405
        ret += self._generate_hg_message_tail(rev)
 
406
        ret += self._generate_git_svn_metadata(rev, encoding)
 
407
        return ret
 
408
 
 
409
    def import_commit(self, commit, lookup_parent_revid):
 
410
        rev, roundtrip_revid, verifiers = super(BzrGitMappingExperimental, self).import_commit(commit, lookup_parent_revid)
 
411
        rev.properties['converted_revision'] = "git %s\n" % commit.id
 
412
        return rev, roundtrip_revid, verifiers
 
413
 
 
414
 
 
415
class GitMappingRegistry(VcsMappingRegistry):
 
416
    """Registry with available git mappings."""
 
417
 
 
418
    def revision_id_bzr_to_foreign(self, bzr_revid):
 
419
        if bzr_revid == NULL_REVISION:
 
420
            from dulwich.protocol import ZERO_SHA
 
421
            return ZERO_SHA, None
 
422
        if not bzr_revid.startswith("git-"):
 
423
            raise errors.InvalidRevisionId(bzr_revid, None)
 
424
        (mapping_version, git_sha) = bzr_revid.split(":", 1)
 
425
        mapping = self.get(mapping_version)
 
426
        return mapping.revision_id_bzr_to_foreign(bzr_revid)
 
427
 
 
428
    parse_revision_id = revision_id_bzr_to_foreign
 
429
 
 
430
 
 
431
mapping_registry = GitMappingRegistry()
 
432
mapping_registry.register_lazy('git-v1', "bzrlib.plugins.git.mapping",
 
433
    "BzrGitMappingv1")
 
434
mapping_registry.register_lazy('git-experimental',
 
435
    "bzrlib.plugins.git.mapping", "BzrGitMappingExperimental")
 
436
mapping_registry.set_default('git-v1')
 
437
 
 
438
 
 
439
class ForeignGit(ForeignVcs):
 
440
    """The Git Stupid Content Tracker"""
 
441
 
 
442
    @property
 
443
    def branch_format(self):
 
444
        from bzrlib.plugins.git.branch import GitBranchFormat
 
445
        return GitBranchFormat()
 
446
 
 
447
    @property
 
448
    def repository_format(self):
 
449
        from bzrlib.plugins.git.repository import GitRepositoryFormat
 
450
        return GitRepositoryFormat()
 
451
 
 
452
    def __init__(self):
 
453
        super(ForeignGit, self).__init__(mapping_registry)
 
454
        self.abbreviation = "git"
 
455
 
 
456
    @classmethod
 
457
    def serialize_foreign_revid(self, foreign_revid):
 
458
        return foreign_revid
 
459
 
 
460
    @classmethod
 
461
    def show_foreign_revid(cls, foreign_revid):
 
462
        return { "git commit": foreign_revid }
 
463
 
 
464
 
 
465
foreign_git = ForeignGit()
 
466
default_mapping = mapping_registry.get_default()()
 
467
 
 
468
 
 
469
def symlink_to_blob(entry):
 
470
    from dulwich.objects import Blob
 
471
    blob = Blob()
 
472
    symlink_target = entry.symlink_target
 
473
    if type(symlink_target) == unicode:
 
474
        symlink_target = symlink_target.encode('utf-8')
 
475
    blob.data = symlink_target
 
476
    return blob
 
477
 
 
478
 
 
479
def mode_is_executable(mode):
 
480
    """Check if mode should be considered executable."""
 
481
    return bool(mode & 0111)
 
482
 
 
483
 
 
484
def mode_kind(mode):
 
485
    """Determine the Bazaar inventory kind based on Unix file mode."""
 
486
    entry_kind = (mode & 0700000) / 0100000
 
487
    if entry_kind == 0:
 
488
        return 'directory'
 
489
    elif entry_kind == 1:
 
490
        file_kind = (mode & 070000) / 010000
 
491
        if file_kind == 0:
 
492
            return 'file'
 
493
        elif file_kind == 2:
 
494
            return 'symlink'
 
495
        elif file_kind == 6:
 
496
            return 'tree-reference'
 
497
        else:
 
498
            raise AssertionError(
 
499
                "Unknown file kind %d, perms=%o." % (file_kind, mode,))
 
500
    else:
 
501
        raise AssertionError(
 
502
            "Unknown kind, perms=%r." % (mode,))
 
503
 
 
504
 
 
505
def object_mode(kind, executable):
 
506
    if kind == 'directory':
 
507
        return stat.S_IFDIR
 
508
    elif kind == 'symlink':
 
509
        mode = stat.S_IFLNK
 
510
        if executable:
 
511
            mode |= 0111
 
512
        return mode
 
513
    elif kind == 'file':
 
514
        mode = stat.S_IFREG | 0644
 
515
        if executable:
 
516
            mode |= 0111
 
517
        return mode
 
518
    elif kind == 'tree-reference':
 
519
        from dulwich.objects import S_IFGITLINK
 
520
        return S_IFGITLINK
 
521
    else:
 
522
        raise AssertionError
 
523
 
 
524
 
 
525
def entry_mode(entry):
 
526
    """Determine the git file mode for an inventory entry."""
 
527
    return object_mode(entry.kind, entry.executable)
 
528
 
 
529
 
 
530
def directory_to_tree(entry, lookup_ie_sha1, unusual_modes, empty_file_name):
 
531
    """Create a Git Tree object from a Bazaar directory.
 
532
 
 
533
    :param entry: Inventory entry
 
534
    :param lookup_ie_sha1: Lookup the Git SHA1 for a inventory entry
 
535
    :param unusual_modes: Dictionary with unusual file modes by file ids
 
536
    :param empty_file_name: Name to use for dummy files in empty directories,
 
537
        None to ignore empty directories.
 
538
    """
 
539
    from dulwich.objects import Blob, Tree
 
540
    tree = Tree()
 
541
    for name, value in entry.children.iteritems():
 
542
        ie = entry.children[name]
 
543
        try:
 
544
            mode = unusual_modes[ie.file_id]
 
545
        except KeyError:
 
546
            mode = entry_mode(ie)
 
547
        hexsha = lookup_ie_sha1(ie)
 
548
        if hexsha is not None:
 
549
            tree.add(mode, name.encode("utf-8"), hexsha)
 
550
    if entry.parent_id is not None and len(tree) == 0:
 
551
        # Only the root can be an empty tree
 
552
        if empty_file_name is not None:
 
553
            tree.add(stat.S_IFREG | 0644, empty_file_name, 
 
554
                Blob().id)
 
555
        else:
 
556
            return None
 
557
    return tree
 
558
 
 
559
 
 
560
def extract_unusual_modes(rev):
 
561
    try:
 
562
        foreign_revid, mapping = mapping_registry.parse_revision_id(
 
563
            rev.revision_id)
 
564
    except errors.InvalidRevisionId:
 
565
        return {}
 
566
    else:
 
567
        return mapping.export_unusual_file_modes(rev)
 
568
 
 
569
 
 
570
def parse_git_svn_id(text):
 
571
    (head, uuid) = text.rsplit(" ", 1)
 
572
    (full_url, rev) = head.rsplit("@", 1)
 
573
    return (full_url, int(rev), uuid)
 
574
 
 
575
 
 
576
class GitFileIdMap(object):
 
577
 
 
578
    def __init__(self, file_ids, mapping):
 
579
        self.file_ids = file_ids
 
580
        self.paths = None
 
581
        self.mapping = mapping
 
582
 
 
583
    def lookup_file_id(self, path):
 
584
        assert type(path) is str
 
585
        try:
 
586
            file_id = self.file_ids[path]
 
587
        except KeyError:
 
588
            file_id = self.mapping.generate_file_id(path)
 
589
        assert type(file_id) is str
 
590
        return file_id
 
591
 
 
592
    def lookup_path(self, file_id):
 
593
        if self.paths is None:
 
594
            self.paths = {}
 
595
            for k, v in self.file_ids.iteritems():
 
596
                self.paths[v] = k
 
597
        try:
 
598
            path = self.paths[file_id]
 
599
        except KeyError:
 
600
            return self.mapping.parse_file_id(file_id)
 
601
        else:
 
602
            assert type(path) is str
 
603
            return path