/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

SupportĀ tagĀ refs.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
# Copyright (C) 2007 Canonical Ltd
 
2
# Copyright (C) 2008-2009 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
    errors,
 
26
    foreign,
 
27
    osutils,
 
28
    trace,
 
29
    urlutils,
 
30
    )
 
31
try:
 
32
    from bzrlib import bencode
 
33
except ImportError:
 
34
    from bzrlib.util import bencode
 
35
from bzrlib.inventory import (
 
36
    ROOT_ID,
 
37
    )
21
38
from bzrlib.foreign import (
22
 
        ForeignRevision,
23
 
        )
 
39
    ForeignVcs,
 
40
    VcsMappingRegistry,
 
41
    ForeignRevision,
 
42
    )
 
43
from bzrlib.revision import (
 
44
    NULL_REVISION,
 
45
    )
 
46
from bzrlib.plugins.git.hg import (
 
47
    format_hg_metadata,
 
48
    extract_hg_metadata,
 
49
    )
 
50
 
 
51
DEFAULT_FILE_MODE = stat.S_IFREG | 0644
24
52
 
25
53
 
26
54
def escape_file_id(file_id):
28
56
 
29
57
 
30
58
def unescape_file_id(file_id):
31
 
    return file_id.replace("_s", " ").replace("__", "_")
 
59
    ret = []
 
60
    i = 0
 
61
    while i < len(file_id):
 
62
        if file_id[i] != '_':
 
63
            ret.append(file_id[i])
 
64
        else:
 
65
            if file_id[i+1] == '_':
 
66
                ret.append("_")
 
67
            elif file_id[i+1] == 's':
 
68
                ret.append(" ")
 
69
            else:
 
70
                raise AssertionError("unknown escape character %s" %
 
71
                    file_id[i+1])
 
72
            i += 1
 
73
        i += 1
 
74
    return "".join(ret)
 
75
 
 
76
 
 
77
def fix_person_identifier(text):
 
78
    if "<" in text and ">" in text:
 
79
        return text
 
80
    return "%s <%s>" % (text, text)
 
81
 
 
82
 
 
83
def warn_escaped(commit, num_escaped):
 
84
    trace.warning("Escaped %d XML-invalid characters in %s. Will be unable "
 
85
                  "to regenerate the SHA map.", num_escaped, commit)
 
86
 
 
87
 
 
88
def warn_unusual_mode(commit, path, mode):
 
89
    trace.mutter("Unusual file mode %o for %s in %s. Storing as revision "
 
90
                 "property. ", mode, path, commit)
 
91
 
 
92
 
 
93
def squash_revision(target_repo, rev):
 
94
    """Remove characters that can't be stored from a revision, if necessary.
 
95
 
 
96
    :param target_repo: Repository in which the revision will be stored
 
97
    :param rev: Revision object, will be modified in-place
 
98
    """
 
99
    if not getattr(target_repo._serializer, "squashes_xml_invalid_characters", True):
 
100
        return
 
101
    from bzrlib.xml_serializer import escape_invalid_chars
 
102
    rev.message, num_escaped = escape_invalid_chars(rev.message)
 
103
    if num_escaped:
 
104
        warn_escaped(rev.foreign_revid, num_escaped)
 
105
    if 'author' in rev.properties:
 
106
        rev.properties['author'], num_escaped = escape_invalid_chars(
 
107
            rev.properties['author'])
 
108
        if num_escaped:
 
109
            warn_escaped(rev.foreign_revid, num_escaped)
 
110
    rev.committer, num_escaped = escape_invalid_chars(rev.committer)
 
111
    if num_escaped:
 
112
        warn_escaped(rev.foreign_revid, num_escaped)
32
113
 
33
114
 
34
115
class BzrGitMapping(foreign.VcsMapping):
35
116
    """Class that maps between Git and Bazaar semantics."""
36
117
    experimental = False
37
118
 
38
 
    def revision_id_foreign_to_bzr(self, git_rev_id):
 
119
    def __init__(self):
 
120
        super(BzrGitMapping, self).__init__(foreign_git)
 
121
 
 
122
    def __eq__(self, other):
 
