/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

Clean up trailing whitespace.

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