/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 breezy/git/mapping.py

  • Committer: Jelmer Vernooij
  • Date: 2020-03-22 01:35:14 UTC
  • mfrom: (7490.7.6 work)
  • mto: This revision was merged to the branch mainline in revision 7499.
  • Revision ID: jelmer@jelmer.uk-20200322013514-7vw1ntwho04rcuj3
merge lp:brz/3.1.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2008-2018 Jelmer Vernooij <jelmer@jelmer.uk>
1
2
# Copyright (C) 2007 Canonical Ltd
 
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
12
14
#
13
15
# You should have received a copy of the GNU General Public License
14
16
# along with this program; if not, write to the Free Software
15
 
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
17
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
18
 
17
19
"""Converters, etc for going between Bazaar and Git ids."""
18
20
 
19
 
from bzrlib import errors
20
 
from bzrlib.plugins.git import foreign
 
21
import base64
 
22
import stat
 
23
 
 
24
from .. import (
 
25
    bencode,
 
26
    errors,
 
27
    foreign,
 
28
    trace,
 
29
    urlutils,
 
30
    )
 
31
from ..foreign import (
 
32
    ForeignVcs,
 
33
    VcsMappingRegistry,
 
34
    ForeignRevision,
 
35
    )
 
36
from ..revision import (
 
37
    NULL_REVISION,
 
38
    Revision,
 
39
    )
 
40
from .errors import (
 
41
    NoPushSupport,
 
42
    )
 
43
from .hg import (
 
44
    format_hg_metadata,
 
45
    extract_hg_metadata,
 
46
    )
 
47
from .roundtrip import (
 
48
    extract_bzr_metadata,
 
49
    inject_bzr_metadata,
 
50
    CommitSupplement,
 
51
    )
 
52
 
 
53
 
 
54
DEFAULT_FILE_MODE = stat.S_IFREG | 0o644
 
55
HG_RENAME_SOURCE = b"HG:rename-source"
 
56
HG_EXTRA = b"HG:extra"
 
57
 
 
58
# This HG extra is used to indicate the commit that this commit was based on.
 
59
HG_EXTRA_AMEND_SOURCE = b"amend_source"
 
60
 
 
61
FILE_ID_PREFIX = b'git:'
 
62
 
 
63
# Always the same.
 
64
ROOT_ID = b"TREE_ROOT"
 
65
 
 
66
 
 
67
class UnknownCommitExtra(errors.BzrError):
 
68
    _fmt = "Unknown extra fields in %(object)r: %(fields)r."
 
69
 
 
70
    def __init__(self, object, fields):
 
71
        errors.BzrError.__init__(self)
 
72
        self.object = object
 
73
        self.fields = ",".join(fields)
 
74
 
 
75
 
 
76
class UnknownMercurialCommitExtra(errors.BzrError):
 
77
    _fmt = "Unknown mercurial extra fields in %(object)r: %(fields)r."
 
78
 
 
79
    def __init__(self, object, fields):
 
80
        errors.BzrError.__init__(self)
 
81
        self.object = object
 
82
        self.fields = b",".join(fields)
 
83
 
 
84
 
 
85
def escape_file_id(file_id):
 
86
    file_id = file_id.replace(b'_', b'__')
 
87
    file_id = file_id.replace(b' ', b'_s')
 
88
    file_id = file_id.replace(b'\x0c', b'_c')
 
89
    return file_id
 
90
 
 
91
 
 
92
def unescape_file_id(file_id):
 
93
    ret = bytearray()
 
94
    i = 0
 
95
    while i < len(file_id):
 
96
        if file_id[i:i + 1] != b'_':
 
97
            ret.append(file_id[i])
 
98
        else:
 
99
            if file_id[i + 1:i + 2] == b'_':
 
100
                ret.append(b"_"[0])
 
101
            elif file_id[i + 1:i + 2] == b's':
 
102
                ret.append(b" "[0])
 
103
            elif file_id[i + 1:i + 2] == b'c':
 
104
                ret.append(b"\x0c"[0])
 
105
            else:
 
106
                raise ValueError("unknown escape character %s" %
 
107
                                 file_id[i + 1:i + 2])
 
108
            i += 1
 
109
        i += 1
 
110
    return bytes(ret)
 
