Problem/Motivation

When using the Revision Manager module, selecting both Age and Amount as criteria for deleting old revisions does not behave as expected.
If revisions are collected based on the Age filter first, the module deletes all revisions older than the configured age, without applying the Amount (maximum number of revisions to retain/delete) constraint.
This results in more revisions being deleted than intended and ignores the configured “maximum amount” limit.

This issue may lead to unintended data loss, especially on content types with many revisions.

Steps to reproduce

Install and enable the Revision Manager module.

Configure a cleanup rule and select both conditions:

Delete revisions older than X days (Age)

Delete only X number of revisions (Amount)

Run the revision cleanup process using cron or manual execution.

Observe the list of revisions selected for deletion.

Actual behavior:
All revisions older than the Age setting are marked for deletion, ignoring the Amount constraint.

Expected behavior:
Revision selection should first filter by Age and then respect the Amount limit, deleting only up to the specified maximum number.

Proposed resolution

Update the revision selection logic so that when both Age and Amount are selected:

Filter revisions based on Age.

Apply the Amount limit to the filtered set.

Ensure the Amount value is respected as a hard cap on the number of revisions deleted.

Add test coverage to verify that combined criteria work as expected.

Review the current query or service responsible for collecting candidate revisions and adjust logic ordering if needed.

Remaining tasks

Confirm exact location of the revision selection logic in the module.

Implement updated logic to apply Amount constraint after Age filtering.

Add automated tests to cover combined Age + Amount cases.

Review and update documentation if needed.

Submit merge request / patch for review on Drupal.org.

User interface changes

No UI changes expected.
All modifications occur in backend logic.
Optionally: Add helper text clarifying how Age and Amount interact when both are selected.

API changes

No public API changes anticipated.
Internal logic adjustments may affect internal service or query behavior, but no API contracts will be altered.

Data model changes

None.
No schema or entity changes are required.

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

raajkumar.kuru created an issue. See original summary.

raajkumar.kuru changed the visibility of the branch 3560354-revision-manager-selecting to hidden.

j-barnes changed the visibility of the branch 3560354-revision-manager-selecting to active.

j-barnes’s picture

@raajkumar.kuru - Thanks for the detailed write up. Originally the idea was to have each plugin work independently in a sequence, each performing its own deletions. The process would look like this:

1. Each plugin independently identifies deletable revisions
2. Each plugin's deletions happen immediately before the next plugin runs
3. There's no coordination between plugins

However, the previous node_revision_delete implementation used an AND operation across all plugins - only deleting revisions that were marked for deletion by all enabled plugins.

I can see where this approach would be beneficial:

- More defensive: Acts as a safety net against unexpected data loss
- More intuitive: Users naturally expect "keep at least N revisions" to be a hard floor, regardless of age
- Order-independent: The result is the same regardless of which plugin runs first

Given that, I think it might make sense to switch the approach. If should be relatively straightforward to implement, essentially taking the intersection of all deletable revisions identified by each plugin before performing any deletions.

raajkumar.kuru’s picture

StatusFileSize
new907 bytes
raajkumar.kuru’s picture

Status: Active » Needs review

j-barnes’s picture

@raajkumar.kuru - Thanks again for the patch and detailed report! I've switched to an intersection strategy for when multiple plugins are enabled. This should match what most users expect. Let me know if it solves your issue, I'll add a patch as well.

Before (sequential):
- Age deletes 5 old revisions first
- Amount then runs on remaining 7, deletes 4 more to keep 3
- Result: 3 remain

After (intersection):
- Age candidates: 5 (old revisions)
- Amount candidates: 9 (keeping newest 3)
- Intersection: 5 (only what both agree on)
- Result: 12 - 5 = 7 remain

We already had integration tests covering this scenario - just updated the expected counts to reflect the new behavior.

Since this change results in more revisions being kept (not fewer), it's the safer direction. Most users probably won't notice the difference, and those who do should appreciate having Amount act as a proper safety net.

j-barnes changed the visibility of the branch 3560354-revision-manager-selecting to hidden.

j-barnes changed the visibility of the branch 3560354-revision-manager-selecting to active.

j-barnes changed the visibility of the branch bugfix/revision-manager-age-amount-constraint to hidden.

j-barnes changed the visibility of the branch revision-manager-age-amount-constraint to hidden.

j-barnes’s picture

StatusFileSize
new2.62 KB
raajkumar.kuru’s picture

When the revisions to delete from the Amount candidate are zero, but the Age candidate returns a non-empty list of revisions to delete, the code incorrectly returns all revisions from the Age candidate.
This happens because the condition is treated as an AND, but the current logic behaves like an OR.
In short:
Age candidate returns some revisions to delete
Amount candidate returns zero revisions
The intersection logic still returns the Age candidate’s revisions
This is incorrect — the intersection should be empty.
The line causing the issue:

$non_empty = array_filter($candidates, static fn (array $arr): bool => $arr !== []);

This line keeps any non-empty candidate list, which breaks intersection logic.

raajkumar.kuru’s picture

.

j-barnes’s picture

StatusFileSize
new9.48 KB

Great catch! Fixed the logic and added tests covering the empty candidate cases. Give it a try and let me know how it goes.

j-barnes’s picture

Version: 1.0.0-beta1 » 1.0.x-dev
Assigned: Unassigned » j-barnes
raajkumar.kuru’s picture

Its working as expected.
Good to merge

sunny-lee’s picture

Status: Needs review » Reviewed & tested by the community

Looks great here! Saw the issue on test as well and on several nodes on test there were only 1 or 2 revisions left but after this fix there was consistently 3 or 5 - the amount it was set for so it is correctly using 'AND'.

  • j-barnes committed a9fea915 on 1.0.x
    Issue #3560354: Use intersection logic for multi-plugin revision...
j-barnes’s picture

Assigned: j-barnes » Unassigned
Status: Reviewed & tested by the community » Fixed

Merged, thanks everyone!

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.

pfrenssen’s picture

Thanks all for this change! I think indeed this is the safest way to do it. There are 2 ways of interpreting the use of multiple plugins together and we should go for the conservative approach.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.