/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/upstream_import.py

  • Committer: Jelmer Vernooij
  • Date: 2019-06-03 21:25:01 UTC
  • mto: This revision was merged to the branch mainline in revision 7318.
  • Revision ID: jelmer@jelmer.uk-20190603212501-zgt2czrlc6oqoi7a
Fix tests on python 2.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2006-2012 Aaron Bentley
 
2
#
 
3
# This program is free software; you can redistribute it and/or modify
 
4
# it under the terms of the GNU General Public License as published by
 
5
# the Free Software Foundation; either version 2 of the License, or
 
6
# (at your option) any later version.
 
7
#
 
8
# This program is distributed in the hope that it will be useful,
 
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
11
# GNU General Public License for more details.
 
12
#
 
13
# You should have received a copy of the GNU General Public License
 
14
# along with this program; if not, write to the Free Software
 
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
16
 
 
17
"""Import upstream source into a branch"""
 
18
 
 
19
from __future__ import absolute_import
 
20
 
 
21
import errno
 
22
from io import (
 
23
    BytesIO,
 
24
    )
 
25
import os
 
26
import re
 
27
import stat
 
28
import tarfile
 
29
import zipfile
 
30
 
 
31
from . import urlutils
 
32
from .bzr import generate_ids
 
33
from .controldir import ControlDir, is_control_filename
 
34
from .errors import (BzrError, NoSuchFile, BzrCommandError, NotBranchError)
 
35
from .osutils import (pathjoin, isdir, file_iterator, basename,
 
36
                      file_kind, splitpath)
 
37
from .sixish import (
 
38
    text_type,
 
39
    )
 
40
from .trace import warning
 
41
from .transform import TreeTransform, resolve_conflicts, cook_conflicts
 
42
from .transport import get_transport
 
43
from .workingtree import WorkingTree
 
44
 
 
45
 
 
46
# TODO(jelmer): Move this to transport.py ?
 
47
def open_from_url(location):
 
48
    location = urlutils.normalize_url(location)
 
49
    dirname, basename = urlutils.split(location)
 
50
    if location.endswith('/') and not basename.endswith('/'):
 
51
        basename += '/'
 
52
    return get_transport(dirname).get(basename)
 
53
 
 
54
 
 
55
class NotArchiveType(BzrError):
 
56
 
 
57
    _fmt = '%(path)s is not an archive.'
 
58
 
 
59
    def __init__(self, path):
 
60
        BzrError.__init__(self)
 
61
        self.path = path
 
62
 
 
63
 
 
64
class ZipFileWrapper(object):
 
65
 
 
66
    def __init__(self, fileobj, mode):
 
67
        self.zipfile = zipfile.ZipFile(fileobj, mode)
 
68
 
 
69
    def getmembers(self):
 
70
        for info in self.zipfile.infolist():
 
71
            yield ZipInfoWrapper(self.zipfile, info)
 
72
 
 
73
    def extractfile(self, infowrapper):
 
74
        return BytesIO(self.zipfile.read(infowrapper.name))
 
75
 
 
76
    def add(self, filename):
 
77
        if isdir(filename):
 
78
            self.zipfile.writestr(filename + '/', '')
 
79
        else:
 
80
            self.zipfile.write(filename)
 
81
 
 
82
    def close(self):
 
83
        self.zipfile.close()
 
84
 
 
85
 
 
86
class ZipInfoWrapper(object):
 
87
 
 
88
    def __init__(self, zipfile, info):
 
89
        self.info = info
 
90
        self.type = None
 
91
        self.name = info.filename
 
92
        self.zipfile = zipfile
 
93
        self.mode = 0o666
 
94
 
 
95
    def isdir(self):
 
96
        # Really? Eeeew!
 
97
        return bool(self.name.endswith('/'))
 
98
 
 
99
    def isreg(self):
 
100
        # Really? Eeeew!
 
101
        return not self.isdir()
 
102
 
 
103
 
 
104
class DirWrapper(object):
 
105
 
 
106
    def __init__(self, fileobj, mode='r'):
 
107
        if mode != 'r':
 
108
            raise AssertionError(
 
109
                'only readonly supported')
 
110
        self.root = os.path.realpath(fileobj.read().decode('utf-8'))
 
111
 
 
112
    def __repr__(self):
 
113
        return 'DirWrapper(%r)' % self.root
 
114
 
 
115
    def getmembers(self, subdir=None):
 
116
        if subdir is not None:
 
117
            mydir = pathjoin(self.root, subdir)
 
118
        else:
 
119
            mydir = self.root
 
120
        for child in os.listdir(mydir):
 
121
            if subdir is not None:
 
122
                child = pathjoin(subdir, child)
 
123
            fi = FileInfo(self.root, child)
 
124
            yield fi
 
125
            if fi.isdir():
 
126
                for v in self.getmembers(child):
 
127
                    yield v
 
128
 
 
129
    def extractfile(self, member):
 
130
        return open(member.fullpath, 'rb')
 
131
 
 
132
 
 
133
class FileInfo(object):
 