111
 
 
112
 
 
113
def fix_person_identifier(text):
 
114
    if b"<" not in text and b">" not in text:
 
115
        username = text
 
116
        email = text
 
117
    else:
 
118
        if text.rindex(b">") < text.rindex(b"<"):
 
119
            raise ValueError(text)
 
120
        username, email = text.split(b"<", 2)[-2:]
 
121
        email = email.split(b">", 1)[0]
 
122
        if username.endswith(b" "):
 
123
            username = username[:-1]
 
124
    return b"%s <%s>" % (username, email)
 
125
 
 
126
 
 
127
def warn_escaped(commit, num_escaped):
 
128
    trace.warning("Escaped %d XML-invalid characters in %s. Will be unable "
 
129
                  "to regenerate the SHA map.", num_escaped, commit)
 
130
 
 
131
 
 
132
def warn_unusual_mode(commit, path, mode):
 
133
    trace.mutter("Unusual file mode %o for %s in %s. Storing as revision "
 
134
                 "property. ", mode, path, commit)
 
135
 
21
136
 
22
137
class BzrGitMapping(foreign.VcsMapping):
23
138
    """Class that maps between Git and Bazaar semantics."""
24
139
    experimental = False
25
140
 
26
 
    def revision_id_foreign_to_bzr(self, git_rev_id):
 
141
    BZR_DUMMY_FILE = None
 
142
 
 
143
    def is_special_file(self, filename):
 
144
        return (filename in (self.BZR_DUMMY_FILE, ))
 
145
 
 
146
    def __init__(self):
 
147
        super(BzrGitMapping, self).__init__(foreign_vcs_git)
 
148
 
 
149
    def __eq__(self, other):
 
150
        return (type(self) == type(other)
 
151
                and self.revid_prefix == other.revid_prefix)
 
152
 
 
153
    @classmethod
 
154
    def revision_id_foreign_to_bzr(cls, git_rev_id):
27
155
        """Convert a git revision id handle to a Bazaar revision id."""
28
 
        return "%s:%s" % (self.revid_prefix, git_rev_id)
 
156
        from dulwich.protocol import ZERO_SHA
 
157
        if git_rev_id == ZERO_SHA:
 
158
            return NULL_REVISION
 
159
        return b"%s:%s" % (cls.revid_prefix, git_rev_id)
29
160
 
30
 
    def revision_id_bzr_to_foreign(self, bzr_rev_id):
 
161
    @classmethod
 
162
    def revision_id_bzr_to_foreign(cls, bzr_rev_id):
