/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar
1534.7.106 by Aaron Bentley
Cleaned up imports, added copyright statements
1
# Copyright (C) 2006 Canonical Ltd
2
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
7
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
# GNU General Public License for more details.
12
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
1534.7.1 by Aaron Bentley
Got creation of a versioned file working
17
import os
1534.7.106 by Aaron Bentley
Cleaned up imports, added copyright statements
18
import errno
1534.7.117 by Aaron Bentley
Simplified permission handling of existing files in transform.
19
from stat import S_ISREG
1534.7.106 by Aaron Bentley
Cleaned up imports, added copyright statements
20
21
from bzrlib import BZRDIR
1534.7.32 by Aaron Bentley
Got conflict handling working when conflicts involve existing files
22
from bzrlib.errors import (DuplicateKey, MalformedTransform, NoSuchFile,
1534.7.162 by Aaron Bentley
Handle failures creating/deleting the Limbo directory
23
                           ReusingTransform, NotVersionedError, CantMoveRoot,
24
                           ExistingLimbo, ImmortalLimbo)
1534.7.106 by Aaron Bentley
Cleaned up imports, added copyright statements
25
from bzrlib.inventory import InventoryEntry
1534.8.3 by Aaron Bentley
Added Diff3 merging for tree transforms
26
from bzrlib.osutils import file_kind, supports_executable, pathjoin
1534.9.1 by Aaron Bentley
Added progress bars to merge
27
from bzrlib.progress import DummyProgress
1534.7.116 by Aaron Bentley
Started work on retaining file mode across replacements
28
from bzrlib.trace import mutter
1534.7.31 by Aaron Bentley
Changed tree root parent to ROOT_PARENT
29
1534.7.167 by Aaron Bentley
PEP8 and comment cleanups
30
1534.7.31 by Aaron Bentley
Changed tree root parent to ROOT_PARENT
31
ROOT_PARENT = "root-parent"
32
1534.7.167 by Aaron Bentley
PEP8 and comment cleanups
33
1534.7.1 by Aaron Bentley
Got creation of a versioned file working
34
def unique_add(map, key, value):
35
    if key in map:
1534.7.5 by Aaron Bentley
Got unique_add under test
36
        raise DuplicateKey(key=key)
1534.7.1 by Aaron Bentley
Got creation of a versioned file working
37
    map[key] = value
38
1534.7.167 by Aaron Bentley
PEP8 and comment cleanups
39
1534.7.1 by Aaron Bentley
Got creation of a versioned file working
40
class TreeTransform(object):
41
    """Represent a tree transformation."""
1534.9.1 by Aaron Bentley
Added progress bars to merge
42
    def __init__(self, tree, pb=DummyProgress()):
1534.7.1 by Aaron Bentley
Got creation of a versioned file working
43
        """Note: a write lock is taken on the tree.
44
        
45
        Use TreeTransform.finalize() to release the lock
46
        """
47
        object.__init__(self)
48
        self._tree = tree
49
        self._tree.lock_write()
1534.7.162 by Aaron Bentley
Handle failures creating/deleting the Limbo directory
50
        try:
51
            control_files = self._tree._control_files
52
            self._limbodir = control_files.controlfilename('limbo')
53
            try:
54
                os.mkdir(self._limbodir)
55
            except OSError, e:
56
                if e.errno == errno.EEXIST:
57
                    raise ExistingLimbo(self._limbodir)
58
        except: 
59
            self._tree.unlock()
60
            raise
61
1534.7.1 by Aaron Bentley
Got creation of a versioned file working
62
        self._id_number = 0
63
        self._new_name = {}
64
        self._new_parent = {}
1534.7.4 by Aaron Bentley
Unified all file types as 'contents'
65
        self._new_contents = {}
1534.7.34 by Aaron Bentley
Proper conflicts for removals
66
        self._removed_contents = set()
1534.7.25 by Aaron Bentley
Added set_executability
67
        self._new_executability = {}
1534.7.1 by Aaron Bentley
Got creation of a versioned file working
68
        self._new_id = {}
1534.7.143 by Aaron Bentley
Prevented get_trans_id from automatically versioning file ids
69
        self._non_present_ids = {}
1534.7.75 by Aaron Bentley
Added reverse-lookup for versioned files and get_trans_id
70
        self._r_new_id = {}
1534.7.39 by Aaron Bentley
Ensured that files can be unversioned (de-versioned?)
71
        self._removed_id = set()
1534.7.7 by Aaron Bentley
Added support for all-file path ids
72
        self._tree_path_ids = {}
1534.7.8 by Aaron Bentley
Added TreeTransform.final_kind
73
        self._tree_id_paths = {}
1534.7.3 by Aaron Bentley
Updated to use a real root id. Or whatever the working tree considers real.
74
        self._new_root = self.get_id_tree(tree.get_root_id())
1534.7.32 by Aaron Bentley
Got conflict handling working when conflicts involve existing files
75
        self.__done = False
1534.9.1 by Aaron Bentley
Added progress bars to merge
76
        self._pb = pb
1534.7.1 by Aaron Bentley
Got creation of a versioned file working
77
1534.7.132 by Aaron Bentley
Got cooked conflicts working
78
    def __get_root(self):
79
        return self._new_root
80
81
    root = property(__get_root)
82
1534.7.1 by Aaron Bentley
Got creation of a versioned file working
83
    def finalize(self):
1534.7.157 by Aaron Bentley
Added more docs
84
        """Release the working tree lock, if held, clean up limbo dir."""
1534.7.1 by Aaron Bentley
Got creation of a versioned file working
85
        if self._tree is None:
86
            return
1534.7.162 by Aaron Bentley
Handle failures creating/deleting the Limbo directory
87
        try:
88
            for trans_id, kind in self._new_contents.iteritems():
89
                path = self._limbo_name(trans_id)
90
                if kind == "directory":
91
                    os.rmdir(path)
92
                else:
93
                    os.unlink(path)
94
            try:
95
                os.rmdir(self._limbodir)
96
            except OSError:
97
                # We don't especially care *why* the dir is immortal.
98
                raise ImmortalLimbo(self._limbodir)
99
        finally:
100
            self._tree.unlock()
101
            self._tree = None
1534.7.1 by Aaron Bentley
Got creation of a versioned file working
102
103
    def _assign_id(self):
104
        """Produce a new tranform id"""
105
        new_id = "new-%s" % self._id_number
106
        self._id_number +=1
107
        return new_id
108
109
    def create_path(self, name, parent):
110
        """Assign a transaction id to a new path"""
111
        trans_id = self._assign_id()
112
        unique_add(self._new_name, trans_id, name)
113
        unique_add(self._new_parent, trans_id, parent)
114
        return trans_id
115
1534.7.6 by Aaron Bentley
Added conflict handling
116
    def adjust_path(self, name, parent, trans_id):
1534.7.21 by Aaron Bentley
Updated docstrings
117
        """Change the path that is assigned to a transaction id."""
1534.7.66 by Aaron Bentley
Ensured we don't accidentally move the root directory
118
        if trans_id == self._new_root:
119
            raise CantMoveRoot
1534.7.6 by Aaron Bentley
Added conflict handling
120
        self._new_name[trans_id] = name
121
        self._new_parent[trans_id] = parent
122
1534.7.68 by Aaron Bentley
Got semi-reasonable root directory renaming working
123
    def adjust_root_path(self, name, parent):
124
        """Emulate moving the root by moving all children, instead.
125
        
126
        We do this by undoing the association of root's transaction id with the
127
        current tree.  This allows us to create a new directory with that
1534.7.69 by Aaron Bentley
Got real root moves working
128
        transaction id.  We unversion the root directory and version the 
129
        physically new directory, and hope someone versions the tree root
130
        later.
1534.7.68 by Aaron Bentley
Got semi-reasonable root directory renaming working
131
        """
132
        old_root = self._new_root
133
        old_root_file_id = self.final_file_id(old_root)
134
        # force moving all children of root
135
        for child_id in self.iter_tree_children(old_root):
136
            if child_id != parent:
137
                self.adjust_path(self.final_name(child_id), 
138
                                 self.final_parent(child_id), child_id)
1534.7.69 by Aaron Bentley
Got real root moves working
139
            file_id = self.final_file_id(child_id)
140
            if file_id is not None:
141
                self.unversion_file(child_id)
142
            self.version_file(file_id, child_id)
1534.7.68 by Aaron Bentley
Got semi-reasonable root directory renaming working
143
        
144
        # the physical root needs a new transaction id
145
        self._tree_path_ids.pop("")
146
        self._tree_id_paths.pop(old_root)
147
        self._new_root = self.get_id_tree(self._tree.get_root_id())
148
        if parent == old_root:
149
            parent = self._new_root
150
        self.adjust_path(name, parent, old_root)
151
        self.create_directory(old_root)
1534.7.69 by Aaron Bentley
Got real root moves working
152
        self.version_file(old_root_file_id, old_root)
153
        self.unversion_file(self._new_root)
1534.7.68 by Aaron Bentley
Got semi-reasonable root directory renaming working
154
1534.7.1 by Aaron Bentley
Got creation of a versioned file working
155
    def get_id_tree(self, inventory_id):
156
        """Determine the transaction id of a working tree file.
157
        
158
        This reflects only files that already exist, not ones that will be
159
        added by transactions.
160
        """
