~abentley/bzrtools/bzrtools.dev

374 by Aaron Bentley
Start work on import plugin
1
"""Import upstream source into a branch"""
2
382 by Aaron Bentley
Handle adds and removes efficiently
3
import errno
374 by Aaron Bentley
Start work on import plugin
4
import os
768 by Aaron Bentley
Fix non-ascii tarball handling
5
import sys
374 by Aaron Bentley
Start work on import plugin
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
768 by Aaron Bentley
Fix non-ascii tarball handling
185
475 by Aaron Bentley
Add zip import support
186
def import_archive(tree, archive_file):
768 by Aaron Bentley
Fix non-ascii tarball handling
187
    tt = TreeTransform(tree)
188
    try:
189
        import_archive_to_transform(tree, archive_file, tt)
190
        tt.apply()
191
    finally:
192
        tt.finalize()
193
194
195
def import_archive_to_transform(tree, archive_file, tt):
475 by Aaron Bentley
Add zip import support
196
    prefix = common_directory(names_of_files(archive_file))
382 by Aaron Bentley
Handle adds and removes efficiently
197
    removed = set()
375 by Aaron Bentley
Correctly extract tarfiles
198
    for path, entry in tree.inventory.iter_entries():
453.1.2 by Aaron Bentley
Handle the fact that roots are included
199
        if entry.parent_id is None:
200
            continue
375 by Aaron Bentley
Correctly extract tarfiles
201
        trans_id = tt.trans_id_tree_path(path)
202
        tt.delete_contents(trans_id)
382 by Aaron Bentley
Handle adds and removes efficiently
203
        removed.add(path)
375 by Aaron Bentley
Correctly extract tarfiles
204
531.2.2 by Charlie Shepherd
Remove all trailing whitespace
205
    added = set()
381 by Aaron Bentley
Handle conflicts and tarfiles that omit directories
206
    implied_parents = set()
385 by Aaron Bentley
Fix double-add bug
207
    seen = set()
475 by Aaron Bentley
Add zip import support
208
    for member in archive_file.getmembers():
383 by Aaron Bentley
Skip the extended header in Linux tarballs
209
        if member.type == 'g':
210
            # type 'g' is a header
211
            continue
768 by Aaron Bentley
Fix non-ascii tarball handling
212
        # Inverse functionality in bzr uses utf-8.  We could also
213
        # interpret relative to fs encoding, which would match native
214
        # behaviour better.
215
        relative_path = member.name.decode('utf-8')
374 by Aaron Bentley
Start work on import plugin
216
        if prefix is not None:
217
            relative_path = relative_path[len(prefix)+1:]
517.1.3 by Aaron Bentley
Handle broken python tar implementations
218
            relative_path = relative_path.rstrip('/')
375 by Aaron Bentley
Correctly extract tarfiles
219
        if relative_path == '':
220
            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.
221
        if should_ignore(relative_path):
222
            continue
381 by Aaron Bentley
Handle conflicts and tarfiles that omit directories
223
        add_implied_parents(implied_parents, relative_path)
375 by Aaron Bentley
Correctly extract tarfiles
224
        trans_id = tt.trans_id_tree_path(relative_path)
382 by Aaron Bentley
Handle adds and removes efficiently
225
        added.add(relative_path.rstrip('/'))
375 by Aaron Bentley
Correctly extract tarfiles
226
        path = tree.abspath(relative_path)
385 by Aaron Bentley
Fix double-add bug
227
        if member.name in seen:
482 by Aaron Bentley
upstream imports honour the execute bit
228
            if tt.final_kind(trans_id) == 'file':
229
                tt.set_executability(None, trans_id)
385 by Aaron Bentley
Fix double-add bug
230
            tt.cancel_creation(trans_id)
231
        seen.add(member.name)
375 by Aaron Bentley
Correctly extract tarfiles
232
        if member.isreg():
531.2.2 by Charlie Shepherd
Remove all trailing whitespace
233
            tt.create_file(file_iterator(archive_file.extractfile(member)),
380 by Aaron Bentley
Got import working decently
234
                           trans_id)
482 by Aaron Bentley
upstream imports honour the execute bit
235
            executable = (member.mode & 0111) != 0
236
            tt.set_executability(executable, trans_id)
375 by Aaron Bentley
Correctly extract tarfiles
237
        elif member.isdir():
382 by Aaron Bentley
Handle adds and removes efficiently
238
            do_directory(tt, trans_id, tree, relative_path, path)
