/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

Add some basic documentation in 'bzr help git'.

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
    bencode,
 
26
    errors,
 
27
    foreign,
 
28
    trace,
 
29
    )
 
30
from bzrlib.inventory import (
 
31
    ROOT_ID,
 
32
    )
21
33
from bzrlib.foreign import (
22
 
        ForeignRevision,
23
 
        )
 
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
24
54
 
25
55
 
26
56
def escape_file_id(file_id):
28
58
 
29
59
 
30
60
def unescape_file_id(file_id):
31
 
    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)
32
93
 
33
94
 
34
95
class BzrGitMapping(foreign.VcsMapping):
35
96
    """Class that maps between Git and Bazaar semantics."""
36
97
    experimental = False
37
98
 
38
 
    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):
39
112
        """Convert a git revision id handle to a Bazaar revision id."""
40
 
        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)
41
117
 
42
 
    def revision_id_bzr_to_foreign(self, bzr_rev_id):
 
118
    @classmethod
 
119
    def revision_id_bzr_to_foreign(cls, bzr_rev_id):
43
120
        """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 }
 
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()
50
124
 
51
125
    def generate_file_id(self, path):
 
126
        # Git paths are just bytestrings
 
127
        # We must just hope they are valid UTF-8..
52
128
        if path == "":
53
129
            return ROOT_ID
54
 
        return escape_file_id(path.encode('utf-8'))
55
 
 
56
 
    def import_commit(self, commit):
 
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 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
        """Turn a Bazaar revision in to a Git commit
 
232
 
 
233
        :param tree_sha: Tree sha for the commit
 
234
        :param parent_lookup: Function for looking up the GIT sha equiv of a
 
235
            bzr revision
 
236
        :return dulwich.objects.Commit represent the revision:
 
237
        """
 
238
        from dulwich.objects import Commit
 
239
        commit = Commit()
 
240
        commit.tree = tree_sha
 
241
        if roundtrip:
 
242
            metadata = BzrGitRevisionMetadata()
 
243
        else:
 
244
            metadata = None
 
245
        parents = []
 
246
        for p in rev.parent_ids:
 
247
            try:
 
248
                git_p = parent_lookup(p)
 
249
            except KeyError:
 
250
                git_p = None
 
251
                if metadata is not None:
 
252
                    metadata.explicit_parent_ids = rev.parent_ids
 
253
            if git_p is not None:
 
254
                assert len(git_p) == 40, "unexpected length for %r" % git_p
 
255
                parents.append(git_p)
 
256
        commit.parents = parents
 
257
        try:
 
258
            encoding = rev.properties['git-explicit-encoding']
 
259
        except KeyError:
 
260
            encoding = rev.properties.get('git-implicit-encoding', 'utf-8')
 
261
        commit.encoding = rev.properties.get('git-explicit-encoding')
 
262
        commit.committer = fix_person_identifier(rev.committer.encode(
 
263
            encoding))
 
264
        commit.author = fix_person_identifier(
 
265
            rev.get_apparent_authors()[0].encode(encoding))
 
266
        commit.commit_time = long(rev.timestamp)
 
267
        if 'author-timestamp' in rev.properties:
 
268
            commit.author_time = long(rev.properties['author-timestamp'])
 
269
        else:
 
270
            commit.author_time = commit.commit_time
 
271
        commit._commit_timezone_neg_utc = "commit-timezone-neg-utc" in rev.properties
 
272
        commit.commit_timezone = rev.timezone
 
273
        commit._author_timezone_neg_utc = "author-timezone-neg-utc" in rev.properties
 
274
        if 'author-timezone' in rev.properties:
 
275
            commit.author_timezone = int(rev.properties['author-timezone'])
 
276
        else:
 
277
            commit.author_timezone = commit.commit_timezone
 
278
        commit.message = self._encode_commit_message(rev, rev.message, 
 
279
            encoding)
 
280
        assert type(commit.message) == str
 
281
        if metadata is not None:
 
282
            try:
 
283
                mapping_registry.parse_revision_id(rev.revision_id)
 
284
            except errors.InvalidRevisionId:
 
285
                metadata.revision_id = rev.revision_id
 
286
            mapping_properties = set(
 
287
                ['author', 'author-timezone', 'author-timezone-neg-utc',
 
288
                 'commit-timezone-neg-utc', 'git-implicit-encoding',
 
289
                 'git-explicit-encoding', 'author-timestamp', 'file-modes'])
 
290
            for k, v in rev.properties.iteritems():
 
291
                if not k in mapping_properties:
 
292
                    metadata.properties[k] = v
 
293
        if self.roundtripping:
 
294
            commit.message = inject_bzr_metadata(commit.message, metadata, 
 
295
                                                 encoding)
 
296
        assert type(commit.message) == str
 
297
        return commit
 
298
 
 
299
    def import_fileid_map(self, blob):
 
300
        """Convert a git file id map blob.
 
