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
28
from . import generate_ids, urlutils
29
from .controldir import ControlDir, is_control_filename
30
from .errors import (BzrError, NoSuchFile, BzrCommandError, NotBranchError)
31
from .osutils import (pathjoin, isdir, file_iterator, basename,
33
from .sixish import StringIO
34
from .trace import warning
35
from .transform import TreeTransform, resolve_conflicts, cook_conflicts
36
from .transport import get_transport
37
from .workingtree import WorkingTree
40
# TODO(jelmer): Move this to transport.py ?
41
def open_from_url(location):
42
location = urlutils.normalize_url(location)
43
dirname, basename = urlutils.split(location)
44
if location.endswith('/') and not basename.endswith('/'):
46
return get_transport(dirname).get(basename)
49
class NotArchiveType(BzrError):
51
_fmt = '%(path)s is not an archive.'
53
def __init__(self, path):
54
BzrError.__init__(self)
58
class ZipFileWrapper(object):
60
def __init__(self, fileobj, mode):
61
self.zipfile = zipfile.ZipFile(fileobj, mode)
64
for info in self.zipfile.infolist():
65
yield ZipInfoWrapper(self.zipfile, info)
67
def extractfile(self, infowrapper):
68
return StringIO(self.zipfile.read(infowrapper.name))
70
def add(self, filename):
72
self.zipfile.writestr(filename+'/', '')
74
self.zipfile.write(filename)
80
class ZipInfoWrapper(object):
82
def __init__(self, zipfile, info):
85
self.name = info.filename
86
self.zipfile = zipfile
91
return bool(self.name.endswith('/'))
95
return not self.isdir()
98
class DirWrapper(object):
100
def __init__(self, fileobj, mode='r'):
102
raise AssertionError(
103
'only readonly supported')
104
self.root = os.path.realpath(fileobj.read())
107
return 'DirWrapper(%r)' % self.root
109
def getmembers(self, subdir=None):
110
if subdir is not None:
111
mydir = pathjoin(self.root, subdir)
114
for child in os.listdir(mydir):
115
if subdir is not None:
116
child = pathjoin(subdir, child)
117
fi = FileInfo(self.root, child)
120
for v in self.getmembers(child):
123
def extractfile(self, member):
124
return open(member.fullpath)
127
class FileInfo(object):
129
def __init__(self, root, filepath):
130
self.fullpath = pathjoin(root, filepath)
133
self.name = pathjoin(basename(root), filepath)
135
print('root %r' % root)
136
self.name = basename(root)
138
stat = os.lstat(self.fullpath)
139
self.mode = stat.st_mode
144
return 'FileInfo(%r)' % self.name
147
return stat.S_ISREG(self.mode)
150
return stat.S_ISDIR(self.mode)
153
if stat.S_ISLNK(self.mode):
154
self.linkname = os.readlink(self.fullpath)
161
"""Return the top directory given in a path."""
162
components = splitpath(path)
163
if len(components) > 0:
169
def common_directory(names):
170
"""Determine a single directory prefix from a list of names"""
171
possible_prefix = None
173
name_top = top_path(name)
176
if possible_prefix is None:
177
possible_prefix = name_top
179
if name_top != possible_prefix:
181
return possible_prefix
184
def do_directory(tt, trans_id, tree, relative_path, path):
185
if isdir(path) and tree.is_versioned(relative_path):
186
tt.cancel_deletion(trans_id)
188
tt.create_directory(trans_id)
191
def add_implied_parents(implied_parents, path):
192
"""Update the set of implied parents from a path"""
193
parent = os.path.dirname(path)
194
if parent in implied_parents:
196
implied_parents.add(parent)
197
add_implied_parents(implied_parents, parent)
200
def names_of_files(tar_file):
201
for member in tar_file.getmembers():
202
if member.type != "g":
206
def should_ignore(relative_path):
207
return is_control_filename(top_path(relative_path))
210
def import_tar(tree, tar_input):
211
"""Replace the contents of a working directory with tarfile contents.
212
The tarfile may be a gzipped stream. File ids will be updated.
214
tar_file = tarfile.open('lala', 'r', tar_input)
215
import_archive(tree, tar_file)
217
def import_zip(tree, zip_input):
218
zip_file = ZipFileWrapper(zip_input, 'r')
219
import_archive(tree, zip_file)
221
def import_dir(tree, dir_input):
222
dir_file = DirWrapper(dir_input)
223
import_archive(tree, dir_file)
226
def import_archive(tree, archive_file):
227
tt = TreeTransform(tree)
229
import_archive_to_transform(tree, archive_file, tt)
235
def import_archive_to_transform(tree, archive_file, tt):
236
prefix = common_directory(names_of_files(archive_file))
238
for path, entry in tree.iter_entries_by_dir():
239
if entry.parent_id is None:
241
trans_id = tt.trans_id_tree_path(path)
242
tt.delete_contents(trans_id)
246
implied_parents = set()
248
for member in archive_file.getmembers():
249
if member.type == 'g':
250
# type 'g' is a header
252
# Inverse functionality in bzr uses utf-8. We could also
253
# interpret relative to fs encoding, which would match native
255
relative_path = member.name.decode('utf-8')
256
if prefix is not None:
257
relative_path = relative_path[len(prefix)+1:]
258
relative_path = relative_path.rstrip('/')
259
if relative_path == '':
261
if should_ignore(relative_path):
263
add_implied_parents(implied_parents, relative_path)
264
trans_id = tt.trans_id_tree_path(relative_path)
265
added.add(relative_path.rstrip('/'))
266
path = tree.abspath(relative_path)
267
if member.name in seen:
268
if tt.final_kind(trans_id) == 'file':
269
tt.set_executability(None, trans_id)
270
tt.cancel_creation(trans_id)
271
seen.add(member.name)
273
tt.create_file(file_iterator(archive_file.extractfile(member)),
275
executable = (member.mode & 0o111) != 0
276
tt.set_executability(executable, trans_id)
278
do_directory(tt, trans_id, tree, relative_path, path)
280
tt.create_symlink(member.linkname, trans_id)
283
if tt.tree_file_id(trans_id) is None:
284
name = basename(member.name.rstrip('/'))
285
file_id = generate_ids.gen_file_id(name)
286
tt.version_file(file_id, trans_id)
288
for relative_path in implied_parents.difference(added):
289
if relative_path == "":
291
trans_id = tt.trans_id_tree_path(relative_path)
292
path = tree.abspath(relative_path)
293
do_directory(tt, trans_id, tree, relative_path, path)
294
if tt.tree_file_id(trans_id) is None:
295
tt.version_file(trans_id, trans_id)
296
added.add(relative_path)
298
for path in removed.difference(added):
299
tt.unversion_file(tt.trans_id_tree_path(path))
301
for conflict in cook_conflicts(resolve_conflicts(tt), tt):
305
def do_import(source, tree_directory=None):
306
"""Implementation of import command. Intended for UI only"""
307
if tree_directory is not None:
309
tree = WorkingTree.open(tree_directory)
310
except NotBranchError:
311
if not os.path.exists(tree_directory):
312
os.mkdir(tree_directory)
313
branch = ControlDir.create_branch_convenience(tree_directory)
314
tree = branch.controldir.open_workingtree()
316
tree = WorkingTree.open_containing('.')[0]
317
with tree.lock_write():
318
if tree.changes_from(tree.basis_tree()).has_changed():
319
raise BzrCommandError("Working tree has uncommitted changes.")
322
archive, external_compressor = get_archive_type(source)
323
except NotArchiveType:
324
if file_kind(source) == 'directory':
329
raise BzrCommandError('Unhandled import source')
332
import_zip(tree, open_from_url(source))
333
elif archive == 'tar':
335
tar_input = open_from_url(source)
336
if external_compressor == 'bz2':
338
tar_input = StringIO(bz2.decompress(tar_input.read()))
339
elif external_compressor == 'lzma':
341
tar_input = StringIO(lzma.decompress(tar_input.read()))
343
if e.errno == errno.ENOENT:
344
raise NoSuchFile(source)
346
import_tar(tree, tar_input)
351
def get_archive_type(path):
352
"""Return the type of archive and compressor indicated by path name.
354
Only external compressors are returned, so zip files are only
355
('zip', None). .tgz is treated as ('tar', 'gz') and '.tar.xz' is treated
358
matches = re.match(r'.*\.(zip|tgz|tar(.(gz|bz2|lzma|xz))?)$', path)
360
raise NotArchiveType(path)
361
external_compressor = None
362
if matches.group(3) is not None:
364
external_compressor = matches.group(3)
365
if external_compressor == 'xz':
366
external_compressor = 'lzma'
367
elif matches.group(1) == 'tgz':
370
archive = matches.group(1)
371
return archive, external_compressor