/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-07-18 23:14:00 UTC
  • mfrom: (7490.40.62 work)
  • mto: This revision was merged to the branch mainline in revision 7519.
  • Revision ID: jelmer@jelmer.uk-20200718231400-jaes9qltn8oi8xss
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)
 
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
83
147
 
84
148
 
85
149
def warn_escaped(commit, num_escaped):
96
160
    """Class that maps between Git and Bazaar semantics."""
97
161
    experimental = False
98
162
 
99
 
    BZR_FILE_IDS_FILE = None
100
 
 
101
163
    BZR_DUMMY_FILE = None
102
164
 
 
165
    def is_special_file(self, filename):
 
166
        return (filename in (self.BZR_DUMMY_FILE, ))
 
167
 
103
168
    def __init__(self):
104
 
        super(BzrGitMapping, self).__init__(foreign_git)
 
169
        super(BzrGitMapping, self).__init__(foreign_vcs_git)
105
170
 
106
171
    def __eq__(self, other):
107
 
        return (type(self) == type(other) and
108
 
                self.revid_prefix == other.revid_prefix)
 
172
        return (type(self) == type(other)
 
173
                and self.revid_prefix == other.revid_prefix)
109
174
 
110
175
    @classmethod
111
176
    def revision_id_foreign_to_bzr(cls, git_rev_id):
113
178
        from dulwich.protocol import ZERO_SHA
114
179
        if git_rev_id == ZERO_SHA:
115
180
            return NULL_REVISION
116
 
        return "%s:%s" % (cls.revid_prefix, git_rev_id)
 
181
        return b"%s:%s" % (cls.revid_prefix, git_rev_id)
117
182
 
118
183
    @classmethod
119
184
    def revision_id_bzr_to_foreign(cls, bzr_rev_id):
120
185
        """Convert a Bazaar revision id to a git revision id handle."""
121
 
        if not bzr_rev_id.startswith("%s:" % cls.revid_prefix):
 
186
        if not bzr_rev_id.startswith(b"%s:" % cls.revid_prefix):
122
187
            raise errors.InvalidRevisionId(bzr_rev_id, cls)
123
 
        return bzr_rev_id[len(cls.revid_prefix)+1:], cls()
 
188
        return bzr_rev_id[len(cls.revid_prefix) + 1:], cls()
124
189
 
125
190
    def generate_file_id(self, path):
126
191
        # Git paths are just bytestrings
127
192
        # We must just hope they are valid UTF-8..
128
 
        if path == "":
 
193
        if isinstance(path, str):
 
194
            path = path.encode("utf-8")
 
195
        if path == b"":
129
196
            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)
 
197
        return FILE_ID_PREFIX + escape_file_id(path)
136
198
 
137
199
    def parse_file_id(self, file_id):
138
200
        if file_id == ROOT_ID:
139
 
            return ""
140
 
        return unescape_file_id(file_id)
141
 
 
142
 
    def revid_as_refname(self, revid):
143
 
        import urllib
144
 
        return "refs/bzr/%s" % urllib.quote(revid)
 
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):]))
145
205
 
146
206
    def import_unusual_file_modes(self, rev, unusual_file_modes):
147
207
        if unusual_file_modes:
148
208
            ret = [(path, unusual_file_modes[path])
149
209
                   for path in sorted(unusual_file_modes.keys())]
150
 
            rev.properties['file-modes'] = bencode.bencode(ret)
 
210
            rev.properties[u'file-modes'] = bencode.bencode(ret)
151
211
 
152
212
    def export_unusual_file_modes(self, rev):
153
213
        try:
154
 
            file_modes = rev.properties['file-modes']
 
214
            file_modes = rev.properties[u'file-modes']
155
215
        except KeyError:
156
216
            return {}
157
217
        else:
158
 
            return dict([(self.generate_file_id(path), mode) for (path, mode) in bencode.bdecode(file_modes.encode("utf-8"))])
 
218
            return dict(bencode.bdecode(file_modes.encode("utf-8")))
159
219
 
160
220
    def _generate_git_svn_metadata(self, rev, encoding):
161
221
        try:
162
 
            git_svn_id = rev.properties["git-svn-id"]
 
222
            git_svn_id = rev.properties[u"git-svn-id"]
163
223
        except KeyError:
164
224
            return ""
165
225
        else:
170
230
        renames = []
171
231
        branch = 'default'
172
232
        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(
 
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(
177
237
                    rev.properties[name])
178
 
            elif name == 'hg:renames':
 
238
            elif name == u'hg:renames':
179
239
                renames = bencode.bdecode(base64.b64decode(
180
 
                    rev.properties['hg:renames']))
 
240
                    rev.properties[u'hg:renames']))