123
        return (type(self) == type(other) and 
 
124
                self.revid_prefix == other.revid_prefix)
 
125
 
 
126
    @classmethod
 
127
    def revision_id_foreign_to_bzr(cls, git_rev_id):
39
128
        """Convert a git revision id handle to a Bazaar revision id."""
40
 
        return "%s:%s" % (self.revid_prefix, git_rev_id)
 
129
        if git_rev_id == "0" * 40:
 
130
            return NULL_REVISION
 
131
        return "%s:%s" % (cls.revid_prefix, git_rev_id)
41
132
 
42
 
    def revision_id_bzr_to_foreign(self, bzr_rev_id):
 
133
    @classmethod
 
134
    def revision_id_bzr_to_foreign(cls, bzr_rev_id):
43
135
        """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 }
 
136
        if not bzr_rev_id.startswith("%s:" % cls.revid_prefix):
 
137
            raise errors.InvalidRevisionId(bzr_rev_id, cls)
 
138
        return bzr_rev_id[len(cls.revid_prefix)+1:], cls()
50
139
 
51
140
    def generate_file_id(self, path):
 
141
        # Git paths are just bytestrings
 
142
        # We must just hope they are valid UTF-8..
52
143
        if path == "":
53
144
            return ROOT_ID
54
 
        return escape_file_id(path.encode('utf-8'))
 
145
        return escape_file_id(path)
 
146
 
 
147
    def parse_file_id(self, file_id):
 
148
        if file_id == ROOT_ID:
 
149
            return ""
 
150
        return unescape_file_id(file_id)
 
151
 
 
152
    def import_unusual_file_modes(self, rev, unusual_file_modes):
 
153
        if unusual_file_modes:
 
154
            ret = [(name, unusual_file_modes[name])
 
155
                   for name in sorted(unusual_file_modes.keys())]
 
156
            rev.properties['file-modes'] = bencode.bencode(ret)
 
157
 
 
158
    def export_unusual_file_modes(self, rev):
 
159
        try:
 
160
            return dict([(self.generate_file_id(path), mode) for (path, mode) in bencode.bdecode(rev.properties['file-modes'].encode("utf-8"))])
 
161
        except KeyError:
 
162
            return {}
 
163
 
 
164
    def _generate_git_svn_metadata(self, rev, encoding):
 
165
        try:
 
166
            return "\ngit-svn-id: %s\n" % rev.properties["git-svn-id"].encode(
 
167
                encoding)
 
168
        except KeyError:
 
169
            return ""
 
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 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 _decode_commit_message(self, rev, message, encoding):
 
213
        return message.decode(encoding)
 
214
 
 
215
    def _encode_commit_message(self, rev, message, encoding):
 
216
        return message.encode(encoding)
 
217
 
 
218
    def export_commit(self, rev, tree_sha, parent_lookup):
 
219
        """Turn a Bazaar revision in to a Git commit
 
220
 
 
221
        :param tree_sha: Tree sha for the commit
 
222
        :param parent_lookup: Function for looking up the GIT sha equiv of a
 
223
            bzr revision
 
224
        :return dulwich.objects.Commit represent the revision:
 
225
        """
 
226
        from dulwich.objects import Commit
 
227
        commit = Commit()
 
228
        commit.tree = tree_sha
 
229
        for p in rev.parent_ids:
 
230
            try:
 
231
                git_p = parent_lookup(p)
 
232
            except KeyError:
 
233
                git_p = None
 
234
            if git_p is not None:
 
235
                assert len(git_p) == 40, "unexpected length for %r" % git_p
 
236
                commit.parents.append(git_p)
 
237
        try:
 
238
            encoding = rev.properties['git-explicit-encoding']
 
239
        except KeyError:
 
240
            encoding = rev.properties.get('git-implicit-encoding', 'utf-8')
 
241
        commit.encoding = rev.properties.get('git-explicit-encoding')
 
242
        commit.committer = fix_person_identifier(rev.committer.encode(
 
243
            encoding))
 
244
        commit.author = fix_person_identifier(
 
245
            rev.get_apparent_authors()[0].encode(encoding))
 
246
        commit.commit_time = long(rev.timestamp)
 
247
        if 'author-timestamp' in rev.properties:
 
248
            commit.author_time = long(rev.properties['author-timestamp'])
 
249
        else:
 
250
            commit.author_time = commit.commit_time
 
251
        commit.commit_timezone = rev.timezone
 
252
        if 'author-timezone' in rev.properties:
 
253
            commit.author_timezone = int(rev.properties['author-timezone'])
 
254
        else:
 
255
            commit.author_timezone = commit.commit_timezone
 
256
        commit.message = self._encode_commit_message(rev, rev.message, 
 
257
            encoding)
 
258
        return commit
55
259
 
56
260
    def import_commit(self, commit):
57
261
        """Convert a git commit to a bzr revision.
