/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 bzrlib/commit.py

Stop accepting non-existant files in commit (#50793)

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2005, 2006 Canonical Ltd
 
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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
16
 
 
17
 
 
18
# XXX: Can we do any better about making interrupted commits change
 
19
# nothing?  
 
20
 
 
21
# TODO: Separate 'prepare' phase where we find a list of potentially
 
22
# committed files.  We then can then pause the commit to prompt for a
 
23
# commit message, knowing the summary will be the same as what's
 
24
# actually used for the commit.  (But perhaps simpler to simply get
 
25
# the tree status, then use that for a selective commit?)
 
26
 
 
27
# The newly committed revision is going to have a shape corresponding
 
28
# to that of the working inventory.  Files that are not in the
 
29
# working tree and that were in the predecessor are reported as
 
30
# removed --- this can include files that were either removed from the
 
31
# inventory or deleted in the working tree.  If they were only
 
32
# deleted from disk, they are removed from the working inventory.
 
33
 
 
34
# We then consider the remaining entries, which will be in the new
 
35
# version.  Directory entries are simply copied across.  File entries
 
36
# must be checked to see if a new version of the file should be
 
37
# recorded.  For each parent revision inventory, we check to see what
 
38
# version of the file was present.  If the file was present in at
 
39
# least one tree, and if it was the same version in all the trees,
 
40
# then we can just refer to that version.  Otherwise, a new version
 
41
# representing the merger of the file versions must be added.
 
42
 
 
43
# TODO: Update hashcache before and after - or does the WorkingTree
 
44
# look after that?
 
45
 
 
46
# TODO: Rather than mashing together the ancestry and storing it back,
 
47
# perhaps the weave should have single method which does it all in one
 
48
# go, avoiding a lot of redundant work.
 
49
 
 
50
# TODO: Perhaps give a warning if one of the revisions marked as
 
51
# merged is already in the ancestry, and then don't record it as a
 
52
# distinct parent.
 
53
 
 
54
# TODO: If the file is newly merged but unchanged from the version it
 
55
# merges from, then it should still be reported as newly added
 
56
# relative to the basis revision.
 
57
 
 
58
# TODO: Do checks that the tree can be committed *before* running the 
 
59
# editor; this should include checks for a pointless commit and for 
 
60
# unknown or missing files.
 
61
 
 
62
# TODO: If commit fails, leave the message in a file somewhere.
 
63
 
 
64
# TODO: Change the parameter 'rev_id' to 'revision_id' to be consistent with
 
65
# the rest of the code; add a deprecation of the old name.
 
66
 
 
67
import os
 
68
import re
 
69
import sys
 
70
import time
 
71
 
 
72
from cStringIO import StringIO
 
73
 
 
74
from bzrlib import tree
 
75
import bzrlib.config
 
76
import bzrlib.errors as errors
 
77
from bzrlib.errors import (BzrError, PointlessCommit,
 
78
                           ConflictsInTree,
 
79
                           StrictCommitFailed
 
80
                           )
 
81
from bzrlib.osutils import (kind_marker, isdir,isfile, is_inside_any, 
 
82
                            is_inside_or_parent_of_any,
 
83
                            quotefn, sha_file, split_lines)
 
84
from bzrlib.testament import Testament
 
85
from bzrlib.trace import mutter, note, warning
 
86
from bzrlib.xml5 import serializer_v5
 
87
from bzrlib.inventory import Inventory, ROOT_ID, InventoryEntry
 
88
from bzrlib import symbol_versioning
 
89
from bzrlib.symbol_versioning import (deprecated_passed,
 
90
        deprecated_function,
 
91
        DEPRECATED_PARAMETER)
 
92
from bzrlib.workingtree import WorkingTree
 
93
 
 
94
 
 
95
class NullCommitReporter(object):
 
96
    """I report on progress of a commit."""
 
97
 
 
98
    def snapshot_change(self, change, path):
 
99
        pass
 
100
 
 
101
    def completed(self, revno, rev_id):
 
102
        pass
 
103
 
 
104
    def deleted(self, file_id):
 
105
        pass
 
106
 
 
107
    def escaped(self, escape_count, message):
 
108
        pass
 
109
 
 
110
    def missing(self, path):
 
111
        pass
 
112
 
 
113
    def renamed(self, change, old_path, new_path):
 
114
        pass
 
115
 
 
116
 
 
117
class ReportCommitToLog(NullCommitReporter):
 
118
 
 
119
    # this may be more useful if 'note' was replaced by an overridable
 
120
    # method on self, which would allow more trivial subclassing.
 
121
    # alternative, a callable could be passed in, allowing really trivial
 
122
    # reuse for some uis. RBC 20060511
 
123
 
 
124
    def snapshot_change(self, change, path):
 
125
        if change == 'unchanged':
 
126
            return
 
127
        note("%s %s", change, path)
 
128
 
 
129
    def completed(self, revno, rev_id):
 
130
        note('Committed revision %d.', revno)
 
131
    
 
132
    def deleted(self, file_id):
 
133
        note('deleted %s', file_id)
 
134
 
 
135
    def escaped(self, escape_count, message):
 
136
        note("replaced %d control characters in message", escape_count)
 
137
 
 
138
    def missing(self, path):
 
139
        note('missing %s', path)
 
140
 
 
141
    def renamed(self, change, old_path, new_path):
 
142
        note('%s %s => %s', change, old_path, new_path)
 
143
 
 
144
 
 
145
class Commit(object):
 
146
    """Task of committing a new revision.
 
147
 
 
148
    This is a MethodObject: it accumulates state as the commit is
 
149
    prepared, and then it is discarded.  It doesn't represent
 
150
    historical revisions, just the act of recording a new one.
 
151
 
 
152
            missing_ids
 
153
            Modified to hold a list of files that have been deleted from
 
154
            the working directory; these should be removed from the
 
155
            working inventory.
 
156
    """
 
157
    def __init__(self,
 
158
                 reporter=None,
 
159
                 config=None):
 
160
        if reporter is not None:
 
161
            self.reporter = reporter
 
162
        else:
 
163
            self.reporter = NullCommitReporter()
 
164
        if config is not None:
 
165
            self.config = config
 
166
        else:
 
167
            self.config = None
 
168
        
 
169
    def commit(self,
 
170
               branch=DEPRECATED_PARAMETER, message=None,
 
171
               timestamp=None,
 
172
               timezone=None,
 
173
               committer=None,
 
174
               specific_files=None,
 
175
               rev_id=None,
 
176
               allow_pointless=True,
 
177
               strict=False,
 
178
               verbose=False,
 
179
               revprops=None,
 
180
               working_tree=None,
 
181
               local=False,
 
182
               reporter=None,
 
183
               config=None):
 
184
        """Commit working copy as a new revision.
 
185
 
 
186
        branch -- the deprecated branch to commit to. New callers should pass in 
 
187
                  working_tree instead
 
188
 
 
189
        message -- the commit message, a mandatory parameter
 
190
 
 
191
        timestamp -- if not None, seconds-since-epoch for a
 
192
             postdated/predated commit.
 
193
 
 
194
        specific_files -- If true, commit only those files.
 
195
 
 
196
        rev_id -- If set, use this as the new revision id.
 
197
            Useful for test or import commands that need to tightly
 
198
            control what revisions are assigned.  If you duplicate
 
199
            a revision id that exists elsewhere it is your own fault.
 
200
            If null (default), a time/random revision id is generated.
 
201
 
 
202
        allow_pointless -- If true (default), commit even if nothing
 
203
            has changed and no merges are recorded.
 
204
 
 
205
        strict -- If true, don't allow a commit if the working tree
 
206
            contains unknown files.
 
207
 
 
208
        revprops -- Properties for new revision
 
209
        :param local: Perform a local only commit.
 
210
        """
 
211
        mutter('preparing to commit')
 
212
 
 
213
        if deprecated_passed(branch):
 
214
            symbol_versioning.warn("Commit.commit (branch, ...): The branch parameter is "
 
215
                 "deprecated as of bzr 0.8. Please use working_tree= instead.",
 
216
                 DeprecationWarning, stacklevel=2)
 
217
            self.branch = branch
 
218
            self.work_tree = self.branch.bzrdir.open_workingtree()
 
219
        elif working_tree is None:
 
220
            raise BzrError("One of branch and working_tree must be passed into commit().")
 
221
        else:
 
222
            self.work_tree = working_tree
 
223
            self.branch = self.work_tree.branch
 
224
        if message is None:
 
225
            raise BzrError("The message keyword parameter is required for commit().")
 
226
 
 
227
        self.bound_branch = None
 
228
        self.local = local
 
229
        self.master_branch = None
 
230
        self.master_locked = False
 
231
        self.rev_id = None
 
232
        self.specific_files = specific_files
 
233
        self.allow_pointless = allow_pointless
 
234
 
 
235
        if reporter is None and self.reporter is None:
 
236
            self.reporter = NullCommitReporter()
 
237
        elif reporter is not None:
 
238
            self.reporter = reporter
 
239
 
 
240
        self.work_tree.lock_write()
 
241
        self.pb = bzrlib.ui.ui_factory.nested_progress_bar()
 
242
        try:
 
243
            # Cannot commit with conflicts present.
 
244
            if len(self.work_tree.conflicts())>0:
 
245
                raise ConflictsInTree
 
246
 
 
247
            # setup the bound branch variables as needed.
 
248
            self._check_bound_branch()
 
249
 
 
250
            # check for out of date working trees
 
251
            try:
 
252
                first_tree_parent = self.work_tree.get_parent_ids()[0]
 
253
            except IndexError:
 
254
                # if there are no parents, treat our parent as 'None'
 
255
                # this is so that we still consier the master branch
 
256
                # - in a checkout scenario the tree may have no
 
257
                # parents but the branch may do.
 
258
                first_tree_parent = None
 
259
            master_last = self.master_branch.last_revision()
 
260
            if (master_last is not None and
 
261
                master_last != first_tree_parent):
 
262
                raise errors.OutOfDateTree(self.work_tree)
 
263
    
 
264
            if strict:
 
265
                # raise an exception as soon as we find a single unknown.
 
266
                for unknown in self.work_tree.unknowns():
 
267
                    raise StrictCommitFailed()
 
268
                   
 
269
            if self.config is None:
 
270
                self.config = self.branch.get_config()
 
271
      
 
272
            if isinstance(message, str):
 
273
                message = message.decode(bzrlib.user_encoding)
 
274
            assert isinstance(message, unicode), type(message)
 
275
            self.message = message
 
276
            self._escape_commit_message()
 
277
 
 
278
            self.work_inv = self.work_tree.inventory
 
279
            self.basis_tree = self.work_tree.basis_tree()
 
280
            self.basis_inv = self.basis_tree.inventory
 
281
            if specific_files is not None:
 
282
                # Ensure specified files are versioned
 
283
                # (We don't actually need the ids here)
 
284
                tree.find_ids_across_trees(specific_files, 
 
285
                                           [self.basis_tree, self.work_tree])
 
286
            # one to finish, one for rev and inventory, and one for each
 
287
            # inventory entry, and the same for the new inventory.
 
288
            # note that this estimate is too long when we do a partial tree
 
289
            # commit which excludes some new files from being considered.
 
290
            # The estimate is corrected when we populate the new inv.
 
291
            self.pb_total = len(self.work_inv) + 5
 
292
            self.pb_count = 0
 
293
 
 
294
            self._gather_parents()
 
295
            if len(self.parents) > 1 and self.specific_files:
 
296
                raise NotImplementedError('selected-file commit of merges is not supported yet: files %r',
 
297
                        self.specific_files)
 
298
            
 
299
            self.builder = self.branch.get_commit_builder(self.parents, 
 
300
                self.config, timestamp, timezone, committer, revprops, rev_id)
 
301
            
 
302
            self._remove_deleted()
 
303
            self._populate_new_inv()
 
304
            self._report_deletes()
 
305
 
 
306
            self._check_pointless()
 
307
 
 
308
            self._emit_progress_update()
 
309
            # TODO: Now the new inventory is known, check for conflicts and
 
310
            # prompt the user for a commit message.
 
311
            # ADHB 2006-08-08: If this is done, populate_new_inv should not add
 
312
            # weave lines, because nothing should be recorded until it is known
 
313
            # that commit will succeed.
 
314
            self.builder.finish_inventory()
 
315
            self._emit_progress_update()
 
316
            self.rev_id = self.builder.commit(self.message)
 
317
            self._emit_progress_update()
 
318
            # revision data is in the local branch now.
 
319
            
 
320
            # upload revision data to the master.
 
321
            # this will propagate merged revisions too if needed.
 
322
            if self.bound_branch:
 
323
                self.master_branch.repository.fetch(self.branch.repository,
 
324
                                                    revision_id=self.rev_id)
 
325
                # now the master has the revision data
 
326
                # 'commit' to the master first so a timeout here causes the local
 
327
                # branch to be out of date
 
328
                self.master_branch.append_revision(self.rev_id)
 
329
 
 
330
            # and now do the commit locally.
 
331
            self.branch.append_revision(self.rev_id)
 
332
 
 
333
            # if the builder gave us the revisiontree it created back, we
 
334
            # could use it straight away here.
 
335
            # TODO: implement this.
 
336
            self.work_tree.set_parent_trees([(self.rev_id,
 
337
                self.branch.repository.revision_tree(self.rev_id))])
 
338
            # now the work tree is up to date with the branch
 
339
            
 
340
            self.reporter.completed(self.branch.revno(), self.rev_id)
 
341
            if self.config.post_commit() is not None:
 
342
                hooks = self.config.post_commit().split(' ')
 
343
                # this would be nicer with twisted.python.reflect.namedAny
 
344
                for hook in hooks:
 
345
                    result = eval(hook + '(branch, rev_id)',
 
346
                                  {'branch':self.branch,
 
347
                                   'bzrlib':bzrlib,
 
348
                                   'rev_id':self.rev_id})
 
349
            self._emit_progress_update()
 
350
        finally:
 
351
            self._cleanup()
 
352
        return self.rev_id
 
353
 
 
354
    def _check_pointless(self):
 
355
        if self.allow_pointless:
 
356
            return
 
357
        # A merge with no effect on files
 
358
        if len(self.parents) > 1:
 
359
            return
 
360
        # work around the fact that a newly-initted tree does differ from its
 
361
        # basis
 
362
        if len(self.builder.new_inventory) != len(self.basis_inv):
 
363
            return
 
364
        if (len(self.builder.new_inventory) != 1 and
 
365
            self.builder.new_inventory != self.basis_inv):
 
366
            return
 
367
        raise PointlessCommit()
 
368
 
 
369
    def _check_bound_branch(self):
 
370
        """Check to see if the local branch is bound.
 
371
 
 
372
        If it is bound, then most of the commit will actually be
 
373
        done using the remote branch as the target branch.
 
374
        Only at the end will the local branch be updated.
 
375
        """
 
376
        if self.local and not self.branch.get_bound_location():
 
377
            raise errors.LocalRequiresBoundBranch()
 
378
 
 
379
        if not self.local:
 
380
            self.master_branch = self.branch.get_master_branch()
 
381
 
 
382
        if not self.master_branch:
 
383
            # make this branch the reference branch for out of date checks.
 
384
            self.master_branch = self.branch
 
385
            return
 
386
 
 
387
        # If the master branch is bound, we must fail
 
388
        master_bound_location = self.master_branch.get_bound_location()
 
389
        if master_bound_location:
 
390
            raise errors.CommitToDoubleBoundBranch(self.branch,
 
391
                    self.master_branch, master_bound_location)
 
392
 
 
393
        # TODO: jam 20051230 We could automatically push local
 
394
        #       commits to the remote branch if they would fit.
 
395
        #       But for now, just require remote to be identical
 
396
        #       to local.
 
397
        
 
398
        # Make sure the local branch is identical to the master
 
399
        master_rh = self.master_branch.revision_history()
 
400
        local_rh = self.branch.revision_history()
 
401
        if local_rh != master_rh:
 
402
            raise errors.BoundBranchOutOfDate(self.branch,
 
403
                    self.master_branch)
 
404
 
 
405
        # Now things are ready to change the master branch
 
406
        # so grab the lock
 
407
        self.bound_branch = self.branch
 
408
        self.master_branch.lock_write()
 
409
        self.master_locked = True
 
410
 
 
411
    def _cleanup(self):
 
412
        """Cleanup any open locks, progress bars etc."""
 
413
        cleanups = [self._cleanup_bound_branch,
 
414
                    self.work_tree.unlock,
 
415
                    self.pb.finished]
 
416
        found_exception = None
 
417
        for cleanup in cleanups:
 
418
            try:
 
419
                cleanup()
 
420
            # we want every cleanup to run no matter what.
 
421
            # so we have a catchall here, but we will raise the
 
422
            # last encountered exception up the stack: and
 
423
            # typically this will be useful enough.
 
424
            except Exception, e:
 
425
                found_exception = e
 
426
        if found_exception is not None: 
 
427
            # don't do a plan raise, because the last exception may have been
 
428
            # trashed, e is our sure-to-work exception even though it loses the
 
429
            # full traceback. XXX: RBC 20060421 perhaps we could check the
 
430
            # exc_info and if its the same one do a plain raise otherwise 
 
431
            # 'raise e' as we do now.
 
432
            raise e
 
433
 
 
434
    def _cleanup_bound_branch(self):
 
435
        """Executed at the end of a try/finally to cleanup a bound branch.
 
436
 
 
437
        If the branch wasn't bound, this is a no-op.
 
438
        If it was, it resents self.branch to the local branch, instead
 
439
        of being the master.
 
440
        """
 
441
        if not self.bound_branch:
 
442
            return
 
443
        if self.master_locked:
 
444
            self.master_branch.unlock()
 
445
 
 
446
    def _escape_commit_message(self):
 
447
        """Replace xml-incompatible control characters."""
 
448
        # FIXME: RBC 20060419 this should be done by the revision
 
449
        # serialiser not by commit. Then we can also add an unescaper
 
450
        # in the deserializer and start roundtripping revision messages
 
451
        # precisely. See repository_implementations/test_repository.py
 
452
        
 
453
        # Python strings can include characters that can't be
 
454
        # represented in well-formed XML; escape characters that
 
455
        # aren't listed in the XML specification
 
456
        # (http://www.w3.org/TR/REC-xml/#NT-Char).
 
457
        self.message, escape_count = re.subn(
 
458
            u'[^\x09\x0A\x0D\u0020-\uD7FF\uE000-\uFFFD]+',
 
459
            lambda match: match.group(0).encode('unicode_escape'),
 
460
            self.message)
 
461
        if escape_count:
 
462
            self.reporter.escaped(escape_count, self.message)
 
463
 
 
464
    def _gather_parents(self):
 
465
        """Record the parents of a merge for merge detection."""
 
466
        # TODO: Make sure that this list doesn't contain duplicate 
 
467
        # entries and the order is preserved when doing this.
 
468
        self.parents = self.work_tree.get_parent_ids()
 
469
        self.parent_invs = []
 
470
        for revision in self.parents:
 
471
            if self.branch.repository.has_revision(revision):
 
472
                mutter('commit parent revision {%s}', revision)
 
473
                inventory = self.branch.repository.get_inventory(revision)
 
474
                self.parent_invs.append(inventory)
 
475
            else:
 
476
                mutter('commit parent ghost revision {%s}', revision)
 
477
 
 
478
    def _remove_deleted(self):
 
479
        """Remove deleted files from the working inventories.
 
480
 
 
481
        This is done prior to taking the working inventory as the
 
482
        basis for the new committed inventory.
 
483
 
 
484
        This returns true if any files
 
485
        *that existed in the basis inventory* were deleted.
 
486
        Files that were added and deleted
 
487
        in the working copy don't matter.
 
488
        """
 
489
        specific = self.specific_files
 
490
        deleted_ids = []
 
491
        deleted_paths = set()
 
492
        for path, ie in self.work_inv.iter_entries():
 
493
            if is_inside_any(deleted_paths, path):
 
494
                # The tree will delete the required ids recursively.
 
495
                continue
 
496
            if specific and not is_inside_any(specific, path):
 
497
                continue
 
498
            if not self.work_tree.has_filename(path):
 
499
                deleted_paths.add(path)
 
500
                self.reporter.missing(path)
 
501
                deleted_ids.append(ie.file_id)
 
502
        self.work_tree.unversion(deleted_ids)
 
503
 
 
504
    def _populate_new_inv(self):
 
505
        """Build revision inventory.
 
506
 
 
507
        This creates a new empty inventory. Depending on
 
508
        which files are selected for commit, and what is present in the
 
509
        current tree, the new inventory is populated. inventory entries 
 
510
        which are candidates for modification have their revision set to
 
511
        None; inventory entries that are carried over untouched have their
 
512
        revision set to their prior value.
 
513
        """
 
514
        # ESEPARATIONOFCONCERNS: this function is diffing and using the diff
 
515
        # results to create a new inventory at the same time, which results
 
516
        # in bugs like #46635.  Any reason not to use/enhance Tree.changes_from?
 
517
        # ADHB 11-07-2006
 
518
        mutter("Selecting files for commit with filter %s", self.specific_files)
 
519
        entries = self.work_inv.iter_entries()
 
520
        if not self.builder.record_root_entry:
 
521
            symbol_versioning.warn('CommitBuilders should support recording'
 
522
                ' the root entry as of bzr 0.10.', DeprecationWarning, 
 
523
                stacklevel=1)
 
524
            self.builder.new_inventory.add(self.basis_inv.root.copy())
 
525
            entries.next()
 
526
            self._emit_progress_update()
 
527
        for path, new_ie in entries:
 
528
            self._emit_progress_update()
 
529
            file_id = new_ie.file_id
 
530
            # mutter('check %s {%s}', path, file_id)
 
531
            if (not self.specific_files or 
 
532
                is_inside_or_parent_of_any(self.specific_files, path)):
 
533
                    # mutter('%s selected for commit', path)
 
534
                    ie = new_ie.copy()
 
535
                    ie.revision = None
 
536
            else:
 
537
                # mutter('%s not selected for commit', path)
 
538
                if self.basis_inv.has_id(file_id):
 
539
                    ie = self.basis_inv[file_id].copy()
 
540
                else:
 
541
                    # this entry is new and not being committed
 
542
                    continue
 
543
 
 
544
            self.builder.record_entry_contents(ie, self.parent_invs, 
 
545
                path, self.work_tree)
 
546
            # describe the nature of the change that has occurred relative to
 
547
            # the basis inventory.
 
548
            if (self.basis_inv.has_id(ie.file_id)):
 
549
                basis_ie = self.basis_inv[ie.file_id]
 
550
            else:
 
551
                basis_ie = None
 
552
            change = ie.describe_change(basis_ie, ie)
 
553
            if change in (InventoryEntry.RENAMED, 
 
554
                InventoryEntry.MODIFIED_AND_RENAMED):
 
555
                old_path = self.basis_inv.id2path(ie.file_id)
 
556
                self.reporter.renamed(change, old_path, path)
 
557
            else:
 
558
                self.reporter.snapshot_change(change, path)
 
559
 
 
560
        if not self.specific_files:
 
561
            return
 
562
 
 
563
        # ignore removals that don't match filespec
 
564
        for path, new_ie in self.basis_inv.iter_entries():
 
565
            if new_ie.file_id in self.work_inv:
 
566
                continue
 
567
            if is_inside_any(self.specific_files, path):
 
568
                continue
 
569
            ie = new_ie.copy()
 
570
            ie.revision = None
 
571
            self.builder.record_entry_contents(ie, self.parent_invs, path,
 
572
                                               self.basis_tree)
 
573
 
 
574
    def _emit_progress_update(self):
 
575
        """Emit an update to the progress bar."""
 
576
        self.pb.update("Committing", self.pb_count, self.pb_total)
 
577
        self.pb_count += 1
 
578
 
 
579
    def _report_deletes(self):
 
580
        for path, ie in self.basis_inv.iter_entries():
 
581
            if ie.file_id not in self.builder.new_inventory:
 
582
                self.reporter.deleted(path)
 
583
 
 
584