/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

UseĀ get_file_revision.

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
    )
20
33
from bzrlib.foreign import (
21
 
        ForeignRevision,
22
 
        )
 
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
23
54
 
24
55
 
25
56
def escape_file_id(file_id):
27
58
 
28
59
 
29
60
def unescape_file_id(file_id):
30
 
    return file_id.replace("_s", " ").replace("__", "_")
 
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)
31
93
 
32
94
 
33
95
class BzrGitMapping(foreign.VcsMapping):
34
96
    """Class that maps between Git and Bazaar semantics."""
35
97
    experimental = False
36
98
 
37
 
    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):
38
112
        """Convert a git revision id handle to a Bazaar revision id."""
39
 
        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)
40
117
 
41
 
    def revision_id_bzr_to_foreign(self, bzr_rev_id):
 
118
    @classmethod
 
119
    def revision_id_bzr_to_foreign(cls, bzr_rev_id):
42
120
        """Convert a Bazaar revision id to a git revision id handle."""
43
 
        if not bzr_rev_id.startswith("%s:" % self.revid_prefix):
44
 
            raise errors.InvalidRevisionId(bzr_rev_id, self)
45
 
        return bzr_rev_id[len(self.revid_prefix)+1:]
46
 
 
47
 
    def show_foreign_revid(self, foreign_revid):
48
 
        return { "git commit": foreign_revid }
 
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()
49
124
 
50
125
    def generate_file_id(self, path):
51
 
        return escape_file_id(path.encode('utf-8'))
52
 
 
53
 
    def import_commit(self, commit):
 
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):
54
312
        """Convert a git commit to a bzr revision.
55
313
 
56
 
        :return: a `bzrlib.revision.Revision` object.
 
314
        :return: a `bzrlib.revision.Revision` object, foreign revid and a
 
315
            testament sha1
57
316
        """
58
317
        if commit is None:
59
318
            raise AssertionError("Commit object can't be None")
60
 
        rev = ForeignRevision(commit.id, self, self.revision_id_foreign_to_bzr(commit.id))
61
 
        rev.parent_ids = tuple([self.revision_id_foreign_to_bzr(p) for p in commit.parents])
62
 
        rev.message = commit.message.decode("utf-8", "replace")
63
 
        rev.committer = str(commit.committer).decode("utf-8", "replace")
64
 
        if commit.committer != commit.author:
65
 
            rev.properties['author'] = str(commit.author).decode("utf-8", "replace")
 
319
        rev = ForeignRevision(commit.id, self,
 
320
                self.revision_id_foreign_to_bzr(commit.id))
 
321
        rev.git_metadata = None
 
322
        def decode_using_encoding(rev, commit, encoding):
 
323
            rev.committer = str(commit.committer).decode(encoding)
 
324
            if commit.committer != commit.author:
 
325
                rev.properties['author'] = str(commit.author).decode(encoding)
 
326
            rev.message, rev.git_metadata = self._decode_commit_message(
 
327
                rev, commit.message, encoding)
 
328
        if commit.encoding is not None:
 
329
            rev.properties['git-explicit-encoding'] = commit.encoding
 
330
            decode_using_encoding(rev, commit, commit.encoding)
 
331
        else:
 
332
            for encoding in ('utf-8', 'latin1'):
 
333
                try:
 
334
                    decode_using_encoding(rev, commit, encoding)
 
335
                except UnicodeDecodeError:
 
336
                    pass
 
337
                else:
 
338
                    if encoding != 'utf-8':
 
339
                        rev.properties['git-implicit-encoding'] = encoding
 
340
                    break
 
341
        if commit.commit_time != commit.author_time:
 
342
            rev.properties['author-timestamp'] = str(commit.author_time)
 
343
        if commit.commit_timezone != commit.author_timezone:
 
344
            rev.properties['author-timezone'] = "%d" % commit.author_timezone
 
345
        if commit._author_timezone_neg_utc:
 
346
            rev.properties['author-timezone-neg-utc'] = ""
 
347
        if commit._commit_timezone_neg_utc:
 
348
            rev.properties['commit-timezone-neg-utc'] = ""
66
349
        rev.timestamp = commit.commit_time
67
 
        rev.timezone = 0
68
 
        return rev
69
 
 
70
 
 
71
 
class BzrGitMappingExperimental(BzrGitMapping):
 
350
        rev.timezone = commit.commit_timezone
 
351
        rev.parent_ids = None
 
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
        if rev.parent_ids is None:
 
363
            rev.parent_ids = tuple([lookup_parent_revid(p) for p in commit.parents])
 
364
        return rev, roundtrip_revid, verifiers
 
365
 
 
366
    def get_fileid_map(self, lookup_object, tree_sha):
 
367
        """Obtain a fileid map for a particular tree.
 
368
 
 
369
        :param lookup_object: Function for looking up an object
 
370
        :param tree_sha: SHA of the root tree
 
371
        :return: GitFileIdMap instance
 
372
        """
 
