/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

Add NEWS entry about hg-git support.

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'])])
 
153
        except KeyError:
 
154
            return {}
 
155
 
 
156
    def _generate_hg_message_tail(self, rev):
 
157
        extra = {}
 
158
        renames = []
 
159
        branch = 'default'
 
160
        for name in rev.properties:
 
161
            if name == 'hg:extra:branch':
 
162
                branch = rev.properties['hg:extra:branch']
 
163
            elif name.startswith('hg:extra'):
 
164
                extra[name[len('hg:extra:'):]] = base64.b64decode(rev.properties[name])
 
165
            elif name == 'hg:renames':
 
166
                renames = bencode.bdecode(base64.b64decode(rev.properties['hg:renames']))
 
167
            # TODO: Export other properties as 'bzr:' extras?
 
168
        return format_hg_metadata(renames, branch, extra)
 
169
 
 
170
    def _extract_hg_metadata(self, rev, message):
 
171
        (message, renames, branch, extra) = extract_hg_metadata(message)
 
172
        if branch is not None:
 
173
            rev.properties['hg:extra:branch'] = branch
 
174
        for name, value in extra.iteritems():
 
175
            rev.properties['hg:extra:' + name] = base64.b64encode(value)
 
176
        if renames:
 
177
            rev.properties['hg:renames'] = base64.b64encode(bencode.bencode([(new, old) for (old, new) in renames.iteritems()]))
 
178
        return message
 
179
 
 
180
    def _decode_commit_message(self, rev, message):
 
181
        return message.decode("utf-8", "replace")
 
182
 
 
183
    def _encode_commit_message(self, rev, message):
 
184
        return message.encode("utf-8")
 
185
 
 
186
    def export_commit(self, rev, tree_sha, parent_lookup):
 
187
        """Turn a Bazaar revision in to a Git commit
 
188
 
 
189
        :param tree_sha: Tree sha for the commit
 
190
        :param parent_lookup: Function for looking up the GIT sha equiv of a bzr revision
 
191
        :return dulwich.objects.Commit represent the revision:
 
192
        """
 
193
        from dulwich.objects import Commit
 
194
        commit = Commit()
 
195
        commit.tree = tree_sha
 
196
        for p in rev.parent_ids:
 
197
            git_p = parent_lookup(p)
 
198
            if git_p is not None:
 
199
                assert len(git_p) == 40, "unexpected length for %r" % git_p
 
200
                commit.parents.append(git_p)
 
201
        commit.committer = fix_person_identifier(rev.committer.encode("utf-8"))
 
202
        commit.author = fix_person_identifier(rev.get_apparent_authors()[0].encode("utf-8"))
 
203
        commit.commit_time = long(rev.timestamp)
 
204
        if 'author-timestamp' in rev.properties:
 
205
            commit.author_time = long(rev.properties['author-timestamp'])
 
206
        else:
 
207
            commit.author_time = commit.commit_time
 
208
        commit.commit_timezone = rev.timezone
 
209
        if 'author-timezone' in rev.properties:
 
210
            commit.author_timezone = int(rev.properties['author-timezone'])
 
211
        else:
 
212
            commit.author_timezone = commit.commit_timezone 
 
213
        commit.message = self._encode_commit_message(rev, rev.message)
 
214
        return commit
61
215
 
62
216
    def import_commit(self, commit):
