/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to breezy/workspace.py

  • Committer: Jelmer Vernooij
  • Date: 2020-07-28 02:11:05 UTC
  • mfrom: (7490.40.78 work)
  • mto: This revision was merged to the branch mainline in revision 7520.
  • Revision ID: jelmer@jelmer.uk-20200728021105-fzq7g6f8bl1g0aet
Merge lp:brz/3.1.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2018-2020 Jelmer Vernooij
 
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
"""Convenience functions for efficiently making changes to a working tree.
 
18
 
 
19
If possible, uses inotify to track changes in the tree - providing
 
20
high performance in large trees with a small number of changes.
 
21
"""
 
22
 
 
23
from __future__ import absolute_import
 
24
 
 
25
import errno
 
26
import os
 
27
import shutil
 
28
 
 
29
 
 
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
 
35
 
 
36
 
 
37
class WorkspaceDirty(BzrError):
 
38
    _fmt = "The directory %(path)s has pending changes."
 
39
 
 
40
    def __init__(self, tree):
 
41
        BzrError(self, path=tree.abspath('.'))
 
42
 
 
43
 
 
44
# TODO(jelmer): Move to .clean_tree?
 
45
def reset_tree(local_tree, subpath=''):
 
46
    """Reset a tree back to its basis tree.
 
47
 
 
48
    This will leave ignored and detritus files alone.
 
49
 
 
50
    Args:
 
51
      local_tree: tree to work on
 
52
      subpath: Subpath to operate on
 
53
    """
 
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)
 
59
 
 
60
 
 
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.
 
64
 
 
65
    Args:
 
66
      local_tree: The tree to check
 
67
    Raises:
 
68
      PendingChanges: When there are pending changes
 
69
    """
 
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)
 
75
 
 
76
 
 
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.
 
81
        """
 
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:
 
85
            raise
 
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)
 
90
        else:
 
91
            try:
 
92
                os.unlink(path)
 
93
            except OSError as e:
 
94
                # We handle only permission error here
 
95
                if e.errno != errno.EACCES:
 
96
                    raise e
 
97
                warning('unable to remove "%s": %s.', path, e.strerror)
 
98
 
 
99
 
 
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:
 
106
        return None
 
107
    else:
 
108
        try:
 
109
            from .dirty_tracker import DirtyTracker
 
110
        except DependencyNotPresent:
 
111
            return None
 
112
        else:
 
113
            return DirtyTracker(local_tree, subpath)
 
114
 
 
115
 
 
116
class Workspace(object):
 
117
    """Create a workspace.
 
118
 
 
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)
 
122
    """
 
123
 
 
124
    def __init__(self, tree, subpath='', use_inotify=None):
 
125
        self.tree = tree
 
126
        self.subpath = subpath
 
127
        self.use_inotify = use_inotify
 
128
        self._dirty_tracker = None
 
129
 
 
130
    @classmethod
 
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)
 
134
 
 
135
    def __enter__(self):
 
136
        check_clean_tree(self.tree)
 
137
        self._dirty_tracker = get_dirty_tracker(
 
138
            self.tree, subpath=self.subpath, use_inotify=self.use_inotify)
 
139
        return self
 
140
 
 
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
 
145
        return False
 
146
 
 
147
    def tree_path(self, path=''):
 
148
        """Return a path relative to the tree subpath used by this workspace.
 
149
        """
 
150
        return os.path.join(self.subpath, path)
 
151
 
 
152
    def abspath(self, path=''):
 
153
        """Return an absolute path for the tree."""
 
154
        return self.tree.abspath(self.tree_path(path))
 
155
 
 
156
    def reset(self):
 
157
        """Reset - revert local changes, revive deleted files, remove added.
 
158
        """
 
159
        if self._dirty_tracker and not self._dirty_tracker.is_dirty():
 
160
            return
 
161
        reset_tree(self.tree, self.subpath)
 
162
        if self._dirty_tracker is not None:
 
163
            self._dirty_tracker.mark_clean()
 
164
 
 
165
    def _stage(self):
 
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)
 
170
            self.tree.add(
 
171
                [p for p in sorted(relpaths)
 
172
                 if self.tree.has_filename(p) and not
 
173
                    self.tree.is_ignored(p)])
 
174
            return [
 
175
                p for p in relpaths
 
176
                if self.tree.is_versioned(p)]
 
177
        else:
 
178
            self.tree.smart_add([self.tree.abspath(self.subpath)])
 
179
            return [self.subpath] if self.subpath else None
 
180
 
 
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:
 
191
                        continue
 
192
                    # "missing" path
 
193
                    change = change.discard_new()
 
194
                yield change
 
195
 
 
196
    def commit(self, **kwargs):
 
197
        """Create a commit.
 
198
 
 
199
        See WorkingTree.commit() for documentation.
 
200
        """
 
201
        if 'specific_files' in kwargs:
 
202
            raise NotImplementedError(self.commit)
 
203
 
 
204
        with self.tree.lock_write():
 
205
            specific_files = self._stage()
 
206
 
 
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)
 
212
 
 
213
            kwargs['specific_files'] = specific_files
 
214
            revid = self.tree.commit(**kwargs)
 
215
            if self._dirty_tracker:
 
216
                self._dirty_tracker.mark_clean()
 
217
            return revid