/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

Use standard names for lookup functions.

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