373
        try:
 
374
            file_id_map_sha = lookup_object(tree_sha)[self.BZR_FILE_IDS_FILE][1]
 
375
        except KeyError:
 
376
            file_ids = {}
 
377
        else:
 
378
            file_ids = self.import_fileid_map(lookup_object(file_id_map_sha))
 
379
        return GitFileIdMap(file_ids, self)
 
380
 
 
381
 
 
382
class BzrGitMappingv1(BzrGitMapping):
 
383
    revid_prefix = 'git-v1'
 
384
    experimental = False
 
385
 
 
386
    def __str__(self):
 
387
        return self.revid_prefix
 
388
 
 
389
 
 
390
class BzrGitMappingExperimental(BzrGitMappingv1):
72
391
    revid_prefix = 'git-experimental'
73
392
    experimental = True
74
 
 
75
 
 
76
 
default_mapping = BzrGitMappingExperimental()
 
393
    roundtripping = True
 
394
 
 
395
    BZR_FILE_IDS_FILE = '.bzrfileids'
 
396
 
 
397
    BZR_DUMMY_FILE = '.bzrdummy'
 
398
 
 
399
    def _decode_commit_message(self, rev, message, encoding):
 
400
        message = self._extract_hg_metadata(rev, message)
 
401
        message = self._extract_git_svn_metadata(rev, message)
 
402
        message, metadata = self._extract_bzr_metadata(rev, message)
 
403
        return message.decode(encoding), metadata
 
404
 
 
405
    def _encode_commit_message(self, rev, message, encoding):
 
406
        ret = message.encode(encoding)
 
407
        ret += self._generate_hg_message_tail(rev)
 
408
        ret += self._generate_git_svn_metadata(rev, encoding)
 
409
        return ret
 
410
 
 
411
    def import_commit(self, commit, lookup_parent_revid):
 
412
        rev, roundtrip_revid, verifiers = super(BzrGitMappingExperimental, self).import_commit(commit, lookup_parent_revid)
 
413
        rev.properties['converted_revision'] = "git %s\n" % commit.id
 
414
        return rev, roundtrip_revid, verifiers
 
415
 
 
416
 
 
417
class GitMappingRegistry(VcsMappingRegistry):
 
418
    """Registry with available git mappings."""
 
419
 
 
420
    def revision_id_bzr_to_foreign(self, bzr_revid):
 
421
        if bzr_revid == NULL_REVISION:
 
422
            from dulwich.protocol import ZERO_SHA
 
423
            return ZERO_SHA, None
 
424
        if not bzr_revid.startswith("git-"):
 
425
            raise errors.InvalidRevisionId(bzr_revid, None)
 
426
        (mapping_version, git_sha) = bzr_revid.split(":", 1)
 
427
        mapping = self.get(mapping_version)
 
428
        return mapping.revision_id_bzr_to_foreign(bzr_revid)
 
429
 
 
430
    parse_revision_id = revision_id_bzr_to_foreign
 
431
 
 
432
 
 
433
mapping_registry = GitMappingRegistry()
 
434
mapping_registry.register_lazy('git-v1', "bzrlib.plugins.git.mapping",
 
435
    "BzrGitMappingv1")
 
436
mapping_registry.register_lazy('git-experimental',
 
437
    "bzrlib.plugins.git.mapping", "BzrGitMappingExperimental")
 
438
mapping_registry.set_default('git-v1')
 
439
 
 
440
 
 
441
class ForeignGit(ForeignVcs):
 
442
    """The Git Stupid Content Tracker"""
 
443
 
 
444
    @property
 
445
    def branch_format(self):
 
446
        from bzrlib.plugins.git.branch import GitBranchFormat
 
447
        return GitBranchFormat()
 
448
 
 
449
    @property
 
450
    def repository_format(self):
 
451
        from bzrlib.plugins.git.repository import GitRepositoryFormat
 
452
        return GitRepositoryFormat()
 
453
 
 
454
    def __init__(self):
 
455
        super(ForeignGit, self).__init__(mapping_registry)
 
456
        self.abbreviation = "git"
 
457
 
 
458
    @classmethod
 
459
    def serialize_foreign_revid(self, foreign_revid):
 
460
        return foreign_revid
 
461
 
 
462
    @classmethod
 
463
    def show_foreign_revid(cls, foreign_revid):
 
464
        return { "git commit": foreign_revid }
 
465
 
 
466
 
 
467
foreign_git = ForeignGit()
 
468
default_mapping = mapping_registry.get_default()()
 
469
 
 
470
 
 
471
def symlink_to_blob(entry):
 
472
    from dulwich.objects import Blob
 
473
    blob = Blob()
 
474
    symlink_target = entry.symlink_target
 
475
    if type(symlink_target) == unicode:
 
476
        symlink_target = symlink_target.encode('utf-8')
 
477
    blob.data = symlink_target
 
478
    return blob
 
