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