/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

Fix more tests.

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 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):
38
115
        """Convert a git revision id handle to a Bazaar revision id."""
39
 
        return "%s:%s" % (self.revid_prefix, git_rev_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)
40
120
 
41
 
    def revision_id_bzr_to_foreign(self, bzr_rev_id):
 
121
    @classmethod
 
122
    def revision_id_bzr_to_foreign(cls, bzr_rev_id):
42
123
        """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 }
 
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()
49
127
 
50
128
    def generate_file_id(self, path):
51
 
        return escape_file_id(path.encode('utf-8'))
52
 
 
53
 
    def import_commit(self, commit):
 
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):
54
315
        """Convert a git commit to a bzr revision.
55
316
 
56
 
        :return: a `bzrlib.revision.Revision` object.
 
317
        :return: a `bzrlib.revision.Revision` object, foreign revid and a
 
318
            testament sha1
57
319
        """
58
320
        if commit is None:
59
321
            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")
 
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'] = ""
66
352
        rev.timestamp = commit.commit_time
67
 
        rev.timezone = 0
68
 
        return rev
69
 
 
70
 
 
71
 
class BzrGitMappingExperimental(BzrGitMapping):
 
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):
72
394
    revid_prefix = 'git-experimental'
73
395
    experimental = True
74
 
 
75
 
 
76
 
default_mapping = BzrGitMappingExperimental()
 
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)