Merge lp:~jelmer/brz/import into lp:brz

Proposed by Jelmer Vernooij
Status: Merged
Approved by: Jelmer Vernooij
Approved revision: no longer in the source branch.
Merge reported by: The Breezy Bot
Merged at revision: not available
Proposed branch: lp:~jelmer/brz/import
Merge into: lp:brz
Diff against target: 865 lines (+805/-0)
7 files modified
breezy/builtins.py (+21/-0)
breezy/tests/__init__.py (+1/-0)
breezy/tests/blackbox/__init__.py (+1/-0)
breezy/tests/blackbox/test_import.py (+85/-0)
breezy/tests/test_upstream_import.py (+319/-0)
breezy/upstream_import.py (+374/-0)
doc/en/release-notes/brz-3.0.txt (+4/-0)
To merge this branch: bzr merge lp:~jelmer/brz/import
Reviewer Review Type Date Requested Status
Martin Packman Approve
Review via email: mp+324813@code.launchpad.net

Commit message

Import the 'import' command from bzrtools.

Description of the change

Import the 'import' command from bzrtools.

To post a comment you must log in.
Revision history for this message
Martin Packman (gz) wrote :

Inclusion seems find to me. Can either do a s/StringIO/BytesIO/g now or stick that on my list todo. :)

review: Approve
Revision history for this message
The Breezy Bot (the-breezy-bot) wrote :

A commit message must be set
http://10.242.247.184:8080/job/brz-dev/18/

Revision history for this message
The Breezy Bot (the-breezy-bot) wrote :

Running landing tests failed
http://10.242.247.184:8080/job/brz-dev/19/

Revision history for this message
The Breezy Bot (the-breezy-bot) wrote :