181
241
            # TODO: Export other properties as 'bzr:' extras?
182
242
        ret = format_hg_metadata(renames, branch, extra)
183
 
        assert isinstance(ret, str)
 
243
        if not isinstance(ret, bytes):
 
244
            raise TypeError(ret)
184
245
        return ret
185
246
 
186
247
    def _extract_git_svn_metadata(self, rev, message):
187
248
        lines = message.split("\n")
188
 
        if not (lines[-1] == "" and len(lines) >= 2 and lines[-2].startswith("git-svn-id:")):
 
249
        if not (lines[-1] == "" and len(lines) >= 2 and
 
250
                lines[-2].startswith("git-svn-id:")):
189
251
            return message
190
252
        git_svn_id = lines[-2].split(": ", 1)[1]
191
 
        rev.properties['git-svn-id'] = git_svn_id
 
253
        rev.properties[u'git-svn-id'] = git_svn_id
192
254
        (url, rev, uuid) = parse_git_svn_id(git_svn_id)
193
255
        # FIXME: Convert this to converted-from property somehow..
194
 
        ret = "\n".join(lines[:-2])
195
 
        assert isinstance(ret, str)
196
 
        return ret
 
256
        return "\n".join(lines[:-2])
197
257
 
198
258
    def _extract_hg_metadata(self, rev, message):
199
259
        (message, renames, branch, extra) = extract_hg_metadata(message)
200
260
        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)
 
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)
204
264
        if renames:
205
 
            rev.properties['hg:renames'] = base64.b64encode(bencode.bencode(
206
 
                [(new, old) for (old, new) in renames.iteritems()]))
 
265
            rev.properties[u'hg:renames'] = base64.b64encode(bencode.bencode(
 
266
                [(new, old) for (old, new) in renames.items()]))
207
267
        return message
208
268
 
209
269
    def _extract_bzr_metadata(self, rev, message):
211
271
        return message, metadata
212
272
 
213
273
    def _decode_commit_message(self, rev, message, encoding):
214
 
        return message.decode(encoding), BzrGitRevisionMetadata()
 
274
        return message.decode(encoding), CommitSupplement()
215
275
 
216
276
    def _encode_commit_message(self, rev, message, encoding):
217
277
        return message.encode(encoding)
218
278
 
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,
 
279
    def export_commit(self, rev, tree_sha, parent_lookup, lossy,
231
280
                      verifiers):
232
281
        """Turn a Bazaar revision in to a Git commit
233
282
 
234
283
        :param tree_sha: Tree sha for the commit
235
284
        :param parent_lookup: Function for looking up the GIT sha equiv of a
236
285
            bzr revision
237
 
        :param roundtrip: Whether to store roundtripping information.
 
286
        :param lossy: Whether to store roundtripping information.
238
287
        :param verifiers: Verifiers info
239
288
        :return dulwich.objects.Commit represent the revision:
240
289
        """
241
 
        from dulwich.objects import Commit
 
290
        from dulwich.objects import Commit, Tag
242
291
        commit = Commit()
243
292
        commit.tree = tree_sha
244
 
        if roundtrip:
245
 
            metadata = BzrGitRevisionMetadata()
 
293
        if not lossy:
 
294
            metadata = CommitSupplement()
246
295
            metadata.verifiers = verifiers
247
296
        else:
248
297
            metadata = None
255
304
                if metadata is not None:
256
305
                    metadata.explicit_parent_ids = rev.parent_ids
257
306
            if git_p is not None:
258
 
                assert len(git_p) == 40, "unexpected length for %r" % git_p
 
307
                if len(git_p) != 40:
 
308
                    raise AssertionError("unexpected length for %r" % git_p)
259
309
                parents.append(git_p)
260
310
        commit.parents = parents
261
311
        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')
 
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
266
320
        commit.committer = fix_person_identifier(rev.committer.encode(
267
321
            encoding))
268
322
        commit.author = fix_person_identifier(
269
323
            rev.get_apparent_authors()[0].encode(encoding))
 
324
        # TODO(jelmer): Don't use this hack.
 
325
        long = getattr(__builtins__, 'long', int)
270
326
        commit.commit_time = long(rev.timestamp)
271
 
        if 'author-timestamp' in rev.properties:
272
 
            commit.author_time = long(rev.properties['author-timestamp'])
 
327
        if u'author-timestamp' in rev.properties:
 
328
            commit.author_time = long(rev.properties[u'author-timestamp'])
273
329
        else:
274
330
            commit.author_time = commit.commit_time
275
 
        commit._commit_timezone_neg_utc = "commit-timezone-neg-utc" in rev.properties
 
