/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-05-24 00:39:50 UTC
  • mto: This revision was merged to the branch mainline in revision 7504.
  • Revision ID: jelmer@jelmer.uk-20200524003950-bbc545r76vc5yajg
Add github action.

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