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