/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

Simplify push a bit further, make dpush without rebase faster.

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
 
21
import stat
 
22
 
 
23
from bzrlib import (
 
24
    errors,
 
25
    foreign,
 
26
    osutils,
 
27
    trace,
 
28
    urlutils,
 
29
    )
 
30
from bzrlib.inventory import (
 
31
    ROOT_ID,
 
32
    )
 
33
from bzrlib.foreign import (
 
34
    ForeignVcs, 
 
35
    VcsMappingRegistry, 
 
36
    ForeignRevision,
 
37
    )
 
38
from bzrlib.xml_serializer import (
 
39
    escape_invalid_chars,
 
40
    )
 
41
 
 
42
DEFAULT_FILE_MODE = stat.S_IFREG | 0644
 
43
 
 
44
 
 
45
def escape_file_id(file_id):
 
46
    return file_id.replace('_', '__').replace(' ', '_s')
 
47
 
 
48
 
 
49
def unescape_file_id(file_id):
 
50
    ret = []
 
51
    i = 0
 
52
    while i < len(file_id):
 
53
        if file_id[i] != '_':
 
54
            ret.append(file_id[i])
 
55
        else:
 
56
            if file_id[i+1] == '_':
 
57
                ret.append("_")
 
58
            elif file_id[i+1] == 's':
 
59
                ret.append(" ")
 
60
            else:
 
61
                raise AssertionError("unknown escape character %s" % file_id[i+1])
 
62
            i += 1
 
63
        i += 1
 
64
    return "".join(ret)
 
65
 
 
66
 
 
67
def fix_person_identifier(text):
 
68
    if "<" in text and ">" in text:
 
69
        return text
 
70
    return "%s <%s>" % (text, text)
 
71
 
 
72
 
 
73
def warn_escaped(commit, num_escaped):
 
74
    trace.warning("Escaped %d XML-invalid characters in %s. Will be unable "
 
75
                  "to regenerate the SHA map.", num_escaped, commit)
 
76
 
 
77
 
 
78
def warn_unusual_mode(commit, path, mode):
 
79
    trace.warning("Unusual file mode %o for %s in %s. Will be unable to "
 
80
                  "regenerate the SHA map.", mode, path, commit)
20
81
 
21
82
 
22
83
class BzrGitMapping(foreign.VcsMapping):
23
84
    """Class that maps between Git and Bazaar semantics."""
24
85
    experimental = False
25
86
 
26
 
    def revision_id_foreign_to_bzr(self, git_rev_id):
 
87
    def __init__(self):
 
88
        super(BzrGitMapping, self).__init__(foreign_git)
 
89
 
 
90
    def __eq__(self, other):
 
91
        return type(self) == type(other) and self.revid_prefix == other.revid_prefix
 
92
 
 
93
    @classmethod
 
94
    def revision_id_foreign_to_bzr(cls, git_rev_id):
27
95
        """Convert a git revision id handle to a Bazaar revision id."""
28
 
        return "%s:%s" % (self.revid_prefix, git_rev_id)
 
96
        return "%s:%s" % (cls.revid_prefix, git_rev_id)
29
97
 
30
 
    def revision_id_bzr_to_foreign(self, bzr_rev_id):
 
98
    @classmethod
 
99
    def revision_id_bzr_to_foreign(cls, bzr_rev_id):
31
100
        """Convert a Bazaar revision id to a git revision id handle."""
32
 
        if not bzr_rev_id.startswith("%s:" % self.revid_prefix):
33
 
            raise errors.InvalidRevisionId(bzr_rev_id, self)
34
 
        return bzr_rev_id[len(self.revid_prefix)+1:]
35
 
 
36
 
    def show_foreign_revid(self, foreign_revid):
37
 
        return { "git commit": foreign_revid }
38
 
 
39
 
 
40
 
class BzrGitMappingExperimental(BzrGitMapping):
 
101
        if not bzr_rev_id.startswith("%s:" % cls.revid_prefix):
 
102
            raise errors.InvalidRevisionId(bzr_rev_id, cls)
 
103
        return bzr_rev_id[len(cls.revid_prefix)+1:], cls()
 
104
 
 
105
    def generate_file_id(self, path):
 
106
        # Git paths are just bytestrings
 
107
        # We must just hope they are valid UTF-8..
 
108
        if path == "":
 
109
            return ROOT_ID
 
110
        return escape_file_id(path)
 
111
 
 
112
    def parse_file_id(self, file_id):
 
113
        if file_id == ROOT_ID:
 
114
            return ""
 
115
        return unescape_file_id(file_id)
 
116
 
 
117
    def import_commit(self, commit):
 
118
        """Convert a git commit to a bzr revision.
 
119
 
 
120
        :return: a `bzrlib.revision.Revision` object.
 
121
        """
 
122
        if commit is None:
 
