/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

Factor out conversion of branch names to refs.

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):
40
119
        super(BzrGitMapping, self).__init__(foreign_git)
41
120
 
42
121
    def __eq__(self, other):
43
 
        return type(self) == type(other) and self.revid_prefix == other.revid_prefix
 
122
        return (type(self) == type(other) and 
 
123
                self.revid_prefix == other.revid_prefix)
44
124
 
45
125
    @classmethod
46
126
    def revision_id_foreign_to_bzr(cls, git_rev_id):
55
135
        return bzr_rev_id[len(cls.revid_prefix)+1:], cls()
56
136
 
57
137
    def generate_file_id(self, path):
 
138
        # Git paths are just bytestrings
 
139
        # We must just hope they are valid UTF-8..
58
140
        if path == "":
59
141
            return ROOT_ID
60
 
        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):
 
162
        try:
 
163
            return "\ngit-svn-id: %s\n" % rev.properties["git-svn-id"].encode("utf-8")
 
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):
 
206
        return message.decode("utf-8", "replace")
 
207
 
 
208
    def _encode_commit_message(self, rev, message):
 
209
        return message.encode("utf-8")
 
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
        commit.committer = fix_person_identifier(rev.committer.encode("utf-8"))
 
230
        commit.author = fix_person_identifier(rev.get_apparent_authors()[0].encode("utf-8"))
 
231
        commit.commit_time = long(rev.timestamp)
 
232
        if 'author-timestamp' in rev.properties:
 
233
            commit.author_time = long(rev.properties['author-timestamp'])
 
234
        else:
 
235
            commit.author_time = commit.commit_time
 
236
        commit.commit_timezone = rev.timezone
 
237
        if 'author-timezone' in rev.properties:
 
238
            commit.author_timezone = int(rev.properties['author-timezone'])
 
239
        else:
 
240
            commit.author_timezone = commit.commit_timezone
 
241
        commit.message = self._encode_commit_message(rev, rev.message)
 
242
        return commit
61
243
 
62
244
    def import_commit(self, commit):
