/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 roundtripping 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
    )
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
    CommitSupplement,
 
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_vcs_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), CommitSupplement()
 
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 = CommitSupplement()
 
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-experimental')
 
439
mapping_registry.set_default('git-v1')
 
440
 
 
441
 
 
442
class ForeignGit(ForeignVcs):
 
443
    """The Git Stupid Content Tracker"""
 
444
 
 
445
    @property
 
446
    def branch_format(self):
 
447
        from bzrlib.plugins.git.branch import GitBranchFormat
 
448
        return GitBranchFormat()
 
449
 
 
450
    @property
 
451
    def repository_format(self):
 
452
        from bzrlib.plugins.git.repository import GitRepositoryFormat
 
453
        return GitRepositoryFormat()
 
454
 
 
455
    def __init__(self):
 
456
        super(ForeignGit, self).__init__(mapping_registry)
 
457
        self.abbreviation = "git"
 
458
 
 
459
    @classmethod
 
460
    def serialize_foreign_revid(self, foreign_revid):
 
461
        return foreign_revid
 
462
 
 
463
    @classmethod
 
464
    def show_foreign_revid(cls, foreign_revid):
 
465
        return { "git commit": foreign_revid }
 
466
 
 
467
 
 
468
foreign_vcs_git = ForeignGit()
 
469
default_mapping = mapping_registry.get_default()()
 
470
 
 
471
 
 
472
def symlink_to_blob(entry):
 
473
    from dulwich.objects import Blob
 
474
    blob = Blob()
 
475
    symlink_target = entry.symlink_target
 
476
    if type(symlink_target) == unicode:
 
477
        symlink_target = symlink_target.encode('utf-8')
 
478
    blob.data = symlink_target
 
479
    return blob
 
480
 
 
481
 
 
482
def mode_is_executable(mode):
 
483
    """Check if mode should be considered executable."""
 
484
    return bool(mode & 0111)
 
485
 
 
486
 
 
487
def mode_kind(mode):
 
488
    """Determine the Bazaar inventory kind based on Unix file mode."""
 
489
    entry_kind = (mode & 0700000) / 0100000
 
490
    if entry_kind == 0:
 
491
        return 'directory'
 
492
    elif entry_kind == 1:
 
493
        file_kind = (mode & 070000) / 010000
 
494
        if file_kind == 0:
 
495
            return 'file'
 
496
        elif file_kind == 2:
 
497
            return 'symlink'
 
498
        elif file_kind == 6:
 
499
            return 'tree-reference'
 
500
        else:
 
501
            raise AssertionError(
 
502
                "Unknown file kind %d, perms=%o." % (file_kind, mode,))
 
503
    else:
 
504
        raise AssertionError(
 
505
            "Unknown kind, perms=%r." % (mode,))
 
506
 
 
507
 
 
508
def object_mode(kind, executable):
 
509
    if kind == 'directory':
 
510
        return stat.S_IFDIR
 
511
    elif kind == 'symlink':
 
512
        mode = stat.S_IFLNK
 
513
        if executable:
 
514
            mode |= 0111
 
515
        return mode
 
516
    elif kind == 'file':
 
517
        mode = stat.S_IFREG | 0644
 
518
        if executable:
 
519
            mode |= 0111
 
520
        return mode
 
521
    elif kind == 'tree-reference':
 
522
        from dulwich.objects import S_IFGITLINK
 
523
        return S_IFGITLINK
 
524
    else:
 
525
        raise AssertionError
 
526
 
 
527
 
 
528
def entry_mode(entry):
 
529
    """Determine the git file mode for an inventory entry."""
 
530
    return object_mode(entry.kind, entry.executable)
 
531
 
 
532
 
 
533
def directory_to_tree(entry, lookup_ie_sha1, unusual_modes, empty_file_name):
 
534
    """Create a Git Tree object from a Bazaar directory.
 
535
 
 
536
    :param entry: Inventory entry
 
537
    :param lookup_ie_sha1: Lookup the Git SHA1 for a inventory entry
 
538
    :param unusual_modes: Dictionary with unusual file modes by file ids
 
539
    :param empty_file_name: Name to use for dummy files in empty directories,
 
540
        None to ignore empty directories.
 
541
    """
 
542
    from dulwich.objects import Blob, Tree
 
543
    tree = Tree()
 
544
    for name, value in entry.children.iteritems():
 
545
        ie = entry.children[name]
 
546
        try:
 
547
            mode = unusual_modes[ie.file_id]
 
548
        except KeyError:
 
549
            mode = entry_mode(ie)
 
550
        hexsha = lookup_ie_sha1(ie)
 
551
        if hexsha is not None:
 
552
            tree.add(name.encode("utf-8"), mode, hexsha)
 
553
    if entry.parent_id is not None and len(tree) == 0:
 
554
        # Only the root can be an empty tree
 
555
        if empty_file_name is not None:
 
556
            tree.add(empty_file_name, stat.S_IFREG | 0644, Blob().id)
 
557
        else:
 
558
            return None
 
559
    return tree
 
560
 
 
561
 
 
562
def extract_unusual_modes(rev):
 
563
    try:
 
564
        foreign_revid, mapping = mapping_registry.parse_revision_id(
 
565
            rev.revision_id)
 
566
    except errors.InvalidRevisionId:
 
567
        return {}
 
568
    else:
 
569
        return mapping.export_unusual_file_modes(rev)
 
570
 
 
571
 
 
572
def parse_git_svn_id(text):
 
573
    (head, uuid) = text.rsplit(" ", 1)
 
574
    (full_url, rev) = head.rsplit("@", 1)
 
575
    return (full_url, int(rev), uuid)
 
576
 
 
577
 
 
578
class GitFileIdMap(object):
 
579
 
 
580
    def __init__(self, file_ids, mapping):
 
581
        self.file_ids = file_ids
 
582
        self.paths = None
 
583
        self.mapping = mapping
 
584
 
 
585
    def set_file_id(self, path, file_id):
 
586
        assert type(path) is str
 
587
        assert type(file_id) is str
 
588
        self.file_ids[path] = file_id
 
589
 
 
590
    def lookup_file_id(self, path):
 
591
        assert type(path) is str
 
592
        try:
 
593
            file_id = self.file_ids[path]
 
594
        except KeyError:
 
595
            file_id = self.mapping.generate_file_id(path)
 
596
        assert type(file_id) is str
 
597
        return file_id
 
598
 
 
599
    def lookup_path(self, file_id):
 
600
        if self.paths is None:
 
601
            self.paths = {}
 
602
            for k, v in self.file_ids.iteritems():
 
603
                self.paths[v] = k
 
604
        try:
 
605
            path = self.paths[file_id]
 
606
        except KeyError:
 
607
            return self.mapping.parse_file_id(file_id)
 
608
        else:
 
609
            assert type(path) is str
 
610
            return path
 
611
 
 
612
    def copy(self):
 
613
        return self.__class__(dict(self.file_ids), self.mapping)