~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():
453.1.2 by Aaron Bentley
Handle the fact that roots are included
76
        if entry.parent_id is None:
77
            continue
375 by Aaron Bentley
Correctly extract tarfiles
78
        trans_id = tt.trans_id_tree_path(path)
79
        tt.delete_contents(trans_id)
382 by Aaron Bentley
Handle adds and removes efficiently
80
        removed.add(path)
375 by Aaron Bentley
Correctly extract tarfiles
81
382 by Aaron Bentley
Handle adds and removes efficiently
82
    added = set() 
381 by Aaron Bentley
Handle conflicts and tarfiles that omit directories
83
    implied_parents = set()
385 by Aaron Bentley
Fix double-add bug
84
    seen = set()
375 by Aaron Bentley
Correctly extract tarfiles
85
    for member in tar_file.getmembers():
383 by Aaron Bentley
Skip the extended header in Linux tarballs
86
        if member.type == 'g':
87
            # type 'g' is a header
88
            continue
375 by Aaron Bentley
Correctly extract tarfiles
89
        relative_path = member.name 
374 by Aaron Bentley
Start work on import plugin
90
        if prefix is not None:
91
            relative_path = relative_path[len(prefix)+1:]
375 by Aaron Bentley
Correctly extract tarfiles
92
        if relative_path == '':
93
            continue
381 by Aaron Bentley
Handle conflicts and tarfiles that omit directories
94
        add_implied_parents(implied_parents, relative_path)
375 by Aaron Bentley
Correctly extract tarfiles
95
        trans_id = tt.trans_id_tree_path(relative_path)
382 by Aaron Bentley
Handle adds and removes efficiently
96
        added.add(relative_path.rstrip('/'))
375 by Aaron Bentley
Correctly extract tarfiles
97
        path = tree.abspath(relative_path)
385 by Aaron Bentley
Fix double-add bug
98
        if member.name in seen:
99
            tt.cancel_creation(trans_id)
100
        seen.add(member.name)
375 by Aaron Bentley
Correctly extract tarfiles
101
        if member.isreg():
380 by Aaron Bentley
Got import working decently
102
            tt.create_file(file_iterator(tar_file.extractfile(member)), 
103
                           trans_id)
375 by Aaron Bentley
Correctly extract tarfiles
104
        elif member.isdir():
382 by Aaron Bentley
Handle adds and removes efficiently
105
            do_directory(tt, trans_id, tree, relative_path, path)
375 by Aaron Bentley
Correctly extract tarfiles
106
        elif member.issym():
107
            tt.create_symlink(member.linkname, trans_id)
381 by Aaron Bentley
Handle conflicts and tarfiles that omit directories
108
382 by Aaron Bentley
Handle adds and removes efficiently
109
    for relative_path in implied_parents.difference(added):
381 by Aaron Bentley
Handle conflicts and tarfiles that omit directories
110
        if relative_path == "":
111
            continue
112
        trans_id = tt.trans_id_tree_path(relative_path)
113
        path = tree.abspath(relative_path)
382 by Aaron Bentley
Handle adds and removes efficiently
114
        do_directory(tt, trans_id, tree, relative_path, path)
115
        added.add(relative_path)
381 by Aaron Bentley
Handle conflicts and tarfiles that omit directories
116
117
    for conflict in cook_conflicts(resolve_conflicts(tt), tt):
118
        warning(conflict)
375 by Aaron Bentley
Correctly extract tarfiles
119
    tt.apply()
382 by Aaron Bentley
Handle adds and removes efficiently
120
    update_ids(tree, added, removed)
121
122
123
def update_ids(tree, added, removed):
377 by Aaron Bentley
Got import command working
124
    """Make sure that all present files files have file_ids.
125
    """
126
    # XXX detect renames
382 by Aaron Bentley
Handle adds and removes efficiently
127
    new = added.difference(removed)
128
    deleted = removed.difference(added)
129
    tree.add(sorted(new))
130
    tree.remove(sorted(deleted, reverse=True))
374 by Aaron Bentley
Start work on import plugin
131
377 by Aaron Bentley
Got import command working
132
380 by Aaron Bentley
Got import working decently
133
def do_import(source, tree_directory=None):
377 by Aaron Bentley
Got import command working
134
    """Implementation of import command.  Intended for UI only"""
380 by Aaron Bentley
Got import working decently
135
    if tree_directory is not None:
136
        try:
137
            tree = WorkingTree.open(tree_directory)
138
        except NotBranchError:
139
            if not os.path.exists(tree_directory):
140
                os.mkdir(tree_directory)
141
            branch = BzrDir.create_branch_convenience(tree_directory)
142
            tree = branch.bzrdir.open_workingtree()
143
    else:
144
        tree = WorkingTree.open_containing('.')[0]
377 by Aaron Bentley
Got import command working
145
    tree.lock_write()
146
    try:
423.1.7 by Aaron Bentley
More updates for 0.9
147
        if tree.changes_from(tree.basis_tree()).has_changed():
378 by Aaron Bentley
Check for modified files
148
            raise BzrCommandError("Working tree has uncommitted changes.")
149
377 by Aaron Bentley
Got import command working
150
        if (source.endswith('.tar') or source.endswith('.tar.gz') or 
384 by Aaron Bentley
Implement bzip support
151
            source.endswith('.tar.bz2')) or source.endswith('.tgz'):
377 by Aaron Bentley
Got import command working
152
            try:
384 by Aaron Bentley
Implement bzip support
153
                if source.endswith('.bz2'):
154
                    tar_input = BZ2File(source, 'r')
155
                    tar_input = StringIO(tar_input.read())
156
                else:
