/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to mapping.py

  • Committer: Jelmer Vernooij
  • Date: 2010-05-13 12:34:24 UTC
  • mto: (0.200.912 trunk)
  • mto: This revision was merged to the branch mainline in revision 6960.
  • Revision ID: jelmer@samba.org-20100513123424-c1sk9vcg2ekrcsol
Some refactoring, support proper file ids in revision deltas.

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