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