1# Copyright (C) 2007-2012, 2016 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
17"""Tests for interface conformance of 'WorkingTree.remove'"""
18
19from breezy.tests.per_workingtree import TestCaseWithWorkingTree
20from breezy import ignores, osutils
21
22
23class TestRemove(TestCaseWithWorkingTree):
24    """Tests WorkingTree.remove"""
25
26    files = ['a', 'b/', 'b/c', 'd/']
27    rfiles = ['b/c', 'b', 'a', 'd']
28    backup_files = ['a.~1~', 'b.~1~/', 'b.~1~/c.~1~', 'd.~1~/']
29    backup_files_no_version_dirs = ['a.~1~', 'b.~1~/', 'b.~1~/c.~1~']
30
31    def get_tree(self, files):
32        tree = self.make_branch_and_tree('.')
33        self.build_tree(files)
34        self.assertPathExists(files)
35        return tree
36
37    def get_committed_tree(self, files, message="Committing"):
38        tree = self.get_tree(files)
39        tree.add(files)
40        tree.commit(message)
41        if not tree.has_versioned_directories():
42            self.assertInWorkingTree(
43                [f for f in files if not f.endswith("/")])
44            self.assertPathExists(files)
45        else:
46            self.assertInWorkingTree(files)
47        return tree
48
49    def assertRemovedAndDeleted(self, files):
50        self.assertNotInWorkingTree(files)
51        self.assertPathDoesNotExist(files)
52
53    def assertRemovedAndNotDeleted(self, files):
54        self.assertNotInWorkingTree(files)
55        self.assertPathExists(files)
56
57    def test_remove_keep(self):
58        """Check that files and directories are unversioned but not deleted."""
59        tree = self.get_tree(TestRemove.files)
60        tree.add(TestRemove.files)
61
62        tree.remove(TestRemove.files)
63        self.assertRemovedAndNotDeleted(TestRemove.files)
64
65    def test_remove_keep_subtree(self):
66        """Check that a directory is unversioned but not deleted."""
67        tree = self.make_branch_and_tree('.')
68        subtree = self.make_branch_and_tree('subtree')
69        subtree.commit('')
70        tree.add('subtree')
71
72        tree.remove('subtree')
73        self.assertRemovedAndNotDeleted('subtree')
74
75    def test_remove_unchanged_files(self):
76        """Check that unchanged files are removed and deleted."""
77        tree = self.get_committed_tree(TestRemove.files)
78        tree.remove(TestRemove.files, keep_files=False)
79        self.assertRemovedAndDeleted(TestRemove.files)
80        tree._validate()
81
82    def test_remove_added_files(self):
83        """Removal of newly added files must back them up."""
84        tree = self.get_tree(TestRemove.files)
85        tree.add(TestRemove.files)
86        tree.remove(TestRemove.files, keep_files=False)
87        self.assertNotInWorkingTree(TestRemove.files)
88        if tree.has_versioned_directories():
89            self.assertPathExists(TestRemove.backup_files)
90        else:
91            self.assertPathExists(TestRemove.backup_files_no_version_dirs)
92        tree._validate()
93
94    def test_remove_changed_file(self):
95        """Removal of changed files must back it up."""
96        tree = self.get_committed_tree(['a'])
97        self.build_tree_contents([('a', b"some other new content!")])
98        self.assertInWorkingTree('a')
99        tree.remove('a', keep_files=False)
100        self.assertNotInWorkingTree(TestRemove.files)
101        self.assertPathExists('a.~1~')
102        tree._validate()
103
104    def test_remove_deleted_files(self):
105        """Check that files are removed if they don't exist any more."""
106        tree = self.get_committed_tree(TestRemove.files)
107        for f in TestRemove.rfiles:
108            osutils.delete_any(f)
109        self.assertPathDoesNotExist(TestRemove.files)
110        tree.remove(TestRemove.files, keep_files=False)
111        self.assertRemovedAndDeleted(TestRemove.files)
112        tree._validate()
113
114    def test_remove_renamed_files(self):
115        """Check that files are removed even if they are renamed."""
116        tree = self.get_committed_tree(TestRemove.files)
117
118        for f in TestRemove.rfiles:
119            tree.rename_one(f, f + 'x')
120        rfilesx = ['bx/cx', 'bx', 'ax', 'dx']
121        self.assertPathExists(rfilesx)
122
123        tree.remove(rfilesx, keep_files=False)
124        self.assertRemovedAndDeleted(rfilesx)
125        tree._validate()
126
127    def test_remove_renamed_changed_files(self):
128        """Check that files that are renamed and changed are backed up."""
129        tree = self.get_committed_tree(TestRemove.files)
130
131        for f in TestRemove.rfiles:
132            tree.rename_one(f, f + 'x')
133        rfilesx = ['bx/cx', 'bx', 'ax', 'dx']
134        self.build_tree_contents([('ax', b'changed and renamed!'),
135                                  ('bx/cx', b'changed and renamed!')])
136        self.assertPathExists(rfilesx)
137
138        tree.remove(rfilesx, keep_files=False)
139        self.assertNotInWorkingTree(rfilesx)
140        self.assertPathExists(['bx.~1~/cx.~1~', 'bx.~1~', 'ax.~1~'])
141        if (tree.supports_rename_tracking() or
142                not tree.has_versioned_directories()):
143            self.assertPathDoesNotExist('dx.~1~')  # unchanged file
144        else:
145            self.assertPathExists('dx.~1~')  # renamed, so appears changed
146        tree._validate()
147
148    def test_force_remove_changed_files(self):
149        """Check that changed files are removed and deleted when forced."""
150        tree = self.get_tree(TestRemove.files)
151        tree.add(TestRemove.files)
152
153        tree.remove(TestRemove.files, keep_files=False, force=True)
154        self.assertRemovedAndDeleted(TestRemove.files)
155        self.assertPathDoesNotExist(['a.~1~', 'b.~1~/', 'b.~1~/c', 'd.~1~/'])
156        tree._validate()
157
158    def test_remove_unknown_files(self):
159        """Unknown files shuld be backed up"""
160        tree = self.get_tree(TestRemove.files)
161        tree.remove(TestRemove.files, keep_files=False)
162        self.assertRemovedAndDeleted(TestRemove.files)
163        if tree.has_versioned_directories():
164            self.assertPathExists(TestRemove.backup_files)
165        else:
166            self.assertPathExists(TestRemove.backup_files_no_version_dirs)
167        tree._validate()
168
169    def test_remove_nonexisting_files(self):
170        """Try to delete non-existing files."""
171        tree = self.get_tree(TestRemove.files)
172        tree.remove([''], keep_files=False)
173        tree.remove(['xyz', 'abc/def'], keep_files=False)
174        tree._validate()
175
176    def test_remove_unchanged_directory(self):
177        """Unchanged directories should be deleted."""
178        files = ['b/', 'b/c', 'b/sub_directory/', 'b/sub_directory/with_file']
179        tree = self.get_committed_tree(files)
180        tree.remove('b', keep_files=False)
181        self.assertRemovedAndDeleted('b')
182        tree._validate()
183
184    def test_remove_absent_directory(self):
185        """Removing a absent directory succeeds without corruption (#150438)."""
186        paths = ['a/', 'a/b']
187        tree = self.get_committed_tree(paths)
188        tree.controldir.root_transport.delete_tree('a')
189        tree.remove(['a'])
190        self.assertRemovedAndDeleted('b')
191        tree._validate()
192
193    def test_remove_unknown_ignored_files(self):
194        """Unknown ignored files should be deleted."""
195        tree = self.get_committed_tree(['b/'])
196        ignores.add_runtime_ignores(["*ignored*"])
197
198        self.build_tree(['unknown_ignored_file'])
199        self.assertNotEqual(None, tree.is_ignored('unknown_ignored_file'))
200        tree.remove('unknown_ignored_file', keep_files=False)
201        self.assertRemovedAndDeleted('unknown_ignored_file')
202
203        self.build_tree(['b/unknown_ignored_file', 'b/unknown_ignored_dir/'])
204        self.assertNotEqual(None, tree.is_ignored('b/unknown_ignored_file'))
205        self.assertNotEqual(None, tree.is_ignored('b/unknown_ignored_dir'))
206        tree.remove('b', keep_files=False)
207        self.assertRemovedAndDeleted('b')
208        tree._validate()
209
210    def test_remove_changed_ignored_files(self):
211        """Changed ignored files should be backed up."""
212        files = ['an_ignored_file']
213        tree = self.get_tree(files)
214        tree.add(files)
215        ignores.add_runtime_ignores(["*ignored*"])
216        self.assertNotEqual(None, tree.is_ignored(files[0]))
217
218        tree.remove(files, keep_files=False)
219        self.assertNotInWorkingTree(files)
220        self.assertPathExists('an_ignored_file.~1~')
221        tree._validate()
222
223    def test_dont_remove_directory_with_unknowns(self):
224        """Directories with unknowns should be backed up."""
225        directories = ['a/', 'b/', 'c/', 'c/c/', 'c/blah']
226        tree = self.get_committed_tree(directories)
227
228        self.build_tree(['a/unknown_file'])
229        tree.remove('a', keep_files=False)
230        self.assertPathExists('a.~1~/unknown_file')
231
232        self.build_tree(['b/unknown_directory'])
233        tree.remove('b', keep_files=False)
234        self.assertPathExists('b.~1~/unknown_directory')
235
236        self.build_tree(['c/c/unknown_file'])
237        tree.remove('c/c', keep_files=False)
238        self.assertPathExists('c/c.~1~/unknown_file')
239
240        tree.remove('c', keep_files=False)
241        self.assertPathExists('c.~1~/')
242
243        self.assertNotInWorkingTree(directories)
244        tree._validate()
245
246    def test_force_remove_directory_with_unknowns(self):
247        """Unchanged non-empty directories should be deleted when forced."""
248        files = ['b/', 'b/c']
249        tree = self.get_committed_tree(files)
250
251        other_files = ['b/unknown_file', 'b/sub_directory/',
252                       'b/sub_directory/with_file', 'b/sub_directory/sub_directory/']
253        self.build_tree(other_files)
254
255        self.assertInWorkingTree(files)
256        self.assertPathExists(files)
257
258        tree.remove('b', keep_files=False, force=True)
259
260        self.assertRemovedAndDeleted(files)
261        self.assertRemovedAndDeleted(other_files)
262        tree._validate()
263
264    def test_remove_directory_with_changed_file(self):
265        """Backup directories with changed files."""
266        files = ['b/', 'b/c']
267        tree = self.get_committed_tree(files)
268        self.build_tree_contents([('b/c', b"some other new content!")])
269
270        tree.remove('b', keep_files=False)
271        self.assertPathExists('b.~1~/c.~1~')
272        self.assertNotInWorkingTree(files)
273
274    def test_remove_force_directory_with_changed_file(self):
275        """Delete directories with changed files when forced."""
276        files = ['b/', 'b/c']
277        tree = self.get_committed_tree(files)
278        self.build_tree_contents([('b/c', b"some other new content!")])
279
280        # see if we can force it now..
281        tree.remove('b', keep_files=False, force=True)
282        self.assertRemovedAndDeleted(files)
283        tree._validate()
284
285    def test_remove_directory_with_changed_emigrated_file(self):
286        # As per bug #129880
287        tree = self.make_branch_and_tree('.')
288        self.build_tree_contents(
289            [('somedir/',), (b'somedir/file', b'contents')])
290        tree.add(['somedir', 'somedir/file'])
291        tree.commit(message="first")
292        self.build_tree_contents([('somedir/file', b'changed')])
293        tree.rename_one('somedir/file', 'moved-file')
294        tree.remove('somedir', keep_files=False)
295        self.assertNotInWorkingTree('somedir')
296        self.assertPathDoesNotExist('somedir')
297        self.assertInWorkingTree('moved-file')
298        self.assertPathExists('moved-file')
299
300    def test_remove_directory_with_renames(self):
301        """Delete directory with renames in or out."""
302
303        files = ['a/', 'a/file', 'a/directory/', 'a/directory/stuff', 'b/']
304        files_to_move = ['a/file', 'a/directory/']
305
306        tree = self.get_committed_tree(files)
307        # move stuff from a=>b
308        tree.move(['a/file', 'a/directory'], to_dir='b')
309
310        moved_files = ['b/file', 'b/directory/']
311        self.assertRemovedAndDeleted(files_to_move)
312        self.assertInWorkingTree(moved_files)
313        self.assertPathExists(moved_files)
314
315        # check if it works with renames out
316        tree.remove('a', keep_files=False)
317        self.assertRemovedAndDeleted(['a/'])
318
319        # check if it works with renames in
320        tree.remove('b', keep_files=False)
321        self.assertRemovedAndDeleted(['b/'])
322        tree._validate()
323
324    def test_non_cwd(self):
325        tree = self.make_branch_and_tree('tree')
326        self.build_tree(['tree/dir/', 'tree/dir/file'])
327        tree.add(['dir', 'dir/file'])
328        tree.commit('add file')
329        tree.remove('dir/', keep_files=False)
330        self.assertPathDoesNotExist('tree/dir/file')
331        self.assertNotInWorkingTree('tree/dir/file', 'tree')
332        tree._validate()
333
334    def test_remove_uncommitted_removed_file(self):
335        # As per bug #152811
336        tree = self.get_committed_tree(['a'])
337        tree.remove('a', keep_files=False)
338        tree.remove('a', keep_files=False)
339        self.assertPathDoesNotExist('a')
340        tree._validate()
341
342    def test_remove_file_and_containing_dir(self):
343        tree = self.get_committed_tree(['config/', 'config/file'])
344        tree.remove('config/file', keep_files=False)
345        tree.remove('config', keep_files=False)
346        self.assertPathDoesNotExist('config/file')
347        self.assertPathDoesNotExist('config')
348        tree._validate()
349
350    def test_remove_dir_before_bzr(self):
351        # As per bug #272648. Note that a file must be present in the directory
352        # or the bug doesn't manifest itself.
353        tree = self.get_committed_tree(['.aaa/', '.aaa/file'])
354        tree.remove('.aaa/', keep_files=False)
355        self.assertPathDoesNotExist('.aaa/file')
356        self.assertPathDoesNotExist('.aaa')
357        tree._validate()
358