1
# Copyright (C) 2018-2020 Jelmer Vernooij
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.
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.
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
17
"""Convenience functions for efficiently making changes to a working tree.
19
If possible, uses inotify to track changes in the tree - providing
20
high performance in large trees with a small number of changes.
23
from __future__ import absolute_import
30
from .clean_tree import iter_deletables
31
from .errors import BzrError, DependencyNotPresent
32
from .trace import warning
33
from .transform import revert
34
from .workingtree import WorkingTree
37
class WorkspaceDirty(BzrError):
38
_fmt = "The directory %(path)s has pending changes."
40
def __init__(self, tree):
41
BzrError(self, path=tree.abspath('.'))
44
# TODO(jelmer): Move to .clean_tree?
45
def reset_tree(local_tree, subpath=''):
46
"""Reset a tree back to its basis tree.
48
This will leave ignored and detritus files alone.
51
local_tree: tree to work on
52
subpath: Subpath to operate on
54
revert(local_tree, local_tree.branch.basis_tree(),
55
[subpath] if subpath not in ('.', '') else None)
56
deletables = list(iter_deletables(
57
local_tree, unknown=True, ignored=False, detritus=False))
58
delete_items(deletables)
61
# TODO(jelmer): Move to .clean_tree?
62
def check_clean_tree(local_tree):
63
"""Check that a tree is clean and has no pending changes or unknown files.
66
local_tree: The tree to check
68
PendingChanges: When there are pending changes
70
# Just check there are no changes to begin with
71
if local_tree.has_changes():
72
raise WorkspaceDirty(local_tree)
73
if list(local_tree.unknowns()):
74
raise WorkspaceDirty(local_tree)
77
def delete_items(deletables, dry_run: bool = False):
78
"""Delete files in the deletables iterable"""
79
def onerror(function, path, excinfo):
80
"""Show warning for errors seen by rmtree.
82
# Handle only permission error while removing files.
83
# Other errors are re-raised.
84
if function is not os.remove or excinfo[1].errno != errno.EACCES:
86
warnings.warn('unable to remove %s' % path)
87
for path, subp in deletables:
88
if os.path.isdir(path):
89
shutil.rmtree(path, onerror=onerror)
94
# We handle only permission error here
95
if e.errno != errno.EACCES:
97
warning('unable to remove "%s": %s.', path, e.strerror)
100
def get_dirty_tracker(local_tree, subpath='', use_inotify=None):
101
"""Create a dirty tracker object."""
102
if use_inotify is True:
103
from .dirty_tracker import DirtyTracker
104
return DirtyTracker(local_tree, subpath)
105
elif use_inotify is False:
109
from .dirty_tracker import DirtyTracker
110
except DependencyNotPresent:
113
return DirtyTracker(local_tree, subpath)
116
class Workspace(object):
117
"""Create a workspace.
119
:param tree: Tree to work in
120
:param subpath: path under which to consider and commit changes
121
:param use_inotify: whether to use inotify (default: yes, if available)
124
def __init__(self, tree, subpath='', use_inotify=None):
126
self.subpath = subpath
127
self.use_inotify = use_inotify
128
self._dirty_tracker = None
131
def from_path(cls, path, use_inotify=None):
132
tree, subpath = WorkingTree.open_containing(path)
133
return cls(tree, subpath, use_inotify=use_inotify)
136
check_clean_tree(self.tree)
137
self._dirty_tracker = get_dirty_tracker(
138
self.tree, subpath=self.subpath, use_inotify=self.use_inotify)
141
def __exit__(self, exc_type, exc_val, exc_tb):
142
if self._dirty_tracker:
143
del self._dirty_tracker
144
self._dirty_tracker = None
147
def tree_path(self, path=''):
148
"""Return a path relative to the tree subpath used by this workspace.
150
return os.path.join(self.subpath, path)
152
def abspath(self, path=''):
153
"""Return an absolute path for the tree."""
154
return self.tree.abspath(self.tree_path(path))
157
"""Reset - revert local changes, revive deleted files, remove added.
159
if self._dirty_tracker and not self._dirty_tracker.is_dirty():
161
reset_tree(self.tree, self.subpath)
162
if self._dirty_tracker is not None:
163
self._dirty_tracker.mark_clean()
166
if self._dirty_tracker:
167
relpaths = self._dirty_tracker.relpaths()
168
# Sort paths so that directories get added before the files they
169
# contain (on VCSes where it matters)
171
[p for p in sorted(relpaths)
172
if self.tree.has_filename(p) and not
173
self.tree.is_ignored(p)])
176
if self.tree.is_versioned(p)]
178
self.tree.smart_add([self.tree.abspath(self.subpath)])
179
return [self.subpath] if self.subpath else None
181
def iter_changes(self):
182
with self.tree.lock_write():
183
specific_files = self._stage()
184
basis_tree = self.tree.basis_tree()
185
# TODO(jelmer): After Python 3.3, use 'yield from'
186
for change in self.tree.iter_changes(
187
basis_tree, specific_files=specific_files,
188
want_unversioned=False, require_versioned=True):
189
if change.kind[1] is None and change.versioned[1]:
190
if change.path[0] is None:
193
change = change.discard_new()
196
def commit(self, **kwargs):
199
See WorkingTree.commit() for documentation.
201
if 'specific_files' in kwargs:
202
raise NotImplementedError(self.commit)
204
with self.tree.lock_write():
205
specific_files = self._stage()
207
if self.tree.supports_setting_file_ids():
208
from .rename_map import RenameMap
209
basis_tree = self.tree.basis_tree()
210
RenameMap.guess_renames(
211
basis_tree, self.tree, dry_run=False)
213
kwargs['specific_files'] = specific_files
214
revid = self.tree.commit(**kwargs)
215
if self._dirty_tracker:
216
self._dirty_tracker.mark_clean()