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 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,
40
from .trace import warning
41
from .transform import 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)
224
def import_zip(tree, zip_input):
225
zip_file = ZipFileWrapper(zip_input, 'r')
226
import_archive(tree, zip_file)
229
def import_dir(tree, dir_input):
230
dir_file = DirWrapper(dir_input)
231
import_archive(tree, dir_file)
234
def import_archive(tree, archive_file):
235
with tree.get_transform() as tt:
236
import_archive_to_transform(tree, archive_file, tt)
240
def import_archive_to_transform(tree, archive_file, tt):
241
prefix = common_directory(names_of_files(archive_file))
243
for path, entry in tree.iter_entries_by_dir():
244
if entry.parent_id is None:
246
trans_id = tt.trans_id_tree_path(path)
247
tt.delete_contents(trans_id)
251
implied_parents = set()
253
for member in archive_file.getmembers():
254
if member.type == 'g':
255
# type 'g' is a header
257
# Inverse functionality in bzr uses utf-8. We could also
258
# interpret relative to fs encoding, which would match native
260
relative_path = member.name
261
if not isinstance(relative_path, text_type):
262
relative_path = relative_path.decode('utf-8')
263
if prefix is not None:
264
relative_path = relative_path[len(prefix) + 1:]
265
relative_path = relative_path.rstrip('/')
266
if relative_path == '':
268
if should_ignore(relative_path):
270
add_implied_parents(implied_parents, relative_path)
271
trans_id = tt.trans_id_tree_path(relative_path)
272
added.add(relative_path.rstrip('/'))
273
path = tree.abspath(relative_path)
274
if member.name in seen:
275
if tt.final_kind(trans_id) == 'file':
276
tt.set_executability(None, trans_id)
277
tt.cancel_creation(trans_id)
278
seen.add(member.name)
280
tt.create_file(file_iterator(archive_file.extractfile(member)),
282
executable = (member.mode & 0o111) != 0
283
tt.set_executability(executable, trans_id)
285
do_directory(tt, trans_id, tree, relative_path, path)
287
tt.create_symlink(member.linkname, trans_id)
290
if tt.tree_file_id(trans_id) is None:
291
name = basename(member.name.rstrip('/'))
292
file_id = generate_ids.gen_file_id(name)
293
tt.version_file(file_id, trans_id)
295
for relative_path in implied_parents.difference(added):
296
if relative_path == "":
298
trans_id = tt.trans_id_tree_path(relative_path)
299
path = tree.abspath(relative_path)
300
do_directory(tt, trans_id, tree, relative_path, path)
301
if tt.tree_file_id(trans_id) is None:
302
tt.version_file(trans_id, trans_id)
303
added.add(relative_path)
305
for path in removed.difference(added):
306
tt.unversion_file(tt.trans_id_tree_path(path))
308
for conflict in cook_conflicts(resolve_conflicts(tt), tt):
312
def do_import(source, tree_directory=None):
313
"""Implementation of import command. Intended for UI only"""
314
if tree_directory is not None:
316
tree = WorkingTree.open(tree_directory)
317
except NotBranchError:
318
if not os.path.exists(tree_directory):
319
os.mkdir(tree_directory)
320
branch = ControlDir.create_branch_convenience(tree_directory)
321
tree = branch.controldir.open_workingtree()
323
tree = WorkingTree.open_containing('.')[0]
324
with tree.lock_write():
325
if tree.changes_from(tree.basis_tree()).has_changed():
326
raise BzrCommandError("Working tree has uncommitted changes.")
329
archive, external_compressor = get_archive_type(source)
330
except NotArchiveType:
331
if file_kind(source) == 'directory':
332
s = BytesIO(source.encode('utf-8'))
336
raise BzrCommandError('Unhandled import source')
339
import_zip(tree, open_from_url(source))
340
elif archive == 'tar':
342
tar_input = open_from_url(source)
343
if external_compressor == 'bz2':
345
tar_input = BytesIO(bz2.decompress(tar_input.read()))
346
elif external_compressor == 'lzma':
348
tar_input = BytesIO(lzma.decompress(tar_input.read()))
350
if e.errno == errno.ENOENT:
351
raise NoSuchFile(source)
353
import_tar(tree, tar_input)
358
def get_archive_type(path):
359
"""Return the type of archive and compressor indicated by path name.
361
Only external compressors are returned, so zip files are only
362
('zip', None). .tgz is treated as ('tar', 'gz') and '.tar.xz' is treated
365
matches = re.match(r'.*\.(zip|tgz|tar(.(gz|bz2|lzma|xz))?)$', path)
367
raise NotArchiveType(path)
368
external_compressor = None
369
if matches.group(3) is not None:
371
external_compressor = matches.group(3)
372
if external_compressor == 'xz':
373
external_compressor = 'lzma'
374
elif matches.group(1) == 'tgz':
377
archive = matches.group(1)
378
return archive, external_compressor