/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-03-22 11:00:09 UTC
  • mto: (0.254.17 index-based)
  • mto: This revision was merged to the branch mainline in revision 6960.
  • Revision ID: jelmer@samba.org-20100322110009-o6js81mt9mvokk1b
Still default to localhost, update NEWS

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