~abentley/bzrtools/bzrtools.dev

374 by Aaron Bentley
Start work on import plugin
1
"""Import upstream source into a branch"""
2
384 by Aaron Bentley
Implement bzip support
3
from bz2 import BZ2File
382 by Aaron Bentley
Handle adds and removes efficiently
4
import errno
374 by Aaron Bentley
Start work on import plugin
5
import os
6
from shutil import rmtree
7
from StringIO import StringIO
8
import tarfile
9
from unittest import makeSuite
475 by Aaron Bentley
Add zip import support
10
import zipfile
374 by Aaron Bentley
Start work on import plugin
11
12
from bzrlib.bzrdir import BzrDir
380 by Aaron Bentley
Got import working decently
13
from bzrlib.errors import NoSuchFile, BzrCommandError, NotBranchError
14
from bzrlib.osutils import pathjoin, isdir, file_iterator
374 by Aaron Bentley
Start work on import plugin
15
from bzrlib.tests import TestCaseInTempDir
381 by Aaron Bentley
Handle conflicts and tarfiles that omit directories
16
from bzrlib.trace import warning
17
from bzrlib.transform import TreeTransform, resolve_conflicts, cook_conflicts
377 by Aaron Bentley
Got import command working
18
from bzrlib.workingtree import WorkingTree
19
475 by Aaron Bentley
Add zip import support
20
class ZipFileWrapper(object):
21
22
    def __init__(self, zipfile):
23
        self.zipfile = zipfile
24
25
    def getmembers(self):
26
        for info in self.zipfile.infolist():
27
            yield ZipInfoWrapper(self.zipfile, info)
28
29
    def extractfile(self, infowrapper):
30
        return StringIO(self.zipfile.read(infowrapper.name))
31
476 by Aaron Bentley
Generalize tests for zip
32
    def add(self, filename):
33
        if isdir(filename):
34
            self.zipfile.writestr(filename+'/', '')
35
        else:
36
            self.zipfile.write(filename)
37
38
39
    def close(self):
40
        self.zipfile.close()
41
475 by Aaron Bentley
Add zip import support
42
43
class ZipInfoWrapper(object):
44
    
45
    def __init__(self, zipfile, info):
46
        self.info = info
47
        self.type = None
48
        self.name = info.filename
49
        self.zipfile = zipfile
50
51
    def isdir(self):
52
        # Really? Eeeew!
53
        return bool(self.name.endswith('/'))
54
55
    def isreg(self):
56
        # Really? Eeeew!
57
        return not self.isdir()
58
374 by Aaron Bentley
Start work on import plugin
59
60
def top_directory(path):
377 by Aaron Bentley
Got import command working
61
    """Return the top directory given in a path."""
374 by Aaron Bentley
Start work on import plugin
62
    dirname = os.path.dirname(path)
63
    last_dirname = dirname
64
    while True:
65
        dirname = os.path.dirname(dirname)
66
        if dirname == '' or dirname == last_dirname:
67
            return last_dirname
68
        last_dirname = dirname
69
70
71
def common_directory(names):
72
    """Determine a single directory prefix from a list of names"""
73
    possible_prefix = None
74
    for name in names:
75
        name_top = top_directory(name)
76
        if possible_prefix is None:
77
            possible_prefix = name_top
78
        else:
79
            if name_top != possible_prefix:
80
                return None
81
    return possible_prefix
82
83
382 by Aaron Bentley
Handle adds and removes efficiently
84
def do_directory(tt, trans_id, tree, relative_path, path):
381 by Aaron Bentley
Handle conflicts and tarfiles that omit directories
85
    if isdir(path) and tree.path2id(relative_path) is not None:
86
        tt.cancel_deletion(trans_id)
87
    else:
88
        tt.create_directory(trans_id)
89
90
91
def add_implied_parents(implied_parents, path):
92
    """Update the set of implied parents from a path"""
93
    parent = os.path.dirname(path)
94
    if parent in implied_parents:
95
        return
96
    implied_parents.add(parent)
97
    add_implied_parents(implied_parents, parent)
98
99
383 by Aaron Bentley
Skip the extended header in Linux tarballs
100
def names_of_files(tar_file):
101
    for member in tar_file.getmembers():
102
        if member.type != "g":
103
            yield member.name
104
105
374 by Aaron Bentley
Start work on import plugin
106
def import_tar(tree, tar_input):
377 by Aaron Bentley
Got import command working
107
    """Replace the contents of a working directory with tarfile contents.
384 by Aaron Bentley
Implement bzip support
108
    The tarfile may be a gzipped stream.  File ids will be updated.
377 by Aaron Bentley
Got import command working
109
    """
374 by Aaron Bentley
Start work on import plugin
110
    tar_file = tarfile.open('lala', 'r', tar_input)
