/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: Gustav Hartvigsson
  • Date: 2021-01-09 21:36:27 UTC
  • Revision ID: gustav.hartvigsson@gmail.com-20210109213627-h1xwcutzy9m7a99b
Added 'Case Preserving Working Tree Use Cases' from Canonical Wiki

* Addod a page from the Canonical Bazaar wiki
  with information on the scmeatics of case
  perserving filesystems an a case insensitive
  filesystem works.
  
  * Needs re-work, but this will do as it is the
    same inforamoton as what was on the linked
    page in the currint documentation.

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