60
264
        """
61
265
        if commit is None:
62
266
            raise AssertionError("Commit object can't be None")
63
 
        rev = ForeignRevision(commit.id, self, self.revision_id_foreign_to_bzr(commit.id))
 
267
        rev = ForeignRevision(commit.id, self,
 
268
                self.revision_id_foreign_to_bzr(commit.id))
64
269
        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")
 
270
        def decode_using_encoding(rev, commit, encoding):
 
271
            rev.committer = str(commit.committer).decode(encoding)
 
272
            if commit.committer != commit.author:
 
273
                rev.properties['author'] = str(commit.author).decode(encoding)
 
274
            rev.message = self._decode_commit_message(rev, commit.message, 
 
275
                encoding)
 
276
        if commit.encoding is not None:
 
277
            rev.properties['git-explicit-encoding'] = commit.encoding
 
278
            decode_using_encoding(rev, commit, commit.encoding)
 
279
        else:
 
280
            for encoding in ('utf-8', 'latin1'):
 
281
                try:
 
282
                    decode_using_encoding(rev, commit, encoding)
 
283
                except UnicodeDecodeError:
 
284
                    pass
 
285
                else:
 
286
                    if encoding != 'utf-8':
 
287
                        rev.properties['git-implicit-encoding'] = encoding
 
288
                    break
 
289
        if commit.commit_time != commit.author_time:
 
290
            rev.properties['author-timestamp'] = str(commit.author_time)
 
291
        if commit.commit_timezone != commit.author_timezone:
 
292
            rev.properties['author-timezone'] = "%d" % commit.author_timezone
69
293
        rev.timestamp = commit.commit_time
70
 
        rev.timezone = 0
 
294
        rev.timezone = commit.commit_timezone
71
295
        return rev
72
296
 
73
297
 
74
 
class BzrGitMappingExperimental(BzrGitMapping):
 
298
class BzrGitMappingv1(BzrGitMapping):
 
299
    revid_prefix = 'git-v1'
 
300
    experimental = False
 
301
 
 
302
    def __str__(self):
 
303
        return self.revid_prefix
 
304
 
 
305
 
 
306
class BzrGitMappingExperimental(BzrGitMappingv1):
75
307
    revid_prefix = 'git-experimental'
76
308
    experimental = True
77
309
 
78
 
 
79
 
default_mapping = BzrGitMappingExperimental()
 
310
    def _decode_commit_message(self, rev, message, encoding):
 
311
        message = self._extract_hg_metadata(rev, message)
 
312
        message = self._extract_git_svn_metadata(rev, message)
 
313
        return message.decode(encoding)
 
314
 
 
315
    def _encode_commit_message(self, rev, message, encoding):
 
316
        ret = message.encode(encoding)
 
317
        ret += self._generate_hg_message_tail(rev)
 
318
        ret += self._generate_git_svn_metadata(rev, encoding)
 
319
        return ret
 
320
 
 
321
    def import_commit(self, commit):
 
322
        rev = super(BzrGitMappingExperimental, self).import_commit(commit)
 
323
        rev.properties['converted_revision'] = "git %s\n" % commit.id
 
324
        return rev
 
325
 
 
326
 
 
327
class GitMappingRegistry(VcsMappingRegistry):
 
328
    """Registry with available git mappings."""
 
329
 
 
330
    def revision_id_bzr_to_foreign(self, bzr_revid):
 
331
        if bzr_revid == NULL_REVISION:
 
332
            return "0" * 20, None
 
333
        if not bzr_revid.startswith("git-"):
 
334
            raise errors.InvalidRevisionId(bzr_revid, None)
 
335
        (mapping_version, git_sha) = bzr_revid.split(":", 1)
 
336
        mapping = self.get(mapping_version)
 
337
        return mapping.revision_id_bzr_to_foreign(bzr_revid)
 
338
 
 
339
    parse_revision_id = revision_id_bzr_to_foreign
 
340
 
 
341
 
 
342
mapping_registry = GitMappingRegistry()
 
343
mapping_registry.register_lazy('git-v1', "bzrlib.plugins.git.mapping",
 
344
    "BzrGitMappingv1")
 
345
mapping_registry.register_lazy('git-experimental',
 
346
    "bzrlib.plugins.git.mapping", "BzrGitMappingExperimental")
 
347
mapping_registry.set_default('git-v1')
 
348
 
 
349
 
 
350
class ForeignGit(ForeignVcs):
 
351
    """The Git Stupid Content Tracker"""
 
352
 
 
353
    @property
 
354
    def branch_format(self):
 
355
        from bzrlib.plugins.git.branch import GitBranchFormat
 
356
        return GitBranchFormat()
 
357
 
 
358
    @property
 
359
    def repository_format(self):
 
360
        from bzrlib.plugins.git.repository import GitRepositoryFormat
 
361
        return GitRepositoryFormat()
 
362
 
 
363
    def __init__(self):
 
364
        super(ForeignGit, self).__init__(mapping_registry)
 
365
        self.abbreviation = "git"
 
366
 
 
367
    @classmethod
 
368
    def serialize_foreign_revid(self, foreign_revid):
 
369
        return foreign_revid
 
370
 
 
371
    @classmethod
 
372
    def show_foreign_revid(cls, foreign_revid):
 
373
        return { "git commit": foreign_revid }
 
374
 
 
375
 
 
376
foreign_git = ForeignGit()
 
377
default_mapping = mapping_registry.get_default()()
 
378
 
 
379
 
 
380
def symlink_to_blob(entry):
 
381
    from dulwich.objects import Blob
 
382
    blob = Blob()
 
383
    symlink_target = entry.symlink_target
 
384
    if type(symlink_target) == unicode:
 
385
        symlink_target = symlink_target.encode('utf-8')
 
386
    blob.data = symlink_target
 
387
    return blob
 
388
 
 
389
 
 
390
def mode_is_executable(mode):
 
391
    """Check if mode should be considered executable."""
 
392
    return bool(mode & 0111)
 
393
 
 
394
 
 
395
def mode_kind(mode):
 
396
    """Determine the Bazaar inventory kind based on Unix file mode."""
 
397
    entry_kind = (mode & 0700000) / 0100000
 
398
    if entry_kind == 0:
 
399
        return 'directory'
 
400
    elif entry_kind == 1:
 
401
        file_kind = (mode & 070000) / 010000
 
402
        if file_kind == 0:
 
403
            return 'file'
 
404
        elif file_kind == 2:
 
405
            return 'symlink'
 
406
        elif file_kind == 6:
 
407
            return 'tree-reference'
 
408
        else:
 
409
            raise AssertionError(
 
410
                "Unknown file kind %d, perms=%o." % (file_kind, mode,))
 
411
    else:
 
412
        raise AssertionError(
 
413
            "Unknown kind, perms=%r." % (mode,))
 
414
 
 
415
 
 
416
def object_mode(kind, executable):
 
417
    if kind == 'directory':
 
418
        return stat.S_IFDIR
 
419
    elif kind == 'symlink':
 
420
        mode = stat.S_IFLNK
 
421
        if executable:
 
422
            mode |= 0111
 
423
        return mode
 
424
    elif kind == 'file':
 
425
        mode = stat.S_IFREG | 0644
 
426
        if executable:
 
427
            mode |= 0111
 
428
        return mode
 
429
    elif kind == 'tree-reference':
 
430
        from dulwich.objects import S_IFGITLINK
 
431
        return S_IFGITLINK
 
432
    else:
 
433
        raise AssertionError
 
434
 
 
435
 
 
436
def entry_mode(entry):
 
437
    """Determine the git file mode for an inventory entry."""
 
438
    return object_mode(entry.kind, entry.executable)
 
439
 
 
440
 
 
441
def directory_to_tree(entry, lookup_ie_sha1, unusual_modes):
 
442
    from dulwich.objects import Tree
 
443
    tree = Tree()
 
444
    for name, value in entry.children.iteritems():
 
445
        ie = entry.children[name]
 
446
        try:
 
447
            mode = unusual_modes[ie.file_id]
 
448
        except KeyError:
 
449
            mode = entry_mode(ie)
 
450
        hexsha = lookup_ie_sha1(ie)
 
451
        if hexsha is not None:
 
452
            tree.add(mode, name.encode("utf-8"), hexsha)
 
453
    if entry.parent_id is not None and len(tree) == 0:
 
454
        # Only the root can be an empty tree
 
455
        return None
 
456
    return tree
 
457
 
 
458
 
 
459
def extract_unusual_modes(rev):
 
460
    try:
 
461
        foreign_revid, mapping = mapping_registry.parse_revision_id(
 
462
            rev.revision_id)
 
463
    except errors.InvalidRevisionId:
 
464
        return {}
 
465
    else:
 
466
        return mapping.export_unusual_file_modes(rev)
 
467
 
 
468
 
 
469
def inventory_to_tree_and_blobs(inventory, texts, mapping, unusual_modes,
 
470
                                cur=None):
 
471
    """Convert a Bazaar tree to a Git tree.
 
