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 | ||||
Related bugs: |
|
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
The Breezy Bot (the-breezy-bot) wrote : | # |
A commit message must be set
http://
Revision history for this message
The Breezy Bot (the-breezy-bot) wrote : | # |
Running landing tests failed
http://
Revision history for this message
The Breezy Bot (the-breezy-bot) wrote : | # |
Running landing tests failed
http://
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 |
Inclusion seems find to me. Can either do a s/StringIO/ BytesIO/ g now or stick that on my list todo. :)