123
            raise AssertionError("Commit object can't be None")
 
124
        rev = ForeignRevision(commit.id, self, self.revision_id_foreign_to_bzr(commit.id))
 
125
        rev.parent_ids = tuple([self.revision_id_foreign_to_bzr(p) for p in commit.parents])
 
126
        rev.message, num_escaped = escape_invalid_chars(commit.message.decode("utf-8", "replace"))
 
127
        if num_escaped:
 
128
            warn_escaped(commit.id, num_escaped)
 
129
        rev.committer, num_escaped = escape_invalid_chars(str(commit.committer).decode("utf-8", "replace"))
 
130
        if num_escaped:
 
131
            warn_escaped(commit.id, num_escaped)
 
132
        if commit.committer != commit.author:
 
133
            rev.properties['author'], num_escaped = escape_invalid_chars(str(commit.author).decode("utf-8", "replace"))
 
134
            if num_escaped:
 
135
                warn_escaped(commit.id, num_escaped)
 
136
 
 
137
        if commit.commit_time != commit.author_time:
 
138
            rev.properties['author-timestamp'] = str(commit.author_time)
 
139
        if commit.commit_timezone != commit.author_timezone:
 
140
            rev.properties['author-timezone'] = "%d" % (commit.author_timezone, )
 
141
        rev.timestamp = commit.commit_time
 
142
        rev.timezone = commit.commit_timezone
 
143
        return rev
 
144
 
 
145
 
 
146
class BzrGitMappingv1(BzrGitMapping):
 
147
    revid_prefix = 'git-v1'
 
148
    experimental = False
 
149
 
 
150
    def __str__(self):
 
151
        return self.revid_prefix
 
152
 
 
153
 
 
154
class BzrGitMappingExperimental(BzrGitMappingv1):
41
155
    revid_prefix = 'git-experimental'
42
156
    experimental = True
43
157
 
44
158
 
45
 
default_mapping = BzrGitMappingExperimental()
 
159
class GitMappingRegistry(VcsMappingRegistry):
 
160
 
 
161
    def revision_id_bzr_to_foreign(self, bzr_revid):
 
162
        if not bzr_revid.startswith("git-"):
 
163
            raise errors.InvalidRevisionId(bzr_revid, None)
 
164
        (mapping_version, git_sha) = bzr_revid.split(":", 1)
 
165
        mapping = self.get(mapping_version)
 
166
        return mapping.revision_id_bzr_to_foreign(bzr_revid)
 
167
 
 
168
    parse_revision_id = revision_id_bzr_to_foreign
 
169
 
 
170
 
 
171
mapping_registry = GitMappingRegistry()
 
172
mapping_registry.register_lazy('git-v1', "bzrlib.plugins.git.mapping",
 
173
                                   "BzrGitMappingv1")
 
174
mapping_registry.register_lazy('git-experimental', "bzrlib.plugins.git.mapping",
 
175
                                   "BzrGitMappingExperimental")
 
176
 
 
177
 
 
178
class ForeignGit(ForeignVcs):
 
179
    """The Git Stupid Content Tracker"""
 
180
 
 
181
    def __init__(self):
 
182
        super(ForeignGit, self).__init__(mapping_registry)
 
183
 
 
184
    @classmethod
 
185
    def show_foreign_revid(cls, foreign_revid):
 
186
        return { "git commit": foreign_revid }
 
187
 
 
188
 
 
189
foreign_git = ForeignGit()
 
190
default_mapping = BzrGitMappingv1()
 
191
 
 
192
 
 
193
def text_to_blob(texts, entry):
 
194
    from dulwich.objects import Blob
 
195
    text = texts.get_record_stream([(entry.file_id, entry.revision)], 'unordered', True).next().get_bytes_as('fulltext')
 
196
    blob = Blob()
 
197
    blob._text = text
 
198
    return blob
 
199
 
 
200
 
 
201
def symlink_to_blob(entry):
 
202
    from dulwich.objects import Blob
 
203
    blob = Blob()
 
204
    blob._text = entry.symlink_target
 
205
    return blob
 
206
 
 
207
def mode_is_executable(mode):
 
208
    return bool(mode & 0111)
 
209
 
 
210
def mode_kind(mode):
 
211
    entry_kind = (mode & 0700000) / 0100000
 
212
    if entry_kind == 0:
 
213
        return 'directory'
 
214
    elif entry_kind == 1:
 
215
        file_kind = (mode & 070000) / 010000
 
216
        if file_kind == 0:
 
217
            return 'file'
 
218
        elif file_kind == 2:
 
219
            return 'symlink'
 
220
        elif file_kind == 6:
 
221
            return 'tree-reference'
 
222
        else:
 
223
            raise AssertionError(
 
224
                "Unknown file kind %d, perms=%o." % (file_kind, mode,))
 
225
    else:
 
