Merge lp:~jelmer/brz/dirty-tracker into lp:brz/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
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',

Subscribers

People subscribed via source and target branches