1
# Copyright (C) 2006-2012 Aaron Bentley
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.
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.
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
17
"""Import upstream source into a branch"""
19
from __future__ import absolute_import
31
from . import generate_ids, urlutils
32
from .controldir import ControlDir, is_control_filename
33
from .errors import (BzrError, NoSuchFile, BzrCommandError, NotBranchError)
34
from .osutils import (pathjoin, isdir, file_iterator, basename,
39
from .trace import warning
40
from .transform import TreeTransform, resolve_conflicts, cook_conflicts
41
from .transport import get_transport
42
from .workingtree import WorkingTree
45
# TODO(jelmer): Move this to transport.py ?
46
def open_from_url(location):
47
location = urlutils.normalize_url(location)
48
dirname, basename = urlutils.split(location)
49
if location.endswith('/') and not basename.endswith('/'):
51
return get_transport(dirname).get(basename)
54
class NotArchiveType(BzrError):
56
_fmt = '%(path)s is not an archive.'
58
def __init__(self, path):
59
BzrError.__init__(self)
63
class ZipFileWrapper(object):
65
def __init__(self, fileobj, mode):
66
self.zipfile = zipfile.ZipFile(fileobj, mode)
69
for info in self.zipfile.infolist():
70
yield ZipInfoWrapper(self.zipfile, info)
72
def extractfile(self, infowrapper):
73
return BytesIO(self.zipfile.read(infowrapper.name))
75
def add(self, filename):
77
self.zipfile.writestr(filename + '/', '')
79
self.zipfile.write(filename)
85
class ZipInfoWrapper(object):
87
def __init__(self, zipfile, info):
90
self.name = info.filename
91
self.zipfile = zipfile
96
return bool(self.name.endswith('/'))
100
return not self.isdir()
103
class DirWrapper(object):
105
def __init__(self, fileobj, mode='r'):
107
raise AssertionError(
108
'only readonly supported')
109
self.root = os.path.realpath(fileobj.read().decode('utf-8'))
112
return 'DirWrapper(%r)' % self.root
114
def getmembers(self, subdir=None):
115
if subdir is not None:
116
mydir = pathjoin(self.root, subdir)
119
for child in os.listdir(mydir):
120
if subdir is not None:
121
child = pathjoin(subdir, child)
122
fi = FileInfo(self.root, child)
125
for v in self.getmembers(child):
128
def extractfile(self, member):
129
return open(member.fullpath, 'rb')
132
class FileInfo(object):
134
def __init__(self, root, filepath):
135
self.fullpath = pathjoin(root, filepath)
138
self.name = pathjoin(basename(root), filepath)
140
print('root %r' % root)
141
self.name = basename(root)
143
stat = os.lstat(self.fullpath)
144
self.mode = stat.st_mode
149
return 'FileInfo(%r)' % self.name
152
return stat.S_ISREG(self.mode)
155
return stat.S_ISDIR(self.mode)
158
if stat.S_ISLNK(self.mode):
159
self.linkname = os.readlink(self.fullpath)
166
"""Return the top directory given in a path."""
167
components = splitpath(path)
168
if len(components) > 0:
174
def common_directory(names):
175
"""Determine a single directory prefix from a list of names"""
176
possible_prefix = None
178
name_top = top_path(name)
181
if possible_prefix is None:
182
possible_prefix = name_top
184
if name_top != possible_prefix:
186
return possible_prefix
189
def do_directory(tt, trans_id, tree, relative_path, path):
190
if isdir(path) and tree.is_versioned(relative_path):
191
tt.cancel_deletion(trans_id)
193
tt.create_directory(trans_id)
196
def add_implied_parents(implied_parents, path):
197
"""Update the set of implied parents from a path"""
198
parent = os.path.dirname(path)
199
if parent in implied_parents:
201
implied_parents.add(parent)
202
add_implied_parents(implied_parents, parent)
205
def names_of_files(tar_file):
206
for member in tar_file.getmembers():
207
if member.type != "g":
211
def should_ignore(relative_path):
212
return is_control_filename(top_path(relative_path))
215
def import_tar(tree, tar_input):
216
"""Replace the contents of a working directory with tarfile contents.
217
The tarfile may be a gzipped stream. File ids will be updated.
219
tar_file = tarfile.open('lala', 'r', tar_input)
220
import_archive(tree, tar_file)
223
def import_zip(tree, zip_input):
224
zip_file = ZipFileWrapper(zip_input, 'r')
225
import_archive(tree, zip_file)
228
def import_dir(tree, dir_input):
229
dir_file = DirWrapper(dir_input)
230
import_archive(tree, dir_file)
233
def import_archive(tree, archive_file):
234
tt = TreeTransform(tree)
236
import_archive_to_transform(tree, archive_file, tt)
242
def import_archive_to_transform(tree, archive_file, tt):
243
prefix = common_directory(names_of_files(archive_file))
245
for path, entry in tree.iter_entries_by_dir():
246
if entry.parent_id is None:
248
trans_id = tt.trans_id_tree_path(path)
249
tt.delete_contents(trans_id)
253
implied_parents = set()
255
for member in archive_file.getmembers():
256
if member.type == 'g':
257
# type 'g' is a header
259
# Inverse functionality in bzr uses utf-8. We could also
260
# interpret relative to fs encoding, which would match native
262
relative_path = member.name
263
if not isinstance(relative_path, text_type):
264
relative_path = relative_path.decode('utf-8')
265
if prefix is not None:
266
relative_path = relative_path[len(prefix) + 1:]
267
relative_path = relative_path.rstrip('/')
268
if relative_path == '':
270
if should_ignore(relative_path):
272
add_implied_parents(implied_parents, relative_path)
273
trans_id = tt.trans_id_tree_path(relative_path)
274
added.add(relative_path.rstrip('/'))
275
path = tree.abspath(relative_path)
276
if member.name in seen:
277
if tt.final_kind(trans_id) == 'file':
278
tt.set_executability(None, trans_id)
279
tt.cancel_creation(trans_id)
280
seen.add(member.name)
282
tt.create_file(file_iterator(archive_file.extractfile(member)),
284
executable = (member.mode & 0o111) != 0
285
tt.set_executability(executable, trans_id)
287
do_directory(tt, trans_id, tree, relative_path, path)
289
tt.create_symlink(member.linkname, trans_id)
292
if tt.tree_file_id(trans_id) is None:
293
name = basename(member.name.rstrip('/'))
294
file_id = generate_ids.gen_file_id(name)
295
tt.version_file(file_id, trans_id)
297
for relative_path in implied_parents.difference(added):
298
if relative_path == "":
300
trans_id = tt.trans_id_tree_path(relative_path)
301
path = tree.abspath(relative_path)
302
do_directory(tt, trans_id, tree, relative_path, path)
303
if tt.tree_file_id(trans_id) is None:
304
tt.version_file(trans_id, trans_id)
305
added.add(relative_path)
307
for path in removed.difference(added):
308
tt.unversion_file(tt.trans_id_tree_path(path))
310
for conflict in cook_conflicts(resolve_conflicts(tt), tt):
314
def do_import(source, tree_directory=None):
315
"""Implementation of import command. Intended for UI only"""
316
if tree_directory is not None:
318
tree = WorkingTree.open(tree_directory)
319
except NotBranchError:
320
if not os.path.exists(tree_directory):
321
os.mkdir(tree_directory)
322
branch = ControlDir.create_branch_convenience(tree_directory)
323
tree = branch.controldir.open_workingtree()
325
tree = WorkingTree.open_containing('.')[0]
326
with tree.lock_write():
327
if tree.changes_from(tree.basis_tree()).has_changed():
328
raise BzrCommandError("Working tree has uncommitted changes.")
331
archive, external_compressor = get_archive_type(source)
332
except NotArchiveType:
333
if file_kind(source) == 'directory':
334
s = BytesIO(source.encode('utf-8'))
338
raise BzrCommandError('Unhandled import source')
341
import_zip(tree, open_from_url(source))
342
elif archive == 'tar':
344
tar_input = open_from_url(source)
345
if external_compressor == 'bz2':
347
tar_input = BytesIO(bz2.decompress(tar_input.read()))
348
elif external_compressor == 'lzma':
350
tar_input = BytesIO(lzma.decompress(tar_input.read()))
352
if e.errno == errno.ENOENT:
353
raise NoSuchFile(source)
355
import_tar(tree, tar_input)
360
def get_archive_type(path):
361
"""Return the type of archive and compressor indicated by path name.
363
Only external compressors are returned, so zip files are only
364
('zip', None). .tgz is treated as ('tar', 'gz') and '.tar.xz' is treated
367
matches = re.match(r'.*\.(zip|tgz|tar(.(gz|bz2|lzma|xz))?)$', path)
369
raise NotArchiveType(path)
370
external_compressor = None
371
if matches.group(3) is not None:
373
external_compressor = matches.group(3)
374
if external_compressor == 'xz':
375
external_compressor = 'lzma'
376
elif matches.group(1) == 'tgz':
379
archive = matches.group(1)
380
return archive, external_compressor