63
245
        """Convert a git commit to a bzr revision.
68
250
            raise AssertionError("Commit object can't be None")
69
251
        rev = ForeignRevision(commit.id, self, self.revision_id_foreign_to_bzr(commit.id))
70
252
        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
253
        rev.committer = str(commit.committer).decode("utf-8", "replace")
73
254
        if commit.committer != commit.author:
74
255
            rev.properties['author'] = str(commit.author).decode("utf-8", "replace")
 
256
 
 
257
        if commit.commit_time != commit.author_time:
 
258
            rev.properties['author-timestamp'] = str(commit.author_time)
 
259
        if commit.commit_timezone != commit.author_timezone:
 
260
            rev.properties['author-timezone'] = "%d" % (commit.author_timezone, )
75
261
        rev.timestamp = commit.commit_time
76
 
        rev.timezone = 0
 
262
        rev.timezone = commit.commit_timezone
 
263
        rev.message = self._decode_commit_message(rev, commit.message)
77
264
        return rev
78
265
 
79
266
 
81
268
    revid_prefix = 'git-v1'
82
269
    experimental = False
83
270
 
 
271
    def __str__(self):
 
272
        return self.revid_prefix
 
273
 
84
274
 
85
275
class BzrGitMappingExperimental(BzrGitMappingv1):
86
276
    revid_prefix = 'git-experimental'
87
277
    experimental = True
88
278
 
 
279
    def _decode_commit_message(self, rev, message):
 
280
        message = self._extract_hg_metadata(rev, message)
 
281
        message = self._extract_git_svn_metadata(rev, message)
 
282
        return message.decode("utf-8", "replace")
 
283
 
 
284
    def _encode_commit_message(self, rev, message):
 
285
        ret = message.encode("utf-8")
 
286
        ret += self._generate_hg_message_tail(rev)
 
287
        ret += self._generate_git_svn_metadata(rev)
 
288
        return ret
 
289
 
 
290
    def import_commit(self, commit):
 
291
        rev = super(BzrGitMappingExperimental, self).import_commit(commit)
 
292
        rev.properties['converted_revision'] = "git %s\n" % commit.id
 
293
        return rev
 
294
 
89
295
 
90
296
class GitMappingRegistry(VcsMappingRegistry):
 
297
    """Registry with available git mappings."""
91
298
 
92
299
    def revision_id_bzr_to_foreign(self, bzr_revid):
 
300
        if bzr_revid == NULL_REVISION:
 
301
            return "0" * 20, None
93
302
        if not bzr_revid.startswith("git-"):
94
303
            raise errors.InvalidRevisionId(bzr_revid, None)
95
304
        (mapping_version, git_sha) = bzr_revid.split(":", 1)
104
313
                                   "BzrGitMappingv1")
105
314
mapping_registry.register_lazy('git-experimental', "bzrlib.plugins.git.mapping",
106
315
                                   "BzrGitMappingExperimental")
 
316
mapping_registry.set_default('git-v1')
107
317
 
108
318
 
109
319
class ForeignGit(ForeignVcs):
110
 
    """Foreign Git."""
 
320
    """The Git Stupid Content Tracker"""
 
321
 
 
322
    @property
 
323
    def branch_format(self):
 
324
        from bzrlib.plugins.git.branch import GitBranchFormat
 
325
        return GitBranchFormat()
 
326
 
 
327
    @property
 
328
    def repository_format(self):
 
329
        from bzrlib.plugins.git.repository import GitRepositoryFormat
 
330
        return GitRepositoryFormat()
111
331
 
112
332
    def __init__(self):
113
333
        super(ForeignGit, self).__init__(mapping_registry)
 
334
        self.abbreviation = "git"
 
335
 
 
336
    @classmethod
 
337
    def serialize_foreign_revid(self, foreign_revid):
 
338
        return foreign_revid
114
339
 
115
340
    @classmethod
116
341
    def show_foreign_revid(cls, foreign_revid):
118
343
 
119
344
 
120
345
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
 
346
default_mapping = mapping_registry.get_default()()
 
347
 
 
348
 
 
349
def text_to_blob(texts, entry):
 
350
    from dulwich.objects import Blob
 
351
    text = texts.get_record_stream([(entry.file_id, entry.revision)], 'unordered', True).next().get_bytes_as('fulltext')
 
352
    blob = Blob()
 
353
    blob._text = text
 
354
    return blob
 
355
 
 
356
 
 
357
def symlink_to_blob(entry):
 
358
    from dulwich.objects import Blob
 
359
    blob = Blob()
 
360
    blob._text = entry.symlink_target
 
361
    return blob
 
362
 
 
363
 
 
364
def mode_is_executable(mode):
 
365
    """Check if mode should be considered executable."""
 
366
    return bool(mode & 0111)
 
367
 
 
368
 
 
369
def mode_kind(mode):
 
370
    """Determine the Bazaar inventory kind based on Unix file mode."""
 
371
    entry_kind = (mode & 0700000) / 0100000
 
372
    if entry_kind == 0:
 
373
        return 'directory'
 
374
    elif entry_kind == 1:
 
375
        file_kind = (mode & 070000) / 010000
 
376
        if file_kind == 0:
 
377
            return 'file'
 
378
        elif file_kind == 2:
 
379
            return 'symlink'
 
380
        elif file_kind == 6:
 
381
            return 'tree-reference'
 
382
        else:
 
383
            raise AssertionError(
 
384
                "Unknown file kind %d, perms=%o." % (file_kind, mode,))
 
385
    else:
 
386
        raise AssertionError(
 
387
            "Unknown kind, perms=%r." % (mode,))
 
388
 
 
389
 
 
390
def object_mode(kind, executable):
 
391
    if kind == 'directory':
 
392
        return stat.S_IFDIR
 
393
    elif kind == 'symlink':
 
394
        mode = stat.S_IFLNK
 
395
        if executable:
 
396
            mode |= 0111
 
397
        return mode
 
398
    elif kind == 'file':
 
399
        mode = stat.S_IFREG | 0644
 
400
        if executable:
 
401
            mode |= 0111
 
402
        return mode
 
403
    elif kind == 'tree-reference':
 
404
        from dulwich.objects import S_IFGITLINK
 
405
        return S_IFGITLINK
 
406
    else:
 
407
        raise AssertionError
 
408
 
 
409
 
 
410
def entry_mode(entry):
 
411
    """Determine the git file mode for an inventory entry."""
 
412
    return object_mode(entry.kind, entry.executable)
 
413
 
 
414
 
 
415
def directory_to_tree(entry, lookup_ie_sha1, unusual_modes):
 
416
    from dulwich.objects import Tree
 
417
    tree = Tree()
 
418
    for name in sorted(entry.children.keys()):
 
419
        ie = entry.children[name]
 
420
        try:
 
421
            mode = unusual_modes[ie.file_id]
 
422
        except KeyError:
 
423
            mode = entry_mode(ie)
 
424
        hexsha = lookup_ie_sha1(ie)
 
425
        if hexsha is not None:
 
426
            tree.add(mode, name.encode("utf-8"), hexsha)
 
427
    if entry.parent_id is not None and len(tree) == 0:
 
428
        # Only the root can be an empty tree
 
429
        return None
 
430
    tree.serialize()
 
431
    return tree
 
432
 
 
433
 
 
434
def extract_unusual_modes(rev):
 
435
    try:
 
436
        foreign_revid, mapping = mapping_registry.parse_revision_id(rev.revision_id)
 
437
    except errors.InvalidRevisionId:
 
438
        return {}
 
439
    else:
 
440
        return mapping.export_unusual_file_modes(rev)
 
441
 
 
442
 
 
443
def inventory_to_tree_and_blobs(inventory, texts, mapping, unusual_modes, cur=None):
 
444
    """Convert a Bazaar tree to a Git tree.
 
