/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

Provide right infrastructure for foreign repository tests from bzrlib.

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