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