479
 
 
480
 
 
481
def mode_is_executable(mode):
 
482
    """Check if mode should be considered executable."""
 
483
    return bool(mode & 0111)
 
484
 
 
485
 
 
486
def mode_kind(mode):
 
487
    """Determine the Bazaar inventory kind based on Unix file mode."""
 
488
    entry_kind = (mode & 0700000) / 0100000
 
489
    if entry_kind == 0:
 
490
        return 'directory'
 
491
    elif entry_kind == 1:
 
492
        file_kind = (mode & 070000) / 010000
 
493
        if file_kind == 0:
 
494
            return 'file'
 
495
        elif file_kind == 2:
 
496
            return 'symlink'
 
497
        elif file_kind == 6:
 
498
            return 'tree-reference'
 
499
        else:
 
500
            raise AssertionError(
 
501
                "Unknown file kind %d, perms=%o." % (file_kind, mode,))
 
502
    else:
 
503
        raise AssertionError(
 
504
            "Unknown kind, perms=%r." % (mode,))
 
505
 
 
506
 
 
507
def object_mode(kind, executable):
 
508
    if kind == 'directory':
 
509
        return stat.S_IFDIR
 
510
    elif kind == 'symlink':
 
511
        mode = stat.S_IFLNK
 
512
        if executable:
 
513
            mode |= 0111
 
514
        return mode
 
515
    elif kind == 'file':
 
516
        mode = stat.S_IFREG | 0644
 
517
        if executable:
 
518
            mode |= 0111
 
519
        return mode
 
520
    elif kind == 'tree-reference':
 
521
        from dulwich.objects import S_IFGITLINK
 
522
        return S_IFGITLINK
 
523
    else:
 
524
        raise AssertionError
 
525
 
 
526
 
 
527
def entry_mode(entry):
 
528
    """Determine the git file mode for an inventory entry."""
 
529
    return object_mode(entry.kind, entry.executable)
 
530
 
 
531
 
 
532
def directory_to_tree(entry, lookup_ie_sha1, unusual_modes, empty_file_name):
 
533
    """Create a Git Tree object from a Bazaar directory.
 
534
 
 
535
    :param entry: Inventory entry
 
536
    :param lookup_ie_sha1: Lookup the Git SHA1 for a inventory entry
 
537
    :param unusual_modes: Dictionary with unusual file modes by file ids
 
538
    :param empty_file_name: Name to use for dummy files in empty directories,
 
539
        None to ignore empty directories.
 
540
    """
 
541
    from dulwich.objects import Blob, Tree
 
542
    tree = Tree()
 
543
    for name, value in entry.children.iteritems():
 
544
        ie = entry.children[name]
 
545
        try:
 
546
            mode = unusual_modes[ie.file_id]
 
547
        except KeyError:
 
548
            mode = entry_mode(ie)
 
549
        hexsha = lookup_ie_sha1(ie)
 
550
        if hexsha is not None:
 
551
            tree.add(name.encode("utf-8"), mode, hexsha)
 
552
    if entry.parent_id is not None and len(tree) == 0:
 
553
        # Only the root can be an empty tree
 
554
        if empty_file_name is not None:
 
555
            tree.add(empty_file_name, stat.S_IFREG | 0644, Blob().id)
 
556
        else:
 
557
            return None
 
558
    return tree
 
559
 
 
560
 
 
561
def extract_unusual_modes(rev):
 
562
    try:
 
563
        foreign_revid, mapping = mapping_registry.parse_revision_id(
 
564
            rev.revision_id)
 
565
    except errors.InvalidRevisionId:
 
566
        return {}
 
567
    else:
 
568
        return mapping.export_unusual_file_modes(rev)
 
569
 
 
570
 
 
571
def parse_git_svn_id(text):
 
572
    (head, uuid) = text.rsplit(" ", 1)
 
573
    (full_url, rev) = head.rsplit("@", 1)
 
574
    return (full_url, int(rev), uuid)
 
575
 
 
576
 
 
577
class GitFileIdMap(object):
 
578
 
 
579
    def __init__(self, file_ids, mapping):
 
580
        self.file_ids = file_ids
 
581
        self.paths = None
 
582
        self.mapping = mapping
 
583
 
 
584
    def lookup_file_id(self, path):
 
585
        assert type(path) is str
 
586
        try:
 
587
            file_id = self.file_ids[path]
 
588
        except KeyError:
 
589
            file_id = self.mapping.generate_file_id(path)
 
590
        assert type(file_id) is str
 
591
        return file_id
 
592
 
 
593
    def lookup_path(self, file_id):
 
594
        if self.paths is None:
 
595
            self.paths = {}
 
596
            for k, v in self.file_ids.iteritems():
 
597
                self.paths[v] = k
 
598
        try:
 
599
            path = self.paths[file_id]
 
600
        except KeyError:
 
601
            return self.mapping.parse_file_id(file_id)
 
602
        else:
 
603
            assert type(path) is str
 
604
            return path