~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 StringIO import StringIO
484 by Aaron Bentley
Get closer to importing directories using the same mechanism as files
7
import stat
374 by Aaron Bentley
Start work on import plugin
8
import tarfile
475 by Aaron Bentley
Add zip import support
9
import zipfile
374 by Aaron Bentley
Start work on import plugin
10
482 by Aaron Bentley
upstream imports honour the execute bit
11
from bzrlib import generate_ids
374 by Aaron Bentley
Start work on import plugin
12
from bzrlib.bzrdir import BzrDir
380 by Aaron Bentley
Got import working decently
13
from bzrlib.errors import NoSuchFile, BzrCommandError, NotBranchError
489 by Aaron Bentley
import now imports directories
14
from bzrlib.osutils import (pathjoin, isdir, file_iterator, basename,
15
                            file_kind)
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
477 by Aaron Bentley
split out upstream_import test cases
22
    def __init__(self, fileobj, mode):
23
        self.zipfile = zipfile.ZipFile(fileobj, mode)
475 by Aaron Bentley
Add zip import support
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
    def close(self):
39
        self.zipfile.close()
40
475 by Aaron Bentley
Add zip import support
41
42
class ZipInfoWrapper(object):
531.2.2 by Charlie Shepherd
Remove all trailing whitespace
43
475 by Aaron Bentley
Add zip import support
44
    def __init__(self, zipfile, info):
45
        self.info = info
46
        self.type = None
47
        self.name = info.filename
48
        self.zipfile = zipfile
482 by Aaron Bentley
upstream imports honour the execute bit
49
        self.mode = 0666
475 by Aaron Bentley
Add zip import support
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
484 by Aaron Bentley
Get closer to importing directories using the same mechanism as files
60
class DirWrapper(object):
61
    def __init__(self, fileobj, mode='r'):
62
        assert mode == 'r', mode
63
        self.root = os.path.realpath(fileobj.read())
64
488 by Aaron Bentley
Fix tests for importing directories
65
    def __repr__(self):
66
        return 'DirWrapper(%r)' % self.root
67
484 by Aaron Bentley
Get closer to importing directories using the same mechanism as files
68
    def getmembers(self, subdir=None):
69
        if subdir is not None:
70
            mydir = pathjoin(self.root, subdir)
71
        else:
72
            mydir = self.root
73
        for child in os.listdir(mydir):
74
            if subdir is not None:
75
                child = pathjoin(subdir, child)
76
            fi = FileInfo(self.root, child)
77
            yield fi
78
            if fi.isdir():
79
                for v in self.getmembers(child):
80
                    yield v
81
82
    def extractfile(self, member):
83
        return open(member.fullpath)
84
85
86
class FileInfo(object):
87
88
    def __init__(self, root, filepath):
89
        self.fullpath = pathjoin(root, filepath)
90
        self.root = root
489 by Aaron Bentley
import now imports directories
91
        if filepath != '':
92
            self.name = pathjoin(basename(root), filepath)
93
        else:
94
            print 'root %r' % root
95
            self.name = basename(root)
484 by Aaron Bentley
Get closer to importing directories using the same mechanism as files
96
        self.type = None
97
        stat = os.lstat(self.fullpath)
98
        self.mode = stat.st_mode
99
        if self.isdir():
100
            self.name += '/'
101
488 by Aaron Bentley
Fix tests for importing directories
102
    def __repr__(self):
103
        return 'FileInfo(%r)' % self.name
104
484 by Aaron Bentley
Get closer to importing directories using the same mechanism as files
105
    def isreg(self):
106
        return stat.S_ISREG(self.mode)
107
108
    def isdir(self):
109
        return stat.S_ISDIR(self.mode)
110
531.2.2 by Charlie Shepherd
Remove all trailing whitespace
111
374 by Aaron Bentley
Start work on import plugin
112
def top_directory(path):
377 by Aaron Bentley
Got import command working
113
    """Return the top directory given in a path."""
374 by Aaron Bentley
Start work on import plugin
114
    dirname = os.path.dirname(path)
115
    last_dirname = dirname
116
    while True:
117
        dirname = os.path.dirname(dirname)
118
        if dirname == '' or dirname == last_dirname:
119
            return last_dirname
120
        last_dirname = dirname
121
122
123
def common_directory(names):
124
    """Determine a single directory prefix from a list of names"""
125
    possible_prefix = None
126
    for name in names:
127
        name_top = top_directory(name)
484 by Aaron Bentley
Get closer to importing directories using the same mechanism as files
128
        if name_top == '':
129
            return None
374 by Aaron Bentley
Start work on import plugin
130
        if possible_prefix is None:
131
            possible_prefix = name_top
132
        else:
133
            if name_top != possible_prefix:
134
                return None
135
    return possible_prefix