1534.7.148 by Aaron Bentley
Handled the remaining file versioning case
161
        path = self._tree.inventory.id2path(inventory_id)
162
        return self.get_tree_path_id(path)
1534.7.7 by Aaron Bentley
Added support for all-file path ids
163
1534.7.75 by Aaron Bentley
Added reverse-lookup for versioned files and get_trans_id
164
    def get_trans_id(self, file_id):
1534.7.156 by Aaron Bentley
PEP8 fixes
165
        """Determine or set the transaction id associated with a file ID.
1534.7.75 by Aaron Bentley
Added reverse-lookup for versioned files and get_trans_id
166
        A new id is only created for file_ids that were never present.  If
167
        a transaction has been unversioned, it is deliberately still returned.
168
        (this will likely lead to an unversioned parent conflict.)
169
        """
170
        if file_id in self._r_new_id and self._r_new_id[file_id] is not None:
171
            return self._r_new_id[file_id]
172
        elif file_id in self._tree.inventory:
173
            return self.get_id_tree(file_id)
1534.7.143 by Aaron Bentley
Prevented get_trans_id from automatically versioning file ids
174
        elif file_id in self._non_present_ids:
175
            return self._non_present_ids[file_id]
1534.7.75 by Aaron Bentley
Added reverse-lookup for versioned files and get_trans_id
176
        else:
177
            trans_id = self._assign_id()
1534.7.143 by Aaron Bentley
Prevented get_trans_id from automatically versioning file ids
178
            self._non_present_ids[file_id] = trans_id
1534.7.75 by Aaron Bentley
Added reverse-lookup for versioned files and get_trans_id
179
            return trans_id
180
1534.7.12 by Aaron Bentley
Added canonical_path function
181
    def canonical_path(self, path):
182
        """Get the canonical tree-relative path"""
183
        # don't follow final symlinks
184
        dirname, basename = os.path.split(self._tree.abspath(path))
185
        dirname = os.path.realpath(dirname)
1534.7.166 by Aaron Bentley
Swapped os.path.join for pathjoin everywhere
186
        return self._tree.relpath(pathjoin(dirname, basename))
1534.7.12 by Aaron Bentley
Added canonical_path function
187
1534.7.7 by Aaron Bentley
Added support for all-file path ids
188
    def get_tree_path_id(self, path):
189
        """Determine (and maybe set) the transaction ID for a tree path."""
1534.7.12 by Aaron Bentley
Added canonical_path function
190
        path = self.canonical_path(path)
1534.7.7 by Aaron Bentley
Added support for all-file path ids
191
        if path not in self._tree_path_ids:
192
            self._tree_path_ids[path] = self._assign_id()
1534.7.8 by Aaron Bentley
Added TreeTransform.final_kind
193
            self._tree_id_paths[self._tree_path_ids[path]] = path
1534.7.7 by Aaron Bentley
Added support for all-file path ids
194
        return self._tree_path_ids[path]
1534.7.1 by Aaron Bentley
Got creation of a versioned file working
195
1534.7.16 by Aaron Bentley
Added get_tree_parent
196
    def get_tree_parent(self, trans_id):
1534.7.31 by Aaron Bentley
Changed tree root parent to ROOT_PARENT
197
        """Determine id of the parent in the tree."""
1534.7.16 by Aaron Bentley
Added get_tree_parent
198
        path = self._tree_id_paths[trans_id]
199
        if path == "":
1534.7.31 by Aaron Bentley
Changed tree root parent to ROOT_PARENT
200
            return ROOT_PARENT
1534.7.16 by Aaron Bentley
Added get_tree_parent
201
        return self.get_tree_path_id(os.path.dirname(path))
202
1534.7.117 by Aaron Bentley
Simplified permission handling of existing files in transform.
203
    def create_file(self, contents, trans_id, mode_id=None):
1534.7.21 by Aaron Bentley
Updated docstrings
204
        """Schedule creation of a new file.
205
206
        See also new_file.
1534.7.1 by Aaron Bentley
Got creation of a versioned file working
207
        
208
        Contents is an iterator of strings, all of which will be written
1534.7.21 by Aaron Bentley
Updated docstrings
209
        to the target destination.
1534.7.117 by Aaron Bentley
Simplified permission handling of existing files in transform.
210
211
        New file takes the permissions of any existing file with that id,
212
        unless mode_id is specified.
1534.7.1 by Aaron Bentley
Got creation of a versioned file working
213
        """
1534.7.73 by Aaron Bentley
Changed model again. Now iterator is used immediately.
214
        f = file(self._limbo_name(trans_id), 'wb')
1534.8.1 by Aaron Bentley
Reference files in limbo before their creation is finished, for finalize.
215
        unique_add(self._new_contents, trans_id, 'file')
1534.7.73 by Aaron Bentley
Changed model again. Now iterator is used immediately.
216
        for segment in contents:
217
            f.write(segment)
218
        f.close()
1534.7.117 by Aaron Bentley
Simplified permission handling of existing files in transform.
219
        self._set_mode(trans_id, mode_id, S_ISREG)
220
221
    def _set_mode(self, trans_id, mode_id, typefunc):
1534.7.157 by Aaron Bentley
Added more docs
222
        """Set the mode of new file contents.
223
        The mode_id is the existing file to get the mode from (often the same
224
        as trans_id).  The operation is only performed if there's a mode match
225
        according to typefunc.
226
        """
1534.7.117 by Aaron Bentley
Simplified permission handling of existing files in transform.
227
        if mode_id is None:
228
            mode_id = trans_id
229
        try:
230
            old_path = self._tree_id_paths[mode_id]
231
        except KeyError:
232
            return
233
        try:
234
            mode = os.stat(old_path).st_mode
235
        except OSError, e:
236
            if e.errno == errno.ENOENT:
237
                return
238
            else:
239
                raise
240
        if typefunc(mode):
241
            os.chmod(self._limbo_name(trans_id), mode)
1534.7.1 by Aaron Bentley
Got creation of a versioned file working
242
1534.7.20 by Aaron Bentley
Added directory handling
243
    def create_directory(self, trans_id):
1534.7.21 by Aaron Bentley
Updated docstrings
244
        """Schedule creation of a new directory.
245
        
246
        See also new_directory.
247
        """
1534.7.73 by Aaron Bentley
Changed model again. Now iterator is used immediately.
248
        os.mkdir(self._limbo_name(trans_id))
249
        unique_add(self._new_contents, trans_id, 'directory')
1534.7.20 by Aaron Bentley
Added directory handling
250
1534.7.22 by Aaron Bentley
Added symlink support
251
    def create_symlink(self, target, trans_id):
252
        """Schedule creation of a new symbolic link.
253
254
        target is a bytestring.
255
        See also new_symlink.
256
        """
1534.7.73 by Aaron Bentley
Changed model again. Now iterator is used immediately.
257
        os.symlink(target, self._limbo_name(trans_id))
258
        unique_add(self._new_contents, trans_id, 'symlink')
1534.7.22 by Aaron Bentley
Added symlink support
259
1534.7.129 by Aaron Bentley
Converted test cases to Tree Transform
260
    @staticmethod
261
    def delete_any(full_path):
1534.7.157 by Aaron Bentley
Added more docs
262
        """Delete a file or directory."""
1534.7.129 by Aaron Bentley
Converted test cases to Tree Transform
263
        try:
264
            os.unlink(full_path)
265
        except OSError, e:
266
        # We may be renaming a dangling inventory id
267
            if e.errno != errno.EISDIR and e.errno != errno.EACCES:
268
                raise
269
            os.rmdir(full_path)
270
271
    def cancel_creation(self, trans_id):
1534.7.157 by Aaron Bentley
Added more docs
272
        """Cancel the creation of new file contents."""
1534.7.129 by Aaron Bentley
Converted test cases to Tree Transform
273
        del self._new_contents[trans_id]
274
        self.delete_any(self._limbo_name(trans_id))
275
1534.7.34 by Aaron Bentley
Proper conflicts for removals
276
    def delete_contents(self, trans_id):
277
        """Schedule the contents of a path entry for deletion"""
1534.7.130 by Aaron Bentley
More conflict handling, test porting
278
        self.tree_kind(trans_id)
1534.7.34 by Aaron Bentley
Proper conflicts for removals
279
        self._removed_contents.add(trans_id)
280
1534.7.61 by Aaron Bentley
Handled parent loops, missing parents, unversioned parents
281
    def cancel_deletion(self, trans_id):
282
        """Cancel a scheduled deletion"""
283
        self._removed_contents.remove(trans_id)
284
1534.7.39 by Aaron Bentley
Ensured that files can be unversioned (de-versioned?)
285
    def unversion_file(self, trans_id):
286
        """Schedule a path entry to become unversioned"""
287
        self._removed_id.add(trans_id)
288
289
    def delete_versioned(self, trans_id):
290
        """Delete and unversion a versioned file"""
291
        self.delete_contents(trans_id)
292
        self.unversion_file(trans_id)
293
1534.7.25 by Aaron Bentley
Added set_executability
294
    def set_executability(self, executability, trans_id):
1534.7.167 by Aaron Bentley
PEP8 and comment cleanups
295
        """Schedule setting of the 'execute' bit
296
        To unschedule, set to None
297
        """
1534.7.26 by Aaron Bentley
Added conflicts for setting executability on unversioned/non-file entries
298
        if executability is None:
299
            del self._new_executability[trans_id]
300
        else:
301
            unique_add(self._new_executability, trans_id, executability)
1534.7.25 by Aaron Bentley
Added set_executability
302
1534.7.1 by Aaron Bentley
Got creation of a versioned file working
303
    def version_file(self, file_id, trans_id):
1534.7.21 by Aaron Bentley
Updated docstrings
304
        """Schedule a file to become versioned."""
1534.7.148 by Aaron Bentley
Handled the remaining file versioning case
305
        assert file_id is not None
1534.7.1 by Aaron Bentley
Got creation of a versioned file working
306
        unique_add(self._new_id, trans_id, file_id)
1534.7.75 by Aaron Bentley
Added reverse-lookup for versioned files and get_trans_id
307
        unique_add(self._r_new_id, file_id, trans_id)
1534.7.1 by Aaron Bentley
Got creation of a versioned file working
308
1534.7.105 by Aaron Bentley
Got merge with rename working
309
    def cancel_versioning(self, trans_id):
310
        """Undo a previous versioning of a file"""
311
        file_id = self._new_id[trans_id]
312
        del self._new_id[trans_id]
313
        del self._r_new_id[file_id]
314
1534.7.1 by Aaron Bentley
Got creation of a versioned file working
315
    def new_paths(self):
1534.7.21 by Aaron Bentley
Updated docstrings
316
        """Determine the paths of all new and changed files"""
1534.7.1 by Aaron Bentley
Got creation of a versioned file working
317
        new_ids = set()
1534.7.132 by Aaron Bentley
Got cooked conflicts working
318
        fp = FinalPaths(self)
1534.7.4 by Aaron Bentley
Unified all file types as 'contents'
319
        for id_set in (self._new_name, self._new_parent, self._new_contents,
1534.7.25 by Aaron Bentley
Added set_executability
320
                       self._new_id, self._new_executability):
1534.7.1 by Aaron Bentley
Got creation of a versioned file working
321
            new_ids.update(id_set)
322
        new_paths = [(fp.get_path(t), t) for t in new_ids]
323
        new_paths.sort()
324
        return new_paths
1534.7.6 by Aaron Bentley
Added conflict handling
325
1534.7.34 by Aaron Bentley
Proper conflicts for removals
326
    def tree_kind(self, trans_id):
1534.7.40 by Aaron Bentley
Updated docs
327
        """Determine the file kind in the working tree.
328
329
        Raises NoSuchFile if the file does not exist
330
        """
1534.7.34 by Aaron Bentley
Proper conflicts for removals
331
        path = self._tree_id_paths.get(trans_id)
332
        if path is None:
333
            raise NoSuchFile(None)
334
        try:
335
            return file_kind(self._tree.abspath(path))
336
        except OSError, e:
337
            if e.errno != errno.ENOENT:
338
                raise
339
            else:
340
                raise NoSuchFile(path)
341
1534.7.8 by Aaron Bentley
Added TreeTransform.final_kind
342
    def final_kind(self, trans_id):
1534.7.156 by Aaron Bentley
PEP8 fixes
343
        """Determine the final file kind, after any changes applied.
1534.7.8 by Aaron Bentley
Added TreeTransform.final_kind
344
        
345
        Raises NoSuchFile if the file does not exist/has no contents.
346
        (It is conceivable that a path would be created without the
347
        corresponding contents insertion command)
348
        """
349
        if trans_id in self._new_contents:
1534.7.73 by Aaron Bentley
Changed model again. Now iterator is used immediately.
350
            return self._new_contents[trans_id]
1534.7.34 by Aaron Bentley
Proper conflicts for removals
351
        elif trans_id in self._removed_contents:
352
            raise NoSuchFile(None)
1534.7.8 by Aaron Bentley
Added TreeTransform.final_kind
353
        else:
1534.7.34 by Aaron Bentley
Proper conflicts for removals
354
            return self.tree_kind(trans_id)
1534.7.8 by Aaron Bentley
Added TreeTransform.final_kind
355
1534.7.41 by Aaron Bentley
Got inventory ID movement working
356
    def get_tree_file_id(self, trans_id):
357
        """Determine the file id associated with the trans_id in the tree"""
358
        try:
359
            path = self._tree_id_paths[trans_id]
360
        except KeyError:
361
            # the file is a new, unversioned file, or invalid trans_id
362
            return None
363
        # the file is old; the old id is still valid
1534.7.68 by Aaron Bentley
Got semi-reasonable root directory renaming working
364
        if self._new_root == trans_id:
365
            return self._tree.inventory.root.file_id
1534.7.148 by Aaron Bentley
Handled the remaining file versioning case
366
        return self._tree.inventory.path2id(path)
1534.7.41 by Aaron Bentley
Got inventory ID movement working
367
1534.7.13 by Aaron Bentley
Implemented final_file_id
368
    def final_file_id(self, trans_id):
1534.7.156 by Aaron Bentley
PEP8 fixes
369
        """Determine the file id after any changes are applied, or None.
1534.7.21 by Aaron Bentley
Updated docstrings
370
        
371
        None indicates that the file will not be versioned after changes are
372
        applied.
373
        """
1534.7.13 by Aaron Bentley
Implemented final_file_id
374
        try:
375
            # there is a new id for this file
1534.7.148 by Aaron Bentley
Handled the remaining file versioning case
376
            assert self._new_id[trans_id] is not None
1534.7.13 by Aaron Bentley
Implemented final_file_id
377
            return self._new_id[trans_id]
378
        except KeyError:
1534.7.39 by Aaron Bentley
Ensured that files can be unversioned (de-versioned?)
379
            if trans_id in self._removed_id:
380
                return None
1534.7.41 by Aaron Bentley
Got inventory ID movement working
381
        return self.get_tree_file_id(trans_id)
1534.7.13 by Aaron Bentley
Implemented final_file_id
382
1534.7.148 by Aaron Bentley
Handled the remaining file versioning case
383
    def inactive_file_id(self, trans_id):
1534.7.157 by Aaron Bentley
Added more docs
384
        """Return the inactive file_id associated with a transaction id.
1534.7.148 by Aaron Bentley
Handled the remaining file versioning case
385
        That is, the one in the tree or in non_present_ids.
386
        The file_id may actually be active, too.
387
        """
388
        file_id = self.get_tree_file_id(trans_id)
389
        if file_id is not None:
390
            return file_id
391
        for key, value in self._non_present_ids.iteritems():
392
            if value == trans_id:
393
                return key
394
1534.7.17 by Aaron Bentley
Added final_parent function
395
    def final_parent(self, trans_id):
1534.7.156 by Aaron Bentley
PEP8 fixes
396
        """Determine the parent file_id, after any changes are applied.
1534.7.21 by Aaron Bentley
Updated docstrings
397
1534.7.31 by Aaron Bentley
Changed tree root parent to ROOT_PARENT
398
        ROOT_PARENT is returned for the tree root.
1534.7.21 by Aaron Bentley
Updated docstrings
399
        """
1534.7.17 by Aaron Bentley
Added final_parent function
400
        try:
401
            return self._new_parent[trans_id]
402
        except KeyError:
403
            return self.get_tree_parent(trans_id)
404
1534.7.32 by Aaron Bentley
Got conflict handling working when conflicts involve existing files
405
    def final_name(self, trans_id):
1534.7.40 by Aaron Bentley
Updated docs
406
        """Determine the final filename, after all changes are applied."""
1534.7.32 by Aaron Bentley
Got conflict handling working when conflicts involve existing files
407
        try:
408
            return self._new_name[trans_id]
409
        except KeyError:
410
            return os.path.basename(self._tree_id_paths[trans_id])
411
412
    def _by_parent(self):
1534.7.40 by Aaron Bentley
Updated docs
413
        """Return a map of parent: children for known parents.
414
        
415
        Only new paths and parents of tree files with assigned ids are used.
416
        """
1534.7.6 by Aaron Bentley
Added conflict handling
417
        by_parent = {}
1534.7.32 by Aaron Bentley
Got conflict handling working when conflicts involve existing files
418
        items = list(self._new_parent.iteritems())
1534.7.76 by Aaron Bentley
Fixed final_parent, for the case where finding a parent adds tree id paths.
419
        items.extend((t, self.final_parent(t)) for t in 
420
                      self._tree_id_paths.keys())
1534.7.32 by Aaron Bentley
Got conflict handling working when conflicts involve existing files
421
        for trans_id, parent_id in items:
1534.7.6 by Aaron Bentley
Added conflict handling
422
            if parent_id not in by_parent:
423
                by_parent[parent_id] = set()
424
            by_parent[parent_id].add(trans_id)
1534.7.32 by Aaron Bentley
Got conflict handling working when conflicts involve existing files
425
        return by_parent
1534.7.11 by Aaron Bentley
Refactored conflict handling
426
1534.7.57 by Aaron Bentley
Enhanced conflict resolution.
427
    def path_changed(self, trans_id):
1534.7.157 by Aaron Bentley
Added more docs
428
        """Return True if a trans_id's path has changed."""
1534.7.57 by Aaron Bentley
Enhanced conflict resolution.
429
        return trans_id in self._new_name or trans_id in self._new_parent
430
1534.7.32 by Aaron Bentley
Got conflict handling working when conflicts involve existing files
431
    def find_conflicts(self):
