Merge lp:~jelmer/brz/dirty-tracker into lp:brz/3.1
- dirty-tracker
- Merge into 3.1
Proposed by
Jelmer Vernooij
Status: | Merged |
---|---|
Approved by: | Jelmer Vernooij |
Approved revision: | no longer in the source branch. |
Merge reported by: | The Breezy Bot |
Merged at revision: | not available |
Proposed branch: | lp:~jelmer/brz/dirty-tracker |
Merge into: | lp:brz/3.1 |
Diff against target: |
706 lines (+635/-1) 8 files modified
breezy/dirty_tracker.py (+108/-0) breezy/tests/__init__.py (+2/-0) breezy/tests/test_dirty_tracker.py (+98/-0) breezy/tests/test_workspace.py (+204/-0) breezy/workspace.py (+217/-0) byov.conf (+1/-1) doc/en/release-notes/brz-3.1.txt (+4/-0) setup.py (+1/-0) |
To merge this branch: | bzr merge lp:~jelmer/brz/dirty-tracker |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Jelmer Vernooij | Approve | ||
Review via email: mp+388160@code.launchpad.net |
Commit message
Add a Workspace module for efficient rapid changes to large working trees.
Description of the change
Add a Workspace module.
To post a comment you must log in.
Revision history for this message
Jelmer Vernooij (jelmer) : | # |
review:
Approve
Revision history for this message
The Breezy Bot (the-breezy-bot) wrote : | # |
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === added file 'breezy/dirty_tracker.py' |
2 | --- breezy/dirty_tracker.py 1970-01-01 00:00:00 +0000 |
3 | +++ breezy/dirty_tracker.py 2020-07-27 22:52:34 +0000 |
4 | @@ -0,0 +1,108 @@ |
5 | +#!/usr/bin/python3 |
6 | +# Copyright (C) 2019 Jelmer Vernooij |
7 | +# |
8 | +# This program is free software; you can redistribute it and/or modify |
9 | +# it under the terms of the GNU General Public License as published by |
10 | +# the Free Software Foundation; either version 2 of the License, or |
11 | +# (at your option) any later version. |
12 | +# |
13 | +# This program is distributed in the hope that it will be useful, |
14 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
15 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
16 | +# GNU General Public License for more details. |
17 | +# |
18 | +# You should have received a copy of the GNU General Public License |
19 | +# along with this program; if not, write to the Free Software |
20 | +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
21 | + |
22 | +"""Track whether a directory structure was touched since last revision. |
23 | +""" |
24 | + |
25 | +from __future__ import absolute_import |
26 | + |
27 | +# TODO(jelmer): Add support for ignore files |
28 | + |
29 | +import os |
30 | +try: |
31 | + from pyinotify import ( |
32 | + WatchManager, |
33 | + IN_CREATE, |
34 | + IN_CLOSE_WRITE, |
35 | + IN_Q_OVERFLOW, |
36 | + IN_DELETE, |
37 | + IN_MOVED_TO, |
38 | + IN_MOVED_FROM, |
39 | + IN_ATTRIB, |
40 | + ProcessEvent, |
41 | + Notifier, |
42 | + Event, |
43 | + ) |
44 | +except ImportError as e: |
45 | + from .errors import DependencyNotPresent |
46 | + raise DependencyNotPresent(library='pyinotify', error=e) |
47 | + |
48 | + |
49 | +MASK = ( |
50 | + IN_CLOSE_WRITE | IN_DELETE | IN_Q_OVERFLOW | IN_MOVED_TO | IN_MOVED_FROM | |
51 | + IN_ATTRIB) |
52 | + |
53 | + |
54 | +class _Process(ProcessEvent): |
55 | + |
56 | + def my_init(self): |
57 | + self.paths = set() |
58 | + self.created = set() |
59 | + |
60 | + def process_default(self, event): |
61 | + path = os.path.join(event.path, event.name) |
62 | + if event.mask & IN_CREATE: |
63 | + self.created.add(path) |
64 | + self.paths.add(path) |
65 | + if event.mask & IN_DELETE and path in self.created: |
66 | + self.paths.remove(path) |
67 | + self.created.remove(path) |
68 | + |
69 | + |
70 | +class DirtyTracker(object): |
71 | + """Track the changes to (part of) a working tree.""" |
72 | + |
73 | + def __init__(self, tree, subpath='.'): |
74 | + self._tree = tree |
75 | + self._wm = WatchManager() |
76 | + self._process = _Process() |
77 | + self._notifier = Notifier(self._wm, self._process) |
78 | + self._notifier.coalesce_events(True) |
79 | + |
80 | + def check_excluded(p): |
81 | + return tree.is_control_filename(tree.relpath(p)) |
82 | + self._wdd = self._wm.add_watch( |
83 | + tree.abspath(subpath), MASK, rec=True, auto_add=True, |
84 | + exclude_filter=check_excluded) |
85 | + |
86 | + def _process_pending(self): |
87 | + if self._notifier.check_events(timeout=0): |
88 | + self._notifier.read_events() |
89 | + self._notifier.process_events() |
90 | + |
91 | + def __del__(self): |
92 | + self._notifier.stop() |
93 | + |
94 | + def mark_clean(self): |
95 | + """Mark the subtree as not having any changes.""" |
96 | + self._process_pending() |
97 | + self._process.paths.clear() |
98 | + self._process.created.clear() |
99 | + |
100 | + def is_dirty(self): |
101 | + """Check whether there are any changes.""" |
102 | + self._process_pending() |
103 | + return bool(self._process.paths) |
104 | + |
105 | + def paths(self): |
106 | + """Return the paths that have changed.""" |
107 | + self._process_pending() |
108 | + return self._process.paths |
109 | + |
110 | + def relpaths(self): |
111 | + """Return the paths relative to the tree root that changed.""" |
112 | + return set(self._tree.relpath(p) for p in self.paths()) |
113 | |
114 | === modified file 'breezy/tests/__init__.py' |
115 | --- breezy/tests/__init__.py 2020-06-28 19:19:25 +0000 |
116 | +++ breezy/tests/__init__.py 2020-07-27 22:52:34 +0000 |
117 | @@ -4057,6 +4057,7 @@ |
118 | 'breezy.tests.test_debug', |
119 | 'breezy.tests.test_diff', |
120 | 'breezy.tests.test_directory_service', |
121 | + 'breezy.tests.test_dirty_tracker', |
122 | 'breezy.tests.test_email_message', |
123 | 'breezy.tests.test_eol_filters', |
124 | 'breezy.tests.test_errors', |
125 | @@ -4181,6 +4182,7 @@ |
126 | 'breezy.tests.test_views', |
127 | 'breezy.tests.test_whitebox', |
128 | 'breezy.tests.test_win32utils', |
129 | + 'breezy.tests.test_workspace', |
130 | 'breezy.tests.test_workingtree', |
131 | 'breezy.tests.test_wsgi', |
132 | ] |
133 | |
134 | === added file 'breezy/tests/test_dirty_tracker.py' |
135 | --- breezy/tests/test_dirty_tracker.py 1970-01-01 00:00:00 +0000 |
136 | +++ breezy/tests/test_dirty_tracker.py 2020-07-27 22:52:34 +0000 |
137 | @@ -0,0 +1,98 @@ |
138 | +#!/usr/bin/python |
139 | +# Copyright (C) 2019 Jelmer Vernooij |
140 | +# |
141 | +# This program is free software; you can redistribute it and/or modify |
142 | +# it under the terms of the GNU General Public License as published by |
143 | +# the Free Software Foundation; either version 2 of the License, or |
144 | +# (at your option) any later version. |
145 | +# |
146 | +# This program is distributed in the hope that it will be useful, |
147 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
148 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
149 | +# GNU General Public License for more details. |
150 | +# |
151 | +# You should have received a copy of the GNU General Public License |
152 | +# along with this program; if not, write to the Free Software |
153 | +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
154 | + |
155 | +"""Tests for lintian_brush.dirty_tracker.""" |
156 | + |
157 | +import os |
158 | + |
159 | +from breezy.tests import ( |
160 | + TestCaseWithTransport, |
161 | + ) |
162 | + |
163 | + |
164 | +class DirtyTrackerTests(TestCaseWithTransport): |
165 | + |
166 | + def setUp(self): |
167 | + super(DirtyTrackerTests, self).setUp() |
168 | + self.tree = self.make_branch_and_tree('tree') |
169 | + try: |
170 | + from lintian_brush.dirty_tracker import DirtyTracker |
171 | + except ImportError: |
172 | + self.skipTest('pyinotify not available') |
173 | + self.tracker = DirtyTracker(self.tree) |
174 | + |
175 | + def test_nothing_changes(self): |
176 | + self.assertFalse(self.tracker.is_dirty()) |
177 | + |
178 | + def test_regular_file_added(self): |
179 | + self.build_tree_contents([('tree/foo', 'bar')]) |
180 | + self.assertTrue(self.tracker.is_dirty()) |
181 | + self.assertEqual(self.tracker.relpaths(), set(['foo'])) |
182 | + |
183 | + def test_many_added(self): |
184 | + self.build_tree_contents( |
185 | + [('tree/f%d' % d, 'content') for d in range(100)]) |
186 | + self.assertTrue(self.tracker.is_dirty()) |
187 | + self.assertEqual( |
188 | + self.tracker.relpaths(), set(['f%d' % d for d in range(100)])) |
189 | + |
190 | + def test_regular_file_in_subdir_added(self): |
191 | + self.build_tree_contents([('tree/foo/', ), ('tree/foo/blah', 'bar')]) |
192 | + self.assertTrue(self.tracker.is_dirty()) |
193 | + self.assertEqual(self.tracker.relpaths(), set(['foo', 'foo/blah'])) |
194 | + |
195 | + def test_directory_added(self): |
196 | + self.build_tree_contents([('tree/foo/', )]) |
197 | + self.assertTrue(self.tracker.is_dirty()) |
198 | + self.assertEqual(self.tracker.relpaths(), set(['foo'])) |
199 | + |
200 | + def test_file_removed(self): |
201 | + self.build_tree_contents([('tree/foo', 'foo')]) |
202 | + self.assertTrue(self.tracker.is_dirty()) |
203 | + self.tracker.mark_clean() |
204 | + self.build_tree_contents([('tree/foo', 'bar')]) |
205 | + self.assertTrue(self.tracker.is_dirty()) |
206 | + self.assertEqual(self.tracker.relpaths(), set(['foo'])) |
207 | + |
208 | + def test_control_file(self): |
209 | + self.tree.commit('Some change') |
210 | + self.assertFalse(self.tracker.is_dirty()) |
211 | + self.assertEqual(self.tracker.relpaths(), set([])) |
212 | + |
213 | + def test_renamed(self): |
214 | + self.build_tree_contents([('tree/foo', 'bar')]) |
215 | + self.tracker.mark_clean() |
216 | + self.assertFalse(self.tracker.is_dirty()) |
217 | + os.rename('tree/foo', 'tree/bar') |
218 | + self.assertTrue(self.tracker.is_dirty()) |
219 | + self.assertEqual(self.tracker.relpaths(), set(['foo', 'bar'])) |
220 | + |
221 | + def test_deleted(self): |
222 | + self.build_tree_contents([('tree/foo', 'bar')]) |
223 | + self.tracker.mark_clean() |
224 | + self.assertFalse(self.tracker.is_dirty()) |
225 | + os.unlink('tree/foo') |
226 | + self.assertTrue(self.tracker.is_dirty(), self.tracker._process.paths) |
227 | + self.assertEqual(self.tracker.relpaths(), set(['foo'])) |
228 | + |
229 | + def test_added_then_deleted(self): |
230 | + self.tracker.mark_clean() |
231 | + self.assertFalse(self.tracker.is_dirty()) |
232 | + self.build_tree_contents([('tree/foo', 'bar')]) |
233 | + os.unlink('tree/foo') |
234 | + self.assertFalse(self.tracker.is_dirty()) |
235 | + self.assertEqual(self.tracker.relpaths(), set([])) |
236 | |
237 | === added file 'breezy/tests/test_workspace.py' |
238 | --- breezy/tests/test_workspace.py 1970-01-01 00:00:00 +0000 |
239 | +++ breezy/tests/test_workspace.py 2020-07-27 22:52:34 +0000 |
240 | @@ -0,0 +1,204 @@ |
241 | +# Copyright (C) 2020 Jelmer Vernooij <jelmer@jelmer.uk> |
242 | +# |
243 | +# This program is free software; you can redistribute it and/or modify |
244 | +# it under the terms of the GNU General Public License as published by |
245 | +# the Free Software Foundation; either version 2 of the License, or |
246 | +# (at your option) any later version. |
247 | +# |
248 | +# This program is distributed in the hope that it will be useful, |
249 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
250 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
251 | +# GNU General Public License for more details. |
252 | +# |
253 | +# You should have received a copy of the GNU General Public License |
254 | +# along with this program; if not, write to the Free Software |
255 | +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
256 | + |
257 | +import os |
258 | + |
259 | +from . import ( |
260 | + TestCaseWithTransport, |
261 | + multiply_scenarios, |
262 | + ) |
263 | +from .scenarios import load_tests_apply_scenarios |
264 | + |
265 | +from ..workspace import ( |
266 | + WorkspaceDirty, |
267 | + Workspace, |
268 | + check_clean_tree, |
269 | + ) |
270 | + |
271 | + |
272 | +load_tests = load_tests_apply_scenarios |
273 | + |
274 | + |
275 | +class CheckCleanTreeTests(TestCaseWithTransport): |
276 | + |
277 | + def make_test_tree(self, format=None): |
278 | + tree = self.make_branch_and_tree('.', format=format) |
279 | + self.build_tree_contents([ |
280 | + ('debian/', ), |
281 | + ('debian/control', """\ |
282 | +Source: blah |
283 | +Vcs-Git: https://example.com/blah |
284 | +Testsuite: autopkgtest |
285 | + |
286 | +Binary: blah |
287 | +Arch: all |
288 | + |
289 | +"""), |
290 | + ('debian/changelog', 'Some contents')]) |
291 | + tree.add(['debian', 'debian/changelog', 'debian/control']) |
292 | + tree.commit('Initial thingy.') |
293 | + return tree |
294 | + |
295 | + def test_pending_changes(self): |
296 | + tree = self.make_test_tree() |
297 | + self.build_tree_contents([('debian/changelog', 'blah')]) |
298 | + with tree.lock_write(): |
299 | + self.assertRaises( |
300 | + WorkspaceDirty, check_clean_tree, tree) |
301 | + |
302 | + def test_pending_changes_bzr_empty_dir(self): |
303 | + # See https://bugs.debian.org/914038 |
304 | + tree = self.make_test_tree(format='bzr') |
305 | + self.build_tree_contents([('debian/upstream/', )]) |
306 | + with tree.lock_write(): |
307 | + self.assertRaises( |
308 | + WorkspaceDirty, check_clean_tree, tree) |
309 | + |
310 | + def test_pending_changes_git_empty_dir(self): |
311 | + # See https://bugs.debian.org/914038 |
312 | + tree = self.make_test_tree(format='git') |
313 | + self.build_tree_contents([('debian/upstream/', )]) |
314 | + with tree.lock_write(): |
315 | + check_clean_tree(tree) |
316 | + |
317 | + def test_pending_changes_git_dir_with_ignored(self): |
318 | + # See https://bugs.debian.org/914038 |
319 | + tree = self.make_test_tree(format='git') |
320 | + self.build_tree_contents([ |
321 | + ('debian/upstream/', ), |
322 | + ('debian/upstream/blah', ''), |
323 | + ('.gitignore', 'blah\n'), |
324 | + ]) |
325 | + tree.add('.gitignore') |
326 | + tree.commit('add gitignore') |
327 | + with tree.lock_write(): |
328 | + check_clean_tree(tree) |
329 | + |
330 | + def test_extra(self): |
331 | + tree = self.make_test_tree() |
332 | + self.build_tree_contents([('debian/foo', 'blah')]) |
333 | + with tree.lock_write(): |
334 | + self.assertRaises( |
335 | + WorkspaceDirty, check_clean_tree, |
336 | + tree) |
337 | + |
338 | + |
339 | +def vary_by_inotify(): |
340 | + return [ |
341 | + ('with_inotify', dict(_use_inotify=True)), |
342 | + ('without_inotify', dict(_use_inotify=False)), |
343 | + ] |
344 | + |
345 | + |
346 | +def vary_by_format(): |
347 | + return [ |
348 | + ('bzr', dict(_format='bzr')), |
349 | + ('git', dict(_format='git')), |
350 | + ] |
351 | + |
352 | + |
353 | +class WorkspaceTests(TestCaseWithTransport): |
354 | + |
355 | + scenarios = multiply_scenarios( |
356 | + vary_by_inotify(), |
357 | + vary_by_format(), |
358 | + ) |
359 | + |
360 | + def test_root_add(self): |
361 | + tree = self.make_branch_and_tree('.', format=self._format) |
362 | + with Workspace(tree, use_inotify=self._use_inotify) as ws: |
363 | + self.build_tree_contents([('afile', 'somecontents')]) |
364 | + changes = [c for c in ws.iter_changes() if c.path[1] != ''] |
365 | + self.assertEqual(1, len(changes), changes) |
366 | + self.assertEqual((None, 'afile'), changes[0].path) |
367 | + ws.commit(message='Commit message') |
368 | + self.assertEqual(list(ws.iter_changes()), []) |
369 | + self.build_tree_contents([('afile', 'newcontents')]) |
370 | + [change] = list(ws.iter_changes()) |
371 | + self.assertEqual(('afile', 'afile'), change.path) |
372 | + |
373 | + def test_root_remove(self): |
374 | + tree = self.make_branch_and_tree('.', format=self._format) |
375 | + self.build_tree_contents([('afile', 'somecontents')]) |
376 | + tree.add(['afile']) |
377 | + tree.commit('Afile') |
378 | + with Workspace(tree, use_inotify=self._use_inotify) as ws: |
379 | + os.remove('afile') |
380 | + changes = list(ws.iter_changes()) |
381 | + self.assertEqual(1, len(changes), changes) |
382 | + self.assertEqual(('afile', None), changes[0].path) |
383 | + ws.commit(message='Commit message') |
384 | + self.assertEqual(list(ws.iter_changes()), []) |
385 | + |
386 | + def test_subpath_add(self): |
387 | + tree = self.make_branch_and_tree('.', format=self._format) |
388 | + self.build_tree(['subpath/']) |
389 | + tree.add('subpath') |
390 | + tree.commit('add subpath') |
391 | + with Workspace( |
392 | + tree, subpath='subpath', use_inotify=self._use_inotify) as ws: |
393 | + self.build_tree_contents([('outside', 'somecontents')]) |
394 | + self.build_tree_contents([('subpath/afile', 'somecontents')]) |
395 | + changes = [c for c in ws.iter_changes() if c.path[1] != 'subpath'] |
396 | + self.assertEqual(1, len(changes), changes) |
397 | + self.assertEqual((None, 'subpath/afile'), changes[0].path) |
398 | + ws.commit(message='Commit message') |
399 | + self.assertEqual(list(ws.iter_changes()), []) |
400 | + |
401 | + def test_dirty(self): |
402 | + tree = self.make_branch_and_tree('.', format=self._format) |
403 | + self.build_tree(['subpath']) |
404 | + self.assertRaises( |
405 | + WorkspaceDirty, Workspace(tree, use_inotify=self._use_inotify).__enter__) |
406 | + |
407 | + def test_reset(self): |
408 | + tree = self.make_branch_and_tree('.', format=self._format) |
409 | + with Workspace(tree, use_inotify=self._use_inotify) as ws: |
410 | + self.build_tree(['blah']) |
411 | + ws.reset() |
412 | + self.assertPathDoesNotExist('blah') |
413 | + |
414 | + def test_tree_path(self): |
415 | + tree = self.make_branch_and_tree('.', format=self._format) |
416 | + tree.mkdir('subdir') |
417 | + tree.commit('Add subdir') |
418 | + with Workspace(tree, use_inotify=self._use_inotify) as ws: |
419 | + self.assertEqual('foo', ws.tree_path('foo')) |
420 | + self.assertEqual('', ws.tree_path()) |
421 | + with Workspace(tree, subpath='subdir', use_inotify=self._use_inotify) as ws: |
422 | + self.assertEqual('subdir/foo', ws.tree_path('foo')) |
423 | + self.assertEqual('subdir/', ws.tree_path()) |
424 | + |
425 | + def test_abspath(self): |
426 | + tree = self.make_branch_and_tree('.', format=self._format) |
427 | + tree.mkdir('subdir') |
428 | + tree.commit('Add subdir') |
429 | + with Workspace(tree, use_inotify=self._use_inotify) as ws: |
430 | + self.assertEqual(tree.abspath('foo'), ws.abspath('foo')) |
431 | + self.assertEqual(tree.abspath(''), ws.abspath()) |
432 | + with Workspace(tree, subpath='subdir', use_inotify=self._use_inotify) as ws: |
433 | + self.assertEqual(tree.abspath('subdir/foo'), ws.abspath('foo')) |
434 | + self.assertEqual(tree.abspath('subdir') + '/', ws.abspath('')) |
435 | + self.assertEqual(tree.abspath('subdir') + '/', ws.abspath()) |
436 | + |
437 | + def test_open_containing(self): |
438 | + tree = self.make_branch_and_tree('.', format=self._format) |
439 | + tree.mkdir('subdir') |
440 | + tree.commit('Add subdir') |
441 | + ws = Workspace.from_path('subdir') |
442 | + self.assertEqual(ws.tree.abspath('.'), tree.abspath('.')) |
443 | + self.assertEqual(ws.subpath, 'subdir') |
444 | + self.assertEqual(None, ws.use_inotify) |
445 | |
446 | === added file 'breezy/workspace.py' |
447 | --- breezy/workspace.py 1970-01-01 00:00:00 +0000 |
448 | +++ breezy/workspace.py 2020-07-27 22:52:34 +0000 |
449 | @@ -0,0 +1,217 @@ |
450 | +# Copyright (C) 2018-2020 Jelmer Vernooij |
451 | +# |
452 | +# This program is free software; you can redistribute it and/or modify |
453 | +# it under the terms of the GNU General Public License as published by |
454 | +# the Free Software Foundation; either version 2 of the License, or |
455 | +# (at your option) any later version. |
456 | +# |
457 | +# This program is distributed in the hope that it will be useful, |
458 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
459 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
460 | +# GNU General Public License for more details. |
461 | +# |
462 | +# You should have received a copy of the GNU General Public License |
463 | +# along with this program; if not, write to the Free Software |
464 | +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
465 | + |
466 | +"""Convenience functions for efficiently making changes to a working tree. |
467 | + |
468 | +If possible, uses inotify to track changes in the tree - providing |
469 | +high performance in large trees with a small number of changes. |
470 | +""" |
471 | + |
472 | +from __future__ import absolute_import |
473 | + |
474 | +import errno |
475 | +import os |
476 | +import shutil |
477 | + |
478 | + |
479 | +from .clean_tree import iter_deletables |
480 | +from .errors import BzrError, DependencyNotPresent |
481 | +from .trace import warning |
482 | +from .transform import revert |
483 | +from .workingtree import WorkingTree |
484 | + |
485 | + |
486 | +class WorkspaceDirty(BzrError): |
487 | + _fmt = "The directory %(path)s has pending changes." |
488 | + |
489 | + def __init__(self, tree): |
490 | + BzrError(self, path=tree.abspath('.')) |
491 | + |
492 | + |
493 | +# TODO(jelmer): Move to .clean_tree? |
494 | +def reset_tree(local_tree, subpath=''): |
495 | + """Reset a tree back to its basis tree. |
496 | + |
497 | + This will leave ignored and detritus files alone. |
498 | + |
499 | + Args: |
500 | + local_tree: tree to work on |
501 | + subpath: Subpath to operate on |
502 | + """ |
503 | + revert(local_tree, local_tree.branch.basis_tree(), |
504 | + [subpath] if subpath not in ('.', '') else None) |
505 | + deletables = list(iter_deletables( |
506 | + local_tree, unknown=True, ignored=False, detritus=False)) |
507 | + delete_items(deletables) |
508 | + |
509 | + |
510 | +# TODO(jelmer): Move to .clean_tree? |
511 | +def check_clean_tree(local_tree): |
512 | + """Check that a tree is clean and has no pending changes or unknown files. |
513 | + |
514 | + Args: |
515 | + local_tree: The tree to check |
516 | + Raises: |
517 | + PendingChanges: When there are pending changes |
518 | + """ |
519 | + # Just check there are no changes to begin with |
520 | + if local_tree.has_changes(): |
521 | + raise WorkspaceDirty(local_tree) |
522 | + if list(local_tree.unknowns()): |
523 | + raise WorkspaceDirty(local_tree) |
524 | + |
525 | + |
526 | +def delete_items(deletables, dry_run: bool = False): |
527 | + """Delete files in the deletables iterable""" |
528 | + def onerror(function, path, excinfo): |
529 | + """Show warning for errors seen by rmtree. |
530 | + """ |
531 | + # Handle only permission error while removing files. |
532 | + # Other errors are re-raised. |
533 | + if function is not os.remove or excinfo[1].errno != errno.EACCES: |
534 | + raise |
535 | + warnings.warn('unable to remove %s' % path) |
536 | + for path, subp in deletables: |
537 | + if os.path.isdir(path): |
538 | + shutil.rmtree(path, onerror=onerror) |
539 | + else: |
540 | + try: |
541 | + os.unlink(path) |
542 | + except OSError as e: |
543 | + # We handle only permission error here |
544 | + if e.errno != errno.EACCES: |
545 | + raise e |
546 | + warning('unable to remove "%s": %s.', path, e.strerror) |
547 | + |
548 | + |
549 | +def get_dirty_tracker(local_tree, subpath='', use_inotify=None): |
550 | + """Create a dirty tracker object.""" |
551 | + if use_inotify is True: |
552 | + from .dirty_tracker import DirtyTracker |
553 | + return DirtyTracker(local_tree, subpath) |
554 | + elif use_inotify is False: |
555 | + return None |
556 | + else: |
557 | + try: |
558 | + from .dirty_tracker import DirtyTracker |
559 | + except DependencyNotPresent: |
560 | + return None |
561 | + else: |
562 | + return DirtyTracker(local_tree, subpath) |
563 | + |
564 | + |
565 | +class Workspace(object): |
566 | + """Create a workspace. |
567 | + |
568 | + :param tree: Tree to work in |
569 | + :param subpath: path under which to consider and commit changes |
570 | + :param use_inotify: whether to use inotify (default: yes, if available) |
571 | + """ |
572 | + |
573 | + def __init__(self, tree, subpath='', use_inotify=None): |
574 | + self.tree = tree |
575 | + self.subpath = subpath |
576 | + self.use_inotify = use_inotify |
577 | + self._dirty_tracker = None |
578 | + |
579 | + @classmethod |
580 | + def from_path(cls, path, use_inotify=None): |
581 | + tree, subpath = WorkingTree.open_containing(path) |
582 | + return cls(tree, subpath, use_inotify=use_inotify) |
583 | + |
584 | + def __enter__(self): |
585 | + check_clean_tree(self.tree) |
586 | + self._dirty_tracker = get_dirty_tracker( |
587 | + self.tree, subpath=self.subpath, use_inotify=self.use_inotify) |
588 | + return self |
589 | + |
590 | + def __exit__(self, exc_type, exc_val, exc_tb): |
591 | + if self._dirty_tracker: |
592 | + del self._dirty_tracker |
593 | + self._dirty_tracker = None |
594 | + return False |
595 | + |
596 | + def tree_path(self, path=''): |
597 | + """Return a path relative to the tree subpath used by this workspace. |
598 | + """ |
599 | + return os.path.join(self.subpath, path) |
600 | + |
601 | + def abspath(self, path=''): |
602 | + """Return an absolute path for the tree.""" |
603 | + return self.tree.abspath(self.tree_path(path)) |
604 | + |
605 | + def reset(self): |
606 | + """Reset - revert local changes, revive deleted files, remove added. |
607 | + """ |
608 | + if self._dirty_tracker and not self._dirty_tracker.is_dirty(): |
609 | + return |
610 | + reset_tree(self.tree, self.subpath) |
611 | + if self._dirty_tracker is not None: |
612 | + self._dirty_tracker.mark_clean() |
613 | + |
614 | + def _stage(self): |
615 | + if self._dirty_tracker: |
616 | + relpaths = self._dirty_tracker.relpaths() |
617 | + # Sort paths so that directories get added before the files they |
618 | + # contain (on VCSes where it matters) |
619 | + self.tree.add( |
620 | + [p for p in sorted(relpaths) |
621 | + if self.tree.has_filename(p) and not |
622 | + self.tree.is_ignored(p)]) |
623 | + return [ |
624 | + p for p in relpaths |
625 | + if self.tree.is_versioned(p)] |
626 | + else: |
627 | + self.tree.smart_add([self.tree.abspath(self.subpath)]) |
628 | + return [self.subpath] if self.subpath else None |
629 | + |
630 | + def iter_changes(self): |
631 | + with self.tree.lock_write(): |
632 | + specific_files = self._stage() |
633 | + basis_tree = self.tree.basis_tree() |
634 | + # TODO(jelmer): After Python 3.3, use 'yield from' |
635 | + for change in self.tree.iter_changes( |
636 | + basis_tree, specific_files=specific_files, |
637 | + want_unversioned=False, require_versioned=True): |
638 | + if change.kind[1] is None and change.versioned[1]: |
639 | + if change.path[0] is None: |
640 | + continue |
641 | + # "missing" path |
642 | + change = change.discard_new() |
643 | + yield change |
644 | + |
645 | + def commit(self, **kwargs): |
646 | + """Create a commit. |
647 | + |
648 | + See WorkingTree.commit() for documentation. |
649 | + """ |
650 | + if 'specific_files' in kwargs: |
651 | + raise NotImplementedError(self.commit) |
652 | + |
653 | + with self.tree.lock_write(): |
654 | + specific_files = self._stage() |
655 | + |
656 | + if self.tree.supports_setting_file_ids(): |
657 | + from .rename_map import RenameMap |
658 | + basis_tree = self.tree.basis_tree() |
659 | + RenameMap.guess_renames( |
660 | + basis_tree, self.tree, dry_run=False) |
661 | + |
662 | + kwargs['specific_files'] = specific_files |
663 | + revid = self.tree.commit(**kwargs) |
664 | + if self._dirty_tracker: |
665 | + self._dirty_tracker.mark_clean() |
666 | + return revid |
667 | |
668 | === modified file 'byov.conf' |
669 | --- byov.conf 2020-05-30 01:48:46 +0000 |
670 | +++ byov.conf 2020-07-27 22:52:34 +0000 |
671 | @@ -30,7 +30,7 @@ |
672 | |
673 | # FIXME: Arguably this should be vm.build_deps=brz but it requires either an |
674 | # available package or at least a debian/ dir ? -- vila 2018-02-23 |
675 | -brz.build_deps = gcc, debhelper, python, python-all-dev, python3-all-dev, python-configobj, python3-configobj, python-docutils, python3-docutils, python-paramiko, python3-paramiko, python-subunit, python3-subunit, python-testtools, python3-testtools, subunit, python-pip, python3-pip, python-setuptools, python3-setuptools, python-flake8, python3-flake8, python-sphinx, python3-sphinx, python-launchpadlib, python3-launchpadlib |
676 | +brz.build_deps = gcc, debhelper, python, python-all-dev, python3-all-dev, python-configobj, python3-configobj, python-docutils, python3-docutils, python-paramiko, python3-paramiko, python-subunit, python3-subunit, python-testtools, python3-testtools, subunit, python-pip, python3-pip, python-setuptools, python3-setuptools, python-flake8, python3-flake8, python-sphinx, python3-sphinx, python-launchpadlib, python3-launchpadlib, python-pyinotify, python3-pyinotify |
677 | subunit.build_deps = python-testscenarios, python3-testscenarios, python-testtools, python3-testtools, cython, cython3, quilt |
678 | vm.packages = {brz.build_deps}, {subunit.build_deps}, bzr, git, python-junitxml |
679 | [brz-xenial] |
680 | |
681 | === modified file 'doc/en/release-notes/brz-3.1.txt' |
682 | --- doc/en/release-notes/brz-3.1.txt 2020-07-21 18:34:48 +0000 |
683 | +++ doc/en/release-notes/brz-3.1.txt 2020-07-27 22:52:34 +0000 |
684 | @@ -93,6 +93,10 @@ |
685 | is currently implemented for GitHub, GitLab and Launchpad. |
686 | (Jelmer Vernooij) |
687 | |
688 | + * A new ``Workspace`` interface is now available for efficiently |
689 | + making changes to large working trees from automation. |
690 | + (Jelmer Vernooij) |
691 | + |
692 | Testing |
693 | ******* |
694 | |
695 | |
696 | === modified file 'setup.py' |
697 | --- setup.py 2020-06-21 11:53:38 +0000 |
698 | +++ setup.py 2020-07-27 22:52:34 +0000 |
699 | @@ -75,6 +75,7 @@ |
700 | 'fastimport': [], |
701 | 'git': [], |
702 | 'launchpad': ['launchpadlib>=1.6.3'], |
703 | + 'workspace': ['pyinotify'], |
704 | }, |
705 | 'tests_require': [ |
706 | 'testtools', |
Running landing tests failed /ci.breezy- vcs.org/ job/brz- 3.1/job/ brz-3.1- land/250/
https:/