136
137
382 by Aaron Bentley
Handle adds and removes efficiently
138
def do_directory(tt, trans_id, tree, relative_path, path):
381 by Aaron Bentley
Handle conflicts and tarfiles that omit directories
139
    if isdir(path) and tree.path2id(relative_path) is not None:
140
        tt.cancel_deletion(trans_id)
141
    else:
142
        tt.create_directory(trans_id)
143
144
145
def add_implied_parents(implied_parents, path):
146
    """Update the set of implied parents from a path"""
147
    parent = os.path.dirname(path)
148
    if parent in implied_parents:
149
        return
150
    implied_parents.add(parent)
151
    add_implied_parents(implied_parents, parent)
152
153
383 by Aaron Bentley
Skip the extended header in Linux tarballs
154
def names_of_files(tar_file):
155
    for member in tar_file.getmembers():
156
        if member.type != "g":
157
            yield member.name
158
159
374 by Aaron Bentley
Start work on import plugin
160
def import_tar(tree, tar_input):
377 by Aaron Bentley
Got import command working
161
    """Replace the contents of a working directory with tarfile contents.
384 by Aaron Bentley
Implement bzip support
162
    The tarfile may be a gzipped stream.  File ids will be updated.
377 by Aaron Bentley
Got import command working
163
    """
374 by Aaron Bentley
Start work on import plugin
164
    tar_file = tarfile.open('lala', 'r', tar_input)
475 by Aaron Bentley
Add zip import support
165
    import_archive(tree, tar_file)
166
167
def import_zip(tree, zip_input):
477 by Aaron Bentley
split out upstream_import test cases
168
    zip_file = ZipFileWrapper(zip_input, 'r')
475 by Aaron Bentley
Add zip import support
169
    import_archive(tree, zip_file)
170
484 by Aaron Bentley
Get closer to importing directories using the same mechanism as files
171
def import_dir(tree, dir_input):
172
    dir_file = DirWrapper(dir_input)
173
    import_archive(tree, dir_file)
174
475 by Aaron Bentley
Add zip import support
175
def import_archive(tree, archive_file):
176
    prefix = common_directory(names_of_files(archive_file))
375 by Aaron Bentley
Correctly extract tarfiles
177
    tt = TreeTransform(tree)
381 by Aaron Bentley
Handle conflicts and tarfiles that omit directories
178
382 by Aaron Bentley
Handle adds and removes efficiently
179
    removed = set()
375 by Aaron Bentley
Correctly extract tarfiles
180
    for path, entry in tree.inventory.iter_entries():
453.1.2 by Aaron Bentley
Handle the fact that roots are included
181
        if entry.parent_id is None:
182
            continue
375 by Aaron Bentley
Correctly extract tarfiles
183
        trans_id = tt.trans_id_tree_path(path)
184
        tt.delete_contents(trans_id)
382 by Aaron Bentley
Handle adds and removes efficiently
185
        removed.add(path)
375 by Aaron Bentley
Correctly extract tarfiles
186
531.2.2 by Charlie Shepherd
Remove all trailing whitespace
187
    added = set()
381 by Aaron Bentley
Handle conflicts and tarfiles that omit directories
188
    implied_parents = set()
385 by Aaron Bentley
Fix double-add bug
189
    seen = set()
475 by Aaron Bentley
Add zip import support
190
    for member in archive_file.getmembers():
383 by Aaron Bentley
Skip the extended header in Linux tarballs
191
        if member.type == 'g':
192
            # type 'g' is a header
193
            continue
531.2.2 by Charlie Shepherd
Remove all trailing whitespace
194
        relative_path = member.name
374 by Aaron Bentley
Start work on import plugin
195
        if prefix is not None:
196
            relative_path = relative_path[len(prefix)+1:]
517.1.3 by Aaron Bentley
Handle broken python tar implementations
197
            relative_path = relative_path.rstrip('/')
375 by Aaron Bentley
Correctly extract tarfiles
198
        if relative_path == '':
199
            continue
381 by Aaron Bentley
Handle conflicts and tarfiles that omit directories
200
        add_implied_parents(implied_parents, relative_path)
375 by Aaron Bentley
Correctly extract tarfiles
201
        trans_id = tt.trans_id_tree_path(relative_path)
382 by Aaron Bentley
Handle adds and removes efficiently
202
        added.add(relative_path.rstrip('/'))
375 by Aaron Bentley
Correctly extract tarfiles
203
        path = tree.abspath(relative_path)
385 by Aaron Bentley
Fix double-add bug
204
        if member.name in seen:
482 by Aaron Bentley
upstream imports honour the execute bit
205
            if tt.final_kind(trans_id) == 'file':
206
                tt.set_executability(None, trans_id)
385 by Aaron Bentley
Fix double-add bug
207
            tt.cancel_creation(trans_id)
208
        seen.add(member.name)
375 by Aaron Bentley
Correctly extract tarfiles
209
        if member.isreg():
531.2.2 by Charlie Shepherd
Remove all trailing whitespace
210
            tt.create_file(file_iterator(archive_file.extractfile(member)),
380 by Aaron Bentley
Got import working decently
211
                           trans_id)
