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)
222
def import_zip(tree, zip_input):
223
zip_file = ZipFileWrapper(zip_input, 'r')
224
import_archive(tree, zip_file)
227
def import_dir(tree, dir_input):
228
dir_file = DirWrapper(dir_input)
229
import_archive(tree, dir_file)
232
def import_archive(tree, archive_file):
233
tt = TreeTransform(tree)
235
import_archive_to_transform(tree, archive_file, tt)
241
def import_archive_to_transform(tree, archive_file, tt):
242
prefix = common_directory(names_of_files(archive_file))
244
for path, entry in tree.iter_entries_by_dir():
245
if entry.parent_id is None:
247
trans_id = tt.trans_id_tree_path(path)
248
tt.delete_contents(trans_id)
252
implied_parents = set()
254
for member in archive_file.getmembers():
255
if member.type == 'g':
256
# type 'g' is a header
258
# Inverse functionality in bzr uses utf-8. We could also
259
# interpret relative to fs encoding, which would match native
261
relative_path = member.name
262
if not isinstance(relative_path, text_type):
263
relative_path = relative_path.decode('utf-8')
264
if prefix is not None:
265
relative_path = relative_path[len(prefix)+1:]
266
relative_path = relative_path.rstrip('/')
267
if relative_path == '':
269
if should_ignore(relative_path):
271
add_implied_parents(implied_parents, relative_path)
272
trans_id = tt.trans_id_tree_path(relative_path)
273
added.add(relative_path.rstrip('/'))
274
path = tree.abspath(relative_path)
275
if member.name in seen:
276
if tt.final_kind(trans_id) == 'file':
277
tt.set_executability(None, trans_id)
278
tt.cancel_creation(trans_id)
279
seen.add(member.name)
281
tt.create_file(file_iterator(archive_file.extractfile(member)),
283
executable = (member.mode & 0o111) != 0
284
tt.set_executability(executable, trans_id)
286
do_directory(tt, trans_id, tree, relative_path, path)
288
tt.create_symlink(member.linkname, trans_id)
291
if tt.tree_file_id(trans_id) is None:
292
name = basename(member.name.rstrip('/'))
293
file_id = generate_ids.gen_file_id(name)
294
tt.version_file(file_id, trans_id)
296
for relative_path in implied_parents.difference(added):
297
if relative_path == "":
299
trans_id = tt.trans_id_tree_path(relative_path)
300
path = tree.abspath(relative_path)
301
do_directory(tt, trans_id, tree, relative_path, path)
302
if tt.tree_file_id(trans_id) is None:
303
tt.version_file(trans_id, trans_id)
304
added.add(relative_path)
306
for path in removed.difference(added):
307
tt.unversion_file(tt.trans_id_tree_path(path))
309
for conflict in cook_conflicts(resolve_conflicts(tt), tt):
313
def do_import(source, tree_directory=None):
314
"""Implementation of import command. Intended for UI only"""
315
if tree_directory is not None:
317
tree = WorkingTree.open(tree_directory)
318
except NotBranchError:
319
if not os.path.exists(tree_directory):
320
os.mkdir(tree_directory)
321
branch = ControlDir.create_branch_convenience(tree_directory)
322
tree = branch.controldir.open_workingtree()
324
tree = WorkingTree.open_containing('.')[0]
325
with tree.lock_write():
326
if tree.changes_from(tree.basis_tree()).has_changed():
327
raise BzrCommandError("Working tree has uncommitted changes.")
330
archive, external_compressor = get_archive_type(source)
331
except NotArchiveType:
332
if file_kind(source) == 'directory':
333
s = BytesIO(source.encode('utf-8'))
337
raise BzrCommandError('Unhandled import source')
340
import_zip(tree, open_from_url(source))
341
elif archive == 'tar':
343
tar_input = open_from_url(source)
344
if external_compressor == 'bz2':
346
tar_input = BytesIO(bz2.decompress(tar_input.read()))
347
elif external_compressor == 'lzma':
349
tar_input = BytesIO(lzma.decompress(tar_input.read()))
351
if e.errno == errno.ENOENT:
352
raise NoSuchFile(source)
354
import_tar(tree, tar_input)
359
def get_archive_type(path):
360
"""Return the type of archive and compressor indicated by path name.
362
Only external compressors are returned, so zip files are only
363
('zip', None). .tgz is treated as ('tar', 'gz') and '.tar.xz' is treated
366
matches = re.match(r'.*\.(zip|tgz|tar(.(gz|bz2|lzma|xz))?)$', path)
368
raise NotArchiveType(path)
369
external_compressor = None
370
if matches.group(3) is not None:
372
external_compressor = matches.group(3)
373
if external_compressor == 'xz':
374
external_compressor = 'lzma'
375
elif matches.group(1) == 'tgz':
378
archive = matches.group(1)
379
return archive, external_compressor