31
163
        """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
 
 
37
 
class BzrGitMappingExperimental(BzrGitMapping):
38
 
    revid_prefix = 'git-experimental'
 
164
        if not bzr_rev_id.startswith(b"%s:" % cls.revid_prefix):
 
165
            raise errors.InvalidRevisionId(bzr_rev_id, cls)
 
166
        return bzr_rev_id[len(cls.revid_prefix) + 1:], cls()
 
167
 
 
168
    def generate_file_id(self, path):
 
169
        # Git paths are just bytestrings
 
170
        # We must just hope they are valid UTF-8..
 
171
        if isinstance(path, str):
 
172
            path = path.encode("utf-8")
 
173
        if path == b"":
 
174
            return ROOT_ID
 
175
        return FILE_ID_PREFIX + escape_file_id(path)
 
176
 
 
177
    def parse_file_id(self, file_id):
 
178
        if file_id == ROOT_ID:
 
179
            return u""
 
180
        if not file_id.startswith(FILE_ID_PREFIX):
 
181
            raise ValueError
 
182
        return unescape_file_id(file_id[len(FILE_ID_PREFIX):]).decode('utf-8')
 
183
 
 
184
    def revid_as_refname(self, revid):
 
185
        if not isinstance(revid, bytes):
 
186
            raise TypeError(revid)
 
187
        revid = revid.decode('utf-8')
 
188
        quoted_revid = urlutils.quote(revid)
 
189
        return b"refs/bzr/" + quoted_revid.encode('utf-8')
 
190
 
 
191
    def import_unusual_file_modes(self, rev, unusual_file_modes):
 
192
        if unusual_file_modes:
 
193
            ret = [(path, unusual_file_modes[path])
 
194
                   for path in sorted(unusual_file_modes.keys())]
 
195
            rev.properties[u'file-modes'] = bencode.bencode(ret)
 
196
 
 
197
    def export_unusual_file_modes(self, rev):
 
198
        try:
 
199
            file_modes = rev.properties[u'file-modes']
 
200
        except KeyError:
 
201
            return {}
 
202
        else:
 
203
            return dict(bencode.bdecode(file_modes.encode("utf-8")))
 
204
 
 
205
    def _generate_git_svn_metadata(self, rev, encoding):
 
206
        try:
 
207
            git_svn_id = rev.properties[u"git-svn-id"]
 
208
        except KeyError:
 
209
            return ""
 
210
        else:
 
211
            return "\ngit-svn-id: %s\n" % git_svn_id.encode(encoding)
 
212
 
 
213
    def _generate_hg_message_tail(self, rev):
 
214
        extra = {}
 
215
        renames = []
 
216
        branch = 'default'
 
217
        for name in rev.properties:
 
218
            if name == u'hg:extra:branch':
 
219
                branch = rev.properties[u'hg:extra:branch']
 
220
            elif name.startswith(u'hg:extra'):
 
221
                extra[name[len(u'hg:extra:'):]] = base64.b64decode(
 
222
                    rev.properties[name])
 
223
            elif name == u'hg:renames':
 
224
                renames = bencode.bdecode(base64.b64decode(
 
225
                    rev.properties[u'hg:renames']))
 
226
            # TODO: Export other properties as 'bzr:' extras?
 
227
        ret = format_hg_metadata(renames, branch, extra)
 
228
        if not isinstance(ret, bytes):
 
229
            raise TypeError(ret)
 
230
        return ret
 
231
 
 
232
    def _extract_git_svn_metadata(self, rev, message):
 
233
        lines = message.split("\n")
 
234
        if not (lines[-1] == "" and len(lines) >= 2 and
 
235
                lines[-2].startswith("git-svn-id:")):
 
236
            return message
 
237
        git_svn_id = lines[-2].split(": ", 1)[1]
 
238
        rev.properties[u'git-svn-id'] = git_svn_id
 
239
        (url, rev, uuid) = parse_git_svn_id(git_svn_id)
 
240
        # FIXME: Convert this to converted-from property somehow..
 
241
        return "\n".join(lines[:-2])
 
242
 
 
243
    def _extract_hg_metadata(self, rev, message):
 
244
        (message, renames, branch, extra) = extract_hg_metadata(message)
 
245
        if branch is not None:
 
246
            rev.properties[u'hg:extra:branch'] = branch
 
247
        for name, value in extra.items():
 
248
            rev.properties[u'hg:extra:' + name] = base64.b64encode(value)
 
249
        if renames:
 
250
            rev.properties[u'hg:renames'] = base64.b64encode(bencode.bencode(
 
251
                [(new, old) for (old, new) in renames.items()]))
 
252
        return message
 
253
 
 
254
    def _extract_bzr_metadata(self, rev, message):
 
255
        (message, metadata) = extract_bzr_metadata(message)
 
256
        return message, metadata
 
257
 
 
258
    def _decode_commit_message(self, rev, message, encoding):
 
259
        return message.decode(encoding), CommitSupplement()
 
260
 
 
261
    def _encode_commit_message(self, rev, message, encoding):
 
262
        return message.encode(encoding)
 
263
 
 
264
    def export_commit(self, rev, tree_sha, parent_lookup, lossy,
 
265
                      verifiers):
 
266
        """Turn a Bazaar revision in to a Git commit
 
267
 
 
268
        :param tree_sha: Tree sha for the commit
 
269
        :param parent_lookup: Function for looking up the GIT sha equiv of a
 
270
            bzr revision
 
271
        :param lossy: Whether to store roundtripping information.
 
272
        :param verifiers: Verifiers info
 
273
        :return dulwich.objects.Commit represent the revision:
 
