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