rsync 3.4.1 drop-in parity (#285-#297) + parity completion (protocol 2.26.0) #298

Merged
TapTap merged 77 commits from feat/rsync-parity into dev 2026-09-17 20:33:17 +02:00
2 changed files with 57 additions and 2 deletions
Showing only changes of commit 376e6500ab - Show all commits
+27 -2
View File
@@ -3099,10 +3099,35 @@ static bool delete_missing_args_budgeted(const Config* config, DeleteManifest* m
free(leaf); free(leaf);
leaf = NULL; leaf = NULL;
if (config->use_delete || config->force_delete) { if (config->use_delete || config->force_delete) {
if (!file_remove_tree_secure(full)) /* Remove the contents entry-by-entry through the budgeted extras
walker so every deleted file/dir counts toward --max-delete (rsync
parity); the now-empty directory itself costs one more. A run that
hits the cap leaves the remaining entries in place. */
ArrayList* no_keeps = array_list_create(free);
size_t remaining = budget->max_delete - budget->deleted;
size_t contents_deleted = 0;
size_t contents_skipped = 0;
DeleteWalkResult walk =
no_keeps ? delete_extras_limited(full, no_keeps, NULL, remaining, NULL, 0,
&contents_deleted, &contents_skipped)
: DELETE_WALK_ERROR;
if (no_keeps)
array_list_delete(no_keeps);
budget->deleted += contents_deleted;
budget->skipped += contents_skipped;
if (walk == DELETE_WALK_LIMIT_REACHED) {
budget->limit_hit = true;
} else if (walk != DELETE_WALK_OK) {
ok = false; ok = false;
else } else if (budget->deleted >= budget->max_delete) {
budget->limit_hit = true;
budget->skipped++;
} else if (file_remove_tree_secure(full)) {
budget->deleted++;
removed = true; removed = true;
} else {
ok = false;
}
} else { } else {
char* escaped = output_escape(rel, log_get_8_bit_output()); char* escaped = output_escape(rel, log_get_8_bit_output());
log_message(LOG_LEVEL_WARNING, log_message(LOG_LEVEL_WARNING,
+30
View File
@@ -2853,6 +2853,36 @@ class TestMissingArgs:
f"missing-args deletions ignored the --max-delete budget: {remaining}" f"missing-args deletions ignored the --max-delete budget: {remaining}"
assert os.path.isfile(os.path.join(received, "a.txt")) assert os.path.isfile(os.path.join(received, "a.txt"))
@pytest.mark.parametrize("mt", [False, True])
def test_delete_missing_nonempty_dir_counts_each_entry_against_budget(self, mt):
"""A non-empty missing-arg directory with --force/--delete is removed
entry-by-entry, each counting toward --max-delete (rsync parity): with a
small cap the run stops after N files and leaves the rest in place."""
source = self._make_source("mg_dirbudget_src")
dest = os.path.join(TEST_DATA_DIR, "mg_dirbudget_dst")
clean_dir(dest)
with ServerManager() as server:
server.start(extra_args=["--allow-delete"])
result, _ = run_client(source, dest, port=server.port)
assert result.returncode == 0, f"seed failed: {result.stderr[:200]}"
received = get_dest_received_dir(dest, source)
gone = os.path.join(received, "gone")
os.makedirs(gone)
for i in range(4):
with open(os.path.join(gone, f"f{i}"), "w") as fh:
fh.write("stale")
lst = _write_rel_list(b"a.txt\ngone\n")
flags = ["--files-from", lst, "--delete-missing-args", "--force",
"--max-delete=2"] + (["--threads"] if mt else [])
result, _ = run_client(source, dest, flags=flags, port=server.port)
assert result.returncode == 25, \
f"non-empty missing-arg dir should cap at 2 and exit 25: {result.stderr[:300]}"
assert os.path.isdir(gone), \
"the non-empty missing-arg directory should survive a capped run"
remaining = len(os.listdir(gone))
assert remaining == 2, f"expected 2 entries left, found {remaining}"
assert os.path.isfile(os.path.join(received, "a.txt"))
@pytest.mark.parametrize("mt", [False, True]) @pytest.mark.parametrize("mt", [False, True])
def test_delete_missing_args_not_blocked_by_exclude_protection(self, mt): def test_delete_missing_args_not_blocked_by_exclude_protection(self, mt):
"""A missing-arg mirror that sits under a filter-excluded directory is an """A missing-arg mirror that sits under a filter-excluded directory is an