/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 support for executable symlinks.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2007-2008 Canonical Ltd
 
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, urlutils
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
 
        ForeignVcs, 
23
 
        VcsMappingRegistry, 
24
 
        ForeignRevision,
25
 
        )
 
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
 
52
 
26
53
 
27
54
def escape_file_id(file_id):
28
55
    return file_id.replace('_', '__').replace(' ', '_s')
29
56
 
30
57
 
31
58
def unescape_file_id(file_id):
32
 
    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)
33
112
 
34
113
 
35
114
class BzrGitMapping(foreign.VcsMapping):
55
134
        return bzr_rev_id[len(cls.revid_prefix)+1:], cls()
56
135
 
57
136
    def generate_file_id(self, path):
 
137
        # Git paths are just bytestrings
 
138
        # We must just hope they are valid UTF-8..
58
139
        if path == "":
59
140
            return ROOT_ID
60
 
        return escape_file_id(path.encode('utf-8'))
 
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
            git_p = parent_lookup(p)
 
221
            if git_p is not None:
 
222
                assert len(git_p) == 40, "unexpected length for %r" % git_p
 
223
                commit.parents.append(git_p)
 
224
        commit.committer = fix_person_identifier(rev.committer.encode("utf-8"))
 
225
        commit.author = fix_person_identifier(rev.get_apparent_authors()[0].encode("utf-8"))
 
226
        commit.commit_time = long(rev.timestamp)
 
227
        if 'author-timestamp' in rev.properties:
 
228
            commit.author_time = long(rev.properties['author-timestamp'])
 
229
        else:
 
230
            commit.author_time = commit.commit_time
 
231
        commit.commit_timezone = rev.timezone
 
232
        if 'author-timezone' in rev.properties:
 
233
            commit.author_timezone = int(rev.properties['author-timezone'])
 
234
        else:
 
235
            commit.author_timezone = commit.commit_timezone
 
236
        commit.message = self._encode_commit_message(rev, rev.message)
 
237
        return commit
61
238
 
62
239
    def import_commit(self, commit):
