/brz/remove-bazaar

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