1534.7.40 by Aaron Bentley
Updated docs
432
        """Find any violations of inventory or filesystem invariants"""
1534.7.32 by Aaron Bentley
Got conflict handling working when conflicts involve existing files
433
        if self.__done is True:
434
            raise ReusingTransform()
435
        conflicts = []
436
        # ensure all children of all existent parents are known
437
        # all children of non-existent parents are known, by definition.
438
        self._add_tree_children()
439
        by_parent = self._by_parent()
1534.7.15 by Aaron Bentley
Add conflict types related to versioning
440
        conflicts.extend(self._unversioned_parents(by_parent))
1534.7.19 by Aaron Bentley
Added tests for parent loops
441
        conflicts.extend(self._parent_loops())
1534.7.11 by Aaron Bentley
Refactored conflict handling
442
        conflicts.extend(self._duplicate_entries(by_parent))
1534.7.50 by Aaron Bentley
Detect duplicate inventory ids
443
        conflicts.extend(self._duplicate_ids())
1534.7.11 by Aaron Bentley
Refactored conflict handling
444
        conflicts.extend(self._parent_type_conflicts(by_parent))
1534.7.15 by Aaron Bentley
Add conflict types related to versioning
445
        conflicts.extend(self._improper_versioning())
1534.7.26 by Aaron Bentley
Added conflicts for setting executability on unversioned/non-file entries
446
        conflicts.extend(self._executability_conflicts())
1534.7.152 by Aaron Bentley
Fixed overwrites
447
        conflicts.extend(self._overwrite_conflicts())
1534.7.15 by Aaron Bentley
Add conflict types related to versioning
448
        return conflicts
449
1534.7.32 by Aaron Bentley
Got conflict handling working when conflicts involve existing files
450
    def _add_tree_children(self):
1534.7.156 by Aaron Bentley
PEP8 fixes
451
        """Add all the children of all active parents to the known paths.
1534.7.40 by Aaron Bentley
Updated docs
452
453
        Active parents are those which gain children, and those which are
454
        removed.  This is a necessary first step in detecting conflicts.
455
        """
1534.7.34 by Aaron Bentley
Proper conflicts for removals
456
        parents = self._by_parent().keys()
457
        parents.extend([t for t in self._removed_contents if 
458
                        self.tree_kind(t) == 'directory'])
1534.7.50 by Aaron Bentley
Detect duplicate inventory ids
459
        for trans_id in self._removed_id:
460
            file_id = self.get_tree_file_id(trans_id)
1534.7.55 by Aaron Bentley
Fixed up the change detection
461
            if self._tree.inventory[file_id].kind in ('directory', 
462
                                                      'root_directory'):
1534.7.50 by Aaron Bentley
Detect duplicate inventory ids
463
                parents.append(trans_id)
464
1534.7.32 by Aaron Bentley
Got conflict handling working when conflicts involve existing files
465
        for parent_id in parents:
1534.7.67 by Aaron Bentley
Refactored _add_tree_children
466
            # ensure that all children are registered with the transaction
467
            list(self.iter_tree_children(parent_id))
468
469
    def iter_tree_children(self, parent_id):
470
        """Iterate through the entry's tree children, if any"""
471
        try:
472
            path = self._tree_id_paths[parent_id]
473
        except KeyError:
474
            return
475
        try:
476
            children = os.listdir(self._tree.abspath(path))
477
        except OSError, e:
1534.7.71 by abentley
All tests pass under Windows
478
            if e.errno != errno.ENOENT and e.errno != errno.ESRCH:
1534.7.67 by Aaron Bentley
Refactored _add_tree_children
479
                raise
480
            return
481
            
482
        for child in children:
483
            childpath = joinpath(path, child)
484
            if childpath == BZRDIR:
485
                continue
486
            yield self.get_tree_path_id(childpath)
1534.7.32 by Aaron Bentley
Got conflict handling working when conflicts involve existing files
487
1534.7.19 by Aaron Bentley
Added tests for parent loops
488
    def _parent_loops(self):
489
        """No entry should be its own ancestor"""
490
        conflicts = []
491
        for trans_id in self._new_parent:
492
            seen = set()
493
            parent_id = trans_id
1534.7.31 by Aaron Bentley
Changed tree root parent to ROOT_PARENT
494
            while parent_id is not ROOT_PARENT:
1534.7.19 by Aaron Bentley
Added tests for parent loops
495
                seen.add(parent_id)
496
                parent_id = self.final_parent(parent_id)
497
                if parent_id == trans_id:
498
                    conflicts.append(('parent loop', trans_id))
499
                if parent_id in seen:
500
                    break
501
        return conflicts
502
1534.7.15 by Aaron Bentley
Add conflict types related to versioning
503
    def _unversioned_parents(self, by_parent):
504
        """If parent directories are versioned, children must be versioned."""
505
        conflicts = []
506
        for parent_id, children in by_parent.iteritems():
1534.7.32 by Aaron Bentley
Got conflict handling working when conflicts involve existing files
507
            if parent_id is ROOT_PARENT:
508
                continue
1534.7.15 by Aaron Bentley
Add conflict types related to versioning
509
            if self.final_file_id(parent_id) is not None:
510
                continue
511
            for child_id in children:
512
                if self.final_file_id(child_id) is not None:
513
                    conflicts.append(('unversioned parent', parent_id))
514
                    break;
515
        return conflicts
516
517
    def _improper_versioning(self):
1534.7.156 by Aaron Bentley
PEP8 fixes
518
        """Cannot version a file with no contents, or a bad type.
1534.7.15 by Aaron Bentley
Add conflict types related to versioning
519
        
520
        However, existing entries with no contents are okay.
521
        """
522
        conflicts = []
523
        for trans_id in self._new_id.iterkeys():
524
            try:
525
                kind = self.final_kind(trans_id)
526
            except NoSuchFile:
527
                conflicts.append(('versioning no contents', trans_id))
528
                continue
529
            if not InventoryEntry.versionable_kind(kind):
1534.7.20 by Aaron Bentley
Added directory handling
530
                conflicts.append(('versioning bad kind', trans_id, kind))
1534.7.11 by Aaron Bentley
Refactored conflict handling
531
        return conflicts
532
1534.7.26 by Aaron Bentley
Added conflicts for setting executability on unversioned/non-file entries
533
    def _executability_conflicts(self):
1534.7.40 by Aaron Bentley
Updated docs
534
        """Check for bad executability changes.
535
        
536
        Only versioned files may have their executability set, because
537
        1. only versioned entries can have executability under windows
538
        2. only files can be executable.  (The execute bit on a directory
539
           does not indicate searchability)
540
        """
1534.7.26 by Aaron Bentley
Added conflicts for setting executability on unversioned/non-file entries
541
        conflicts = []
542
        for trans_id in self._new_executability:
543
            if self.final_file_id(trans_id) is None:
544
                conflicts.append(('unversioned executability', trans_id))
1534.7.34 by Aaron Bentley
Proper conflicts for removals
545
            else:
546
                try:
547
                    non_file = self.final_kind(trans_id) != "file"
548
                except NoSuchFile:
549
                    non_file = True
550
                if non_file is True:
551
                    conflicts.append(('non-file executability', trans_id))
1534.7.26 by Aaron Bentley
Added conflicts for setting executability on unversioned/non-file entries
552
        return conflicts
553
1534.7.152 by Aaron Bentley
Fixed overwrites
554
    def _overwrite_conflicts(self):
555
        """Check for overwrites (not permitted on Win32)"""
556
        conflicts = []
557
        for trans_id in self._new_contents:
558
            try:
559
                self.tree_kind(trans_id)
560
            except NoSuchFile:
561
                continue
562
            if trans_id not in self._removed_contents:
563
                conflicts.append(('overwrite', trans_id,
564
                                 self.final_name(trans_id)))
565
        return conflicts
566
1534.7.11 by Aaron Bentley
Refactored conflict handling
567
    def _duplicate_entries(self, by_parent):
568
        """No directory may have two entries with the same name."""
569
        conflicts = []
1534.7.6 by Aaron Bentley
Added conflict handling
570
        for children in by_parent.itervalues():
1534.7.32 by Aaron Bentley
Got conflict handling working when conflicts involve existing files
571
            name_ids = [(self.final_name(t), t) for t in children]
1534.7.6 by Aaron Bentley
Added conflict handling
572
            name_ids.sort()
573
            last_name = None
574
            last_trans_id = None
575
            for name, trans_id in name_ids:
576
                if name == last_name:
1534.7.32 by Aaron Bentley
Got conflict handling working when conflicts involve existing files
577
                    conflicts.append(('duplicate', last_trans_id, trans_id,
578
                    name))
1534.7.6 by Aaron Bentley
Added conflict handling
579
                last_name = name
580
                last_trans_id = trans_id
1534.7.11 by Aaron Bentley
Refactored conflict handling
581
        return conflicts
582
1534.7.50 by Aaron Bentley
Detect duplicate inventory ids
583
    def _duplicate_ids(self):
584
        """Each inventory id may only be used once"""
585
        conflicts = []
586
        removed_tree_ids = set((self.get_tree_file_id(trans_id) for trans_id in
587
                                self._removed_id))
588
        active_tree_ids = set((f for f in self._tree.inventory if
589
                               f not in removed_tree_ids))
590
        for trans_id, file_id in self._new_id.iteritems():
591
            if file_id in active_tree_ids:
592
                old_trans_id = self.get_id_tree(file_id)
593
                conflicts.append(('duplicate id', old_trans_id, trans_id))
594
        return conflicts
595
1534.7.11 by Aaron Bentley
Refactored conflict handling
596
    def _parent_type_conflicts(self, by_parent):
597
        """parents must have directory 'contents'."""
598
        conflicts = []
1534.7.37 by Aaron Bentley
Allowed removed dirs to have content-free children.
599
        for parent_id, children in by_parent.iteritems():
1534.7.32 by Aaron Bentley
Got conflict handling working when conflicts involve existing files
600
            if parent_id is ROOT_PARENT:
601
                continue
1534.7.37 by Aaron Bentley
Allowed removed dirs to have content-free children.
602
            if not self._any_contents(children):
603
                continue
604
            for child in children:
605
                try:
606
                    self.final_kind(child)
607
                except NoSuchFile:
608
                    continue
1534.7.10 by Aaron Bentley
Implemented missing parent and non-directory parent conflicts
609
            try:
610
                kind = self.final_kind(parent_id)
611
            except NoSuchFile:
612
                kind = None
613
            if kind is None:
614
                conflicts.append(('missing parent', parent_id))
615
            elif kind != "directory":
616
                conflicts.append(('non-directory parent', parent_id))
1534.7.6 by Aaron Bentley
Added conflict handling
617
        return conflicts
1534.7.37 by Aaron Bentley
Allowed removed dirs to have content-free children.
618
619
    def _any_contents(self, trans_ids):
620
        """Return true if any of the trans_ids, will have contents."""
621
        for trans_id in trans_ids:
622
            try:
623
                kind = self.final_kind(trans_id)
624
            except NoSuchFile:
625
                continue
626
            return True
627
        return False
1534.7.6 by Aaron Bentley
Added conflict handling
628
            
1534.7.1 by Aaron Bentley
Got creation of a versioned file working
629
    def apply(self):
1534.7.156 by Aaron Bentley
PEP8 fixes
630
        """Apply all changes to the inventory and filesystem.
1534.7.21 by Aaron Bentley
Updated docstrings
631
        
632
        If filesystem or inventory conflicts are present, MalformedTransform
633
        will be thrown.
634
        """
1534.7.49 by Aaron Bentley
Printed conflicts in MalformedTransform
635
        conflicts = self.find_conflicts()
636
        if len(conflicts) != 0:
637
            raise MalformedTransform(conflicts=conflicts)
1534.7.41 by Aaron Bentley
Got inventory ID movement working
638
        limbo_inv = {}
1534.7.1 by Aaron Bentley
Got creation of a versioned file working
639
        inv = self._tree.inventory
1534.7.41 by Aaron Bentley
Got inventory ID movement working
640
        self._apply_removals(inv, limbo_inv)
641
        self._apply_insertions(inv, limbo_inv)
1534.7.35 by Aaron Bentley
Got file renaming working
642
        self._tree._write_inventory(inv)
643
        self.__done = True
1534.7.59 by Aaron Bentley
Simplified tests
644
        self.finalize()
1534.7.35 by Aaron Bentley
Got file renaming working
645
1534.7.72 by Aaron Bentley
Moved new content generation to pre-renames
646
    def _limbo_name(self, trans_id):
647
        """Generate the limbo name of a file"""
1534.7.166 by Aaron Bentley
Swapped os.path.join for pathjoin everywhere
648
        return pathjoin(self._limbodir, trans_id)
1534.7.72 by Aaron Bentley
Moved new content generation to pre-renames
649
1534.7.41 by Aaron Bentley
Got inventory ID movement working
650
    def _apply_removals(self, inv, limbo_inv):
1534.7.36 by Aaron Bentley
Added rename tests
651
        """Perform tree operations that remove directory/inventory names.
652
        
653
        That is, delete files that are to be deleted, and put any files that
654
        need renaming into limbo.  This must be done in strict child-to-parent
655
        order.
656
        """
1534.7.35 by Aaron Bentley
Got file renaming working
657
        tree_paths = list(self._tree_path_ids.iteritems())
658
        tree_paths.sort(reverse=True)
1534.9.1 by Aaron Bentley
Added progress bars to merge
659
        for num, data in enumerate(tree_paths):
660
            path, trans_id = data
661
            self._pb.update('removing file', num+1, len(tree_paths))
1534.7.43 by abentley
Fixed some Windows bugs, introduced a conflicts bug
662
            full_path = self._tree.abspath(path)
1534.7.34 by Aaron Bentley
Proper conflicts for removals
663
            if trans_id in self._removed_contents:
1534.7.129 by Aaron Bentley
Converted test cases to Tree Transform
664
                self.delete_any(full_path)
1534.7.35 by Aaron Bentley
Got file renaming working
665
            elif trans_id in self._new_name or trans_id in self._new_parent:
1534.7.48 by Aaron Bentley
Ensured we can move/rename dangling inventory entries
666
                try:
1534.7.118 by Aaron Bentley
Dirty merge of the mainline
667
                    os.rename(full_path, self._limbo_name(trans_id))
1534.7.48 by Aaron Bentley
Ensured we can move/rename dangling inventory entries
668
                except OSError, e:
669
                    if e.errno != errno.ENOENT:
670
                        raise
1534.7.39 by Aaron Bentley
Ensured that files can be unversioned (de-versioned?)
671
            if trans_id in self._removed_id:
1534.7.69 by Aaron Bentley
Got real root moves working
672
                if trans_id == self._new_root:
673
                    file_id = self._tree.inventory.root.file_id
674
                else:
675
                    file_id = self.get_tree_file_id(trans_id)
676
                del inv[file_id]
1534.7.41 by Aaron Bentley
Got inventory ID movement working
677
            elif trans_id in self._new_name or trans_id in self._new_parent:
678
                file_id = self.get_tree_file_id(trans_id)
1534.7.57 by Aaron Bentley
Enhanced conflict resolution.
679
                if file_id is not None:
680
                    limbo_inv[trans_id] = inv[file_id]
681
                    del inv[file_id]
1534.9.1 by Aaron Bentley
Added progress bars to merge
682
        self._pb.clear()
1534.7.34 by Aaron Bentley
Proper conflicts for removals
683
1534.7.41 by Aaron Bentley
Got inventory ID movement working
684
    def _apply_insertions(self, inv, limbo_inv):
1534.7.36 by Aaron Bentley
Added rename tests
685
        """Perform tree operations that insert directory/inventory names.
686
        
687
        That is, create any files that need to be created, and restore from
688
        limbo any files that needed renaming.  This must be done in strict
689
        parent-to-child order.
690
        """
1534.9.1 by Aaron Bentley
Added progress bars to merge
691
        new_paths = self.new_paths()
692
        for num, (path, trans_id) in enumerate(new_paths):
693
            self._pb.update('adding file', num+1, len(new_paths))
1534.7.4 by Aaron Bentley
Unified all file types as 'contents'
694
            try:
1534.7.73 by Aaron Bentley
Changed model again. Now iterator is used immediately.
695
                kind = self._new_contents[trans_id]
1534.7.4 by Aaron Bentley
Unified all file types as 'contents'
696
            except KeyError:
697
                kind = contents = None
1534.7.72 by Aaron Bentley
Moved new content generation to pre-renames
698
            if trans_id in self._new_contents or self.path_changed(trans_id):
1534.7.48 by Aaron Bentley
Ensured we can move/rename dangling inventory entries
699
                full_path = self._tree.abspath(path)
700
                try:
1534.7.72 by Aaron Bentley
Moved new content generation to pre-renames
701
                    os.rename(self._limbo_name(trans_id), full_path)
1534.7.48 by Aaron Bentley
Ensured we can move/rename dangling inventory entries
702
                except OSError, e:
703
                    # We may be renaming a dangling inventory id
704
                    if e.errno != errno.ENOENT:
705
                        raise
1534.7.73 by Aaron Bentley
Changed model again. Now iterator is used immediately.
706
                if trans_id in self._new_contents:
707
                    del self._new_contents[trans_id]
1534.7.1 by Aaron Bentley
Got creation of a versioned file working
708
709
            if trans_id in self._new_id:
710
                if kind is None:
1534.7.14 by Aaron Bentley
Fixed file_kind call
711
                    kind = file_kind(self._tree.abspath(path))
1534.7.149 by Aaron Bentley
Removed bare except
712
                inv.add_path(path, kind, self._new_id[trans_id])
1534.7.41 by Aaron Bentley
Got inventory ID movement working
713
            elif trans_id in self._new_name or trans_id in self._new_parent:
1534.7.56 by Aaron Bentley
Implemented the backup file detritus
714
                entry = limbo_inv.get(trans_id)
715
                if entry is not None:
716
                    entry.name = self.final_name(trans_id)
1534.7.150 by Aaron Bentley
Handled simultaneous renames of parent and child better
717
                    parent_path = os.path.dirname(path)
718
                    entry.parent_id = self._tree.inventory.path2id(parent_path)
1534.7.56 by Aaron Bentley
Implemented the backup file detritus
719
                    inv.add(entry)
1534.7.41 by Aaron Bentley
Got inventory ID movement working
720
1534.7.25 by Aaron Bentley
Added set_executability
721
            # requires files and inventory entries to be in place
722
            if trans_id in self._new_executability:
723
                self._set_executability(path, inv, trans_id)
1534.9.1 by Aaron Bentley
Added progress bars to merge
724
        self._pb.clear()
1534.7.40 by Aaron Bentley
Updated docs
725
1534.7.25 by Aaron Bentley
Added set_executability
726
    def _set_executability(self, path, inv, trans_id):
1534.7.40 by Aaron Bentley
Updated docs
727
        """Set the executability of versioned files """
1534.7.25 by Aaron Bentley
Added set_executability
728
        file_id = inv.path2id(path)
729
        new_executability = self._new_executability[trans_id]
730
        inv[file_id].executable = new_executability
731
        if supports_executable():
732
            abspath = self._tree.abspath(path)
733
            current_mode = os.stat(abspath).st_mode
734
            if new_executability:
735
                umask = os.umask(0)
736
                os.umask(umask)
737
                to_mode = current_mode | (0100 & ~umask)
738
                # Enable x-bit for others only if they can read it.
739
                if current_mode & 0004:
740
                    to_mode |= 0001 & ~umask
741
                if current_mode & 0040:
742
                    to_mode |= 0010 & ~umask
743
            else:
744
                to_mode = current_mode & ~0111
745
            os.chmod(abspath, to_mode)
746
1534.7.23 by Aaron Bentley
Transform.new_entry -> Transform._new_entry
747
    def _new_entry(self, name, parent_id, file_id):
1534.7.21 by Aaron Bentley
Updated docstrings
748
        """Helper function to create a new filesystem entry."""
1534.7.2 by Aaron Bentley
Added convenience function
749
        trans_id = self.create_path(name, parent_id)
750
        if file_id is not None:
751
            self.version_file(file_id, trans_id)
752
        return trans_id
753
1534.7.27 by Aaron Bentley
Added execute bit to new_file method
754
    def new_file(self, name, parent_id, contents, file_id=None, 
755
                 executable=None):
1534.7.156 by Aaron Bentley
PEP8 fixes
756
        """Convenience method to create files.
1534.7.21 by Aaron Bentley
Updated docstrings
757
        
758
        name is the name of the file to create.
759
        parent_id is the transaction id of the parent directory of the file.
760
        contents is an iterator of bytestrings, which will be used to produce
761
        the file.
762
        file_id is the inventory ID of the file, if it is to be versioned.
763
        """
1534.7.23 by Aaron Bentley
Transform.new_entry -> Transform._new_entry
764
        trans_id = self._new_entry(name, parent_id, file_id)
1534.7.20 by Aaron Bentley
Added directory handling
765
        self.create_file(contents, trans_id)
1534.7.27 by Aaron Bentley
Added execute bit to new_file method
766
        if executable is not None:
767
            self.set_executability(executable, trans_id)
1534.7.20 by Aaron Bentley
Added directory handling
768
        return trans_id
769
770
    def new_directory(self, name, parent_id, file_id=None):
1534.7.156 by Aaron Bentley
PEP8 fixes
771
        """Convenience method to create directories.
1534.7.21 by Aaron Bentley
Updated docstrings
772
773
        name is the name of the directory to create.
774
        parent_id is the transaction id of the parent directory of the
775
        directory.
776
        file_id is the inventory ID of the directory, if it is to be versioned.
777
        """
1534.7.23 by Aaron Bentley
Transform.new_entry -> Transform._new_entry
778
        trans_id = self._new_entry(name, parent_id, file_id)
1534.7.20 by Aaron Bentley
Added directory handling
779
        self.create_directory(trans_id)
780
        return trans_id 
781
1534.7.22 by Aaron Bentley
Added symlink support
782
    def new_symlink(self, name, parent_id, target, file_id=None):
1534.7.156 by Aaron Bentley
PEP8 fixes
783
        """Convenience method to create symbolic link.
1534.7.22 by Aaron Bentley
Added symlink support
784
        
785
        name is the name of the symlink to create.
786
        parent_id is the transaction id of the parent directory of the symlink.
787
        target is a bytestring of the target of the symlink.
788
        file_id is the inventory ID of the file, if it is to be versioned.
789
        """
1534.7.23 by Aaron Bentley
Transform.new_entry -> Transform._new_entry
790
        trans_id = self._new_entry(name, parent_id, file_id)
1534.7.22 by Aaron Bentley
Added symlink support
791
        self.create_symlink(target, trans_id)
792
        return trans_id
793
1534.7.32 by Aaron Bentley
Got conflict handling working when conflicts involve existing files
794
def joinpath(parent, child):
1534.7.40 by Aaron Bentley
Updated docs
795
    """Join tree-relative paths, handling the tree root specially"""
1534.7.32 by Aaron Bentley
Got conflict handling working when conflicts involve existing files
796
    if parent is None or parent == "":
797
        return child
798
    else:
1534.7.166 by Aaron Bentley
Swapped os.path.join for pathjoin everywhere
799
        return pathjoin(parent, child)
1534.7.1 by Aaron Bentley
Got creation of a versioned file working
800
1534.7.167 by Aaron Bentley
PEP8 and comment cleanups
801
1534.7.1 by Aaron Bentley
Got creation of a versioned file working
802
class FinalPaths(object):
1534.7.156 by Aaron Bentley
PEP8 fixes
803
    """Make path calculation cheap by memoizing paths.
1534.7.21 by Aaron Bentley
Updated docstrings
804
805
    The underlying tree must not be manipulated between calls, or else
806
    the results will likely be incorrect.
807
    """
1534.7.132 by Aaron Bentley
Got cooked conflicts working
808
    def __init__(self, transform):
1534.7.1 by Aaron Bentley
Got creation of a versioned file working
809
        object.__init__(self)
810
        self._known_paths = {}
1534.7.33 by Aaron Bentley
Fixed naming
811
        self.transform = transform
1534.7.1 by Aaron Bentley
Got creation of a versioned file working
812
813
    def _determine_path(self, trans_id):
1534.7.132 by Aaron Bentley
Got cooked conflicts working
814
        if trans_id == self.transform.root:
1534.7.1 by Aaron Bentley
Got creation of a versioned file working
815
            return ""
1534.7.33 by Aaron Bentley
Fixed naming
816
        name = self.transform.final_name(trans_id)
817
        parent_id = self.transform.final_parent(trans_id)
1534.7.132 by Aaron Bentley
Got cooked conflicts working
818
        if parent_id == self.transform.root:
1534.7.1 by Aaron Bentley
Got creation of a versioned file working
819
            return name
820
        else:
1534.7.166 by Aaron Bentley
Swapped os.path.join for pathjoin everywhere
821
            return pathjoin(self.get_path(parent_id), name)
1534.7.1 by Aaron Bentley
Got creation of a versioned file working
822
823
    def get_path(self, trans_id):
1534.7.157 by Aaron Bentley
Added more docs
824
        """Find the final path associated with a trans_id"""
1534.7.1 by Aaron Bentley
Got creation of a versioned file working
825
        if trans_id not in self._known_paths:
826
            self._known_paths[trans_id] = self._determine_path(trans_id)
827
        return self._known_paths[trans_id]
1534.7.28 by Aaron Bentley
Nearly-working build_tree replacement
828
1534.7.30 by Aaron Bentley
Factored out topological id sorting
829
def topology_sorted_ids(tree):
1534.7.40 by Aaron Bentley
Updated docs
830
    """Determine the topological order of the ids in a tree"""
1534.7.30 by Aaron Bentley
Factored out topological id sorting
831
    file_ids = list(tree)
832
    file_ids.sort(key=tree.id2path)
833
    return file_ids
1534.7.28 by Aaron Bentley
Nearly-working build_tree replacement
834
1534.7.165 by Aaron Bentley
Switched to build_tree instead of revert
835
def build_tree(tree, wt):
1534.7.40 by Aaron Bentley
Updated docs
836
    """Create working tree for a branch, using a Transaction."""
1534.7.28 by Aaron Bentley
Nearly-working build_tree replacement
837
    file_trans_id = {}
838
    tt = TreeTransform(wt)
839
    try:
840
        file_trans_id[wt.get_root_id()] = tt.get_id_tree(wt.get_root_id())
1534.7.30 by Aaron Bentley
Factored out topological id sorting
841
        file_ids = topology_sorted_ids(tree)
1534.7.29 by Aaron Bentley
Got build passing all tests
842
        for file_id in file_ids:
1534.7.28 by Aaron Bentley
Nearly-working build_tree replacement
843
            entry = tree.inventory[file_id]
844
            if entry.parent_id is None:
845
                continue
846
            if entry.parent_id not in file_trans_id:
847
                raise repr(entry.parent_id)
848
            parent_id = file_trans_id[entry.parent_id]
1534.7.47 by Aaron Bentley
Started work on 'revert'
849
            file_trans_id[file_id] = new_by_entry(tt, entry, parent_id, tree)
850
        tt.apply()
851
    finally:
852
        tt.finalize()
853
854
def new_by_entry(tt, entry, parent_id, tree):
1534.7.157 by Aaron Bentley
Added more docs
855
    """Create a new file according to its inventory entry"""
1534.7.47 by Aaron Bentley
Started work on 'revert'
856
    name = entry.name
857
    kind = entry.kind
858
    if kind == 'file':
1534.7.79 by Aaron Bentley
Stopped calling get_file_lines on WorkingTree
859
        contents = tree.get_file(entry.file_id).readlines()