475 by Aaron Bentley
Add zip import support
111
    import_archive(tree, tar_file)
112
113
def import_zip(tree, zip_input):
114
    zip_file = ZipFileWrapper(zipfile.ZipFile(zip_input))
115
    import_archive(tree, zip_file)
116
117
def import_archive(tree, archive_file):
118
    prefix = common_directory(names_of_files(archive_file))
375 by Aaron Bentley
Correctly extract tarfiles
119
    tt = TreeTransform(tree)
381 by Aaron Bentley
Handle conflicts and tarfiles that omit directories
120
382 by Aaron Bentley
Handle adds and removes efficiently
121
    removed = set()
375 by Aaron Bentley
Correctly extract tarfiles
122
    for path, entry in tree.inventory.iter_entries():
453.1.2 by Aaron Bentley
Handle the fact that roots are included
123
        if entry.parent_id is None:
124
            continue
375 by Aaron Bentley
Correctly extract tarfiles
125
        trans_id = tt.trans_id_tree_path(path)
126
        tt.delete_contents(trans_id)
382 by Aaron Bentley
Handle adds and removes efficiently
127
        removed.add(path)
375 by Aaron Bentley
Correctly extract tarfiles
128
382 by Aaron Bentley
Handle adds and removes efficiently
129
    added = set() 
381 by Aaron Bentley
Handle conflicts and tarfiles that omit directories
130
    implied_parents = set()
385 by Aaron Bentley
Fix double-add bug
131
    seen = set()
475 by Aaron Bentley
Add zip import support
132
    for member in archive_file.getmembers():
383 by Aaron Bentley
Skip the extended header in Linux tarballs
133
        if member.type == 'g':
134
            # type 'g' is a header
135
            continue
375 by Aaron Bentley
Correctly extract tarfiles
136
        relative_path = member.name 
374 by Aaron Bentley
Start work on import plugin
137
        if prefix is not None:
138
            relative_path = relative_path[len(prefix)+1:]
375 by Aaron Bentley
Correctly extract tarfiles
139
        if relative_path == '':
140
            continue
381 by Aaron Bentley
Handle conflicts and tarfiles that omit directories
141
        add_implied_parents(implied_parents, relative_path)
375 by Aaron Bentley
Correctly extract tarfiles
142
        trans_id = tt.trans_id_tree_path(relative_path)
382 by Aaron Bentley
Handle adds and removes efficiently
143
        added.add(relative_path.rstrip('/'))
375 by Aaron Bentley
Correctly extract tarfiles
144
        path = tree.abspath(relative_path)
385 by Aaron Bentley
Fix double-add bug
145
        if member.name in seen:
146
            tt.cancel_creation(trans_id)
147
        seen.add(member.name)
375 by Aaron Bentley
Correctly extract tarfiles
148
        if member.isreg():
475 by Aaron Bentley
Add zip import support
149
            tt.create_file(file_iterator(archive_file.extractfile(member)), 
380 by Aaron Bentley
Got import working decently
150
                           trans_id)
375 by Aaron Bentley
Correctly extract tarfiles
151
        elif member.isdir():
382 by Aaron Bentley
Handle adds and removes efficiently
152
            do_directory(tt, trans_id, tree, relative_path, path)
375 by Aaron Bentley
Correctly extract tarfiles
153
        elif member.issym():
154
            tt.create_symlink(member.linkname, trans_id)
381 by Aaron Bentley
Handle conflicts and tarfiles that omit directories
155
382 by Aaron Bentley
Handle adds and removes efficiently
156
    for relative_path in implied_parents.difference(added):
381 by Aaron Bentley
Handle conflicts and tarfiles that omit directories
157
        if relative_path == "":
158
            continue
159
        trans_id = tt.trans_id_tree_path(relative_path)
160
        path = tree.abspath(relative_path)
382 by Aaron Bentley
Handle adds and removes efficiently
161
        do_directory(tt, trans_id, tree, relative_path, path)
162
        added.add(relative_path)
381 by Aaron Bentley
Handle conflicts and tarfiles that omit directories
163
164
    for conflict in cook_conflicts(resolve_conflicts(tt), tt):
165
        warning(conflict)
375 by Aaron Bentley
Correctly extract tarfiles
166
    tt.apply()
382 by Aaron Bentley
Handle adds and removes efficiently
167
    update_ids(tree, added, removed)
168
169
170
def update_ids(tree, added, removed):
377 by Aaron Bentley
Got import command working
171
    """Make sure that all present files files have file_ids.
172
    """
173
    # XXX detect renames
382 by Aaron Bentley
Handle adds and removes efficiently
174
    new = added.difference(removed)
175
    deleted = removed.difference(added)