375 by Aaron Bentley
Correctly extract tarfiles
239
        elif member.issym():
240
            tt.create_symlink(member.linkname, trans_id)
482 by Aaron Bentley
upstream imports honour the execute bit
241
        else:
242
            continue
243
        if tt.tree_file_id(trans_id) is None:
244
            name = basename(member.name.rstrip('/'))
245
            file_id = generate_ids.gen_file_id(name)
246
            tt.version_file(file_id, trans_id)
381 by Aaron Bentley
Handle conflicts and tarfiles that omit directories
247
382 by Aaron Bentley
Handle adds and removes efficiently
248
    for relative_path in implied_parents.difference(added):
381 by Aaron Bentley
Handle conflicts and tarfiles that omit directories
249
        if relative_path == "":
250
            continue
251
        trans_id = tt.trans_id_tree_path(relative_path)
252
        path = tree.abspath(relative_path)
382 by Aaron Bentley
Handle adds and removes efficiently
253
        do_directory(tt, trans_id, tree, relative_path, path)
482 by Aaron Bentley
upstream imports honour the execute bit
254
        if tt.tree_file_id(trans_id) is None:
255
            tt.version_file(trans_id, trans_id)
382 by Aaron Bentley
Handle adds and removes efficiently
256
        added.add(relative_path)
381 by Aaron Bentley
Handle conflicts and tarfiles that omit directories
257
482 by Aaron Bentley
upstream imports honour the execute bit
258
    for path in removed.difference(added):
259
        tt.unversion_file(tt.trans_id_tree_path(path))
260
381 by Aaron Bentley
Handle conflicts and tarfiles that omit directories
261
    for conflict in cook_conflicts(resolve_conflicts(tt), tt):
262
        warning(conflict)
374 by Aaron Bentley
Start work on import plugin
263
377 by Aaron Bentley
Got import command working
264
380 by Aaron Bentley
Got import working decently
265
def do_import(source, tree_directory=None):
377 by Aaron Bentley
Got import command working
266
    """Implementation of import command.  Intended for UI only"""
380 by Aaron Bentley
Got import working decently
267
    if tree_directory is not None:
268
        try:
269
            tree = WorkingTree.open(tree_directory)
270
        except NotBranchError:
271
            if not os.path.exists(tree_directory):
272
                os.mkdir(tree_directory)
273
            branch = BzrDir.create_branch_convenience(tree_directory)
274
            tree = branch.bzrdir.open_workingtree()
275
    else:
276
        tree = WorkingTree.open_containing('.')[0]
377 by Aaron Bentley
Got import command working
277
    tree.lock_write()
278
    try:
423.1.7 by Aaron Bentley
More updates for 0.9
279
        if tree.changes_from(tree.basis_tree()).has_changed():
378 by Aaron Bentley
Check for modified files
280
            raise BzrCommandError("Working tree has uncommitted changes.")
281
531.2.2 by Charlie Shepherd
Remove all trailing whitespace
282
        if (source.endswith('.tar') or source.endswith('.tar.gz') or
384 by Aaron Bentley
Implement bzip support
283
            source.endswith('.tar.bz2')) or source.endswith('.tgz'):
377 by Aaron Bentley
Got import command working
284
            try:
563 by Aaron Bentley
Allow importing directly from a URL
285
                tar_input = open_from_url(source)
384 by Aaron Bentley
Implement bzip support
286
                if source.endswith('.bz2'):
563 by Aaron Bentley
Allow importing directly from a URL
287
                    tar_input = StringIO(tar_input.read().decode('bz2'))
382 by Aaron Bentley
Handle adds and removes efficiently
288
            except IOError, e:
377 by Aaron Bentley
Got import command working
289
                if e.errno == errno.ENOENT:
290
                    raise NoSuchFile(source)
291
            try:
292
                import_tar(tree, tar_input)
293
            finally:
294
                tar_input.close()
475 by Aaron Bentley
Add zip import support
295
        elif source.endswith('.zip'):
563 by Aaron Bentley
Allow importing directly from a URL
296
            import_zip(tree, open_from_url(source))
489 by Aaron Bentley
import now imports directories
297
        elif file_kind(source) == 'directory':
298
            s = StringIO(source)
299
            s.seek(0)
300
            import_dir(tree, s)
475 by Aaron Bentley
Add zip import support
301
        else:
302
            raise BzrCommandError('Unhandled import source')
377 by Aaron Bentley
Got import command working
303
    finally:
304
        tree.unlock()