301
 
 
302
        :param blob: Git blob object with fileid map
 
303
        :return: Dictionary mapping paths to file ids
 
304
        """
 
305
        return deserialize_fileid_map(blob.data)
 
306
 
 
307
    def import_commit(self, commit, lookup_parent_revid):
57
308
        """Convert a git commit to a bzr revision.
58
309
 
59
 
        :return: a `bzrlib.revision.Revision` object.
 
310
        :return: a `bzrlib.revision.Revision` object and a 
 
311
            dictionary of path -> file ids
60
312
        """
61
313
        if commit is None:
62
314
            raise AssertionError("Commit object can't be None")
63
 
        rev = ForeignRevision(commit.id, self, self.revision_id_foreign_to_bzr(commit.id))
64
 
        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")
 
315
        rev = ForeignRevision(commit.id, self,
 
316
                self.revision_id_foreign_to_bzr(commit.id))
 
317
        rev.parent_ids = tuple([lookup_parent_revid(p) for p in commit.parents])
 
318
        rev.git_metadata = None
 
319
        def decode_using_encoding(rev, commit, encoding):
 
320
            rev.committer = str(commit.committer).decode(encoding)
 
321
            if commit.committer != commit.author:
 
322
                rev.properties['author'] = str(commit.author).decode(encoding)
 
323
            rev.message, rev.git_metadata = self._decode_commit_message(
 
324
                rev, commit.message, encoding)
 
325
        if commit.encoding is not None:
 
326
            rev.properties['git-explicit-encoding'] = commit.encoding
 
327
            decode_using_encoding(rev, commit, commit.encoding)
 
328
        else:
 
329
            for encoding in ('utf-8', 'latin1'):
 
330
                try:
 
331
                    decode_using_encoding(rev, commit, encoding)
 
332
                except UnicodeDecodeError:
 
333
                    pass
 
334
                else:
 
335
                    if encoding != 'utf-8':
 
336
                        rev.properties['git-implicit-encoding'] = encoding
 
337
                    break
 
338
        if commit.commit_time != commit.author_time:
 
339
            rev.properties['author-timestamp'] = str(commit.author_time)
 
340
        if commit.commit_timezone != commit.author_timezone:
 
341
            rev.properties['author-timezone'] = "%d" % commit.author_timezone
 
342
        if commit._author_timezone_neg_utc:
 
343
            rev.properties['author-timezone-neg-utc'] = ""
 
344
        if commit._commit_timezone_neg_utc:
 
345
            rev.properties['commit-timezone-neg-utc'] = ""
69
346
        rev.timestamp = commit.commit_time
70
 
        rev.timezone = 0
 
347
        rev.timezone = commit.commit_timezone
 
348
        if rev.git_metadata is not None:
 
349
            md = rev.git_metadata
 
350
            if md.revision_id:
 
351
                rev.revision_id = md.revision_id
 
352
            if md.explicit_parent_ids:
 
353
                rev.parent_ids = md.explicit_parent_ids
 
354
            rev.properties.update(md.properties)
71
355
        return rev
72
356
 
73
 
 
74
 
class BzrGitMappingExperimental(BzrGitMapping):
 
357
    def get_fileid_map(self, lookup_object, tree_sha):
 
358
        """Obtain a fileid map for a particular tree.
 
359
 
 
360
        :param lookup_object: Function for looking up an object
 
361
        :param tree_sha: SHA of the root tree
 
362
        :return: GitFileIdMap instance
 