176
    tree.add(sorted(new))
177
    tree.remove(sorted(deleted, reverse=True))
374 by Aaron Bentley
Start work on import plugin
178
377 by Aaron Bentley
Got import command working
179
380 by Aaron Bentley
Got import working decently
180
def do_import(source, tree_directory=None):
377 by Aaron Bentley
Got import command working
181
    """Implementation of import command.  Intended for UI only"""
380 by Aaron Bentley
Got import working decently
182
    if tree_directory is not None:
183
        try:
184
            tree = WorkingTree.open(tree_directory)
185
        except NotBranchError:
186
            if not os.path.exists(tree_directory):
187
                os.mkdir(tree_directory)
188
            branch = BzrDir.create_branch_convenience(tree_directory)
189
            tree = branch.bzrdir.open_workingtree()
190
    else:
191
        tree = WorkingTree.open_containing('.')[0]
377 by Aaron Bentley
Got import command working
192
    tree.lock_write()
193
    try:
423.1.7 by Aaron Bentley
More updates for 0.9
194
        if tree.changes_from(tree.basis_tree()).has_changed():
378 by Aaron Bentley
Check for modified files
195
            raise BzrCommandError("Working tree has uncommitted changes.")
196
377 by Aaron Bentley
Got import command working
197
        if (source.endswith('.tar') or source.endswith('.tar.gz') or 
384 by Aaron Bentley
Implement bzip support
198
            source.endswith('.tar.bz2')) or source.endswith('.tgz'):
377 by Aaron Bentley
Got import command working
199
            try:
384 by Aaron Bentley
Implement bzip support
200
                if source.endswith('.bz2'):
201
                    tar_input = BZ2File(source, 'r')
202
                    tar_input = StringIO(tar_input.read())
203
                else:
204
                    tar_input = file(source, 'rb')
382 by Aaron Bentley
Handle adds and removes efficiently
205
            except IOError, e:
377 by Aaron Bentley
Got import command working
206
                if e.errno == errno.ENOENT:
207
                    raise NoSuchFile(source)
208
            try:
209
                import_tar(tree, tar_input)
210
            finally:
211
                tar_input.close()
475 by Aaron Bentley
Add zip import support
212
        elif source.endswith('.zip'):
213
            import_zip(tree, open(source, 'rb'))
214
        else:
215
            raise BzrCommandError('Unhandled import source')
377 by Aaron Bentley
Got import command working
216
    finally:
217
        tree.unlock()
218
374 by Aaron Bentley
Start work on import plugin
219
class TestImport(TestCaseInTempDir):
220
384 by Aaron Bentley
Implement bzip support
221
    def make_tar(self, mode='w'):
476 by Aaron Bentley
Generalize tests for zip
222
        def maker(fileobj):
223
            return tarfile.open('project-0.1.tar', mode, fileobj)
224
        return self.make_archive(maker)
225
226
    def make_archive(self, maker):
374 by Aaron Bentley
Start work on import plugin
227
        result = StringIO()
476 by Aaron Bentley
Generalize tests for zip
228
        archive_file = maker(result)
374 by Aaron Bentley
Start work on import plugin
229
        os.mkdir('project-0.1')
476 by Aaron Bentley
Generalize tests for zip
230
        archive_file.add('project-0.1')
379 by Aaron Bentley
Avoid deleting directories when not necessary
231
        os.mkdir('project-0.1/junk')
476 by Aaron Bentley
Generalize tests for zip
232
        archive_file.add('project-0.1/junk')
374 by Aaron Bentley
Start work on import plugin
233
        
234
        f = file('project-0.1/README', 'wb')
235
        f.write('What?')
236
        f.close()
476 by Aaron Bentley
Generalize tests for zip
237
        archive_file.add('project-0.1/README')
374 by Aaron Bentley
Start work on import plugin
238
239
        f = file('project-0.1/FEEDME', 'wb')
240
        f.write('Hungry!!')
241
        f.close()
476 by Aaron Bentley
Generalize tests for zip
242
        archive_file.add('project-0.1/FEEDME')
374 by Aaron Bentley
Start work on import plugin
243
476 by Aaron Bentley
Generalize tests for zip
244
        archive_file.close()
374 by Aaron Bentley
Start work on import plugin
245
        rmtree('project-0.1')
246
        result.seek(0)
247
        return result
248
249
    def make_tar2(self):
250
        result = StringIO()
251
        tar_file = tarfile.open('project-0.2.tar', 'w', result)
252
        os.mkdir('project-0.2')
253
        tar_file.add('project-0.2')
254
        
379 by Aaron Bentley
Avoid deleting directories when not necessary
255
        os.mkdir('project-0.2/junk')
256
        tar_file.add('project-0.2/junk')
