/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 trivial object store tests.

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