445
 
 
446
    :return: Yields tuples with object sha1, object and path
 
447
    """
 
448
    from dulwich.objects import Tree
127
449
    import stat
128
450
    stack = []
129
 
    cur = ""
 
451
    if cur is None:
 
452
        cur = ""
130
453
    tree = Tree()
131
454
 
132
 
    inv = repo.get_inventory(revision_id)
133
 
 
134
 
    # stack contains the set of trees that we haven't 
 
455
    # stack contains the set of trees that we haven't
135
456
    # finished constructing
136
 
 
137
 
    for path, entry in inv.iter_entries():
138
 
        while stack and not path.startswith(cur):
 
457
    for path, entry in inventory.iter_entries():
 
458
        while stack and not path.startswith(osutils.pathjoin(cur, "")):
 
459
            # We've hit a file that's not a child of the previous path
139
460
            tree.serialize()
140
 
            sha = tree.sha().hexdigest()
141
 
            yield sha, tree, cur
142
 
            t = (stat.S_IFDIR, urlutils.basename(cur).encode('UTF-8'), sha)
 
461
            sha = tree.id
 
462
            yield sha, tree, cur.encode("utf-8")
 
463
            mode = unusual_modes.get(cur.encode("utf-8"), stat.S_IFDIR)
 
464
            t = (mode, urlutils.basename(cur).encode('UTF-8'), sha)
143
465
            cur, tree = stack.pop()
144
466
            tree.add(*t)
145
467
 
146
 
        if type(entry) == InventoryDirectory:
 
468
        if entry.kind == "directory":
147
469
            stack.append((cur, tree))
148
470
            cur = path
149
471
            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
 
 
 
472
        else:
 
473
            if entry.kind == "file":
 
474
                blob = text_to_blob(texts, entry)
 
475
            elif entry.kind == "symlink":
 
476
                blob = symlink_to_blob(entry)
 
477
            else:
 
478
                raise AssertionError("Unknown kind %s" % entry.kind)
 
479
            sha = blob.id
 
480
            yield sha, blob, path.encode("utf-8")
159
481
            name = urlutils.basename(path).encode("utf-8")
160
 
            mode = stat.S_IFREG | 0644
161
 
            if entry.executable:
162
 
                mode |= 0111
 
482
            mode = unusual_modes.get(path.encode("utf-8"), entry_mode(entry))
163
483
            tree.add(mode, name, sha)
164
484
 
165
485
    while len(stack) > 1:
166
486
        tree.serialize()
167
 
        sha = tree.sha().hexdigest()
168
 
        yield sha, tree, cur
169
 
        t = (stat.S_IFDIR, urlutils.basename(cur).encode('UTF-8'), sha)
 
487
        sha = tree.id
 
488
        yield sha, tree, cur.encode("utf-8")
 
489
        mode = unusual_modes.get(cur.encode('utf-8'), stat.S_IFDIR)
 
490
        t = (mode, urlutils.basename(cur).encode('UTF-8'), sha)
170
491
        cur, tree = stack.pop()
171
492
        tree.add(*t)
172
493
 
173
494
    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
 
495
    yield tree.id, tree, cur.encode("utf-8")
 
496
 
 
497
 
 
498
def parse_git_svn_id(text):
 
499
    (head, uuid) = text.rsplit(" ", 1)
 
500
    (full_url, rev) = head.rsplit("@", 1)
 
501
    return (full_url, int(rev), uuid)