/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 ids.py

GitBranchBuilder now handles file names with newlines correctly.

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