/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: John Arbash Meinel
  • Date: 2010-01-12 22:51:31 UTC
  • mto: This revision was merged to the branch mainline in revision 4955.
  • Revision ID: john@arbash-meinel.com-20100112225131-he8h411p6aeeb947
Delay grabbing an output stream until we actually go to show a diff.

This makes the test suite happy, but it also seems to be reasonable.
If we aren't going to write anything, we don't need to hold an
output stream open.

Show diffs side-by-side

added added

removed removed

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