331
        commit._commit_timezone_neg_utc = (
 
332
            u"commit-timezone-neg-utc" in rev.properties)
276
333
        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'])
 
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'])
280
338
        else:
281
339
            commit.author_timezone = commit.commit_timezone
282
 
        commit.message = self._encode_commit_message(rev, rev.message, 
283
 
            encoding)
284
 
        assert type(commit.message) == str
 
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)
285
347
        if metadata is not None:
286
348
            try:
287
349
                mapping_registry.parse_revision_id(rev.revision_id)
288
350
            except errors.InvalidRevisionId:
289
351
                metadata.revision_id = rev.revision_id
290
352
            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:
 
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:
296
359
                    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
 
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()])
301
379
        return commit
302
380
 
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):
 
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):
312
397
        """Convert a git commit to a bzr revision.
313
398
 
314
 
        :return: a `bzrlib.revision.Revision` object, foreign revid and a
 
399
        :return: a `breezy.revision.Revision` object, foreign revid and a
315
400
            testament sha1
316
401
        """
317
402
        if commit is None:
318
403
            raise AssertionError("Commit object can't be None")
319
404
        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])
 
405
                              self.revision_id_foreign_to_bzr(commit.id))
322
406
        rev.git_metadata = None
 
407
 
323
408
        def decode_using_encoding(rev, commit, encoding):
324
 
            rev.committer = str(commit.committer).decode(encoding)
 
409
            rev.committer = commit.committer.decode(encoding)
325
410
            if commit.committer != commit.author:
326
 
                rev.properties['author'] = str(commit.author).decode(encoding)
 
411
                rev.properties[u'author'] = commit.author.decode(encoding)
327
412
            rev.message, rev.git_metadata = self._decode_commit_message(
328
413
                rev, commit.message, encoding)
329
414
        if commit.encoding is not None:
330
 
            rev.properties['git-explicit-encoding'] = commit.encoding
331
 
            decode_using_encoding(rev, commit, commit.encoding)
 
415
            rev.properties[u'git-explicit-encoding'] = commit.encoding.decode(
 
416
                'ascii')
 
417
            decode_using_encoding(rev, commit, commit.encoding.decode('ascii'))
332
418
        else:
333
419
            for encoding in ('utf-8', 'latin1'):
334
420
                try:
337
423
                    pass
338
424
                else:
339
425
                    if encoding != 'utf-8':
340
 
                        rev.properties['git-implicit-encoding'] = encoding
 
426
                        rev.properties[u'git-implicit-encoding'] = encoding
341
427
                    break
342
428
        if commit.commit_time != commit.author_time:
343
 
            rev.properties['author-timestamp'] = str(commit.author_time)
 
429
            rev.properties[u'author-timestamp'] = str(commit.author_time)
344
430
        if commit.commit_timezone != commit.author_timezone:
345
 
            rev.properties['author-timezone'] = "%d" % commit.author_timezone
 
431
            rev.properties[u'author-timezone'] = "%d" % commit.author_timezone
346
432
        if commit._author_timezone_neg_utc:
347
 
            rev.properties['author-timezone-neg-utc'] = ""
 
433
            rev.properties[u'author-timezone-neg-utc'] = ""
348
434
        if commit._commit_timezone_neg_utc:
349
 
            rev.properties['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()
350
442
        rev.timestamp = commit.commit_time
351
443
        rev.timezone = commit.commit_timezone
 
444
        rev.parent_ids = None
352
445
        if rev.git_metadata is not None:
353
446
            md = rev.git_metadata
354
447
            roundtrip_revid = md.revision_id
359
452
        else:
360
453
            roundtrip_revid = None
361
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)
362
481
        return rev, roundtrip_revid, verifiers
363
482
 
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
483
 
380
484
class BzrGitMappingv1(BzrGitMapping):
381
 
    revid_prefix = 'git-v1'
 
485
    revid_prefix = b'git-v1'
382
486
    experimental = False
383
487
 
384
488
    def __str__(self):
386
490
 
387
491
 
388
492
class BzrGitMappingExperimental(BzrGitMappingv1):
389
 
    revid_prefix = 'git-experimental'
 
493
    revid_prefix = b'git-experimental'
390
494
    experimental = True
391
 
    roundtripping = True
392
 
 
393
 
    BZR_FILE_IDS_FILE = '.bzrfileids'
 
495
    roundtripping = False
394
496
 
395
497
    BZR_DUMMY_FILE = '.bzrdummy'
396
498
 
397
499
    def _decode_commit_message(self, rev, message, encoding):
 
500
        if rev is None:
 
