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
32
from . import generate_ids, urlutils
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,
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
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('/'):
52
return get_transport(dirname).get(basename)
55
class NotArchiveType(BzrError):
57
_fmt = '%(path)s is not an archive.'
59
def __init__(self, path):
60
BzrError.__init__(self)
64
class ZipFileWrapper(object):
66
def __init__(self, fileobj, mode):
67
self.zipfile = zipfile.ZipFile(fileobj, mode)
70
for info in self.zipfile.infolist():
71
yield ZipInfoWrapper(self.zipfile, info)
73
def extractfile(self, infowrapper):
74
return BytesIO(self.zipfile.read(infowrapper.name))
76
def add(self, filename):
78
self.zipfile.writestr(filename+'/', '')
80
self.zipfile.write(filename)
86
class ZipInfoWrapper(object):
88
def __init__(self, zipfile, info):
91
self.name = info.filename
92
self.zipfile = zipfile
97
return bool(self.name.endswith('/'))
101
return not self.isdir()
104
class DirWrapper(object):
106
def __init__(self, fileobj, mode='r'):
108
raise AssertionError(
109
'only readonly supported')
110
self.root = os.path.realpath(fileobj.read().decode('utf-8'))
113
return 'DirWrapper(%r)' % self.root
115
def getmembers(self, subdir=None):
116
if subdir is not None:
117
mydir = pathjoin(self.root, subdir)
120
for child in os.listdir(mydir):
121
if subdir is not None:
122
child = pathjoin(subdir, child)
123
fi = FileInfo(self.root, child)
126
for v in self.getmembers(child):
129
def extractfile(self, member):
130
return open(member.fullpath, 'rb')
133
class FileInfo(object):
135
def __init__(self, root, filepath):
136
self.fullpath = pathjoin(root, filepath)
139
self.name = pathjoin(basename(root), filepath)
141
print('root %r' % root)
142
self.name = basename(root)
144
stat = os.lstat(self.fullpath)
145
self.mode = stat.st_mode
150
return 'FileInfo(%r)' % self.name
153
return stat.S_ISREG(self.mode)
156
return stat.S_ISDIR(self.mode)
159
if stat.S_ISLNK(self.mode):
160
self.linkname = os.readlink(self.fullpath)
167
"""Return the top directory given in a path."""
168
components = splitpath(path)
169
if len(components) > 0:
175
def common_directory(names):
176
"""Determine a single directory prefix from a list of names"""
177
possible_prefix = None
179
name_top = top_path(name)
182
if possible_prefix is None:
183
possible_prefix = name_top
185
if name_top != possible_prefix:
187
return possible_prefix
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)
194
tt.create_directory(trans_id)
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:
202
implied_parents.add(parent)
203
add_implied_parents(implied_parents, parent)
206
def names_of_files(tar_file):
207
for member in tar_file.getmembers():
208
if member.type != "g":
212
def should_ignore(relative_path):
213
return is_control_filename(top_path(relative_path))
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.
220
tar_file = tarfile.open('lala', 'r', tar_input)
221
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