226
        raise AssertionError(
 
227
            "Unknown kind, perms=%r." % (mode,))
 
228
 
 
229
 
 
230
def entry_mode(entry):
 
231
    if entry.kind == 'directory':
 
232
        return stat.S_IFDIR
 
233
    elif entry.kind == 'symlink':
 
234
        return stat.S_IFLNK
 
235
    elif entry.kind == 'file':
 
236
        mode = stat.S_IFREG | 0644
 
237
        if entry.executable:
 
238
            mode |= 0111
 
239
        return mode
 
240
    else:
 
241
        raise AssertionError
 
242
 
 
243
 
 
244
def directory_to_tree(entry, lookup_ie_sha1):
 
245
    from dulwich.objects import Tree
 
246
    tree = Tree()
 
247
    for name in sorted(entry.children.keys()):
 
248
        ie = entry.children[name]
 
249
        tree.add(entry_mode(ie), name.encode("utf-8"), lookup_ie_sha1(ie))
 
250
    tree.serialize()
 
251
    return tree
 
252
 
 
253
 
 
254
def inventory_to_tree_and_blobs(inventory, texts, mapping, cur=None):
 
255
    """Convert a Bazaar tree to a Git tree.
 
256
 
 
257
    :return: Yields tuples with object sha1, object and path
 
258
    """
 
259
    from dulwich.objects import Tree
 
260
    import stat
 
261
    stack = []
 
262
    if cur is None:
 
263
        cur = ""
 
264
    tree = Tree()
 
265
 
 
266
    # stack contains the set of trees that we haven't 
 
267
    # finished constructing
 
268
    for path, entry in inventory.iter_entries():
 
269
        while stack and not path.startswith(osutils.pathjoin(cur, "")):
 
270
            # We've hit a file that's not a child of the previous path
 
271
            tree.serialize()
 
272
            sha = tree.id
 
273
            yield sha, tree, cur.encode("utf-8")
 
274
            t = (stat.S_IFDIR, urlutils.basename(cur).encode('UTF-8'), sha)
 
275
            cur, tree = stack.pop()
 
276
            tree.add(*t)
 
277
 
 
278
        if entry.kind == "directory":
 
279
            stack.append((cur, tree))
 
280
            cur = path
 
281
            tree = Tree()
 
282
        else:
 
283
            if entry.kind == "file":
 
284
                blob = text_to_blob(texts, entry)
 
285
            elif entry.kind == "symlink":
 
286
                blob = symlink_to_blob(entry)
 
287
            else:
 
288
                raise AssertionError("Unknown kind %s" % entry.kind)
 
289
            sha = blob.id
 
290
            yield sha, blob, path.encode("utf-8")
 
291
            name = urlutils.basename(path).encode("utf-8")
 
292
            tree.add(entry_mode(entry), name, sha)
 
293
 
 
294
    while len(stack) > 1:
 
295
        tree.serialize()
 
296
        sha = tree.id
 
297
        yield sha, tree, cur.encode("utf-8")
 
298
        t = (stat.S_IFDIR, urlutils.basename(cur).encode('UTF-8'), sha)
 
299
        cur, tree = stack.pop()
 
300
        tree.add(*t)
 
301
 
 
302
    tree.serialize()
 
303
    yield tree.id, tree, cur.encode("utf-8")
 
304
 
 
305
 
 
306
def revision_to_commit(rev, tree_sha, parent_lookup):
 
307
    """Turn a Bazaar revision in to a Git commit
 
308
 
 
309
    :param tree_sha: Tree sha for the commit
 
310
    :param parent_lookup: Function for looking up the GIT sha equiv of a bzr revision
 
311
    :return dulwich.objects.Commit represent the revision:
 
312
    """
 
313
    from dulwich.objects import Commit
 
314
    commit = Commit()
 
315
    commit.tree = tree_sha
 
316
    for p in rev.parent_ids:
 
317
        git_p = parent_lookup(p)
 
318
        if git_p is not None:
 
319
            assert len(git_p) == 40, "unexpected length for %r" % git_p
 
320
            commit.parents.append(git_p)
 
321
    commit.message = rev.message.encode("utf-8")
 
322
    commit.committer = fix_person_identifier(rev.committer.encode("utf-8"))
 
323
    commit.author = fix_person_identifier(rev.get_apparent_authors()[0].encode("utf-8"))
 
324
    commit.commit_time = long(rev.timestamp)
 
325
    if 'author-timestamp' in rev.properties:
 
326
        commit.author_time = long(rev.properties['author-timestamp'])
 
327
    else:
 
328
        commit.author_time = commit.commit_time
 
329
    commit.commit_timezone = rev.timezone
 
330
    if 'author-timezone' in rev.properties:
 
331
        commit.author_timezone = int(rev.properties['author-timezone'])
 
332
    else:
 
333
        commit.author_timezone = commit.commit_timezone 
 
334
    return commit