501
            rev = Revision()
398
502
        message = self._extract_hg_metadata(rev, message)
399
503
        message = self._extract_git_svn_metadata(rev, message)
400
504
        message, metadata = self._extract_bzr_metadata(rev, message)
406
510
        ret += self._generate_git_svn_metadata(rev, encoding)
407
511
        return ret
408
512
 
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
 
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
412
518
        return rev, roundtrip_revid, verifiers
413
519
 
414
520
 
419
525
        if bzr_revid == NULL_REVISION:
420
526
            from dulwich.protocol import ZERO_SHA
421
527
            return ZERO_SHA, None
422
 
        if not bzr_revid.startswith("git-"):
 
528
        if not bzr_revid.startswith(b"git-"):
423
529
            raise errors.InvalidRevisionId(bzr_revid, None)
424
 
        (mapping_version, git_sha) = bzr_revid.split(":", 1)
 
530
        (mapping_version, git_sha) = bzr_revid.split(b":", 1)
425
531
        mapping = self.get(mapping_version)
426
532
        return mapping.revision_id_bzr_to_foreign(bzr_revid)
427
533
 
429
535
 
430
536
 
431
537
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')
 
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')
437
549
 
438
550
 
439
551
class ForeignGit(ForeignVcs):
441
553
 
442
554
    @property
443
555
    def branch_format(self):
444
 
        from bzrlib.plugins.git.branch import GitBranchFormat
445
 
        return GitBranchFormat()
 
556
        from .branch import LocalGitBranchFormat
 
557
        return LocalGitBranchFormat()
446
558
 
447
559
    @property
448
560
    def repository_format(self):
449
 
        from bzrlib.plugins.git.repository import GitRepositoryFormat
 
561
        from .repository import GitRepositoryFormat
450
562
        return GitRepositoryFormat()
451
563
 
452
564
    def __init__(self):
459
571
 
460
572
    @classmethod
461
573
    def show_foreign_revid(cls, foreign_revid):
462
 
        return { "git commit": foreign_revid }
463
 
 
464
 
 
465
 
foreign_git = ForeignGit()
 
574
        return {"git commit": foreign_revid.decode('utf-8')}
 
575
 
 
576
 
 
577
foreign_vcs_git = ForeignGit()
466
578
default_mapping = mapping_registry.get_default()()
467
579
 
468
580
 
469
 
def symlink_to_blob(entry):
 
581
def symlink_to_blob(symlink_target):
470
582
    from dulwich.objects import Blob
471
583
    blob = Blob()
472
 
    symlink_target = entry.symlink_target
473
 
    if type(symlink_target) == unicode:
474
 
        symlink_target = symlink_target.encode('utf-8')
 
584
    if isinstance(symlink_target, str):
 
585
        symlink_target = encode_git_path(symlink_target)
475
586
    blob.data = symlink_target
476
587
    return blob
477
588
 
478
589
 
479
590
def mode_is_executable(mode):
480
591
    """Check if mode should be considered executable."""
481
 
    return bool(mode & 0111)
 
592
    return bool(mode & 0o111)
482
593
 
483
594
 
484
595
def mode_kind(mode):
485
596
    """Determine the Bazaar inventory kind based on Unix file mode."""
486
 
    entry_kind = (mode & 0700000) / 0100000
 
597
    if mode is None:
 
598
        return None
 
599
    entry_kind = (mode & 0o700000) / 0o100000
487
600
    if entry_kind == 0:
488
601
        return 'directory'
489
602
    elif entry_kind == 1:
490
 
        file_kind = (mode & 070000) / 010000
 
603
        file_kind = (mode & 0o70000) / 0o10000
491
604
        if file_kind == 0:
492
605
            return 'file'
493
606
        elif file_kind == 2:
508
621
    elif kind == 'symlink':
509
622
        mode = stat.S_IFLNK
510
623
        if executable:
511
 
            mode |= 0111
 
624
            mode |= 0o111
512
625
        return mode
513
626
    elif kind == 'file':
514
 
        mode = stat.S_IFREG | 0644
 
627
        mode = stat.S_IFREG | 0o644
515
628
        if executable:
516
 
            mode |= 0111
 
629
            mode |= 0o111
517
630
        return mode
518
631
    elif kind == 'tree-reference':
519
632
        from dulwich.objects import S_IFGITLINK
524
637
 
525
638
def entry_mode(entry):
526
639
    """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
 
640
    return object_mode(entry.kind, getattr(entry, 'executable', False))
558
641
 
559
642
 
560
643
def extract_unusual_modes(rev):
573
656
    return (full_url, int(rev), uuid)
574
657
 
575
658
 
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
 
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