/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

Merge http support.

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