63
240
        """Convert a git commit to a bzr revision.
68
245
            raise AssertionError("Commit object can't be None")
69
246
        rev = ForeignRevision(commit.id, self, self.revision_id_foreign_to_bzr(commit.id))
70
247
        rev.parent_ids = tuple([self.revision_id_foreign_to_bzr(p) for p in commit.parents])
71
 
        rev.message = commit.message.decode("utf-8", "replace")
72
248
        rev.committer = str(commit.committer).decode("utf-8", "replace")
73
249
        if commit.committer != commit.author:
74
250
            rev.properties['author'] = str(commit.author).decode("utf-8", "replace")
 
251
 
 
252
        if commit.commit_time != commit.author_time:
 
253
            rev.properties['author-timestamp'] = str(commit.author_time)
 
254
        if commit.commit_timezone != commit.author_timezone:
 
255
            rev.properties['author-timezone'] = "%d" % (commit.author_timezone, )
75
256
        rev.timestamp = commit.commit_time
76
 
        rev.timezone = 0
 
257
        rev.timezone = commit.commit_timezone
 
258
        rev.message = self._decode_commit_message(rev, commit.message)
77
259
        return rev
78
260
 
79
261
 
81
263
    revid_prefix = 'git-v1'
82
264
    experimental = False
83
265
 
 
266
    def __str__(self):
 
267
        return self.revid_prefix
 
268
 
84
269
 
85
270
class BzrGitMappingExperimental(BzrGitMappingv1):
86
271
    revid_prefix = 'git-experimental'
87
272
    experimental = True
88
273
 
 
274
    def _decode_commit_message(self, rev, message):
 
275
        message = self._extract_hg_metadata(rev, message)
 
276
        message = self._extract_git_svn_metadata(rev, message)
 
277
        return message.decode("utf-8", "replace")
 
278
 
 
279
    def _encode_commit_message(self, rev, message):
 
280
        ret = message.encode("utf-8")
 
281
        ret += self._generate_hg_message_tail(rev)
 
282
        ret += self._generate_git_svn_metadata(rev)
 
283
        return ret
 
284
 
 
285
    def import_commit(self, commit):
 
286
        rev = super(BzrGitMappingExperimental, self).import_commit(commit)
 
287
        rev.properties['converted_revision'] = "git %s\n" % commit.id
 
288
        return rev
 
289
 
89
290
 
90
291
class GitMappingRegistry(VcsMappingRegistry):
 
292
    """Registry with available git mappings."""
91
293
 
92
294
    def revision_id_bzr_to_foreign(self, bzr_revid):
 
295
        if bzr_revid == NULL_REVISION:
 
296
            return "0" * 20, None
93
297
        if not bzr_revid.startswith("git-"):
94
298
            raise errors.InvalidRevisionId(bzr_revid, None)
95
299
        (mapping_version, git_sha) = bzr_revid.split(":", 1)
104
308
                                   "BzrGitMappingv1")
105
309
mapping_registry.register_lazy('git-experimental', "bzrlib.plugins.git.mapping",
106
310
                                   "BzrGitMappingExperimental")
 
311
mapping_registry.set_default('git-v1')
107
312
 
108
313
 
109
314
class ForeignGit(ForeignVcs):
110
 
    """Foreign Git."""
 
315
    """The Git Stupid Content Tracker"""
 
316
 
 
317
    @property
 
318
    def branch_format(self):
 
319
        from bzrlib.plugins.git.branch import GitBranchFormat
 
320
        return GitBranchFormat()
 
321
 
 
322
    @property
 
323
    def repository_format(self):
 
324
        from bzrlib.plugins.git.repository import GitRepositoryFormat
 
325
        return GitRepositoryFormat()
111
326
 
112
327
    def __init__(self):
113
328
        super(ForeignGit, self).__init__(mapping_registry)
 
329
        self.abbreviation = "git"
 
330
 
 
331
    @classmethod
 
332
    def serialize_foreign_revid(self, foreign_revid):
 
333
        return foreign_revid
114
334
 
115
335
    @classmethod
116
336
    def show_foreign_revid(cls, foreign_revid):
118
338
 
119
339
 
120
340
foreign_git = ForeignGit()
121
 
default_mapping = BzrGitMappingv1()
122
 
 
123
 
 
124
 
def inventory_to_tree_and_blobs(repo, mapping, revision_id):
125
 
    from dulwich.objects import Tree, Blob
126
 
    from bzrlib.inventory import InventoryDirectory, InventoryFile
 
341
default_mapping = mapping_registry.get_default()()
 
342
 
 
343
 
 
344
def text_to_blob(texts, entry):
 
345
    from dulwich.objects import Blob
 
346
    text = texts.get_record_stream([(entry.file_id, entry.revision)], 'unordered', True).next().get_bytes_as('fulltext')
 
347
    blob = Blob()
 
348
    blob._text = text
 
349
    return blob
 
350
 
 
351
 
 
352
def symlink_to_blob(entry):
 
353
    from dulwich.objects import Blob
 
354
    blob = Blob()
 
355
    blob._text = entry.symlink_target
 
356
    return blob
 
357
 
 
358
 
 
359
def mode_is_executable(mode):
 
360
    """Check if mode should be considered executable."""
 
361
    return bool(mode & 0111)
 
362
 
 
363
 
 
364
def mode_kind(mode):
 
365
    """Determine the Bazaar inventory kind based on Unix file mode."""
 
366
    entry_kind = (mode & 0700000) / 0100000
 
367
    if entry_kind == 0:
 
368
        return 'directory'
 
369
    elif entry_kind == 1:
 
370
        file_kind = (mode & 070000) / 010000
 
371
        if file_kind == 0:
 
372
            return 'file'
 
373
        elif file_kind == 2:
 
374
            return 'symlink'
 
375
        elif file_kind == 6:
 
376
            return 'tree-reference'
 
377
        else:
 
378
            raise AssertionError(
 
379
                "Unknown file kind %d, perms=%o." % (file_kind, mode,))
 
380
    else:
 
381
        raise AssertionError(
 
382
            "Unknown kind, perms=%r." % (mode,))
 
383
 
 
384
 
 
385
def object_mode(kind, executable):
 
386
    if kind == 'directory':
 
387
        return stat.S_IFDIR
 
388
    elif kind == 'symlink':
 
389
        mode = stat.S_IFLNK
 
390
        if executable:
 
391
            mode |= 0111
 
392
        return mode
 
393
    elif kind == 'file':
 
394
        mode = stat.S_IFREG | 0644
 
395
        if executable:
 
396
            mode |= 0111
 
397
        return mode
 
398
    elif kind == 'tree-reference':
 
399
        from dulwich.objects import S_IFGITLINK
 
400
        return S_IFGITLINK
 
401
    else:
 
402
        raise AssertionError
 
403
 
 
404
 
 
405
def entry_mode(entry):
 
406
    """Determine the git file mode for an inventory entry."""
 
407
    return object_mode(entry.kind, entry.executable)
 
408
 
 
409
 
 
410
def directory_to_tree(entry, lookup_ie_sha1, unusual_modes):
 
411
    from dulwich.objects import Tree
 
412
    tree = Tree()
 
413
    for name in sorted(entry.children.keys()):
 
414
        ie = entry.children[name]
 
415
        try:
 
416
            mode = unusual_modes[ie.file_id]
 
417
        except KeyError:
 
418
            mode = entry_mode(ie)
 
419
        hexsha = lookup_ie_sha1(ie)
 
420
        if hexsha is not None:
 
421
            tree.add(mode, name.encode("utf-8"), hexsha)
 
422
    if entry.parent_id is not None and len(tree) == 0:
 
423
        # Only the root can be an empty tree
 
424
        return None
 
425
    tree.serialize()
 
426
    return tree
 
427
 
 
428
 
 
429
def extract_unusual_modes(rev):
 
430
    try:
 
431
        foreign_revid, mapping = mapping_registry.parse_revision_id(rev.revision_id)
 
432
    except errors.InvalidRevisionId:
 
433
        return {}
 
434
    else:
 
435
        return mapping.export_unusual_file_modes(rev)
 
436
 
 
437
 
 
438
def inventory_to_tree_and_blobs(inventory, texts, mapping, unusual_modes, cur=None):
 
439
    """Convert a Bazaar tree to a Git tree.
 