134
 
 
135
    def __init__(self, root, filepath):
 
136
        self.fullpath = pathjoin(root, filepath)
 
137
        self.root = root
 
138
        if filepath != '':
 
139
            self.name = pathjoin(basename(root), filepath)
 
140
        else:
 
141
            print('root %r' % root)
 
142
            self.name = basename(root)
 
143
        self.type = None
 
144
        stat = os.lstat(self.fullpath)
 
145
        self.mode = stat.st_mode
 
146
        if self.isdir():
 
147
            self.name += '/'
 
148
 
 
149
    def __repr__(self):
 
150
        return 'FileInfo(%r)' % self.name
 
151
 
 
152
    def isreg(self):
 
153
        return stat.S_ISREG(self.mode)
 
154
 
 
155
    def isdir(self):
 
156
        return stat.S_ISDIR(self.mode)
 
157
 
 
158
    def issym(self):
 
159
        if stat.S_ISLNK(self.mode):
 
160
            self.linkname = os.readlink(self.fullpath)
 
161
            return True
 
162
        else:
 
163
            return False
 
164
 
 
165
 
 
166
def top_path(path):
 
167
    """Return the top directory given in a path."""
 
168
    components = splitpath(path)
 
169
    if len(components) > 0:
 
170
        return components[0]
 
171
    else:
 
172
        return ''
 
173
 
 
174
 
 
175
def common_directory(names):
 
176
    """Determine a single directory prefix from a list of names"""
 
177
    possible_prefix = None
 
178
    for name in names:
 
179
        name_top = top_path(name)
 
180
        if name_top == '':
 
181
            return None
 
182
        if possible_prefix is None:
 
183
            possible_prefix = name_top
 
184
        else:
 
185
            if name_top != possible_prefix:
 
186
                return None
 
187
    return possible_prefix
 
188
 
 
189
 
 
190
def do_directory(tt, trans_id, tree, relative_path, path):
 
191
    if isdir(path) and tree.is_versioned(relative_path):
 
192
        tt.cancel_deletion(trans_id)
 
193
    else:
 
194
        tt.create_directory(trans_id)
 
195
 
 
196
 
 
197
def add_implied_parents(implied_parents, path):
 
198
    """Update the set of implied parents from a path"""
 
199
    parent = os.path.dirname(path)
 
200
    if parent in implied_parents:
 
201
        return
 
202
    implied_parents.add(parent)
 
203
    add_implied_parents(implied_parents, parent)
 
204
 
 
205
 
 
206
def names_of_files(tar_file):
 
207
    for member in tar_file.getmembers():
 
208
        if member.type != "g":
 
209
            yield member.name
 
210
 
 
211
 
 
212
def should_ignore(relative_path):
 
213
    return is_control_filename(top_path(relative_path))
 
214
 
 
215
 
 
216
def import_tar(tree, tar_input):
 
217
    """Replace the contents of a working directory with tarfile contents.
 
218
    The tarfile may be a gzipped stream.  File ids will be updated.
 
219
    """
 
220
    tar_file = tarfile.open('lala', 'r', tar_input)
 
221
    import_archive(tree, tar_file)
 
222
 
 
223
 
 
224
def import_zip(tree, zip_input):
 
225
    zip_file = ZipFileWrapper(zip_input, 'r')
 
226
    import_archive(tree, zip_file)
 
227
 
 
228
 
 
229
def import_dir(tree, dir_input):
 
230
    dir_file = DirWrapper(dir_input)
 
231
    import_archive(tree, dir_file)
 
232
 
 
233
 
 
234
def import_archive(tree, archive_file):
 
235
    tt = TreeTransform(tree)
 
236
    try:
 
237
        import_archive_to_transform(tree, archive_file, tt)
 
238
        tt.apply()
 
239
    finally:
 
240
        tt.finalize()
 
241
 
 
242
 
 
243
def import_archive_to_transform(tree, archive_file, tt):
 
244
    prefix = common_directory(names_of_files(archive_file))
 
245
    removed = set()
 
246
    for path, entry in tree.iter_entries_by_dir():
 
247
        if entry.parent_id is None:
 
248
            continue
 
249
        trans_id = tt.trans_id_tree_path(path)
 
250
        tt.delete_contents(trans_id)
 
251
        removed.add(path)
 
252
 
 
253
    added = set()
 
254
    implied_parents = set()
 
255
    seen = set()
 
256
    for member in archive_file.getmembers():
 
257
        if member.type == 'g':
 
258
            # type 'g' is a header
 
259
            continue
 
260
        # Inverse functionality in bzr uses utf-8.  We could also
 
261
        # interpret relative to fs encoding, which would match native
 
262
        # behaviour better.
 
263
        relative_path = member.name
 
264
        if not isinstance(relative_path, text_type):
 
265
            relative_path = relative_path.decode('utf-8')
 
266
        if prefix is not None:
 
267
            relative_path = relative_path[len(prefix) + 1:]
 
268
            relative_path = relative_path.rstrip('/')
 
269
        if relative_path == '':
 
270
            continue
 
271
        if should_ignore(relative_path):
 
