/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

  • Committer: Jelmer Vernooij
  • Date: 2010-05-13 09:40:48 UTC
  • mto: (0.200.912 trunk)
  • mto: This revision was merged to the branch mainline in revision 6960.
  • Revision ID: jelmer@samba.org-20100513094048-ntsaywuymtmcufai
Fix Repository.all_revision_ids.

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