feat(progress): opt-in paths-only pre-count for rsync to-chk parity

- when --progress/--info=progress is requested, a metadata-only pre-scan
  builds the full file-list total and directory names so the to-chk
  denominator counts every regular/dir/link/special entry like rsync
- per-directory/symlink/special name lines emitted; sequential and --threads
- reuses the --delete-during/delay pre-scan when present; non-progress runs
  take no extra pass
- --progress stays a caveat (emission order still differs); single-file output
  remains byte-identical
This commit is contained in:
2026-09-19 12:49:56 +02:00
parent 6119e1e75c
commit c80098623f
7 changed files with 535 additions and 19 deletions
+50
View File
@@ -1240,6 +1240,55 @@ static int collect_scan_info_parallel(ParallelScanner* scanner, const char* root
return failed ? -1 : count;
}
/* The --progress paths-only pre-count relies on `list_dirs` emitting every
* directory (including empty ones) exactly once, alongside the files and
* symlinks the streaming scanner already emits. */
static void test_scanner_list_dirs_counts_every_entry() {
const char* root = "test_scan_listdirs";
EXPECT_EQ_INT(mkdir(root, 0755), 0);
EXPECT_EQ_INT(mkdir("test_scan_listdirs/sub1", 0755), 0);
EXPECT_EQ_INT(mkdir("test_scan_listdirs/sub1/deep", 0755), 0);
EXPECT_EQ_INT(mkdir("test_scan_listdirs/sub2", 0755), 0);
EXPECT_EQ_INT(mkdir("test_scan_listdirs/emptydir", 0755), 0);
create_test_file("test_scan_listdirs/a.txt", "a");
create_test_file("test_scan_listdirs/sub1/c.txt", "c");
create_test_file("test_scan_listdirs/sub1/deep/d.txt", "d");
create_test_file("test_scan_listdirs/sub2/e.txt", "e");
EXPECT_EQ_INT(symlink("a.txt", "test_scan_listdirs/link1"), 0);
ScannerOptions options = {0};
options.list_dirs = true;
options.emit_empty_dirs = true;
options.follow_symlinks = true; /* -l/--links: carry symlinks, don't skip */
ScanInfo infos[16];
int count = collect_scan_info(root, &options, infos, 16);
EXPECT_EQ_INT(count, 9);
int dirs = 0;
for (int i = 0; i < count; i++) {
if (infos[i].is_dir)
dirs++;
}
EXPECT_EQ_INT(dirs, 4);
EXPECT_TRUE(scan_info_present(infos, count, "sub1", true, NULL));
EXPECT_TRUE(scan_info_present(infos, count, "sub1/deep", true, NULL));
EXPECT_TRUE(scan_info_present(infos, count, "sub2", true, NULL));
EXPECT_TRUE(scan_info_present(infos, count, "emptydir", true, NULL));
EXPECT_TRUE(scan_info_present(infos, count, "a.txt", false, ""));
EXPECT_TRUE(scan_info_present(infos, count, "sub1/c.txt", false, ""));
EXPECT_TRUE(scan_info_present(infos, count, "link1", false, ""));
unlink("test_scan_listdirs/a.txt");
unlink("test_scan_listdirs/sub1/c.txt");
unlink("test_scan_listdirs/sub1/deep/d.txt");
unlink("test_scan_listdirs/sub2/e.txt");
unlink("test_scan_listdirs/link1");
rmdir("test_scan_listdirs/sub1/deep");
rmdir("test_scan_listdirs/sub1");
rmdir("test_scan_listdirs/sub2");
rmdir("test_scan_listdirs/emptydir");
rmdir(root);
}
/* -d without --files-from emits exactly the source-root directory (empty) and
* never descends. */
static void test_dirs_no_descent() {
@@ -1667,6 +1716,7 @@ void test_scanner() {
test_per_dir_filter_override(true);
test_dirs_no_descent();
test_dirs_files_from();
test_scanner_list_dirs_counts_every_entry();
test_files_from_relative_send_path();
test_scanner_captures_directory_times();
test_scanner_chunk_ownership();