257
374 by Aaron Bentley
Start work on import plugin
258
        f = file('project-0.2/README', 'wb')
259
        f.write('Now?')
260
        f.close()
261
        tar_file.add('project-0.2/README')
262
        tar_file.close()
385 by Aaron Bentley
Fix double-add bug
263
264
        tar_file = tarfile.open('project-0.2.tar', 'a', result)
265
        tar_file.add('project-0.2/README')
266
374 by Aaron Bentley
Start work on import plugin
267
        rmtree('project-0.2')
268
        return result
269
270
    def make_messed_tar(self):
271
        result = StringIO()
272
        tar_file = tarfile.open('project-0.1.tar', 'w', result)
273
        os.mkdir('project-0.1')
274
        tar_file.add('project-0.1')
275
276
        os.mkdir('project-0.2')
277
        tar_file.add('project-0.2')
278
        
279
        f = file('project-0.1/README', 'wb')
280
        f.write('What?')
281
        f.close()
282
        tar_file.add('project-0.1/README')
283
        tar_file.close()
284
        rmtree('project-0.1')
285
        result.seek(0)
286
        return result
287
476 by Aaron Bentley
Generalize tests for zip
288
    def make_zip(self):
289
        def maker(fileobj):
290
            return ZipFileWrapper(zipfile.ZipFile(fileobj, 'w'))
291
        return self.make_archive(maker)
475 by Aaron Bentley
Add zip import support
292
374 by Aaron Bentley
Start work on import plugin
293
    def test_top_directory(self):
294
        self.assertEqual(top_directory('ab/b/c'), 'ab')
295
        self.assertEqual(top_directory('/etc'), '/')
296
297
    def test_common_directory(self):
298
        self.assertEqual(common_directory(['ab/c/d', 'ab/c/e']), 'ab')
299
        self.assertIs(common_directory(['ab/c/d', 'ac/c/e']), None)
300
301
    def test_untar(self):
302
        tar_file = self.make_tar()
303
        tree = BzrDir.create_standalone_workingtree('tree')
304
        import_tar(tree, tar_file)
305
        self.assertTrue(tree.path2id('README') is not None) 
306
        self.assertTrue(tree.path2id('FEEDME') is not None)
375 by Aaron Bentley
Correctly extract tarfiles
307
        self.assertTrue(os.path.isfile(tree.abspath('README')))
308
        self.assertEqual(tree.inventory[tree.path2id('README')].kind, 'file')
309
        self.assertEqual(tree.inventory[tree.path2id('FEEDME')].kind, 'file')
374 by Aaron Bentley
Start work on import plugin
310
        
379 by Aaron Bentley
Avoid deleting directories when not necessary
311
        f = file(tree.abspath('junk/food'), 'wb')
312
        f.write('I like food\n')
313
        f.close()
314
374 by Aaron Bentley
Start work on import plugin
315
        tar_file = self.make_tar2()
316
        import_tar(tree, tar_file)
317
        self.assertTrue(tree.path2id('README') is not None) 
375 by Aaron Bentley
Correctly extract tarfiles
318
        self.assertTrue(not os.path.exists(tree.abspath('FEEDME')))
374 by Aaron Bentley
Start work on import plugin
319
320
321
    def test_untar2(self):
322
        tar_file = self.make_messed_tar()
323
        tree = BzrDir.create_standalone_workingtree('tree')
324
        import_tar(tree, tar_file)
325
        self.assertTrue(tree.path2id('project-0.1/README') is not None) 
326
384 by Aaron Bentley
Implement bzip support
327
    def test_untar_gzip(self):
328
        tar_file = self.make_tar(mode='w:gz')
329
        tree = BzrDir.create_standalone_workingtree('tree')
330
        import_tar(tree, tar_file)
331
        self.assertTrue(tree.path2id('README') is not None) 
332
475 by Aaron Bentley
Add zip import support
333
    def test_unzip(self):
334
        zip_file = self.make_zip()
335
        tree = BzrDir.create_standalone_workingtree('tree')
336
        import_zip(tree, zip_file)
337
        self.assertTrue(tree.path2id('README') is not None) 
338
        self.assertTrue(tree.path2id('FEEDME') is not None)
339
        self.assertTrue(os.path.isfile(tree.abspath('README')))
340
        self.assertEqual(tree.inventory[tree.path2id('README')].kind, 'file')
341
        self.assertEqual(tree.inventory[tree.path2id('FEEDME')].kind, 'file')
342
        
343
        f = file(tree.abspath('junk/food'), 'wb')
344
        f.write('I like food\n')
345
        f.close()
346
377 by Aaron Bentley
Got import command working
347
374 by Aaron Bentley
Start work on import plugin
348
def test_suite():
349
    return makeSuite(TestImport)