63
217
        """Convert a git commit to a bzr revision.
68
222
            raise AssertionError("Commit object can't be None")
69
223
        rev = ForeignRevision(commit.id, self, self.revision_id_foreign_to_bzr(commit.id))
70
224
        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
225
        rev.committer = str(commit.committer).decode("utf-8", "replace")
73
226
        if commit.committer != commit.author:
74
227
            rev.properties['author'] = str(commit.author).decode("utf-8", "replace")
 
228
 
 
229
        if commit.commit_time != commit.author_time:
 
230
            rev.properties['author-timestamp'] = str(commit.author_time)
 
231
        if commit.commit_timezone != commit.author_timezone:
 
232
            rev.properties['author-timezone'] = "%d" % (commit.author_timezone, )
75
233
        rev.timestamp = commit.commit_time
76
 
        rev.timezone = 0
 
234
        rev.timezone = commit.commit_timezone
 
235
        rev.message = self._decode_commit_message(rev, commit.message)
77
236
        return rev
78
237
 
79
238
 
81
240
    revid_prefix = 'git-v1'
82
241
    experimental = False
83
242
 
 
243
    def __str__(self):
 
244
        return self.revid_prefix
 
245
 
84
246
 
85
247
class BzrGitMappingExperimental(BzrGitMappingv1):
86
248
    revid_prefix = 'git-experimental'
87
249
    experimental = True
88
250
 
 
251
    def _decode_commit_message(self, rev, message):
 
252
        message = self._extract_hg_metadata(rev, message)
 
253
        return message.decode("utf-8", "replace")
 
254
 
 
255
    def _encode_commit_message(self, rev, message):
 
256
        ret = message.encode("utf-8")
 
257
        ret += self._generate_hg_message_tail(rev)
 
258
        return ret
 
259
 
89
260
 
90
261
class GitMappingRegistry(VcsMappingRegistry):
 
262
    """Registry with available git mappings."""
91
263
 
92
264
    def revision_id_bzr_to_foreign(self, bzr_revid):
93
265
        if not bzr_revid.startswith("git-"):
104
276
                                   "BzrGitMappingv1")
105
277
mapping_registry.register_lazy('git-experimental', "bzrlib.plugins.git.mapping",
106
278
                                   "BzrGitMappingExperimental")
 
279
mapping_registry.set_default('git-v1')
107
280
 
108
281
 
109
282
class ForeignGit(ForeignVcs):
110
 
    """Foreign Git."""
 
283
    """The Git Stupid Content Tracker"""
111
284
 
112
285
    def __init__(self):
113
286
        super(ForeignGit, self).__init__(mapping_registry)
118
291
 
119
292
 
120
293
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
 
294
default_mapping = mapping_registry.get_default()()
 
295
 
 
296
 
 
297
def text_to_blob(texts, entry):
 
298
    from dulwich.objects import Blob
 
299
    text = texts.get_record_stream([(entry.file_id, entry.revision)], 'unordered', True).next().get_bytes_as('fulltext')
 
300
    blob = Blob()
 
301
    blob._text = text
 
302
    return blob
 
303
 
 
304
 
 
305
def symlink_to_blob(entry):
 
306
    from dulwich.objects import Blob
 
307
    blob = Blob()
 
308
    blob._text = entry.symlink_target
 
309
    return blob
 
310
 
 
311
 
 
312
def mode_is_executable(mode):
 
313
    """Check if mode should be considered executable."""
 
314
    return bool(mode & 0111)
 
315
 
 
316
 
 
317
def mode_kind(mode):
 
318
    """Determine the Bazaar inventory kind based on Unix file mode."""
 
319
    entry_kind = (mode & 0700000) / 0100000
 
320
    if entry_kind == 0:
 
321
        return 'directory'
 
322
    elif entry_kind == 1:
 
323
        file_kind = (mode & 070000) / 010000
 
324
        if file_kind == 0:
 
325
            return 'file'
 
326
        elif file_kind == 2:
 
327
            return 'symlink'
 
328
        elif file_kind == 6:
 
329
            return 'tree-reference'
 
330
        else:
 
331
            raise AssertionError(
 
332
                "Unknown file kind %d, perms=%o." % (file_kind, mode,))
 
333
    else:
 
334
        raise AssertionError(
 
335
            "Unknown kind, perms=%r." % (mode,))
 
336
 
 
337
 
 
338
def object_mode(kind, executable):
 
339
    if kind == 'directory':
 
340
        return stat.S_IFDIR
 
341
    elif kind == 'symlink':
 
342
        return stat.S_IFLNK
 
343
    elif kind == 'file':
 
344
        mode = stat.S_IFREG | 0644
 
345
        if executable:
 
346
            mode |= 0111
 
347
        return mode
 
348
    else:
 
349
        raise AssertionError
 
350
 
 
351
 
 
352
def entry_mode(entry):
 
353
    """Determine the git file mode for an inventory entry."""
 
354
    return object_mode(entry.kind, entry.executable)
 
355
 
 
356
 
 
357
def directory_to_tree(entry, lookup_ie_sha1, unusual_modes):
 
358
    from dulwich.objects import Tree
 
359
    tree = Tree()
 
360
    for name in sorted(entry.children.keys()):
 
361
        ie = entry.children[name]
 
362
        try:
 
363
            mode = unusual_modes[ie.file_id]
 
364
        except KeyError:
 
365
            mode = entry_mode(ie)
 
366
        hexsha = lookup_ie_sha1(ie)
 
367
        if hexsha is not None:
 
368
            tree.add(mode, name.encode("utf-8"), hexsha)
 
369
    if entry.parent_id is not None and len(tree) == 0:
 
370
        # Only the root can be an empty tree
 
371
        return None
 
372
    tree.serialize()
 
373
    return tree
 
374
 
 
375
 
 
376
def extract_unusual_modes(rev):
 
377
    try:
 
378
        foreign_revid, mapping = mapping_registry.parse_revision_id(rev.revision_id)
 
379
    except errors.InvalidRevisionId:
 
380
        return {}
 
381
    else:
 
382
        return mapping.export_unusual_file_modes(rev)
 
383
 
 
384
 
 
385
def inventory_to_tree_and_blobs(inventory, texts, mapping, unusual_modes, cur=None):
 
386
    """Convert a Bazaar tree to a Git tree.
 