440
 
 
441
    :return: Yields tuples with object sha1, object and path
 
442
    """
 
443
    from dulwich.objects import Tree
127
444
    import stat
128
445
    stack = []
129
 
    cur = ""
 
446
    if cur is None:
 
447
        cur = ""
130
448
    tree = Tree()
131
449
 
132
 
    inv = repo.get_inventory(revision_id)
133
 
 
134
 
    # stack contains the set of trees that we haven't 
 
450
    # stack contains the set of trees that we haven't
135
451
    # finished constructing
136
 
 
137
 
    for path, entry in inv.iter_entries():
138
 
        while stack and not path.startswith(cur):
 
452
    for path, entry in inventory.iter_entries():
 
453
        while stack and not path.startswith(osutils.pathjoin(cur, "")):
 
454
            # We've hit a file that's not a child of the previous path
139
455
            tree.serialize()
140
 
            sha = tree.sha().hexdigest()
141
 
            yield sha, tree, cur
142
 
            t = (stat.S_IFDIR, urlutils.basename(cur).encode('UTF-8'), sha)
 
456
            sha = tree.id
 
457
            yield sha, tree, cur.encode("utf-8")
 
458
            mode = unusual_modes.get(cur.encode("utf-8"), stat.S_IFDIR)
 
459
            t = (mode, urlutils.basename(cur).encode('UTF-8'), sha)
143
460
            cur, tree = stack.pop()
144
461
            tree.add(*t)
145
462
 
146
 
        if type(entry) == InventoryDirectory:
 
463
        if entry.kind == "directory":
147
464
            stack.append((cur, tree))
148
465
            cur = path
149
466
            tree = Tree()
150
 
 
151
 
        if type(entry) == InventoryFile:
152
 
            #FIXME: We can make potentially make this Lazy to avoid shaing lots of stuff
153
 
            # and having all these objects in memory at once
154
 
            blob = Blob()
155
 
            _, blob._text = repo.iter_files_bytes([(entry.file_id, entry.revision, path)]).next()
156
 
            sha = blob.sha().hexdigest()
157
 
            yield sha, blob, path
158
 
 
 
467
        else:
 
468
            if entry.kind == "file":
 
469
                blob = text_to_blob(texts, entry)
 
470
            elif entry.kind == "symlink":
 
471
                blob = symlink_to_blob(entry)
 
472
            else:
 
473
                raise AssertionError("Unknown kind %s" % entry.kind)
 
474
            sha = blob.id
 
475
            yield sha, blob, path.encode("utf-8")
159
476
            name = urlutils.basename(path).encode("utf-8")
160
 
            mode = stat.S_IFREG | 0644
161
 
            if entry.executable:
162
 
                mode |= 0111
 
477
            mode = unusual_modes.get(path.encode("utf-8"), entry_mode(entry))
163
478
            tree.add(mode, name, sha)
164
479
 
165
480
    while len(stack) > 1:
166
481
        tree.serialize()
167
 
        sha = tree.sha().hexdigest()
168
 
        yield sha, tree, cur
169
 
        t = (stat.S_IFDIR, urlutils.basename(cur).encode('UTF-8'), sha)
 
482
        sha = tree.id
 
483
        yield sha, tree, cur.encode("utf-8")
 
484
        mode = unusual_modes.get(cur.encode('utf-8'), stat.S_IFDIR)
 
485
        t = (mode, urlutils.basename(cur).encode('UTF-8'), sha)
170
486
        cur, tree = stack.pop()
171
487
        tree.add(*t)
172
488
 
173
489
    tree.serialize()
174
 
    yield tree.sha().hexdigest(), tree, cur
175
 
 
176
 
 
177
 
def revision_to_commit(rev, tree_sha, parent_lookup):
178
 
    """Turn a Bazaar revision in to a Git commit
179
 
 
180
 
    :param tree_sha: Tree sha for the commit
181
 
    :param parent_lookup: Function for looking up the GIT sha equiv of a bzr revision
182
 
    :return dulwich.objects.Commit represent the revision:
183
 
    """
184
 
    from dulwich.objects import Commit
185
 
    commit = Commit()
186
 
    commit._tree = tree_sha
187
 
    for p in rev.parent_ids:
188
 
        git_p = parent_lookup(p)
189
 
        if git_p is not None:
190
 
            commit._parents.append(git_p)
191
 
    commit._message = rev.message.encode("utf-8")
192
 
    commit._committer = rev.committer.encode("utf-8")
193
 
    commit._author = rev.get_apparent_author().encode("utf-8")
194
 
    commit._commit_time = long(rev.timestamp)
195
 
    commit.serialize()
196
 
    return commit
 
490
    yield tree.id, tree, cur.encode("utf-8")
 
491
 
 
492
 
 
493
def parse_git_svn_id(text):
 
494
    (head, uuid) = text.rsplit(" ", 1)
 
495
    (full_url, rev) = head.rsplit("@", 1)
 
496
    return (full_url, int(rev), uuid)