482 by Aaron Bentley
upstream imports honour the execute bit
212
            executable = (member.mode & 0111) != 0
213
            tt.set_executability(executable, trans_id)
375 by Aaron Bentley
Correctly extract tarfiles
214
        elif member.isdir():
382 by Aaron Bentley
Handle adds and removes efficiently
215
            do_directory(tt, trans_id, tree, relative_path, path)
375 by Aaron Bentley
Correctly extract tarfiles
216
        elif member.issym():
217
            tt.create_symlink(member.linkname, trans_id)
482 by Aaron Bentley
upstream imports honour the execute bit
218
        else:
219
            continue
220
        if tt.tree_file_id(trans_id) is None:
221
            name = basename(member.name.rstrip('/'))
222
            file_id = generate_ids.gen_file_id(name)
223
            tt.version_file(file_id, trans_id)
381 by Aaron Bentley
Handle conflicts and tarfiles that omit directories
224
382 by Aaron Bentley
Handle adds and removes efficiently
225
    for relative_path in implied_parents.difference(added):
381 by Aaron Bentley
Handle conflicts and tarfiles that omit directories
226
        if relative_path == "":
227
            continue
228
        trans_id = tt.trans_id_tree_path(relative_path)
229
        path = tree.abspath(relative_path)
382 by Aaron Bentley
Handle adds and removes efficiently
230
        do_directory(tt, trans_id, tree, relative_path, path)
482 by Aaron Bentley
upstream imports honour the execute bit
231
        if tt.tree_file_id(trans_id) is None:
232
            tt.version_file(trans_id, trans_id)
382 by Aaron Bentley
Handle adds and removes efficiently
233
        added.add(relative_path)
381 by Aaron Bentley
Handle conflicts and tarfiles that omit directories
234
482 by Aaron Bentley
upstream imports honour the execute bit
235
    for path in removed.difference(added):
236
        tt.unversion_file(tt.trans_id_tree_path(path))
237
381 by Aaron Bentley
Handle conflicts and tarfiles that omit directories
238
    for conflict in cook_conflicts(resolve_conflicts(tt), tt):
239
        warning(conflict)
375 by Aaron Bentley
Correctly extract tarfiles
240
    tt.apply()
374 by Aaron Bentley
Start work on import plugin
241
377 by Aaron Bentley
Got import command working
242
380 by Aaron Bentley
Got import working decently
243
def do_import(source, tree_directory=None):
377 by Aaron Bentley
Got import command working
244
    """Implementation of import command.  Intended for UI only"""
380 by Aaron Bentley
Got import working decently
245
    if tree_directory is not None:
246
        try:
247
            tree = WorkingTree.open(tree_directory)
248
        except NotBranchError:
249
            if not os.path.exists(tree_directory):
250
                os.mkdir(tree_directory)
251
            branch = BzrDir.create_branch_convenience(tree_directory)
252
            tree = branch.bzrdir.open_workingtree()
253
    else:
254
        tree = WorkingTree.open_containing('.')[0]
377 by Aaron Bentley
Got import command working
255
    tree.lock_write()
256
    try:
423.1.7 by Aaron Bentley
More updates for 0.9
257
        if tree.changes_from(tree.basis_tree()).has_changed():
378 by Aaron Bentley
Check for modified files
258
            raise BzrCommandError("Working tree has uncommitted changes.")
259
531.2.2 by Charlie Shepherd
Remove all trailing whitespace
260
        if (source.endswith('.tar') or source.endswith('.tar.gz') or
384 by Aaron Bentley
Implement bzip support
261
            source.endswith('.tar.bz2')) or source.endswith('.tgz'):
377 by Aaron Bentley
Got import command working
262
            try:
384 by Aaron Bentley
Implement bzip support
263
                if source.endswith('.bz2'):
264
                    tar_input = BZ2File(source, 'r')
265
                    tar_input = StringIO(tar_input.read())
266
                else:
267
                    tar_input = file(source, 'rb')
382 by Aaron Bentley
Handle adds and removes efficiently
268
            except IOError, e:
377 by Aaron Bentley
Got import command working
269
                if e.errno == errno.ENOENT:
270
                    raise NoSuchFile(source)
271
            try:
272
                import_tar(tree, tar_input)
273
            finally:
274
                tar_input.close()
475 by Aaron Bentley
Add zip import support
275
        elif source.endswith('.zip'):
276
            import_zip(tree, open(source, 'rb'))
489 by Aaron Bentley
import now imports directories
277
        elif file_kind(source) == 'directory':
278
            s = StringIO(source)
279
            s.seek(0)
280
            import_dir(tree, s)
475 by Aaron Bentley
Add zip import support
281
        else:
282
            raise BzrCommandError('Unhandled import source')
377 by Aaron Bentley
Got import command working
283
    finally:
284
        tree.unlock()