~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
535.1.1 by Reinhard Tartler
bugfix: make it possible to import upstream sources containing symlinks. solution: implement FileInfo.issym
111
    def issym(self):
112
        if stat.S_ISLNK(self.mode):
113
            self.linkname = os.readlink(self.fullpath)
114
            return True
115
        else:
116
            return False
117
531.2.2 by Charlie Shepherd
Remove all trailing whitespace
118
374 by Aaron Bentley
Start work on import plugin
119
def top_directory(path):
377 by Aaron Bentley
Got import command working
120
    """Return the top directory given in a path."""
374 by Aaron Bentley
Start work on import plugin
121
    dirname = os.path.dirname(path)
122
    last_dirname = dirname
123
    while True:
124
        dirname = os.path.dirname(dirname)
125
        if dirname == '' or dirname == last_dirname:
126
            return last_dirname
127
        last_dirname = dirname
128
129
130
def common_directory(names):
131
    """Determine a single directory prefix from a list of names"""
132
    possible_prefix = None
133
    for name in names:
134
        name_top = top_directory(name)
484 by Aaron Bentley
Get closer to importing directories using the same mechanism as files
135
        if name_top == '':
136
            return None
374 by Aaron Bentley
Start work on import plugin
137
        if possible_prefix is None:
138
            possible_prefix = name_top
139
        else:
140
            if name_top != possible_prefix:
141
                return None
142
    return possible_prefix
143
144
382 by Aaron Bentley
Handle adds and removes efficiently
145
def do_directory(tt, trans_id, tree, relative_path, path):
381 by Aaron Bentley
Handle conflicts and tarfiles that omit directories
146
    if isdir(path) and tree.path2id(relative_path) is not None:
147
        tt.cancel_deletion(trans_id)
148
    else:
149
        tt.create_directory(trans_id)
150
151
152
def add_implied_parents(implied_parents, path):
153
    """Update the set of implied parents from a path"""
154
    parent = os.path.dirname(path)
155
    if parent in implied_parents:
156
        return
157
    implied_parents.add(parent)
158
    add_implied_parents(implied_parents, parent)
159
160
383 by Aaron Bentley
Skip the extended header in Linux tarballs
161
def names_of_files(tar_file):
162
    for member in tar_file.getmembers():
163
        if member.type != "g":
164
            yield member.name
165
166
374 by Aaron Bentley
Start work on import plugin
167
def import_tar(tree, tar_input):
377 by Aaron Bentley
Got import command working
168
    """Replace the contents of a working directory with tarfile contents.
384 by Aaron Bentley
Implement bzip support
169
    The tarfile may be a gzipped stream.  File ids will be updated.
377 by Aaron Bentley
Got import command working
170
    """
374 by Aaron Bentley
Start work on import plugin
171
    tar_file = tarfile.open('lala', 'r', tar_input)
475 by Aaron Bentley
Add zip import support
172
    import_archive(tree, tar_file)
173
174
def import_zip(tree, zip_input):
477 by Aaron Bentley
split out upstream_import test cases
175
    zip_file = ZipFileWrapper(zip_input, 'r')
475 by Aaron Bentley
Add zip import support
176
    import_archive(tree, zip_file)
177
484 by Aaron Bentley
Get closer to importing directories using the same mechanism as files
178
def import_dir(tree, dir_input):
179
    dir_file = DirWrapper(dir_input)
180
    import_archive(tree, dir_file)
181
475 by Aaron Bentley
Add zip import support
182
def import_archive(tree, archive_file):
183
    prefix = common_directory(names_of_files(archive_file))
375 by Aaron Bentley
Correctly extract tarfiles
184
    tt = TreeTransform(tree)
381 by Aaron Bentley
Handle conflicts and tarfiles that omit directories
185
382 by Aaron Bentley
Handle adds and removes efficiently
186
    removed = set()
375 by Aaron Bentley
Correctly extract tarfiles
187
    for path, entry in tree.inventory.iter_entries():
453.1.2 by Aaron Bentley
Handle the fact that roots are included
188
        if entry.parent_id is None:
189
            continue
375 by Aaron Bentley
Correctly extract tarfiles
190
        trans_id = tt.trans_id_tree_path(path)
191
        tt.delete_contents(trans_id)
382 by Aaron Bentley
Handle adds and removes efficiently
192
        removed.add(path)
375 by Aaron Bentley
Correctly extract tarfiles
193
531.2.2 by Charlie Shepherd
Remove all trailing whitespace
194
    added = set()
381 by Aaron Bentley
Handle conflicts and tarfiles that omit directories
195
    implied_parents = set()
385 by Aaron Bentley
Fix double-add bug
196
    seen = set()
475 by Aaron Bentley
Add zip import support
197
    for member in archive_file.getmembers():
383 by Aaron Bentley
Skip the extended header in Linux tarballs
198
        if member.type == 'g':
199
            # type 'g' is a header
200
            continue
531.2.2 by Charlie Shepherd
Remove all trailing whitespace
201
        relative_path = member.name
374 by Aaron Bentley
Start work on import plugin
202
        if prefix is not None:
203
            relative_path = relative_path[len(prefix)+1:]
517.1.3 by Aaron Bentley
Handle broken python tar implementations
204
            relative_path = relative_path.rstrip('/')
375 by Aaron Bentley
Correctly extract tarfiles
205
        if relative_path == '':
206
            continue
381 by Aaron Bentley
Handle conflicts and tarfiles that omit directories
207
        add_implied_parents(implied_parents, relative_path)
375 by Aaron Bentley
Correctly extract tarfiles
208
        trans_id = tt.trans_id_tree_path(relative_path)