472
 
 
473
    :return: Yields tuples with object sha1, object and path
 
474
    """
 
475
    from dulwich.objects import Tree
 
476
    import stat
 
477
    stack = []
 
478
    if cur is None:
 
479
        cur = ""
 
480
    tree = Tree()
 
481
 
 
482
    # stack contains the set of trees that we haven't
 
483
    # finished constructing
 
484
    for path, entry in inventory.iter_entries():
 
485
        while stack and not path.startswith(osutils.pathjoin(cur, "")):
 
486
            # We've hit a file that's not a child of the previous path
 
487
            sha = tree.id
 
488
            yield sha, tree, cur.encode("utf-8")
 
489
            mode = unusual_modes.get(cur.encode("utf-8"), stat.S_IFDIR)
 
490
            t = (mode, urlutils.basename(cur).encode('UTF-8'), sha)
 
491
            cur, tree = stack.pop()
 
492
            tree.add(*t)
 
493
 
 
494
        if entry.kind == "directory":
 
495
            stack.append((cur, tree))
 
496
            cur = path
 
497
            tree = Tree()
 
498
        else:
 
499
            if entry.kind == "file":
 
500
                from dulwich.objects import Blob
 
501
                stream = texts.get_record_stream(
 
502
                    [(entry.file_id, entry.revision)], 'unordered', True)
 
503
                blob = Blob()
 
504
                blob.chunked = stream.next().get_bytes_as('chunks')
 
505
            elif entry.kind == "symlink":
 
506
                blob = symlink_to_blob(entry)
 
507
            else:
 
508
                raise AssertionError("Unknown kind %s" % entry.kind)
 
509
            sha = blob.id
 
510
            yield sha, blob, path.encode("utf-8")
 
511
            name = urlutils.basename(path).encode("utf-8")
 
512
            mode = unusual_modes.get(path.encode("utf-8"), entry_mode(entry))
 
513
            tree.add(mode, name, sha)
 
514
 
 
515
    while len(stack) > 1:
 
516
        sha = tree.id
 
517
        yield sha, tree, cur.encode("utf-8")
 
518
        mode = unusual_modes.get(cur.encode('utf-8'), stat.S_IFDIR)
 
519
        t = (mode, urlutils.basename(cur).encode('UTF-8'), sha)
 
520
        cur, tree = stack.pop()
 
521
        tree.add(*t)
 
522
 
 
523
    yield tree.id, tree, cur.encode("utf-8")
 
524
 
 
525
 
 
526
def parse_git_svn_id(text):
 
527
    (head, uuid) = text.rsplit(" ", 1)
 
528
    (full_url, rev) = head.rsplit("@", 1)
 
529
    return (full_url, int(rev), uuid)