1534.7.47 by Aaron Bentley
Started work on 'revert'
860
        executable = tree.is_executable(entry.file_id)
861
        return tt.new_file(name, parent_id, contents, entry.file_id, 
862
                           executable)
863
    elif kind == 'directory':
1534.7.54 by Aaron Bentley
Fixed thinko
864
        return tt.new_directory(name, parent_id, entry.file_id)
1534.7.47 by Aaron Bentley
Started work on 'revert'
865
    elif kind == 'symlink':
866
        target = entry.get_symlink_target(file_id)
867
        return tt.new_symlink(name, parent_id, target, file_id)
868
1534.7.117 by Aaron Bentley
Simplified permission handling of existing files in transform.
869
def create_by_entry(tt, entry, tree, trans_id, lines=None, mode_id=None):
1534.7.157 by Aaron Bentley
Added more docs
870
    """Create new file contents according to an inventory entry."""
1534.7.47 by Aaron Bentley
Started work on 'revert'
871
    if entry.kind == "file":
1534.7.97 by Aaron Bentley
Ensured foo.BASE is a directory if there's a conflict
872
        if lines == None:
873
            lines = tree.get_file(entry.file_id).readlines()
1534.7.117 by Aaron Bentley
Simplified permission handling of existing files in transform.
874
        tt.create_file(lines, trans_id, mode_id=mode_id)
1534.7.47 by Aaron Bentley
Started work on 'revert'
875
    elif entry.kind == "symlink":
1534.7.101 by Aaron Bentley
Got conflicts on symlinks working properly
876
        tt.create_symlink(tree.get_symlink_target(entry.file_id), trans_id)
1534.7.47 by Aaron Bentley
Started work on 'revert'
877
    elif entry.kind == "directory":
1534.7.51 by Aaron Bentley
New approach to revert
878
        tt.create_directory(trans_id)
1534.7.47 by Aaron Bentley
Started work on 'revert'
879
1534.7.89 by Aaron Bentley
Handle all content types in three-way
880
def create_entry_executability(tt, entry, trans_id):
1534.7.157 by Aaron Bentley
Added more docs
881
    """Set the executability of a trans_id according to an inventory entry"""
1534.7.89 by Aaron Bentley
Handle all content types in three-way
882
    if entry.kind == "file":
883
        tt.set_executability(entry.executable, trans_id)
1534.7.47 by Aaron Bentley
Started work on 'revert'
884
1534.7.157 by Aaron Bentley
Added more docs
885
1534.7.55 by Aaron Bentley
Fixed up the change detection
886
def find_interesting(working_tree, target_tree, filenames):
1534.7.157 by Aaron Bentley
Added more docs
887
    """Find the ids corresponding to specified filenames."""
1534.7.55 by Aaron Bentley
Fixed up the change detection
888
    if not filenames:
889
        interesting_ids = None
890
    else:
891
        interesting_ids = set()
1534.7.118 by Aaron Bentley
Dirty merge of the mainline
892
        for tree_path in filenames:
1534.7.55 by Aaron Bentley
Fixed up the change detection
893
            for tree in (working_tree, target_tree):
894
                not_found = True
895
                file_id = tree.inventory.path2id(tree_path)
896
                if file_id is not None:
897
                    interesting_ids.add(file_id)
898
                    not_found = False
899
                if not_found:
1534.7.123 by Aaron Bentley
Fixed handling of unversioned files
900
                    raise NotVersionedError(path=tree_path)
1534.7.55 by Aaron Bentley
Fixed up the change detection
901
    return interesting_ids
902
903
1534.7.56 by Aaron Bentley
Implemented the backup file detritus
904
def change_entry(tt, file_id, working_tree, target_tree, 
905
                 get_trans_id, backups, trans_id):
1534.7.157 by Aaron Bentley
Added more docs
906
    """Replace a file_id's contents with those from a target tree."""
1534.7.55 by Aaron Bentley
Fixed up the change detection
907
    e_trans_id = get_trans_id(file_id)
908
    entry = target_tree.inventory[file_id]
909
    has_contents, contents_mod, meta_mod, = _entry_changes(file_id, entry, 
910
                                                           working_tree)
911
    if contents_mod:
1534.7.117 by Aaron Bentley
Simplified permission handling of existing files in transform.
912
        mode_id = e_trans_id
1534.7.55 by Aaron Bentley
Fixed up the change detection
913
        if has_contents:
1534.7.56 by Aaron Bentley
Implemented the backup file detritus
914
            if not backups:
915
                tt.delete_contents(e_trans_id)
916
            else:
917
                parent_trans_id = get_trans_id(entry.parent_id)
918
                tt.adjust_path(entry.name+"~", parent_trans_id, e_trans_id)
919
                tt.unversion_file(e_trans_id)
920
                e_trans_id = tt.create_path(entry.name, parent_trans_id)
921
                tt.version_file(file_id, e_trans_id)
922
                trans_id[file_id] = e_trans_id
1534.7.117 by Aaron Bentley
Simplified permission handling of existing files in transform.
923
        create_by_entry(tt, entry, target_tree, e_trans_id, mode_id=mode_id)
1534.7.89 by Aaron Bentley
Handle all content types in three-way
924
        create_entry_executability(tt, entry, e_trans_id)
925
1534.7.55 by Aaron Bentley
Fixed up the change detection
926
    elif meta_mod:
1534.7.58 by abentley
Fixed executability bug
927
        tt.set_executability(entry.executable, e_trans_id)
1534.7.55 by Aaron Bentley
Fixed up the change detection
928
    if tt.final_name(e_trans_id) != entry.name:
929
        adjust_path  = True
930
    else:
931
        parent_id = tt.final_parent(e_trans_id)
932
        parent_file_id = tt.final_file_id(parent_id)
933
        if parent_file_id != entry.parent_id:
934
            adjust_path = True
935
        else:
936
            adjust_path = False
937
    if adjust_path:
1534.7.56 by Aaron Bentley
Implemented the backup file detritus
938
        parent_trans_id = get_trans_id(entry.parent_id)
939
        tt.adjust_path(entry.name, parent_trans_id, e_trans_id)
1534.7.55 by Aaron Bentley
Fixed up the change detection
940
941
942
def _entry_changes(file_id, entry, working_tree):
1534.7.156 by Aaron Bentley
PEP8 fixes
943
    """Determine in which ways the inventory entry has changed.
1534.7.55 by Aaron Bentley
Fixed up the change detection
944
945
    Returns booleans: has_contents, content_mod, meta_mod
946
    has_contents means there are currently contents, but they differ
947
    contents_mod means contents need to be modified
948
    meta_mod means the metadata needs to be modified
949
    """
950
    cur_entry = working_tree.inventory[file_id]
951
    try:
952
        working_kind = working_tree.kind(file_id)
953
        has_contents = True
954
    except OSError, e:
955
        if e.errno != errno.ENOENT:
956
            raise
957
        has_contents = False
958
        contents_mod = True
959
        meta_mod = False
960
    if has_contents is True:
961
        real_e_kind = entry.kind
962
        if real_e_kind == 'root_directory':
963
            real_e_kind = 'directory'
964
        if real_e_kind != working_kind:
965
            contents_mod, meta_mod = True, False
966
        else:
967
            cur_entry._read_tree_state(working_tree.id2path(file_id), 
968
                                       working_tree)
969
            contents_mod, meta_mod = entry.detect_changes(cur_entry)
970
    return has_contents, contents_mod, meta_mod
971
1534.7.56 by Aaron Bentley
Implemented the backup file detritus
972
973
def revert(working_tree, target_tree, filenames, backups=False):
1534.7.157 by Aaron Bentley
Added more docs
974
    """Revert a working tree's contents to those of a target tree."""
1534.7.55 by Aaron Bentley
Fixed up the change detection
975
    interesting_ids = find_interesting(working_tree, target_tree, filenames)
976
    def interesting(file_id):
977
        return interesting_ids is None or file_id in interesting_ids
978
1534.7.47 by Aaron Bentley
Started work on 'revert'
979
    tt = TreeTransform(working_tree)
980
    try:
1534.7.51 by Aaron Bentley
New approach to revert
981
        trans_id = {}
982
        def get_trans_id(file_id):
1534.7.47 by Aaron Bentley
Started work on 'revert'
983
            try:
1534.7.51 by Aaron Bentley
New approach to revert
984
                return trans_id[file_id]
1534.7.47 by Aaron Bentley
Started work on 'revert'
985
            except KeyError:
1534.7.51 by Aaron Bentley
New approach to revert
986
                return tt.get_id_tree(file_id)
987
988
        for file_id in topology_sorted_ids(target_tree):
1534.7.55 by Aaron Bentley
Fixed up the change detection
989
            if not interesting(file_id):
1534.7.51 by Aaron Bentley
New approach to revert
990
                continue
1534.7.52 by Aaron Bentley
Revert fixes with disappearing roots
991
            if file_id not in working_tree.inventory:
1534.7.51 by Aaron Bentley
New approach to revert
992
                entry = target_tree.inventory[file_id]
993
                parent_id = get_trans_id(entry.parent_id)
994
                e_trans_id = new_by_entry(tt, entry, parent_id, target_tree)
995
                trans_id[file_id] = e_trans_id
1534.7.47 by Aaron Bentley
Started work on 'revert'
996
            else:
1534.7.55 by Aaron Bentley
Fixed up the change detection
997
                change_entry(tt, file_id, working_tree, target_tree, 
1534.7.56 by Aaron Bentley
Implemented the backup file detritus
998
                             get_trans_id, backups, trans_id)
1534.7.51 by Aaron Bentley
New approach to revert
999
        for file_id in working_tree:
1534.7.55 by Aaron Bentley
Fixed up the change detection
1000
            if not interesting(file_id):
1001
                continue
1002
            if file_id not in target_tree:
1534.7.52 by Aaron Bentley
Revert fixes with disappearing roots
1003
                tt.unversion_file(tt.get_id_tree(file_id))
1534.7.170 by Aaron Bentley
Cleaned up filesystem conflict handling
1004
        conflicts = resolve_conflicts(tt)
1534.7.28 by Aaron Bentley
Nearly-working build_tree replacement
1005
        tt.apply()
1006
    finally:
1007
        tt.finalize()
1534.7.51 by Aaron Bentley
New approach to revert
1008
1534.7.57 by Aaron Bentley
Enhanced conflict resolution.
1009
1534.9.1 by Aaron Bentley
Added progress bars to merge
1010
def resolve_conflicts(tt, pb=DummyProgress()):
1534.7.57 by Aaron Bentley
Enhanced conflict resolution.
1011
    """Make many conflict-resolution attempts, but die if they fail"""
1534.7.169 by Aaron Bentley
Add filesystem/inventory conflicts to conflict output
1012
    new_conflicts = set()
1534.9.1 by Aaron Bentley
Added progress bars to merge
1013
    try:
1014
        for n in range(10):
1015
            pb.update('Resolution pass', n+1, 10)
1016
            conflicts = tt.find_conflicts()
1017
            if len(conflicts) == 0:
1018
                return new_conflicts
1019
            new_conflicts.update(conflict_pass(tt, conflicts))
1020
        raise MalformedTransform(conflicts=conflicts)
1021
    finally:
1022
        pb.clear()
1534.7.57 by Aaron Bentley
Enhanced conflict resolution.
1023
1024
1025
def conflict_pass(tt, conflicts):
1534.7.157 by Aaron Bentley
Added more docs
1026
    """Resolve some classes of conflicts."""
1534.7.169 by Aaron Bentley
Add filesystem/inventory conflicts to conflict output
1027
    new_conflicts = set()
1534.7.61 by Aaron Bentley
Handled parent loops, missing parents, unversioned parents
1028
    for c_type, conflict in ((c[0], c) for c in conflicts):
1029
        if c_type == 'duplicate id':
1534.7.51 by Aaron Bentley
New approach to revert
1030
            tt.unversion_file(conflict[1])
1534.7.170 by Aaron Bentley
Cleaned up filesystem conflict handling
1031
            new_conflicts.add((c_type, 'Unversioned existing file',
1032
                               conflict[1], conflict[2], ))
1534.7.61 by Aaron Bentley
Handled parent loops, missing parents, unversioned parents
1033
        elif c_type == 'duplicate':
1534.7.57 by Aaron Bentley
Enhanced conflict resolution.
1034
            # files that were renamed take precedence
1035
            new_name = tt.final_name(conflict[1])+'.moved'
1036
            final_parent = tt.final_parent(conflict[1])
1037
            if tt.path_changed(conflict[1]):
1038
                tt.adjust_path(new_name, final_parent, conflict[2])
1534.7.171 by Aaron Bentley
Implemented stringifying filesystem conflicts
1039
                new_conflicts.add((c_type, 'Moved existing file to', 
1040
                                   conflict[2], conflict[1]))
1534.7.57 by Aaron Bentley
Enhanced conflict resolution.
1041
            else:
1042
                tt.adjust_path(new_name, final_parent, conflict[1])
1534.7.171 by Aaron Bentley
Implemented stringifying filesystem conflicts
1043
                new_conflicts.add((c_type, 'Moved existing file to', 
1044
                                  conflict[1], conflict[2]))
1534.7.61 by Aaron Bentley
Handled parent loops, missing parents, unversioned parents
1045
        elif c_type == 'parent loop':
1046
            # break the loop by undoing one of the ops that caused the loop
1047
            cur = conflict[1]
1048
            while not tt.path_changed(cur):
1049
                cur = tt.final_parent(cur)
1534.7.170 by Aaron Bentley
Cleaned up filesystem conflict handling
1050
            new_conflicts.add((c_type, 'Cancelled move', cur,
1051
                               tt.final_parent(cur),))
1534.7.61 by Aaron Bentley
Handled parent loops, missing parents, unversioned parents
1052
            tt.adjust_path(tt.final_name(cur), tt.get_tree_parent(cur), cur)
1534.7.169 by Aaron Bentley
Add filesystem/inventory conflicts to conflict output
1053
            
1534.7.61 by Aaron Bentley
Handled parent loops, missing parents, unversioned parents
1054
        elif c_type == 'missing parent':
1534.7.128 by Aaron Bentley
Got missing contents test working
1055
            trans_id = conflict[1]
1056
            try:
1057
                tt.cancel_deletion(trans_id)
1534.7.169 by Aaron Bentley
Add filesystem/inventory conflicts to conflict output
1058
                new_conflicts.add((c_type, 'Not deleting', trans_id))
1534.7.128 by Aaron Bentley
Got missing contents test working
1059
            except KeyError:
1060
                tt.create_directory(trans_id)
1534.7.169 by Aaron Bentley
Add filesystem/inventory conflicts to conflict output
1061
                new_conflicts.add((c_type, 'Created directory.', trans_id))
1534.7.61 by Aaron Bentley
Handled parent loops, missing parents, unversioned parents
1062
        elif c_type == 'unversioned parent':
1534.7.148 by Aaron Bentley
Handled the remaining file versioning case
1063
            tt.version_file(tt.inactive_file_id(conflict[1]), conflict[1])
1534.7.171 by Aaron Bentley
Implemented stringifying filesystem conflicts
1064
            new_conflicts.add((c_type, 'Versioned directory', conflict[1]))
1534.7.169 by Aaron Bentley
Add filesystem/inventory conflicts to conflict output
1065
    return new_conflicts
1066
1067
def cook_conflicts(raw_conflicts, tt):
1534.7.170 by Aaron Bentley
Cleaned up filesystem conflict handling
1068
    """Generate a list of cooked conflicts, sorted by file path"""
1069
    def key(conflict):
1070
        if conflict[2] is not None:
1071
            return conflict[2], conflict[0]
1072
        elif len(conflict) == 6:
1073
            return conflict[4], conflict[0]
1074
        else:
1075
            return None, conflict[0]
1076
1077
    return sorted(list(iter_cook_conflicts(raw_conflicts, tt)), key=key)
1534.7.169 by Aaron Bentley
Add filesystem/inventory conflicts to conflict output
1078
1079
def iter_cook_conflicts(raw_conflicts, tt):
1080
    cooked_conflicts = []
1534.7.170 by Aaron Bentley
Cleaned up filesystem conflict handling
1081
    fp = FinalPaths(tt)
1534.7.169 by Aaron Bentley
Add filesystem/inventory conflicts to conflict output
1082
    for conflict in raw_conflicts:
1083
        c_type = conflict[0]
1534.7.170 by Aaron Bentley
Cleaned up filesystem conflict handling
1084
        action = conflict[1]
1085
        modified_path = fp.get_path(conflict[2])
1086
        modified_id = tt.final_file_id(conflict[2])
1087
        if len(conflict) == 3:
1088
            yield c_type, action, modified_path, modified_id
1089
        else:
1090
            conflicting_path = fp.get_path(conflict[3])
1091
            conflicting_id = tt.final_file_id(conflict[3])
1092
            yield (c_type, action, modified_path, modified_id, 
1093
                   conflicting_path, conflicting_id)
1534.7.171 by Aaron Bentley
Implemented stringifying filesystem conflicts
1094
1095
1096
def conflicts_strings(conflicts):
1097
    """Generate strings for the provided conflicts"""
1098
    for conflict in conflicts:
1099
        conflict_type = conflict[0]
1100
        if conflict_type == 'text conflict':
1101
            yield 'Text conflict in %s' % conflict[2]
1102
        elif conflict_type == 'contents conflict':
1103
            yield 'Contents conflict in %s' % conflict[2]
1104
        elif conflict_type == 'path conflict':
1105
            yield 'Path conflict: %s / %s' % conflict[2:]
1106
        elif conflict_type == 'duplicate id':
1107
            vals = (conflict[4], conflict[1], conflict[2])
1108
            yield 'Conflict adding id to %s.  %s %s.' % vals
1109
        elif conflict_type == 'duplicate':
1110
            vals = (conflict[4], conflict[1], conflict[2])
1111
            yield 'Conflict adding file %s.  %s %s.' % vals
1112
        elif conflict_type == 'parent loop':
1113
            vals = (conflict[4], conflict[2], conflict[1])
1114
            yield 'Conflict moving %s into %s.  %s.' % vals
1115
        elif conflict_type == 'unversioned parent':
1116
            vals = (conflict[2], conflict[1])
1117
            yield 'Conflict adding versioned files to %s.  %s.' % vals
1118
        elif conflict_type == 'missing parent':
1119
            vals = (conflict[2], conflict[1])
1120
            yield 'Conflict adding files to %s.  %s.' % vals