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"""
29
from . import urlutils
30
from .bzr import generate_ids
31
from .controldir import ControlDir, is_control_filename
32
from .errors import (BzrError, NoSuchFile, BzrCommandError, NotBranchError)
33
from .osutils import (pathjoin, isdir, file_iterator, basename,
35
from .trace import warning
36
from .transform import resolve_conflicts, cook_conflicts
37
from .transport import get_transport
38
from .workingtree import WorkingTree
41
# TODO(jelmer): Move this to transport.py ?
42
def open_from_url(location):
43
location = urlutils.normalize_url(location)
44
dirname, basename = urlutils.split(location)
45
if location.endswith('/') and not basename.endswith('/'):
47
return get_transport(dirname).get(basename)
50
class NotArchiveType(BzrError):
52
_fmt = '%(path)s is not an archive.'
54
def __init__(self, path):
55
BzrError.__init__(self)
59
class ZipFileWrapper(object):
61
def __init__(self, fileobj, mode):
62
self.zipfile = zipfile.ZipFile(fileobj, mode)
65
for info in self.zipfile.infolist():
66
yield ZipInfoWrapper(self.zipfile, info)
68
def extractfile(self, infowrapper):
69
return BytesIO(self.zipfile.read(infowrapper.name))
71
def add(self, filename):
73
self.zipfile.writestr(filename + '/', '')
75
self.zipfile.write(filename)
81
class ZipInfoWrapper(object):
83
def __init__(self, zipfile, info):
86
self.name = info.filename
87
self.zipfile = zipfile
92
return bool(self.name.endswith('/'))
96
return not self.isdir()
99
class DirWrapper(object):
101
def __init__(self, fileobj, mode='r'):
103
raise AssertionError(
104
'only readonly supported')
105
self.root = os.path.realpath(fileobj.read().decode('utf-8'))
108
return 'DirWrapper(%r)' % self.root
110
def getmembers(self, subdir=None):
111
if subdir is not None:
112
mydir = pathjoin(self.root, subdir)
115
for child in os.listdir(mydir):
116
if subdir is not None:
117
child = pathjoin(subdir, child)
118
fi = FileInfo(self.root, child)
121
for v in self.getmembers(child):
124
def extractfile(self, member):
125
return open(member.fullpath, 'rb')
128
class FileInfo(object):
130
def __init__(self, root, filepath):
131
self.fullpath = pathjoin(root, filepath)
134
self.name = pathjoin(basename(root), filepath)
136
print('root %r' % root)
137
self.name = basename(root)
139
stat = os.lstat(self.fullpath)
140
self.mode = stat.st_mode
145
return 'FileInfo(%r)' % self.name
148
return stat.S_ISREG(self.mode)
151
return stat.S_ISDIR(self.mode)
154
if stat.S_ISLNK(self.mode):
155
self.linkname = os.readlink(self.fullpath)
162
"""Return the top directory given in a path."""
163
components = splitpath(path)
164
if len(components) > 0:
170
def common_directory(names):
171
"""Determine a single directory prefix from a list of names"""
172
possible_prefix = None
174
name_top = top_path(name)
177
if possible_prefix is None:
178
possible_prefix = name_top
180
if name_top != possible_prefix:
182
return possible_prefix
185
def do_directory(tt, trans_id, tree, relative_path, path):
186
if isdir(path) and tree.is_versioned(relative_path):
187
tt.cancel_deletion(trans_id)
189
tt.create_directory(trans_id)
192
def add_implied_parents(implied_parents, path):
193
"""Update the set of implied parents from a path"""
194
parent = os.path.dirname(path)
195
if parent in implied_parents:
197
implied_parents.add(parent)
198
add_implied_parents(implied_parents, parent)
201
def names_of_files(tar_file):
202
for member in tar_file.getmembers():
203
if member.type != "g":
207
def should_ignore(relative_path):
208
return is_control_filename(top_path(relative_path))
211
def import_tar(tree, tar_input):
212
"""Replace the contents of a working directory with tarfile contents.
213
The tarfile may be a gzipped stream. File ids will be updated.
215
tar_file = tarfile.open('lala', 'r', tar_input)
216
import_archive(tree, tar_file)
219
def import_zip(tree, zip_input):
220
zip_file = ZipFileWrapper(zip_input, 'r')
221
import_archive(tree, zip_file)
224
def import_dir(tree, dir_input):
225
dir_file = DirWrapper(dir_input)
226
import_archive(tree, dir_file)
229
def import_archive(tree, archive_file):
230
with tree.get_transform() as tt:
231
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
256
if not isinstance(relative_path, str):
257
relative_path = relative_path.decode('utf-8')
258
if prefix is not None:
259
relative_path = relative_path[len(prefix) + 1:]
260
relative_path = relative_path.rstrip('/')
261
if relative_path == '':
263
if should_ignore(relative_path):
265
add_implied_parents(implied_parents, relative_path)
266
trans_id = tt.trans_id_tree_path(relative_path)
267
added.add(relative_path.rstrip('/'))
268
path = tree.abspath(relative_path)
269
if member.name in seen:
270
if tt.final_kind(trans_id) == 'file':
271
tt.set_executability(None, trans_id)
272
tt.cancel_creation(trans_id)
273
seen.add(member.name)
275
tt.create_file(file_iterator(archive_file.extractfile(member)),
277
executable = (member.mode & 0o111) != 0
278
tt.set_executability(executable, trans_id)
280
do_directory(tt, trans_id, tree, relative_path, path)
282
tt.create_symlink(member.linkname, trans_id)
285
if tt.tree_file_id(trans_id) is None:
286
name = basename(member.name.rstrip('/'))
287
file_id = generate_ids.gen_file_id(name)
288
tt.version_file(file_id, trans_id)
290
for relative_path in implied_parents.difference(added):
291
if relative_path == "":
293
trans_id = tt.trans_id_tree_path(relative_path)
294
path = tree.abspath(relative_path)
295
do_directory(tt, trans_id, tree, relative_path, path)
296
if tt.tree_file_id(trans_id) is None:
297
tt.version_file(trans_id, trans_id)
298
added.add(relative_path)
300
for path in removed.difference(added):
301
tt.unversion_file(tt.trans_id_tree_path(path))
303
for conflict in cook_conflicts(resolve_conflicts(tt), tt):
307
def do_import(source, tree_directory=None):
308
"""Implementation of import command. Intended for UI only"""
309
if tree_directory is not None:
311
tree = WorkingTree.open(tree_directory)
312
except NotBranchError:
313
if not os.path.exists(tree_directory):
314
os.mkdir(tree_directory)
315
branch = ControlDir.create_branch_convenience(tree_directory)
316
tree = branch.controldir.open_workingtree()
318
tree = WorkingTree.open_containing('.')[0]
319
with tree.lock_write():
320
if tree.changes_from(tree.basis_tree()).has_changed():
321
raise BzrCommandError("Working tree has uncommitted changes.")
324
archive, external_compressor = get_archive_type(source)
325
except NotArchiveType:
326
if file_kind(source) == 'directory':
327
s = BytesIO(source.encode('utf-8'))
331
raise BzrCommandError('Unhandled import source')
334
import_zip(tree, open_from_url(source))
335
elif archive == 'tar':
337
tar_input = open_from_url(source)
338
if external_compressor == 'bz2':
340
tar_input = BytesIO(bz2.decompress(tar_input.read()))
341
elif external_compressor == 'lzma':
343
tar_input = BytesIO(lzma.decompress(tar_input.read()))
345
if e.errno == errno.ENOENT:
346
raise NoSuchFile(source)
348
import_tar(tree, tar_input)
353
def get_archive_type(path):
354
"""Return the type of archive and compressor indicated by path name.
356
Only external compressors are returned, so zip files are only
357
('zip', None). .tgz is treated as ('tar', 'gz') and '.tar.xz' is treated
360
matches = re.match(r'.*\.(zip|tgz|tar(.(gz|bz2|lzma|xz))?)$', path)
362
raise NotArchiveType(path)
363
external_compressor = None
364
if matches.group(3) is not None:
366
external_compressor = matches.group(3)
367
if external_compressor == 'xz':
368
external_compressor = 'lzma'
369
elif matches.group(1) == 'tgz':
372
archive = matches.group(1)
373
return archive, external_compressor