274
        """
 
275
        from dulwich.objects import Commit, Tag
 
276
        commit = Commit()
 
277
        commit.tree = tree_sha
 
278
        if not lossy:
 
279
            metadata = CommitSupplement()
 
280
            metadata.verifiers = verifiers
 
281
        else:
 
282
            metadata = None
 
283
        parents = []
 
284
        for p in rev.parent_ids:
 
285
            try:
 
286
                git_p = parent_lookup(p)
 
287
            except KeyError:
 
288
                git_p = None
 
289
                if metadata is not None:
 
290
                    metadata.explicit_parent_ids = rev.parent_ids
 
291
            if git_p is not None:
 
292
                if len(git_p) != 40:
 
293
                    raise AssertionError("unexpected length for %r" % git_p)
 
294
                parents.append(git_p)
 
295
        commit.parents = parents
 
296
        try:
 
297
            encoding = rev.properties[u'git-explicit-encoding']
 
298
        except KeyError:
 
299
            encoding = rev.properties.get(u'git-implicit-encoding', 'utf-8')
 
300
        try:
 
301
            commit.encoding = rev.properties[u'git-explicit-encoding'].encode(
 
302
                'ascii')
 
303
        except KeyError:
 
304
            pass
 
305
        commit.committer = fix_person_identifier(rev.committer.encode(
 
306
            encoding))
 
307
        commit.author = fix_person_identifier(
 
308
            rev.get_apparent_authors()[0].encode(encoding))
 
309
        # TODO(jelmer): Don't use this hack.
 
310
        long = getattr(__builtins__, 'long', int)
 
311
        commit.commit_time = long(rev.timestamp)
 
312
        if u'author-timestamp' in rev.properties:
 
313
            commit.author_time = long(rev.properties[u'author-timestamp'])
 
314
        else:
 
315
            commit.author_time = commit.commit_time
 
316
        commit._commit_timezone_neg_utc = (
 
317
            u"commit-timezone-neg-utc" in rev.properties)
 
318
        commit.commit_timezone = rev.timezone
 
319
        commit._author_timezone_neg_utc = (
 
320
            u"author-timezone-neg-utc" in rev.properties)
 
321
        if u'author-timezone' in rev.properties:
 
322
            commit.author_timezone = int(rev.properties[u'author-timezone'])
 
323
        else:
 
324
            commit.author_timezone = commit.commit_timezone
 
325
        if u'git-gpg-signature' in rev.properties:
 
326
            commit.gpgsig = rev.properties[u'git-gpg-signature'].encode(
 
327
                'ascii')
 
328
        commit.message = self._encode_commit_message(rev, rev.message,
 
329
                                                     encoding)
 
330
        if not isinstance(commit.message, bytes):
 
331
            raise TypeError(commit.message)
 
332
        if metadata is not None:
 
333
            try:
 
334
                mapping_registry.parse_revision_id(rev.revision_id)
 
335
            except errors.InvalidRevisionId:
 
336
                metadata.revision_id = rev.revision_id
 
337
            mapping_properties = set(
 
338
                [u'author', u'author-timezone', u'author-timezone-neg-utc',
 
339
                 u'commit-timezone-neg-utc', u'git-implicit-encoding',
 
340
                 u'git-gpg-signature', u'git-explicit-encoding',
 
341
                 u'author-timestamp', u'file-modes'])
 
342
            for k, v in rev.properties.items():
 
343
                if k not in mapping_properties:
 
344
                    metadata.properties[k] = v
 
345
        if not lossy and metadata:
 
346
            if self.roundtripping:
 
347
                commit.message = inject_bzr_metadata(commit.message, metadata,
 
348
                                                     encoding)
 
349
            else:
 
350
                raise NoPushSupport(
 
351
                    None, None, self, revision_id=rev.revision_id)
 
352
        if not isinstance(commit.message, bytes):
 
353
            raise TypeError(commit.message)
 
354
        i = 0
 
355
        propname = u'git-mergetag-0'
 
356
        while propname in rev.properties:
 
357
            commit.mergetag.append(Tag.from_string(rev.properties[propname]))
 
358
            i += 1
 
359
            propname = u'git-mergetag-%d' % i
 
360
        if u'git-extra' in rev.properties:
 
361
            commit.extra.extend(
 
362
                [l.split(b' ', 1)
 
363
                 for l in rev.properties[u'git-extra'].splitlines()])
 
364
        return commit
 
365
 
 
366
    def get_revision_id(self, commit):
 
367
        if commit.encoding:
 
368
            encoding = commit.encoding.decode('ascii')
 
369
        else:
 
370
            encoding = 'utf-8'
 
371
        try:
 
372
            message, metadata = self._decode_commit_message(
 
373
                None, commit.message, encoding)
 
374
        except UnicodeDecodeError:
 
375
            pass
 
376
        else:
 
377
            if metadata.revision_id:
 
378
                return metadata.revision_id
 
379
        return self.revision_id_foreign_to_bzr(commit.id)
 
380
 
 
381
    def import_commit(self, commit, lookup_parent_revid, strict=True):
 
382
        """Convert a git commit to a bzr revision.
 
