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
24
from StringIO import StringIO
29
from . import generate_ids, urlutils
30
from .controldir import ControlDir
31
from .errors import (BzrError, NoSuchFile, BzrCommandError, NotBranchError)
32
from .osutils import (pathjoin, isdir, file_iterator, basename,
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.path2id(relative_path) is not None:
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 top_path(relative_path) == '.bzr'
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 & 0111) != 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]
319
if tree.changes_from(tree.basis_tree()).has_changed():
320
raise BzrCommandError("Working tree has uncommitted changes.")
323
archive, external_compressor = get_archive_type(source)
324
except NotArchiveType:
325
if file_kind(source) == 'directory':
330
raise BzrCommandError('Unhandled import source')
333
import_zip(tree, open_from_url(source))
334
elif archive == 'tar':
336
tar_input = open_from_url(source)
337
if external_compressor == 'bz2':
339
tar_input = StringIO(bz2.decompress(tar_input.read()))
340
elif external_compressor == 'lzma':
342
tar_input = StringIO(lzma.decompress(tar_input.read()))
344
if e.errno == errno.ENOENT:
345
raise NoSuchFile(source)
347
import_tar(tree, tar_input)
354
def get_archive_type(path):
355
"""Return the type of archive and compressor indicated by path name.
357
Only external compressors are returned, so zip files are only
358
('zip', None). .tgz is treated as ('tar', 'gz') and '.tar.xz' is treated
361
matches = re.match(r'.*\.(zip|tgz|tar(.(gz|bz2|lzma|xz))?)$', path)
363
raise NotArchiveType(path)
364
external_compressor = None
365
if matches.group(3) is not None:
367
external_compressor = matches.group(3)
368
if external_compressor == 'xz':
369
external_compressor = 'lzma'
370
elif matches.group(1) == 'tgz':
373
archive = matches.group(1)
374
return archive, external_compressor