/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

update copyright years

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