383
 
 
384
        :return: a `breezy.revision.Revision` object, foreign revid and a
 
385
            testament sha1
 
386
        """
 
387
        if commit is None:
 
388
            raise AssertionError("Commit object can't be None")
 
389
        rev = ForeignRevision(commit.id, self,
 
390
                              self.revision_id_foreign_to_bzr(commit.id))
 
391
        rev.git_metadata = None
 
392
 
 
393
        def decode_using_encoding(rev, commit, encoding):
 
394
            rev.committer = commit.committer.decode(encoding)
 
395
            if commit.committer != commit.author:
 
396
                rev.properties[u'author'] = commit.author.decode(encoding)
 
397
            rev.message, rev.git_metadata = self._decode_commit_message(
 
398
                rev, commit.message, encoding)
 
399
        if commit.encoding is not None:
 
400
            rev.properties[u'git-explicit-encoding'] = commit.encoding.decode(
 
401
                'ascii')
 
402
            decode_using_encoding(rev, commit, commit.encoding.decode('ascii'))
 
403
        else:
 
404
            for encoding in ('utf-8', 'latin1'):
 
405
                try:
 
406
                    decode_using_encoding(rev, commit, encoding)
 
407
                except UnicodeDecodeError:
 
408
                    pass
 
409
                else:
 
410
                    if encoding != 'utf-8':
 
411
                        rev.properties[u'git-implicit-encoding'] = encoding
 
412
                    break
 
413
        if commit.commit_time != commit.author_time:
 
414
            rev.properties[u'author-timestamp'] = str(commit.author_time)
 
415
        if commit.commit_timezone != commit.author_timezone:
 
416
            rev.properties[u'author-timezone'] = "%d" % commit.author_timezone
 
417
        if commit._author_timezone_neg_utc:
 
418
            rev.properties[u'author-timezone-neg-utc'] = ""
 
419
        if commit._commit_timezone_neg_utc:
 
420
            rev.properties[u'commit-timezone-neg-utc'] = ""
 
421
        if commit.gpgsig:
 
422
            rev.properties[u'git-gpg-signature'] = commit.gpgsig.decode(
 
423
                'ascii')
 
424
        if commit.mergetag:
 
425
            for i, tag in enumerate(commit.mergetag):
 
426
                rev.properties[u'git-mergetag-%d' % i] = tag.as_raw_string()
 
427
        rev.timestamp = commit.commit_time
 
428
        rev.timezone = commit.commit_timezone
 
429
        rev.parent_ids = None
 
430
        if rev.git_metadata is not None:
 
431
            md = rev.git_metadata
 
432
            roundtrip_revid = md.revision_id
 
433
            if md.explicit_parent_ids:
 
434
                rev.parent_ids = md.explicit_parent_ids
 
435
            rev.properties.update(md.properties)
 
436
            verifiers = md.verifiers
 
437
        else:
 
438
            roundtrip_revid = None
 
439
            verifiers = {}
 
440
        if rev.parent_ids is None:
 
441
            parents = []
 
442
            for p in commit.parents:
 
443
                try:
 
444
                    parents.append(lookup_parent_revid(p))
 
445
                except KeyError:
 
446
                    parents.append(self.revision_id_foreign_to_bzr(p))
 
447
            rev.parent_ids = list(parents)
 
448
        unknown_extra_fields = []
 
449
        extra_lines = []
 
450
        for k, v in commit.extra:
 
451
            if k == HG_RENAME_SOURCE:
 
452
                extra_lines.append(k + b' ' + v + b'\n')
 
453
            elif k == HG_EXTRA:
 
454
                hgk, hgv = v.split(b':', 1)
 
455
                if hgk not in (HG_EXTRA_AMEND_SOURCE, ) and strict:
 
456
                    raise UnknownMercurialCommitExtra(commit, [hgk])
 
457
                extra_lines.append(k + b' ' + v + b'\n')
 
458
            else:
 
459
                unknown_extra_fields.append(k)
 
460
        if unknown_extra_fields and strict:
 
461
            raise UnknownCommitExtra(
 
462
                commit,
 
463
                [f.decode('ascii', 'replace') for f in unknown_extra_fields])
 
464
        if extra_lines:
 
465
            rev.properties[u'git-extra'] = b''.join(extra_lines)
 
466
        return rev, roundtrip_revid, verifiers
 
467
 
 
468
 
 
469
class BzrGitMappingv1(BzrGitMapping):
 
470
    revid_prefix = b'git-v1'
 
471
    experimental = False
 
472
 
 
473
    def __str__(self):
 
474
        return self.revid_prefix
 
475
 
 
476
 
 
477
class BzrGitMappingExperimental(BzrGitMappingv1):
 
478
    revid_prefix = b'git-experimental'
39
479
    experimental = True
40
 
 
41
 
 
42
 
default_mapping = BzrGitMappingExperimental()
 
480
    roundtripping = False
 
481
 
 
482
    BZR_DUMMY_FILE = '.bzrdummy'
 
483
 
 
484
    def _decode_commit_message(self, rev, message, encoding):
 
485
        if rev is None:
 
486
            rev = Revision()
 
487
        message = self._extract_hg_metadata(rev, message)
 
488
        message = self._extract_git_svn_metadata(rev, message)
 
489
        message, metadata = self._extract_bzr_metadata(rev, message)
 
490
        return message.decode(encoding), metadata
 
491
 
 
492
    def _encode_commit_message(self, rev, message, encoding):
 
493
        ret = message.encode(encoding)
 
494
        ret += self._generate_hg_message_tail(rev)
 
495
        ret += self._generate_git_svn_metadata(rev, encoding)
 
496
        return ret
 
497
 
 
498
    def import_commit(self, commit, lookup_parent_revid, strict=True):
 
499
        rev, roundtrip_revid, verifiers = super(
 
500
            BzrGitMappingExperimental, self).import_commit(
 
501
                commit, lookup_parent_revid, strict)
 
502
        rev.properties[u'converted_revision'] = "git %s\n" % commit.id
 
503
        return rev, roundtrip_revid, verifiers
 
504
 
 
505
 
 
506
class GitMappingRegistry(VcsMappingRegistry):
 
507
    """Registry with available git mappings."""
 
508
 
 
509
    def revision_id_bzr_to_foreign(self, bzr_revid):
 
510
        if bzr_revid == NULL_REVISION:
 
511
            from dulwich.protocol import ZERO_SHA
 
512
            return ZERO_SHA, None
 
513
        if not bzr_revid.startswith(b"git-"):
 
514
            raise errors.InvalidRevisionId(bzr_revid, None)
 
515
        (mapping_version, git_sha) = bzr_revid.split(b":", 1)
 
516
        mapping = self.get(mapping_version)
 
517
        return mapping.revision_id_bzr_to_foreign(bzr_revid)
 
518
 
 
519
    parse_revision_id = revision_id_bzr_to_foreign
 
520
 
 
521
 
 
522
mapping_registry = GitMappingRegistry()
 
523
mapping_registry.register_lazy(b'git-v1', __name__,
 
524
                               "BzrGitMappingv1")
 
525
mapping_registry.register_lazy(b'git-experimental',
 
526
                               __name__, "BzrGitMappingExperimental")
 
527
# Uncomment the next line to enable the experimental bzr-git mappings.
 
528
# This will make sure all bzr metadata is pushed into git, allowing for
 
529
# full roundtripping later.
 
530
# NOTE: THIS IS EXPERIMENTAL. IT MAY EAT YOUR DATA OR CORRUPT
 
531
# YOUR BZR OR GIT REPOSITORIES. USE WITH CARE.
 
532
# mapping_registry.set_default('git-experimental')
 
533
mapping_registry.set_default(b'git-v1')
 
534
 
 
535
 
 
536
class ForeignGit(ForeignVcs):
 
537
    """The Git Stupid Content Tracker"""
 
538
 
 
539
    @property
 
540
    def branch_format(self):
 
541
        from .branch import LocalGitBranchFormat
 
542
        return LocalGitBranchFormat()
 
543
 
 
544
    @property
 
545
    def repository_format(self):
 
546
        from .repository import GitRepositoryFormat
 
547
        return GitRepositoryFormat()
 
548
 
 
549
    def __init__(self):
 
550
        super(ForeignGit, self).__init__(mapping_registry)
 
551
        self.abbreviation = "git"
 
552
 
 
553
    @classmethod
 
554
    def serialize_foreign_revid(self, foreign_revid):
 
555
        return foreign_revid
 
556
 
 
557
    @classmethod
 
558
    def show_foreign_revid(cls, foreign_revid):
 
559
        return {"git commit": foreign_revid.decode('utf-8')}
 
560
 
 
561
 
 
562
foreign_vcs_git = ForeignGit()
 
563
default_mapping = mapping_registry.get_default()()
 
564
 
 
565
 
 
566
def symlink_to_blob(symlink_target):
 
567
    from dulwich.objects import Blob
 
568
    blob = Blob()
 
569
    if isinstance(symlink_target, str):
 
570
        symlink_target = symlink_target.encode('utf-8')
 
571
    blob.data = symlink_target
 
572
    return blob
 
573
 
 
574
 
 
575
def mode_is_executable(mode):
 
576
    """Check if mode should be considered executable."""
 
577
    return bool(mode & 0o111)
 
578
 
 
579
 
 
580
def mode_kind(mode):
 
581
    """Determine the Bazaar inventory kind based on Unix file mode."""
 
582
    if mode is None:
 
583
        return None
 
584
    entry_kind = (mode & 0o700000) / 0o100000
 
585
    if entry_kind == 0:
 
586
        return 'directory'
 
587
    elif entry_kind == 1:
 
588
        file_kind = (mode & 0o70000) / 0o10000
 
589
        if file_kind == 0:
 
590
            return 'file'
 
591
        elif file_kind == 2:
 
592
            return 'symlink'
 
593
        elif file_kind == 6:
 
594
            return 'tree-reference'
 
595
        else:
 
596
            raise AssertionError(
 
597
                "Unknown file kind %d, perms=%o." % (file_kind, mode,))
 
598
    else:
 
599
        raise AssertionError(
 
600
            "Unknown kind, perms=%r." % (mode,))
 
601
 
 
602
 
 
603
def object_mode(kind, executable):
 
604
    if kind == 'directory':
 
605
        return stat.S_IFDIR
 
606
    elif kind == 'symlink':
 
607
        mode = stat.S_IFLNK
 
608
        if executable:
 
609
            mode |= 0o111
 
610
        return mode
 
611
    elif kind == 'file':
 
612
        mode = stat.S_IFREG | 0o644
 
613
        if executable:
 
614
            mode |= 0o111
 
615
        return mode
 
616
    elif kind == 'tree-reference':
 
617
        from dulwich.objects import S_IFGITLINK
 
618
        return S_IFGITLINK
 
619
    else:
 
620
        raise AssertionError
 
621
 
 
622
 
 
623
def entry_mode(entry):
 
624
    """Determine the git file mode for an inventory entry."""
 
625
    return object_mode(entry.kind, getattr(entry, 'executable', False))
 
626
 
 
627
 
 
628
def extract_unusual_modes(rev):
 
629
    try:
 
630
        foreign_revid, mapping = mapping_registry.parse_revision_id(
 
631
            rev.revision_id)
 
632
    except errors.InvalidRevisionId:
 
633
        return {}
 
634
    else:
 
635
        return mapping.export_unusual_file_modes(rev)
 
636
 
 
637
 
 
638
def parse_git_svn_id(text):
 
639
    (head, uuid) = text.rsplit(" ", 1)
 
640
    (full_url, rev) = head.rsplit("@", 1)
 
641
    return (full_url, int(rev), uuid)
 
642
 
 
643
 
 
644
def needs_roundtripping(repo, revid):
 
645
    try:
 
646
        mapping_registry.parse_revision_id(revid)
 
647
    except errors.InvalidRevisionId:
 
648
        return True
 
649
    else:
 
650
        return False