157
                    tar_input = file(source, 'rb')
382 by Aaron Bentley
Handle adds and removes efficiently
158
            except IOError, e:
377 by Aaron Bentley
Got import command working
159
                if e.errno == errno.ENOENT:
160
                    raise NoSuchFile(source)
161
            try:
162
                import_tar(tree, tar_input)
163
            finally:
164
                tar_input.close()
165
    finally:
166
        tree.unlock()
167
374 by Aaron Bentley
Start work on import plugin
168
class TestImport(TestCaseInTempDir):
169
384 by Aaron Bentley
Implement bzip support
170
    def make_tar(self, mode='w'):
374 by Aaron Bentley
Start work on import plugin
171
        result = StringIO()
384 by Aaron Bentley
Implement bzip support
172
        tar_file = tarfile.open('project-0.1.tar', mode, result)
374 by Aaron Bentley
Start work on import plugin
173
        os.mkdir('project-0.1')
174
        tar_file.add('project-0.1')
379 by Aaron Bentley
Avoid deleting directories when not necessary
175
        os.mkdir('project-0.1/junk')
176
        tar_file.add('project-0.1/junk')
374 by Aaron Bentley
Start work on import plugin
177
        
178
        f = file('project-0.1/README', 'wb')
179
        f.write('What?')
180
        f.close()
181
        tar_file.add('project-0.1/README')
182
183
        f = file('project-0.1/FEEDME', 'wb')
184
        f.write('Hungry!!')
185
        f.close()
186
        tar_file.add('project-0.1/FEEDME')
187
188
        tar_file.close()
189
        rmtree('project-0.1')
190
        result.seek(0)
191
        return result
192
193
    def make_tar2(self):
194
        result = StringIO()
195
        tar_file = tarfile.open('project-0.2.tar', 'w', result)
196
        os.mkdir('project-0.2')
197
        tar_file.add('project-0.2')
198
        
379 by Aaron Bentley
Avoid deleting directories when not necessary
199
        os.mkdir('project-0.2/junk')
200
        tar_file.add('project-0.2/junk')
201
374 by Aaron Bentley
Start work on import plugin
202
        f = file('project-0.2/README', 'wb')
203
        f.write('Now?')
204
        f.close()
205
        tar_file.add('project-0.2/README')
206
        tar_file.close()
385 by Aaron Bentley
Fix double-add bug
207
208
        tar_file = tarfile.open('project-0.2.tar', 'a', result)
209
        tar_file.add('project-0.2/README')
210
374 by Aaron Bentley
Start work on import plugin
211
        rmtree('project-0.2')
212
        return result
213
214
    def make_messed_tar(self):
215
        result = StringIO()
216
        tar_file = tarfile.open('project-0.1.tar', 'w', result)
217
        os.mkdir('project-0.1')
218
        tar_file.add('project-0.1')
219
220
        os.mkdir('project-0.2')
221
        tar_file.add('project-0.2')
222
        
223
        f = file('project-0.1/README', 'wb')
224
        f.write('What?')
225
        f.close()
226
        tar_file.add('project-0.1/README')
227
        tar_file.close()
228
        rmtree('project-0.1')
229
        result.seek(0)
230
        return result
231
232
    def test_top_directory(self):
233
        self.assertEqual(top_directory('ab/b/c'), 'ab')
234
        self.assertEqual(top_directory('/etc'), '/')
235
236
    def test_common_directory(self):
237
        self.assertEqual(common_directory(['ab/c/d', 'ab/c/e']), 'ab')
238
        self.assertIs(common_directory(['ab/c/d', 'ac/c/e']), None)
239
240
    def test_untar(self):
241
        tar_file = self.make_tar()
242
        tree = BzrDir.create_standalone_workingtree('tree')
243
        import_tar(tree, tar_file)
244
        self.assertTrue(tree.path2id('README') is not None) 
245
        self.assertTrue(tree.path2id('FEEDME') is not None)
375 by Aaron Bentley
Correctly extract tarfiles
246
        self.assertTrue(os.path.isfile(tree.abspath('README')))
247
        self.assertEqual(tree.inventory[tree.path2id('README')].kind, 'file')
248
        self.assertEqual(tree.inventory[tree.path2id('FEEDME')].kind, 'file')
374 by Aaron Bentley
Start work on import plugin
249
        
379 by Aaron Bentley
Avoid deleting directories when not necessary
250
        f = file(tree.abspath('junk/food'), 'wb')
251
        f.write('I like food\n')
252
        f.close()
253
374 by Aaron Bentley
Start work on import plugin
254
        tar_file = self.make_tar2()
255
        import_tar(tree, tar_file)
256
        self.assertTrue(tree.path2id('README') is not None) 
375 by Aaron Bentley
Correctly extract tarfiles
257
        self.assertTrue(not os.path.exists(tree.abspath('FEEDME')))
374 by Aaron Bentley
Start work on import plugin
258
259
260
    def test_untar2(self):
261
        tar_file = self.make_messed_tar()
262
        tree = BzrDir.create_standalone_workingtree('tree')
263
        import_tar(tree, tar_file)
264
        self.assertTrue(tree.path2id('project-0.1/README') is not None) 
265
384 by Aaron Bentley
Implement bzip support
266
    def test_untar_gzip(self):
267
        tar_file = self.make_tar(mode='w:gz')
268
        tree = BzrDir.create_standalone_workingtree('tree')
269
        import_tar(tree, tar_file)
270
        self.assertTrue(tree.path2id('README') is not None) 
271
377 by Aaron Bentley
Got import command working
272
374 by Aaron Bentley
Start work on import plugin
273
def test_suite():
274
    return makeSuite(TestImport)