/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

Mark git formats as supporting colocated branches.

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