382 by Aaron Bentley
Handle adds and removes efficiently
209
        added.add(relative_path.rstrip('/'))
375 by Aaron Bentley
Correctly extract tarfiles
210
        path = tree.abspath(relative_path)
385 by Aaron Bentley
Fix double-add bug
211
        if member.name in seen:
482 by Aaron Bentley
upstream imports honour the execute bit
212
            if tt.final_kind(trans_id) == 'file':
213
                tt.set_executability(None, trans_id)
385 by Aaron Bentley
Fix double-add bug
214
            tt.cancel_creation(trans_id)
215
        seen.add(member.name)
375 by Aaron Bentley
Correctly extract tarfiles
216
        if member.isreg():
531.2.2 by Charlie Shepherd
Remove all trailing whitespace
217
            tt.create_file(file_iterator(archive_file.extractfile(member)),
380 by Aaron Bentley
Got import working decently
218
                           trans_id)
482 by Aaron Bentley
upstream imports honour the execute bit
219
            executable = (member.mode & 0111) != 0
220
            tt.set_executability(executable, trans_id)
375 by Aaron Bentley
Correctly extract tarfiles
221
        elif member.isdir():
382 by Aaron Bentley
Handle adds and removes efficiently
222
            do_directory(tt, trans_id, tree, relative_path, path)
375 by Aaron Bentley
Correctly extract tarfiles
223
        elif member.issym():
224
            tt.create_symlink(member.linkname, trans_id)
482 by Aaron Bentley
upstream imports honour the execute bit
225
        else:
226
            continue
227
        if tt.tree_file_id(trans_id) is None:
228
            name = basename(member.name.rstrip('/'))
229
            file_id = generate_ids.gen_file_id(name)
230
            tt.version_file(file_id, trans_id)
381 by Aaron Bentley
Handle conflicts and tarfiles that omit directories
231
382 by Aaron Bentley
Handle adds and removes efficiently
232
    for relative_path in implied_parents.difference(added):
381 by Aaron Bentley
Handle conflicts and tarfiles that omit directories
233
        if relative_path == "":
234
            continue
235
        trans_id = tt.trans_id_tree_path(relative_path)
236
        path = tree.abspath(relative_path)
382 by Aaron Bentley
Handle adds and removes efficiently
237
        do_directory(tt, trans_id, tree, relative_path, path)
482 by Aaron Bentley
upstream imports honour the execute bit
238
        if tt.tree_file_id(trans_id) is None:
239
            tt.version_file(trans_id, trans_id)
382 by Aaron Bentley
Handle adds and removes efficiently
240
        added.add(relative_path)
381 by Aaron Bentley
Handle conflicts and tarfiles that omit directories
241
482 by Aaron Bentley
upstream imports honour the execute bit
242
    for path in removed.difference(added):
243
        tt.unversion_file(tt.trans_id_tree_path(path))
244
381 by Aaron Bentley
Handle conflicts and tarfiles that omit directories
245
    for conflict in cook_conflicts(resolve_conflicts(tt), tt):
246
        warning(conflict)
375 by Aaron Bentley
Correctly extract tarfiles
247
    tt.apply()
374 by Aaron Bentley
Start work on import plugin
248
377 by Aaron Bentley
Got import command working
249
380 by Aaron Bentley
Got import working decently
250
def do_import(source, tree_directory=None):
377 by Aaron Bentley
Got import command working
251
    """Implementation of import command.  Intended for UI only"""
380 by Aaron Bentley
Got import working decently
252
    if tree_directory is not None:
253
        try:
254
            tree = WorkingTree.open(tree_directory)
255
        except NotBranchError:
256
            if not os.path.exists(tree_directory):
257
                os.mkdir(tree_directory)
258
            branch = BzrDir.create_branch_convenience(tree_directory)
259
            tree = branch.bzrdir.open_workingtree()
260
    else:
261
        tree = WorkingTree.open_containing('.')[0]
377 by Aaron Bentley
Got import command working
262
    tree.lock_write()
263
    try:
423.1.7 by Aaron Bentley
More updates for 0.9
264
        if tree.changes_from(tree.basis_tree()).has_changed():
378 by Aaron Bentley
Check for modified files
265
            raise BzrCommandError("Working tree has uncommitted changes.")
266
531.2.2 by Charlie Shepherd
Remove all trailing whitespace
267
        if (source.endswith('.tar') or source.endswith('.tar.gz') or
384 by Aaron Bentley
Implement bzip support
268
            source.endswith('.tar.bz2')) or source.endswith('.tgz'):
377 by Aaron Bentley
Got import command working
269
            try:
384 by Aaron Bentley
Implement bzip support
270
                if source.endswith('.bz2'):
271
                    tar_input = BZ2File(source, 'r')
272
                    tar_input = StringIO(tar_input.read())
273
                else:
274
                    tar_input = file(source, 'rb')
382 by Aaron Bentley
Handle adds and removes efficiently
275
            except IOError, e:
377 by Aaron Bentley
Got import command working
276
                if e.errno == errno.ENOENT:
277
                    raise NoSuchFile(source)
278
            try:
279
                import_tar(tree, tar_input)
280
            finally:
281
                tar_input.close()
475 by Aaron Bentley
Add zip import support
282
        elif source.endswith('.zip'):
283
            import_zip(tree, open(source, 'rb'))
489 by Aaron Bentley
import now imports directories
284
        elif file_kind(source) == 'directory':
285
            s = StringIO(source)
286
            s.seek(0)
287
            import_dir(tree, s)
475 by Aaron Bentley
Add zip import support
288
        else:
289
            raise BzrCommandError('Unhandled import source')
377 by Aaron Bentley
Got import command working
290
    finally:
291
        tree.unlock()