363
        """
 
364
        try:
 
365
            file_id_map_sha = lookup_object(tree_sha)[self.BZR_FILE_IDS_FILE][1]
 
366
        except KeyError:
 
367
            file_ids = {}
 
368
        else:
 
369
            file_ids = self.import_fileid_map(lookup_object(file_id_map_sha))
 
370
        return GitFileIdMap(file_ids, self)
 
371
 
 
372
 
 
373
class BzrGitMappingv1(BzrGitMapping):
 
374
    revid_prefix = 'git-v1'
 
375
    experimental = False
 
376
 
 
377
    def __str__(self):
 
378
        return self.revid_prefix
 
379
 
 
380
 
 
381
class BzrGitMappingExperimental(BzrGitMappingv1):
75
382
    revid_prefix = 'git-experimental'
76
383
    experimental = True
77
 
 
78
 
 
79
 
default_mapping = BzrGitMappingExperimental()
 
384
    roundtripping = True
 
385
 
 
386
    BZR_FILE_IDS_FILE = '.bzrfileids'
 
387
 
 
388
    BZR_DUMMY_FILE = '.bzrdummy'
 
389
 
 
390
    def _decode_commit_message(self, rev, message, encoding):
 
391
        message = self._extract_hg_metadata(rev, message)
 
392
        message = self._extract_git_svn_metadata(rev, message)
 
393
        message, metadata = self._extract_bzr_metadata(rev, message)
 
394
        return message.decode(encoding), metadata
 
395
 
 
396
    def _encode_commit_message(self, rev, message, encoding):
 
397
        ret = message.encode(encoding)
 
398
        ret += self._generate_hg_message_tail(rev)
 
399
        ret += self._generate_git_svn_metadata(rev, encoding)
 
400
        return ret
 
401
 
 
402
    def import_commit(self, commit, lookup_parent_revid):
 
403
        rev, file_ids = super(BzrGitMappingExperimental, self).import_commit(commit, lookup_parent_revid)
 
404
        rev.properties['converted_revision'] = "git %s\n" % commit.id
 
405
        return rev, file_ids
 
406
 
 
407
 
 
408
class GitMappingRegistry(VcsMappingRegistry):
 
409
    """Registry with available git mappings."""
 
410
 
 
411
    def revision_id_bzr_to_foreign(self, bzr_revid):
 
412
        if bzr_revid == NULL_REVISION:
 
413
            from dulwich.protocol import ZERO_SHA
 
414
            return ZERO_SHA, None
 
415
        if not bzr_revid.startswith("git-"):
 
416
            raise errors.InvalidRevisionId(bzr_revid, None)
 
417
        (mapping_version, git_sha) = bzr_revid.split(":", 1)
 
418
        mapping = self.get(mapping_version)
 
419
        return mapping.revision_id_bzr_to_foreign(bzr_revid)
 
420
 
 
421
    parse_revision_id = revision_id_bzr_to_foreign
 
422
 
 
423
 
 
424
mapping_registry = GitMappingRegistry()
 
425
mapping_registry.register_lazy('git-v1', "bzrlib.plugins.git.mapping",
 
426
    "BzrGitMappingv1")
 
427
mapping_registry.register_lazy('git-experimental',
 
428
    "bzrlib.plugins.git.mapping", "BzrGitMappingExperimental")
 
429
mapping_registry.set_default('git-v1')
 
430
 
 
431
 
 
432
class ForeignGit(ForeignVcs):
 
433
    """The Git Stupid Content Tracker"""
 
434
 
 
435
    @property
 
436
    def branch_format(self):
 
437
        from bzrlib.plugins.git.branch import GitBranchFormat
 
438
        return GitBranchFormat()
 
439
 
 
440
    @property
 
441
    def repository_format(self):
 
442
        from bzrlib.plugins.git.repository import GitRepositoryFormat
 
443
        return GitRepositoryFormat()
 
444
 
 
445
    def __init__(self):
 
446
        super(ForeignGit, self).__init__(mapping_registry)
 
447
        self.abbreviation = "git"
 
448
 
 
449
    @classmethod
 
450
    def serialize_foreign_revid(self, foreign_revid):
 
451
        return foreign_revid
 
452
 
 
453
    @classmethod
 
454
    def show_foreign_revid(cls, foreign_revid):
 
455
        return { "git commit": foreign_revid }
 
456
 
 
457
 
 
458
foreign_git = ForeignGit()
 
459
default_mapping = mapping_registry.get_default()()
 
460
 
 
461
 
 
462
def symlink_to_blob(entry):
 
463
    from dulwich.objects import Blob
 
464
    blob = Blob()
 
465
    symlink_target = entry.symlink_target
 
466
    if type(symlink_target) == unicode:
 
467
        symlink_target = symlink_target.encode('utf-8')
 
468
    blob.data = symlink_target
 
469
    return blob
 
470
 
 
471
 
 
472
def mode_is_executable(mode):
 
473
    """Check if mode should be considered executable."""
 
474
    return bool(mode & 0111)
 
475
 
 
476
 
 
477
def mode_kind(mode):
 
478
    """Determine the Bazaar inventory kind based on Unix file mode."""
 
479
    entry_kind = (mode & 0700000) / 0100000
 
480
    if entry_kind == 0:
 
481
        return 'directory'
 
482
    elif entry_kind == 1:
 
483
        file_kind = (mode & 070000) / 010000
 
484
        if file_kind == 0:
 
485
            return 'file'
 
486
        elif file_kind == 2:
 
487
            return 'symlink'
 
488
        elif file_kind == 6:
 
489
            return 'tree-reference'
 
490
        else:
 
491
            raise AssertionError(
 
492
                "Unknown file kind %d, perms=%o." % (file_kind, mode,))
 
493
    else:
 
494
        raise AssertionError(
 
495
            "Unknown kind, perms=%r." % (mode,))
 
496
 
 
497
 
 
498
def object_mode(kind, executable):
 
499
    if kind == 'directory':
 
500
        return stat.S_IFDIR
 
501
    elif kind == 'symlink':
 
502
        mode = stat.S_IFLNK
 
503
        if executable:
 
504
            mode |= 0111
 
505
        return mode
 
506
    elif kind == 'file':
 
507
        mode = stat.S_IFREG | 0644
 
508
        if executable:
 
509
            mode |= 0111
 
510
        return mode
 
511
    elif kind == 'tree-reference':
 
512
        from dulwich.objects import S_IFGITLINK
 
513
        return S_IFGITLINK
 
514
    else:
 
515
        raise AssertionError
 
516
 
 
517
 
 
518
def entry_mode(entry):
 
519
    """Determine the git file mode for an inventory entry."""
 
520
    return object_mode(entry.kind, entry.executable)
 
521
 
 
522
 
 
523
def directory_to_tree(entry, lookup_ie_sha1, unusual_modes, empty_file_name):
 
524
    """Create a Git Tree object from a Bazaar directory.
 
