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