387
 
 
388
    :return: Yields tuples with object sha1, object and path
 
389
    """
 
390
    from dulwich.objects import Tree
127
391
    import stat
128
392
    stack = []
129
 
    cur = ""
 
393
    if cur is None:
 
394
        cur = ""
130
395
    tree = Tree()
131
396
 
132
 
    inv = repo.get_inventory(revision_id)
133
 
 
134
397
    # stack contains the set of trees that we haven't 
135
398
    # finished constructing
136
 
 
137
 
    for path, entry in inv.iter_entries():
138
 
        while stack and not path.startswith(cur):
 
399
    for path, entry in inventory.iter_entries():
 
400
        while stack and not path.startswith(osutils.pathjoin(cur, "")):
 
401
            # We've hit a file that's not a child of the previous path
139
402
            tree.serialize()
140
 
            sha = tree.sha().hexdigest()
141
 
            yield sha, tree, cur
142
 
            t = (stat.S_IFDIR, urlutils.basename(cur).encode('UTF-8'), sha)
 
403
            sha = tree.id
 
404
            yield sha, tree, cur.encode("utf-8")
 
405
            mode = unusual_modes.get(cur.encode("utf-8"), stat.S_IFDIR)
 
406
            t = (mode, urlutils.basename(cur).encode('UTF-8'), sha)
143
407
            cur, tree = stack.pop()
144
408
            tree.add(*t)
145
409
 
146
 
        if type(entry) == InventoryDirectory:
 
410
        if entry.kind == "directory":
147
411
            stack.append((cur, tree))
148
412
            cur = path
149
413
            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
 
 
 
414
        else:
 
415
            if entry.kind == "file":
 
416
                blob = text_to_blob(texts, entry)
 
417
            elif entry.kind == "symlink":
 
418
                blob = symlink_to_blob(entry)
 
419
            else:
 
420
                raise AssertionError("Unknown kind %s" % entry.kind)
 
421
            sha = blob.id
 
422
            yield sha, blob, path.encode("utf-8")
159
423
            name = urlutils.basename(path).encode("utf-8")
160
 
            mode = stat.S_IFREG | 0644
161
 
            if entry.executable:
162
 
                mode |= 0111
 
424
            mode = unusual_modes.get(path.encode("utf-8"), entry_mode(entry))
163
425
            tree.add(mode, name, sha)
164
426
 
165
427
    while len(stack) > 1:
166
428
        tree.serialize()
167
 
        sha = tree.sha().hexdigest()
168
 
        yield sha, tree, cur
169
 
        t = (stat.S_IFDIR, urlutils.basename(cur).encode('UTF-8'), sha)
 
429
        sha = tree.id
 
430
        yield sha, tree, cur.encode("utf-8")
 
431
        mode = unusual_modes.get(cur.encode('utf-8'), stat.S_IFDIR)
 
432
        t = (mode, urlutils.basename(cur).encode('UTF-8'), sha)
170
433
        cur, tree = stack.pop()
171
434
        tree.add(*t)
172
435
 
173
436
    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
 
437
    yield tree.id, tree, cur.encode("utf-8")
 
438