525
 
 
526
    :param entry: Inventory entry
 
527
    :param lookup_ie_sha1: Lookup the Git SHA1 for a inventory entry
 
528
    :param unusual_modes: Dictionary with unusual file modes by file ids
 
529
    :param empty_file_name: Name to use for dummy files in empty directories,
 
530
        None to ignore empty directories.
 
531
    """
 
532
    from dulwich.objects import Blob, Tree
 
533
    tree = Tree()
 
534
    for name, value in entry.children.iteritems():
 
535
        ie = entry.children[name]
 
536
        try:
 
537
            mode = unusual_modes[ie.file_id]
 
538
        except KeyError:
 
539
            mode = entry_mode(ie)
 
540
        hexsha = lookup_ie_sha1(ie)
 
541
        if hexsha is not None:
 
542
            tree.add(mode, name.encode("utf-8"), hexsha)
 
543
    if entry.parent_id is not None and len(tree) == 0:
 
544
        # Only the root can be an empty tree
 
545
        if empty_file_name is not None:
 
546
            tree.add(stat.S_IFREG | 0644, empty_file_name, 
 
547
                Blob().id)
 
548
        else:
 
549
            return None
 
550
    return tree
 
551
 
 
552
 
 
553
def extract_unusual_modes(rev):
 
554
    try:
 
555
        foreign_revid, mapping = mapping_registry.parse_revision_id(
 
556
            rev.revision_id)
 
557
    except errors.InvalidRevisionId:
 
558
        return {}
 
559
    else:
 
560
        return mapping.export_unusual_file_modes(rev)
 
561
 
 
562
 
 
563
def parse_git_svn_id(text):
 
564
    (head, uuid) = text.rsplit(" ", 1)
 
565
    (full_url, rev) = head.rsplit("@", 1)
 
566
    return (full_url, int(rev), uuid)
 
567
 
 
568
 
 
569
class GitFileIdMap(object):
 
570
 
 
571
    def __init__(self, file_ids, mapping):
 
572
        self.file_ids = file_ids
 
573
        self.paths = None
 
574
        self.mapping = mapping
 
575
 
 
576
    def lookup_file_id(self, path):
 
577
        assert type(path) is str
 
578
        try:
 
579
            file_id = self.file_ids[path]
 
580
        except KeyError:
 
581
            file_id = self.mapping.generate_file_id(path)
 
582
        assert type(file_id) is str
 
583
        return file_id
 
584
 
 
585
    def lookup_path(self, file_id):
 
586
        if self.paths is None:
 
587
            self.paths = {}
 
588
            for k, v in self.file_ids.iteritems():
 
589
                self.paths[v] = k
 
590
        try:
 
591
            path = self.paths[file_id]
 
592
        except KeyError:
 
593
            return self.mapping.parse_file_id(file_id)
 
594
        else:
 
595
            assert type(path) is str
 
596
            return path