Running landing tests failed
http://10.242.247.184:8080/job/brz-dev/23/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'breezy/builtins.py'
2--- breezy/builtins.py 2017-05-30 19:32:13 +0000
3+++ breezy/builtins.py 2017-05-30 22:09:44 +0000
4@@ -6690,6 +6690,27 @@
5 export_pot(self.outf, plugin, include_duplicates)
6
7
8+class cmd_import(Command):
9+ __doc__ = """Import sources from a directory, tarball or zip file
10+
11+ This command will import a directory, tarball or zip file into a bzr
12+ tree, replacing any versioned files already present. If a directory is
13+ specified, it is used as the target. If the directory does not exist, or
14+ is not versioned, it is created.
15+
16+ Tarballs may be gzip or bzip2 compressed. This is autodetected.
17+
18+ If the tarball or zip has a single root directory, that directory is
19+ stripped when extracting the tarball. This is not done for directories.
20+ """
21+
22+ takes_args = ['source', 'tree?']
23+
24+ def run(self, source, tree=None):
25+ from .upstream_import import do_import
26+ do_import(source, tree)
27+
28+
29 def _register_lazy_builtins():
30 # register lazy builtins from other modules; called at startup and should
31 # be only called once.
32
33=== modified file 'breezy/tests/__init__.py'
34--- breezy/tests/__init__.py 2017-05-30 19:32:13 +0000
35+++ breezy/tests/__init__.py 2017-05-30 22:09:44 +0000
36@@ -4060,6 +4060,7 @@
37 'breezy.tests.test_uncommit',
38 'breezy.tests.test_upgrade',
39 'breezy.tests.test_upgrade_stacked',
40+ 'breezy.tests.test_upstream_import',
41 'breezy.tests.test_urlutils',
42 'breezy.tests.test_utextwrap',
43 'breezy.tests.test_version',
44
45=== modified file 'breezy/tests/blackbox/__init__.py'
46--- breezy/tests/blackbox/__init__.py 2017-05-30 19:16:23 +0000
47+++ breezy/tests/blackbox/__init__.py 2017-05-30 22:09:44 +0000
48@@ -66,6 +66,7 @@
49 'test_find_merge_base',
50 'test_help',
51 'test_hooks',
52+ 'test_import',
53 'test_ignore',
54 'test_ignored',
55 'test_info',
56
57=== added file 'breezy/tests/blackbox/test_import.py'
58--- breezy/tests/blackbox/test_import.py 1970-01-01 00:00:00 +0000
59+++ breezy/tests/blackbox/test_import.py 2017-05-30 22:09:44 +0000
60@@ -0,0 +1,85 @@
61+# Copyright (C) 2006-2012 Aaron Bentley
62+#
63+# This program is free software; you can redistribute it and/or modify
64+# it under the terms of the GNU General Public License as published by
65+# the Free Software Foundation; either version 2 of the License, or
66+# (at your option) any later version.
67+#
68+# This program is distributed in the hope that it will be useful,
69+# but WITHOUT ANY WARRANTY; without even the implied warranty of
70+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
71+# GNU General Public License for more details.
72+#
73+# You should have received a copy of the GNU General Public License
74+# along with this program; if not, write to the Free Software
75+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
76+#
77+
78+"""Tests of the 'brz import' command."""
79+
80+import os
81+
82+from ... import (
83+ osutils,
84+ tests,
85+ urlutils,
86+ )
87+from .. import (
88+ features,
89+ script,
90+ )
91+
92+
93+LzmaFeature = features.ModuleAvailableFeature("lzma")
94+
95+
96+class TestImport(tests.TestCaseWithTransport):
97+
98+ def test_import_upstream(self):
99+ self.run_bzr('init source')
100+ os.mkdir('source/src')
101+ f = file('source/src/myfile', 'wb')
102+ f.write('hello?')
103+ f.close()
104+ os.chdir('source')
105+ self.run_bzr('add')
106+ self.run_bzr('commit -m hello')
107+ self.run_bzr('export ../source-0.1.tar.gz')
108+ self.run_bzr('export ../source-0.1.tar.bz2')
109+ self.run_bzr('export ../source-0.1')
110+ self.run_bzr('init ../import')
111+ os.chdir('../import')
112+ self.run_bzr('import ../source-0.1.tar.gz')
113+ self.assertPathExists('src/myfile')
114+ result = self.run_bzr('import ../source-0.1.tar.gz', retcode=3)[1]
115+ self.assertContainsRe(result, 'Working tree has uncommitted changes')
116+ self.run_bzr('commit -m commit')
117+ self.run_bzr('import ../source-0.1.tar.gz')
118+ os.chdir('..')
119+ self.run_bzr('init import2')
120+ self.run_bzr('import source-0.1.tar.gz import2')
121+ self.assertPathExists('import2/src/myfile')
122+ self.run_bzr('import source-0.1.tar.gz import3')
123+ self.assertPathExists('import3/src/myfile')
124+ self.run_bzr('import source-0.1.tar.bz2 import4')
125+ self.assertPathExists('import4/src/myfile')
126+ self.run_bzr('import source-0.1 import5')
127+ self.assertPathExists('import5/src/myfile')
128+
129+ def test_import_upstream_lzma(self):
130+ self.requireFeature(LzmaFeature)
131+ self.run_bzr('init source')
132+ os.mkdir('source/src')
133+ f = file('source/src/myfile', 'wb')
134+ f.write('hello?')
135+ f.close()
136+ os.chdir('source')
137+ self.run_bzr('add')
138+ self.run_bzr('commit -m hello')
139+ self.run_bzr('export ../source-0.1.tar.lzma')
140+ self.run_bzr('export ../source-0.1.tar.xz')
141+ os.chdir('..')
142+ self.run_bzr('import source-0.1.tar.lzma import1')
143+ self.assertPathExists('import1/src/myfile')
144+ self.run_bzr('import source-0.1.tar.xz import2')
145+ self.assertPathExists('import2/src/myfile')
146
147=== added file 'breezy/tests/test_upstream_import.py'
148--- breezy/tests/test_upstream_import.py 1970-01-01 00:00:00 +0000
149+++ breezy/tests/test_upstream_import.py 2017-05-30 22:09:44 +0000
150@@ -0,0 +1,319 @@
151+# Copyright (C) 2006-2012 Aaron Bentley
152+#
153+# This program is free software; you can redistribute it and/or modify
154+# it under the terms of the GNU General Public License as published by
155+# the Free Software Foundation; either version 2 of the License, or
156+# (at your option) any later version.
157+#
158+# This program is distributed in the hope that it will be useful,
159+# but WITHOUT ANY WARRANTY; without even the implied warranty of
160+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
161+# GNU General Public License for more details.
162+#
163+# You should have received a copy of the GNU General Public License
164+# along with this program; if not, write to the Free Software
165+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
166+
167+import os
168+from StringIO import StringIO
169+from shutil import rmtree, copy2, copytree
170+import tarfile
171+import tempfile
172+import warnings
173+
174+from .. import (
175+ osutils,
176+ revision as _mod_revision,
177+ transform
178+ )
179+from ..bzrdir import BzrDir
180+from ..export import export
181+from ..upstream_import import (
182+ common_directory,
183+ get_archive_type,
184+ import_archive,
185+ import_tar,
186+ import_zip,
187+ import_dir,
188+ NotArchiveType,
189+ top_path,
190+ ZipFileWrapper,
191+)
192+from . import (
193+ TestCaseInTempDir,
194+ TestCaseWithTransport,
195+ )
196+from .features import UnicodeFilenameFeature
197+
198+
199+def import_tar_broken(tree, tar_input):
200+ """
201+ Import a tarfile with names that that end in //, e.g. Feisty Python 2.5
202+ """
203+ tar_file = tarfile.open('lala', 'r', tar_input)
204+ for member in tar_file.members:
205+ if member.name.endswith('/'):
206+ member.name += '/'
207+ import_archive(tree, tar_file)
208+
209+
210+class DirFileWriter(object):
211+
212+ def __init__(self, fileobj, mode):
213+ # We may be asked to 'append'. If so, fileobj already has a path.
214+ # So we copy the existing tree, and overwrite afterward.
215+ fileobj.seek(0)
216+ existing = fileobj.read()
217+ fileobj.seek(0)
218+ path = tempfile.mkdtemp(dir=os.getcwd())
219+ if existing != '':
220+ # copytree requires the directory not to exist
221+ os.rmdir(path)
222+ copytree(existing, path)
223+ fileobj.write(path)
224+ self.root = path
225+
226+ def add(self, path):
227+ target_path = os.path.join(self.root, path)
228+ parent = osutils.dirname(target_path)
229+ if not os.path.exists(parent):
230+ os.makedirs(parent)
231+ kind = osutils.file_kind(path)
232+ if kind == 'file':
233+ copy2(path, target_path)
234+ if kind == 'directory':
235+ os.mkdir(target_path)
236+
237+ def close(self):
238+ pass
239+
240+
241+class TestImport(TestCaseInTempDir):
242+
243+ def make_tar(self, mode='w'):
244+ def maker(fileobj):
245+ return tarfile.open('project-0.1.tar', mode, fileobj)
246+ return self.make_archive(maker)
247+
248+ def make_archive(self, maker, subdir=True):
249+ result = StringIO()
250+ archive_file = maker(result)
251+ try:
252+ os.mkdir('project-0.1')
253+ if subdir:
254+ prefix='project-0.1/'
255+ archive_file.add('project-0.1')
256+ else:
257+ prefix=''
258+ os.chdir('project-0.1')
259+ os.mkdir(prefix + 'junk')
260+ archive_file.add(prefix + 'junk')
261+
262+ f = file(prefix + 'README', 'wb')
263+ f.write('What?')
264+ f.close()
265+ archive_file.add(prefix + 'README')
266+
267+ f = file(prefix + 'FEEDME', 'wb')
268+ f.write('Hungry!!')
269+ f.close()
270+ archive_file.add(prefix + 'FEEDME')
271+
272+ archive_file.close()
273+ finally:
274+ if not subdir:
275+ os.chdir('..')
276+ rmtree('project-0.1')
277+ result.seek(0)
278+ return result
279+
280+ def make_archive2(self, builder, subdir):
281+ result = StringIO()
282+ archive_file = builder(result)
283+ os.mkdir('project-0.2')
284+ try:
285+ if subdir:
286+ prefix='project-0.2/'
287+ archive_file.add('project-0.2')
288+ else:
289+ prefix=''
290+ os.chdir('project-0.2')
291+
292+ os.mkdir(prefix + 'junk')
293+ archive_file.add(prefix + 'junk')
294+
295+ f = file(prefix + 'README', 'wb')
296+ f.write('Now?')
297+ f.close()
298+ archive_file.add(prefix + 'README')
299+
300+ f = file(prefix + 'README', 'wb')
301+ f.write('Wow?')
302+ f.close()
303+ # Add a second entry for README with different contents.
304+ archive_file.add(prefix + 'README')
305+ archive_file.close()
306+
307+ finally:
308+ if not subdir:
309+ os.chdir('..')
310+ result.seek(0)
311+ return result
312+
313+ def make_messed_tar(self):
314+ result = StringIO()
315+ tar_file = tarfile.open('project-0.1.tar', 'w', result)
316+ os.mkdir('project-0.1')
317+ tar_file.add('project-0.1')
318+
319+ os.mkdir('project-0.2')
320+ tar_file.add('project-0.2')
321+
322+ f = file('project-0.1/README', 'wb')
323+ f.write('What?')
324+ f.close()
325+ tar_file.add('project-0.1/README')
326+ tar_file.close()
327+ rmtree('project-0.1')
328+ result.seek(0)
329+ return result
330+
331+ def make_zip(self):
332+ def maker(fileobj):
333+ return ZipFileWrapper(fileobj, 'w')
334+ return self.make_archive(maker)
335+
336+ def make_tar_with_bzrdir(self):
337+ result = StringIO()
338+ tar_file = tarfile.open('tar-with-bzrdir.tar', 'w', result)
339+ os.mkdir('toplevel-dir')
340+ tar_file.add('toplevel-dir')
341+ os.mkdir('toplevel-dir/.bzr')
342+ tar_file.add('toplevel-dir/.bzr')
343+ tar_file.close()
344+ rmtree('toplevel-dir')
345+ result.seek(0)
346+ return result
347+
348+ def test_top_path(self):
349+ self.assertEqual(top_path('ab/b/c'), 'ab')
350+ self.assertEqual(top_path('etc'), 'etc')
351+ self.assertEqual(top_path('project-0.1'), 'project-0.1')
352+
353+ def test_common_directory(self):
354+ self.assertEqual(common_directory(['ab/c/d', 'ab/c/e']), 'ab')
355+ self.assertIs(common_directory(['ab/c/d', 'ac/c/e']), None)
356+ self.assertEqual('FEEDME', common_directory(['FEEDME']))
357+
358+ def test_untar(self):
359+ def builder(fileobj, mode='w'):
360+ return tarfile.open('project-0.1.tar', mode, fileobj)
361+ self.archive_test(builder, import_tar)
362+
363+ def test_broken_tar(self):
364+ def builder(fileobj, mode='w'):
365+ return tarfile.open('project-0.1.tar', mode, fileobj)
366+ self.archive_test(builder, import_tar_broken, subdir=True)
367+
368+ def test_unzip(self):
369+ def builder(fileobj, mode='w'):
370+ return ZipFileWrapper(fileobj, mode)
371+ self.archive_test(builder, import_zip)
372+
373+ def test_copydir_nosub(self):
374+ def builder(fileobj, mode='w'):
375+ return DirFileWriter(fileobj, mode)
376+ # It would be bogus to test with the result in a subdirectory,
377+ # because for directories, the input root is always the output root.
378+ self.archive_test(builder, import_dir)
379+
380+ def archive_test(self, builder, importer, subdir=False):
381+ archive_file = self.make_archive(builder, subdir)
382+ tree = BzrDir.create_standalone_workingtree('tree')
383+ tree.lock_write()
384+ try:
385+ importer(tree, archive_file)
386+ self.assertTrue(tree.path2id('README') is not None)
387+ self.assertTrue(tree.path2id('FEEDME') is not None)
388+ self.assertTrue(os.path.isfile(tree.abspath('README')))
389+ self.assertEqual(tree.stored_kind(tree.path2id('README')),
390+ 'file')
391+ self.assertEqual(tree.stored_kind(tree.path2id('FEEDME')),
392+ 'file')
393+ f = file(tree.abspath('junk/food'), 'wb')
394+ f.write('I like food\n')
395+ f.close()
396+
397+ with warnings.catch_warnings():
398+ warnings.simplefilter('ignore')
399+ archive_file = self.make_archive2(builder, subdir)
400+ importer(tree, archive_file)
401+ self.assertTrue(tree.path2id('README') is not None)
402+ # Ensure the second version of the file is used.
403+ self.assertEqual(tree.get_file_text(tree.path2id('README')),
404+ 'Wow?')
405+ self.assertTrue(not os.path.exists(tree.abspath('FEEDME')))
406+ finally:
407+ tree.unlock()
408+
409+
410+ def test_untar2(self):
411+ tar_file = self.make_messed_tar()
412+ tree = BzrDir.create_standalone_workingtree('tree')
413+ import_tar(tree, tar_file)
414+ self.assertTrue(tree.path2id('project-0.1/README') is not None)
415+
416+ def test_untar_gzip(self):
417+ tar_file = self.make_tar(mode='w:gz')
418+ tree = BzrDir.create_standalone_workingtree('tree')
419+ import_tar(tree, tar_file)
420+ self.assertTrue(tree.path2id('README') is not None)
421+
422+ def test_no_crash_with_bzrdir(self):
423+ tar_file = self.make_tar_with_bzrdir()
424+ tree = BzrDir.create_standalone_workingtree('tree')
425+ import_tar(tree, tar_file)
426+ # So long as it did not crash, that should be ok
427+
428+ def test_get_archive_type(self):
429+ self.assertEqual(('tar', None), get_archive_type('foo.tar'))
430+ self.assertEqual(('zip', None), get_archive_type('foo.zip'))
431+ self.assertRaises(NotArchiveType, get_archive_type, 'foo.gif')
432+ self.assertEqual(('tar', 'gz'), get_archive_type('foo.tar.gz'))
433+ self.assertRaises(NotArchiveType, get_archive_type,
434+ 'foo.zip.gz')
435+ self.assertEqual(('tar', 'gz'), get_archive_type('foo.tgz'))
436+ self.assertEqual(('tar', 'lzma'), get_archive_type('foo.tar.lzma'))
437+ self.assertEqual(('tar', 'lzma'), get_archive_type('foo.tar.xz'))
438+ self.assertEqual(('tar', 'bz2'), get_archive_type('foo.tar.bz2'))
439+
440+
441+class TestWithStuff(TestCaseWithTransport):
442+
443+ def transform_to_tar(self, tt):
444+ stream = StringIO()
445+ export(tt.get_preview_tree(), root='', fileobj=stream, format='tar',
446+ dest=None)
447+ return stream
448+
449+ def get_empty_tt(self):
450+ b = self.make_repository('foo')
451+ null_tree = b.revision_tree(_mod_revision.NULL_REVISION)
452+ tt = transform.TransformPreview(null_tree)
453+ root = tt.new_directory('', transform.ROOT_PARENT, 'tree-root')
454+ tt.fixup_new_roots()
455+ self.addCleanup(tt.finalize)
456+ return tt
457+
458+ def test_nonascii_paths(self):
459+ self.requireFeature(UnicodeFilenameFeature)
460+ tt = self.get_empty_tt()
461+ encoded_file = tt.new_file(
462+ u'\u1234file', tt.root, 'contents', 'new-file')
463+ encoded_file = tt.new_file(
464+ 'other', tt.root, 'contents', 'other-file')
465+ tarfile = self.transform_to_tar(tt)
466+ tarfile.seek(0)
467+ tree = self.make_branch_and_tree('bar')
468+ import_tar(tree, tarfile)
469+ self.assertPathExists(u'bar/\u1234file')
470
471=== added file 'breezy/upstream_import.py'
472--- breezy/upstream_import.py 1970-01-01 00:00:00 +0000
473+++ breezy/upstream_import.py 2017-05-30 22:09:44 +0000
474@@ -0,0 +1,374 @@
475+# Copyright (C) 2006-2012 Aaron Bentley
476+#
477+# This program is free software; you can redistribute it and/or modify
478+# it under the terms of the GNU General Public License as published by
479+# the Free Software Foundation; either version 2 of the License, or
480+# (at your option) any later version.
481+#
482+# This program is distributed in the hope that it will be useful,
483+# but WITHOUT ANY WARRANTY; without even the implied warranty of
484+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
485+# GNU General Public License for more details.
486+#
487+# You should have received a copy of the GNU General Public License
488+# along with this program; if not, write to the Free Software
489+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
490+
491+"""Import upstream source into a branch"""
492+
493+from __future__ import absolute_import
494+
495+import errno
496+import os
497+import re
498+from StringIO import StringIO
499+import stat
500+import tarfile
501+import zipfile
502+
503+from . import generate_ids, urlutils
504+from .bzrdir import BzrDir
505+from .errors import (BzrError, NoSuchFile, BzrCommandError, NotBranchError)
506+from .osutils import (pathjoin, isdir, file_iterator, basename,
507+ file_kind, splitpath)
508+from .trace import warning
509+from .transform import TreeTransform, resolve_conflicts, cook_conflicts
510+from .transport import get_transport
511+from .workingtree import WorkingTree
512+
513+
514+# TODO(jelmer): Move this to transport.py ?
515+def open_from_url(location):
516+ location = urlutils.normalize_url(location)
517+ dirname, basename = urlutils.split(location)
518+ if location.endswith('/') and not basename.endswith('/'):
519+ basename += '/'
520+ return get_transport(dirname).get(basename)
521+
522+
523+class NotArchiveType(BzrError):
524+
525+ _fmt = '%(path)s is not an archive.'
526+
527+ def __init__(self, path):
528+ BzrError.__init__(self)
529+ self.path = path
530+
531+
532+class ZipFileWrapper(object):
533+
534+ def __init__(self, fileobj, mode):
535+ self.zipfile = zipfile.ZipFile(fileobj, mode)
536+
537+ def getmembers(self):
538+ for info in self.zipfile.infolist():
539+ yield ZipInfoWrapper(self.zipfile, info)
540+
541+ def extractfile(self, infowrapper):
542+ return StringIO(self.zipfile.read(infowrapper.name))
543+
544+ def add(self, filename):
545+ if isdir(filename):
546+ self.zipfile.writestr(filename+'/', '')
547+ else:
548+ self.zipfile.write(filename)
549+
550+ def close(self):
551+ self.zipfile.close()
552+
553+
554+class ZipInfoWrapper(object):
555+
556+ def __init__(self, zipfile, info):
557+ self.info = info
558+ self.type = None
559+ self.name = info.filename
560+ self.zipfile = zipfile
561+ self.mode = 0666
562+
563+ def isdir(self):
564+ # Really? Eeeew!
565+ return bool(self.name.endswith('/'))
566+
567+ def isreg(self):
568+ # Really? Eeeew!
569+ return not self.isdir()
570+
571+
572+class DirWrapper(object):
573+
574+ def __init__(self, fileobj, mode='r'):
575+ if mode != 'r':
576+ raise AssertionError(
577+ 'only readonly supported')
578+ self.root = os.path.realpath(fileobj.read())
579+
580+ def __repr__(self):
581+ return 'DirWrapper(%r)' % self.root
582+
583+ def getmembers(self, subdir=None):
584+ if subdir is not None:
585+ mydir = pathjoin(self.root, subdir)
586+ else:
587+ mydir = self.root
588+ for child in os.listdir(mydir):
589+ if subdir is not None:
590+ child = pathjoin(subdir, child)
591+ fi = FileInfo(self.root, child)
592+ yield fi
593+ if fi.isdir():
594+ for v in self.getmembers(child):
595+ yield v
596+
597+ def extractfile(self, member):
598+ return open(member.fullpath)
599+
600+
601+class FileInfo(object):
602+
603+ def __init__(self, root, filepath):
604+ self.fullpath = pathjoin(root, filepath)
605+ self.root = root
606+ if filepath != '':
607+ self.name = pathjoin(basename(root), filepath)
608+ else:
609+ print 'root %r' % root
610+ self.name = basename(root)
611+ self.type = None
612+ stat = os.lstat(self.fullpath)
613+ self.mode = stat.st_mode
614+ if self.isdir():
615+ self.name += '/'
616+
617+ def __repr__(self):
618+ return 'FileInfo(%r)' % self.name
619+
620+ def isreg(self):
621+ return stat.S_ISREG(self.mode)
622+
623+ def isdir(self):
624+ return stat.S_ISDIR(self.mode)
625+
626+ def issym(self):
627+ if stat.S_ISLNK(self.mode):
628+ self.linkname = os.readlink(self.fullpath)
629+ return True
630+ else:
631+ return False
632+
633+
634+def top_path(path):
635+ """Return the top directory given in a path."""
636+ components = splitpath(path)
637+ if len(components) > 0:
638+ return components[0]
639+ else:
640+ return ''
641+
642+
643+def common_directory(names):
644+ """Determine a single directory prefix from a list of names"""
645+ possible_prefix = None
646+ for name in names:
647+ name_top = top_path(name)
648+ if name_top == '':
649+ return None
650+ if possible_prefix is None:
651+ possible_prefix = name_top
652+ else:
653+ if name_top != possible_prefix:
654+ return None
655+ return possible_prefix
656+
657+
658+def do_directory(tt, trans_id, tree, relative_path, path):
659+ if isdir(path) and tree.path2id(relative_path) is not None:
660+ tt.cancel_deletion(trans_id)
661+ else:
662+ tt.create_directory(trans_id)
663+
664+
665+def add_implied_parents(implied_parents, path):
666+ """Update the set of implied parents from a path"""
667+ parent = os.path.dirname(path)
668+ if parent in implied_parents:
669+ return
670+ implied_parents.add(parent)
671+ add_implied_parents(implied_parents, parent)
672+
673+
674+def names_of_files(tar_file):
675+ for member in tar_file.getmembers():
676+ if member.type != "g":
677+ yield member.name
678+
679+
680+def should_ignore(relative_path):
681+ return top_path(relative_path) == '.bzr'
682+
683+
684+def import_tar(tree, tar_input):
685+ """Replace the contents of a working directory with tarfile contents.
686+ The tarfile may be a gzipped stream. File ids will be updated.
687+ """
688+ tar_file = tarfile.open('lala', 'r', tar_input)
689+ import_archive(tree, tar_file)
690+
691+def import_zip(tree, zip_input):
692+ zip_file = ZipFileWrapper(zip_input, 'r')
693+ import_archive(tree, zip_file)
694+
695+def import_dir(tree, dir_input):
696+ dir_file = DirWrapper(dir_input)
697+ import_archive(tree, dir_file)
698+
699+
700+def import_archive(tree, archive_file):
701+ tt = TreeTransform(tree)
702+ try:
703+ import_archive_to_transform(tree, archive_file, tt)
704+ tt.apply()
705+ finally:
706+ tt.finalize()
707+
708+
709+def import_archive_to_transform(tree, archive_file, tt):
710+ prefix = common_directory(names_of_files(archive_file))
711+ removed = set()
712+ for path, entry in tree.iter_entries_by_dir():
713+ if entry.parent_id is None:
714+ continue
715+ trans_id = tt.trans_id_tree_path(path)
716+ tt.delete_contents(trans_id)
717+ removed.add(path)
718+
719+ added = set()
720+ implied_parents = set()
721+ seen = set()
722+ for member in archive_file.getmembers():
723+ if member.type == 'g':
724+ # type 'g' is a header
725+ continue
726+ # Inverse functionality in bzr uses utf-8. We could also
727+ # interpret relative to fs encoding, which would match native
728+ # behaviour better.
729+ relative_path = member.name.decode('utf-8')
730+ if prefix is not None:
731+ relative_path = relative_path[len(prefix)+1:]
732+ relative_path = relative_path.rstrip('/')
733+ if relative_path == '':
734+ continue
735+ if should_ignore(relative_path):
736+ continue
737+ add_implied_parents(implied_parents, relative_path)
738+ trans_id = tt.trans_id_tree_path(relative_path)
739+ added.add(relative_path.rstrip('/'))
740+ path = tree.abspath(relative_path)
741+ if member.name in seen:
742+ if tt.final_kind(trans_id) == 'file':
743+ tt.set_executability(None, trans_id)
744+ tt.cancel_creation(trans_id)
745+ seen.add(member.name)
746+ if member.isreg():
747+ tt.create_file(file_iterator(archive_file.extractfile(member)),
748+ trans_id)
749+ executable = (member.mode & 0111) != 0
750+ tt.set_executability(executable, trans_id)
751+ elif member.isdir():
752+ do_directory(tt, trans_id, tree, relative_path, path)
753+ elif member.issym():
754+ tt.create_symlink(member.linkname, trans_id)
755+ else:
756+ continue
757+ if tt.tree_file_id(trans_id) is None:
758+ name = basename(member.name.rstrip('/'))
759+ file_id = generate_ids.gen_file_id(name)
760+ tt.version_file(file_id, trans_id)
761+
762+ for relative_path in implied_parents.difference(added):
763+ if relative_path == "":
764+ continue
765+ trans_id = tt.trans_id_tree_path(relative_path)
766+ path = tree.abspath(relative_path)
767+ do_directory(tt, trans_id, tree, relative_path, path)
768+ if tt.tree_file_id(trans_id) is None:
769+ tt.version_file(trans_id, trans_id)
770+ added.add(relative_path)
771+
772+ for path in removed.difference(added):
773+ tt.unversion_file(tt.trans_id_tree_path(path))
774+
775+ for conflict in cook_conflicts(resolve_conflicts(tt), tt):
776+ warning(conflict)
777+
778+
779+def do_import(source, tree_directory=None):
780+ """Implementation of import command. Intended for UI only"""
781+ if tree_directory is not None:
782+ try:
783+ tree = WorkingTree.open(tree_directory)
784+ except NotBranchError:
785+ if not os.path.exists(tree_directory):
786+ os.mkdir(tree_directory)
787+ branch = BzrDir.create_branch_convenience(tree_directory)
788+ tree = branch.bzrdir.open_workingtree()
789+ else:
790+ tree = WorkingTree.open_containing('.')[0]
791+ tree.lock_write()
792+ try:
793+ if tree.changes_from(tree.basis_tree()).has_changed():
794+ raise BzrCommandError("Working tree has uncommitted changes.")
795+
796+ try:
797+ archive, external_compressor = get_archive_type(source)
798+ except NotArchiveType:
799+ if file_kind(source) == 'directory':
800+ s = StringIO(source)
801+ s.seek(0)
802+ import_dir(tree, s)
803+ else:
804+ raise BzrCommandError('Unhandled import source')
805+ else:
806+ if archive == 'zip':
807+ import_zip(tree, open_from_url(source))
808+ elif archive == 'tar':
809+ try:
810+ tar_input = open_from_url(source)
811+ if external_compressor == 'bz2':
812+ import bz2
813+ tar_input = StringIO(bz2.decompress(tar_input.read()))
814+ elif external_compressor == 'lzma':
815+ import lzma
816+ tar_input = StringIO(lzma.decompress(tar_input.read()))
817+ except IOError, e:
818+ if e.errno == errno.ENOENT:
819+ raise NoSuchFile(source)
820+ try:
821+ import_tar(tree, tar_input)
822+ finally:
823+ tar_input.close()
824+ finally:
825+ tree.unlock()
826+
827+
828+def get_archive_type(path):
829+ """Return the type of archive and compressor indicated by path name.
830+
831+ Only external compressors are returned, so zip files are only
832+ ('zip', None). .tgz is treated as ('tar', 'gz') and '.tar.xz' is treated
833+ as ('tar', 'lzma').
834+ """
835+ matches = re.match(r'.*\.(zip|tgz|tar(.(gz|bz2|lzma|xz))?)$', path)
836+ if not matches:
837+ raise NotArchiveType(path)
838+ external_compressor = None
839+ if matches.group(3) is not None:
840+ archive = 'tar'
841+ external_compressor = matches.group(3)
842+ if external_compressor == 'xz':
843+ external_compressor = 'lzma'
844+ elif matches.group(1) == 'tgz':
845+ return 'tar', 'gz'
846+ else:
847+ archive = matches.group(1)
848+ return archive, external_compressor
849
850=== modified file 'doc/en/release-notes/brz-3.0.txt'
851--- doc/en/release-notes/brz-3.0.txt 2017-05-30 19:32:13 +0000
852+++ doc/en/release-notes/brz-3.0.txt 2017-05-30 22:09:44 +0000
853@@ -26,9 +26,13 @@
854 ************
855
856 * The 'bisect' plugin is now shipped with bzr. (Jelmer Vernooij)
857+
858 * The 'fastimport' plugin is now bundled with Bazaar.
859 (Jelmer Vernooij)
860
861+ * The 'import' command is now bundled with brz.
862+ Imported from bzrtools by Aaron Bentley. (Jelmer Vernooij, #773241)
863+
864 Improvements
865 ************
866

Subscribers

People subscribed via source and target branches