272
            continue
 
273
        add_implied_parents(implied_parents, relative_path)
 
274
        trans_id = tt.trans_id_tree_path(relative_path)
 
275
        added.add(relative_path.rstrip('/'))
 
276
        path = tree.abspath(relative_path)
 
277
        if member.name in seen:
 
278
            if tt.final_kind(trans_id) == 'file':
 
279
                tt.set_executability(None, trans_id)
 
280
            tt.cancel_creation(trans_id)
 
281
        seen.add(member.name)
 
282
        if member.isreg():
 
283
            tt.create_file(file_iterator(archive_file.extractfile(member)),
 
284
                           trans_id)
 
285
            executable = (member.mode & 0o111) != 0
 
286
            tt.set_executability(executable, trans_id)
 
287
        elif member.isdir():
 
288
            do_directory(tt, trans_id, tree, relative_path, path)
 
289
        elif member.issym():
 
290
            tt.create_symlink(member.linkname, trans_id)
 
291
        else:
 
292
            continue
 
293
        if tt.tree_file_id(trans_id) is None:
 
294
            name = basename(member.name.rstrip('/'))
 
295
            file_id = generate_ids.gen_file_id(name)
 
296
            tt.version_file(file_id, trans_id)
 
297
 
 
298
    for relative_path in implied_parents.difference(added):
 
299
        if relative_path == "":
 
300
            continue
 
301
        trans_id = tt.trans_id_tree_path(relative_path)
 
302
        path = tree.abspath(relative_path)
 
303
        do_directory(tt, trans_id, tree, relative_path, path)
 
304
        if tt.tree_file_id(trans_id) is None:
 
305
            tt.version_file(trans_id, trans_id)
 
306
        added.add(relative_path)
 
307
 
 
308
    for path in removed.difference(added):
 
309
        tt.unversion_file(tt.trans_id_tree_path(path))
 
310
 
 
311
    for conflict in cook_conflicts(resolve_conflicts(tt), tt):
 
312
        warning(conflict)
 
313
 
 
314
 
 
315
def do_import(source, tree_directory=None):
 
316
    """Implementation of import command.  Intended for UI only"""
 
317
    if tree_directory is not None:
 
318
        try:
 
319
            tree = WorkingTree.open(tree_directory)
 
320
        except NotBranchError:
 
321
            if not os.path.exists(tree_directory):
 
322
                os.mkdir(tree_directory)
 
323
            branch = ControlDir.create_branch_convenience(tree_directory)
 
324
            tree = branch.controldir.open_workingtree()
 
325
    else:
 
326
        tree = WorkingTree.open_containing('.')[0]
 
327
    with tree.lock_write():
 
328
        if tree.changes_from(tree.basis_tree()).has_changed():
 
329
            raise BzrCommandError("Working tree has uncommitted changes.")
 
330
 
 
331
        try:
 
332
            archive, external_compressor = get_archive_type(source)
 
333
        except NotArchiveType:
 
334
            if file_kind(source) == 'directory':
 
335
                s = BytesIO(source.encode('utf-8'))
 
336
                s.seek(0)
 
337
                import_dir(tree, s)
 
338
            else:
 
339
                raise BzrCommandError('Unhandled import source')
 
340
        else:
 
341
            if archive == 'zip':
 
342
                import_zip(tree, open_from_url(source))
 
343
            elif archive == 'tar':
 
344
                try:
 
345
                    tar_input = open_from_url(source)
 
346
                    if external_compressor == 'bz2':
 
347
                        import bz2
 
348
                        tar_input = BytesIO(bz2.decompress(tar_input.read()))
 
349
                    elif external_compressor == 'lzma':
 
350
                        import lzma
 
351
                        tar_input = BytesIO(lzma.decompress(tar_input.read()))
 
352
                except IOError as e:
 
353
                    if e.errno == errno.ENOENT:
 
354
                        raise NoSuchFile(source)
 
355
                try:
 
356
                    import_tar(tree, tar_input)
 
357
                finally:
 
358
                    tar_input.close()
 
359
 
 
360
 
 
361
def get_archive_type(path):
 
362
    """Return the type of archive and compressor indicated by path name.
 
363
 
 
364
    Only external compressors are returned, so zip files are only
 
365
    ('zip', None).  .tgz is treated as ('tar', 'gz') and '.tar.xz' is treated
 
366
    as ('tar', 'lzma').
 
367
    """
 
368
    matches = re.match(r'.*\.(zip|tgz|tar(.(gz|bz2|lzma|xz))?)$', path)
 
369
    if not matches:
 
370
        raise NotArchiveType(path)
 
371
    external_compressor = None
 
372
    if matches.group(3) is not None:
 
373
        archive = 'tar'
 
374
        external_compressor = matches.group(3)
 
375
        if external_compressor == 'xz':
 
376
            external_compressor = 'lzma'
 
377
    elif matches.group(1) == 'tgz':
 
378
        return 'tar', 'gz'
 
379
    else:
 
380
        archive = matches.group(1)
 
381
    return archive, external_compressor