feat(protocol): destination-state itemize for dirs/symlinks; --stats deleted breakdown
This commit is contained in:
@@ -123,8 +123,15 @@ static char itemize_type_char(const ChangeEvent* event) {
|
||||
static bool times_match(const Config* config, const ChangeEvent* event) {
|
||||
if (!event->dest.known || !event->dest.existed)
|
||||
return false;
|
||||
if (event->mtime_sec == event->dest.mtime_sec)
|
||||
if (event->mtime_sec == event->dest.mtime_sec) {
|
||||
/* A regular file's sub-second mtime IS preserved by the receiver, so an nsec
|
||||
difference is a real change. A directory or symlink has no preserved
|
||||
sub-second mtime (rsync's quick-check compares whole seconds there), so a
|
||||
nanosecond-only difference must not render a spurious `.d..t` / `.L..t`. */
|
||||
if (event->is_directory || event->is_symlink || event->is_special)
|
||||
return true;
|
||||
return event->mtime_nsec == event->dest.mtime_nsec;
|
||||
}
|
||||
long long delta = (long long)event->mtime_sec - (long long)event->dest.mtime_sec;
|
||||
if (delta < 0)
|
||||
delta = -delta;
|
||||
@@ -145,6 +152,10 @@ static void itemize_code(const Config* config, const ChangeEvent* event, char co
|
||||
/* rsync: an existing directory that only has attribute changes carries no
|
||||
transfer, so the update column is `.` rather than `>`. */
|
||||
update = '.';
|
||||
else if (event->is_symlink)
|
||||
/* rsync: an existing symlink whose target is unchanged is a `.` update
|
||||
(attributes only); a changed target is `c` (the link value changed). */
|
||||
update = event->dest.target_matches ? '.' : 'c';
|
||||
else
|
||||
update = '>';
|
||||
code[0] = update;
|
||||
@@ -155,12 +166,20 @@ static void itemize_code(const Config* config, const ChangeEvent* event, char co
|
||||
code[11] = '\0';
|
||||
return;
|
||||
}
|
||||
bool size_diff = event->size != event->dest.size;
|
||||
bool time_diff = !times_match(config, event);
|
||||
/* rsync's value/checksum column: `c` for a symlink whose target changed (the
|
||||
link value is the compared content); no destination digest is available for
|
||||
a regular file. */
|
||||
bool value_diff = event->is_symlink && !event->dest.target_matches;
|
||||
/* rsync itemizes size only for regular files: a directory's st_size and a
|
||||
symlink's target length are not compared. */
|
||||
bool size_diff = !event->is_directory && !event->is_symlink && !event->is_special &&
|
||||
event->size != event->dest.size;
|
||||
/* rsync itemizes the time column only when -t/--times is in effect. */
|
||||
bool time_diff = config->preserve_times && !times_match(config, event);
|
||||
bool perms_diff = (event->mode & 07777) != (event->dest.mode & 07777);
|
||||
bool owner_diff = event->uid != (uid_t)event->dest.uid;
|
||||
bool group_diff = event->gid != (gid_t)event->dest.gid;
|
||||
code[2] = '.'; /* checksum: no destination digest available */
|
||||
code[2] = value_diff ? 'c' : '.';
|
||||
code[3] = size_diff ? 's' : '.';
|
||||
code[4] = time_diff ? 't' : '.';
|
||||
code[5] = (config->preserve_perms && perms_diff) ? 'p' : '.';
|
||||
@@ -172,6 +191,24 @@ static void itemize_code(const Config* config, const ChangeEvent* event, char co
|
||||
code[11] = '\0';
|
||||
}
|
||||
|
||||
/* True when the itemized destination entry is unchanged, i.e. rsync would print
|
||||
* no line at all. Reuses itemize_code so suppression is exactly consistent
|
||||
* with what would have been rendered: the update column must be `.` and every
|
||||
* attribute column must be `.`. */
|
||||
static bool itemize_is_unchanged(const Config* config, const ChangeEvent* event) {
|
||||
if (!event->dest.known || !event->dest.existed)
|
||||
return false;
|
||||
char code[12];
|
||||
itemize_code(config, event, code);
|
||||
if (code[0] != '.')
|
||||
return false;
|
||||
for (int i = 2; i < 11; i++) {
|
||||
if (code[i] != '.')
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* rsync %n: the transfer-relative name, with a trailing slash for directories.
|
||||
* The transfer root is `.` (so `%n` renders `./`), matching rsync's root entry. */
|
||||
static bool append_name(StrBuf* buf, const ChangeEvent* event) {
|
||||
@@ -678,6 +715,19 @@ void change_emit_file_sent_bytes(const Config* config, const File* file,
|
||||
char* name = NULL;
|
||||
char* path = NULL;
|
||||
fill_event_from_file(config, file, &event, &name, &path);
|
||||
if (file->is_symlink) {
|
||||
/* Output parity (protocol 2.30.0): an unchanged symlink is silent, like
|
||||
rsync's quick check. The itemize/log stream suppresses it only when every
|
||||
attribute matches; the name stream suppresses it whenever the link target
|
||||
is unchanged (rsync names a symlink only when it relinks or creates it). */
|
||||
bool itemize_output = config->itemize_changes || config->out_format != NULL ||
|
||||
(config->log_file != NULL && config->log_file_format != NULL);
|
||||
bool suppress = itemize_output
|
||||
? itemize_is_unchanged(config, &event)
|
||||
: (event.dest.known && event.dest.existed && event.dest.target_matches);
|
||||
if (suppress)
|
||||
event.decision = CHANGE_UP_TO_DATE;
|
||||
}
|
||||
if (name != NULL && path != NULL) {
|
||||
fill_event_checksum(config, file, &event);
|
||||
change_emit(config, &event);
|
||||
@@ -729,6 +779,19 @@ void change_emit_dir_sent(const Config* config, const File* file) {
|
||||
char* name = NULL;
|
||||
char* path = NULL;
|
||||
fill_event_from_file(config, file, &event, &name, &path);
|
||||
/* Output parity (protocol 2.30.0): suppress a directory rsync would leave
|
||||
silent. The itemize/log stream suppresses it only when every attribute
|
||||
matches (`.d.........`); the name stream suppresses any pre-existing
|
||||
directory (rsync names a directory only when it is created). */
|
||||
bool itemize_output = config->itemize_changes || config->out_format != NULL ||
|
||||
(config->log_file != NULL && config->log_file_format != NULL);
|
||||
bool suppress = itemize_output ? itemize_is_unchanged(config, &event)
|
||||
: (event.dest.known && event.dest.existed);
|
||||
/* The transfer root's line is an unconditional FastSync residual (rsync keys
|
||||
it off the root's own attribute change); keep emitting it. */
|
||||
bool is_root = event.name != NULL && event.name[0] == '\0';
|
||||
if (suppress && !is_root)
|
||||
event.decision = CHANGE_UP_TO_DATE;
|
||||
if (name != NULL && path != NULL)
|
||||
change_emit(config, &event);
|
||||
free(name);
|
||||
|
||||
@@ -2807,8 +2807,11 @@ static int cli_finalize_config(Config* config, bool verbose, bool no_delta, bool
|
||||
* need the pre-transfer destination snapshot (new vs modified and which
|
||||
* attributes differ), so ask the receiver to report it on every per-file
|
||||
* check. This is a wire field. */
|
||||
bool progress_active =
|
||||
!config->quiet && (config->show_progress || (config->info_level & LOG_INFO_PROGRESS) != 0);
|
||||
config->report_dest_info = config->itemize_changes || config->out_format != NULL ||
|
||||
(config->log_file != NULL && config->log_file_format != NULL);
|
||||
(config->log_file != NULL && config->log_file_format != NULL) ||
|
||||
progress_active;
|
||||
/* Wire-stats parity: --stats, --progress/-P, an --out-format token that needs
|
||||
* a wire counter (%b/%c), or a dry-run --delete need the receiver's
|
||||
* end-of-transfer STATUS_STATS report. This is a wire field (protocol
|
||||
|
||||
+145
-4
@@ -162,6 +162,9 @@ void report_transfer_stats(const Config* config, const TransferStats* stats, tim
|
||||
created_breakdown, sizeof(created_breakdown));
|
||||
unsigned long long created_total =
|
||||
recv->created_reg + recv->created_dir + recv->created_link + recv->created_special;
|
||||
char deleted_breakdown[128];
|
||||
type_breakdown(recv->deleted_reg, recv->deleted_dir, recv->deleted_link, recv->deleted_special,
|
||||
deleted_breakdown, sizeof(deleted_breakdown));
|
||||
printf("\n");
|
||||
if (breakdown[0] != '\0')
|
||||
printf("Number of files: %llu %s\n", flist_total, breakdown);
|
||||
@@ -173,7 +176,13 @@ void report_transfer_stats(const Config* config, const TransferStats* stats, tim
|
||||
printf("Number of created files: %llu %s\n", created_total, created_breakdown);
|
||||
else
|
||||
printf("Number of created files: %llu\n", created_total);
|
||||
printf("Number of deleted files: %llu\n", recv->deleted_files);
|
||||
/* Protocol 2.30.0: the receiver reports the removed entries split by type, so
|
||||
this line matches rsync's `Number of deleted files: X (reg: A, dir: B,
|
||||
link: C, special: D)` (only the non-zero categories are listed). */
|
||||
if (deleted_breakdown[0] != '\0')
|
||||
printf("Number of deleted files: %llu %s\n", recv->deleted_files, deleted_breakdown);
|
||||
else
|
||||
printf("Number of deleted files: %llu\n", recv->deleted_files);
|
||||
printf("Number of regular files transferred: %llu\n", stats->transferred_regular);
|
||||
printf("Total file size: %s bytes\n", total);
|
||||
printf("Total transferred file size: %s bytes\n", transferred);
|
||||
@@ -526,9 +535,16 @@ static void client_progress_emit_ancestors(const Config* config, const char* rel
|
||||
if (dir != NULL)
|
||||
change_emit_dir_sent(config, dir);
|
||||
} else {
|
||||
char* escaped = output_escape(prefix, config->eight_bit_output);
|
||||
printf("%s/\n", escaped ? escaped : prefix);
|
||||
free(escaped);
|
||||
/* --progress/-P names a directory only when it is newly created;
|
||||
rsync stays silent for a pre-existing directory even when one of
|
||||
its children changed (protocol 2.30.0 dest-state report). */
|
||||
const File* dir = progress_dir_lookup(prefix);
|
||||
bool existed = dir != NULL && dir->dest_state.known && dir->dest_state.existed;
|
||||
if (!existed) {
|
||||
char* escaped = output_escape(prefix, config->eight_bit_output);
|
||||
printf("%s/\n", escaped ? escaped : prefix);
|
||||
free(escaped);
|
||||
}
|
||||
}
|
||||
g_progress_index++;
|
||||
} else {
|
||||
@@ -553,6 +569,122 @@ void client_change_emit_ancestors(const Config* config, const File* file) {
|
||||
client_progress_emit_ancestors(config, rel);
|
||||
}
|
||||
|
||||
/* Send one STATUS_MKDIR probe (probe=1) for a pre-count directory and cache the
|
||||
* receiver's pre-transfer destination snapshot in `dir->dest_state`. Returns
|
||||
* false on a protocol/transport error. */
|
||||
static bool client_probe_dir_state(int fd, File* dir) {
|
||||
if (dir == NULL || file_wire_path(dir) == NULL)
|
||||
return true;
|
||||
if (!send_status(fd, STATUS_MKDIR) || !send_int(fd, 1) || !send_wire_str(fd, file_wire_path(dir)))
|
||||
return false;
|
||||
Status status = STATUS_ERROR;
|
||||
if (!receive_status(fd, &status) || status != STATUS_DEST_INFO ||
|
||||
!format_dest_state_receive(fd, &dir->dest_state)) {
|
||||
log_message(LOG_LEVEL_ERROR, "Directory destination-state probe failed");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Output parity (protocol 2.30.0): ask the receiver for each not-yet-probed
|
||||
* ancestor directory's pre-transfer state BEFORE the entry that first triggers
|
||||
* it is sent, so the ancestor's -i/--out-format line renders rsync's
|
||||
* `.d..t......` (existing, attributes changed) versus `cd+++++++++` (created)
|
||||
* and an unchanged directory is suppressed. The probe is a STATUS_MKDIR frame
|
||||
* with probe=1 (the receiver reports and creates nothing), so it must run before
|
||||
* the receiver implicitly creates the parent for the child. Each directory is
|
||||
* probed at most once; the result is cached in the pre-count File's dest_state.
|
||||
* Returns false on a protocol/transport error (the caller aborts the transfer). */
|
||||
bool client_change_probe_ancestors(const Config* config, const File* file, int fd) {
|
||||
if (config == NULL || file == NULL || fd < 0 || !config->report_dest_info)
|
||||
return true;
|
||||
if (!g_progress_dir_index_valid || !g_progress_precount.dir_refs)
|
||||
return true;
|
||||
const char* rel = delete_display_path(config, file_wire_path(file));
|
||||
if (rel == NULL)
|
||||
return true;
|
||||
size_t rel_len = strlen(rel);
|
||||
for (size_t i = 1; i < rel_len; i++) {
|
||||
if (rel[i] != '/')
|
||||
continue;
|
||||
char* prefix = malloc(i + 1);
|
||||
if (prefix == NULL)
|
||||
return false;
|
||||
memcpy(prefix, rel, i);
|
||||
prefix[i] = '\0';
|
||||
if (path_index_contains(&g_progress_dir_index, prefix)) {
|
||||
File* dir = progress_dir_lookup(prefix);
|
||||
/* The probe path is the same wire path the real STATUS_MKDIR would carry
|
||||
(the pre-count File's send_path, or its absolute source path for a
|
||||
plain recursive scan), not the display-relative prefix. */
|
||||
if (dir != NULL && !dir->dest_state.known && !client_probe_dir_state(fd, dir)) {
|
||||
free(prefix);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
free(prefix);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Mark a directory the data pass already itemized/named so the end-of-transfer
|
||||
* pending-directory flush does not report it a second time. `file` is a
|
||||
* transferred directory entry (an empty-directory STATUS_MKDIR). */
|
||||
void client_change_mark_dir(const Config* config, const File* file) {
|
||||
if (config == NULL || file == NULL || !g_progress_emitted_valid ||
|
||||
g_progress_emitted_keys == NULL)
|
||||
return;
|
||||
const char* rel = delete_display_path(config, file_wire_path(file));
|
||||
if (rel == NULL || rel[0] == '\0' || str_hash_set_lookup(&g_progress_emitted, rel))
|
||||
return;
|
||||
char* key = str_dup(rel);
|
||||
if (key == NULL)
|
||||
return;
|
||||
if (!array_list_add(g_progress_emitted_keys, key)) {
|
||||
free(key);
|
||||
return;
|
||||
}
|
||||
str_hash_set_insert_ref(&g_progress_emitted, key);
|
||||
}
|
||||
|
||||
/* Emit the itemize lines for source directories that CHANGED but had no
|
||||
* transferred child, so no ancestor emission reached them (rsync reports a
|
||||
* directory whose attributes changed even when its contents did not). Runs at
|
||||
* the end of the data pass, BEFORE the deferred STATUS_DIR_TIMES apply, so the
|
||||
* probe still observes each untouched directory's pre-transfer state. Only the
|
||||
* itemize/out-format/log streams report attribute-only directory changes;
|
||||
* --progress/-P stays silent for them, matching rsync. Best-effort: a probe
|
||||
* failure simply stops the flush (the transfer's verdict is unaffected). */
|
||||
void client_change_emit_pending_dirs(const Config* config, int fd) {
|
||||
if (config == NULL || fd < 0 || !config->report_dest_info)
|
||||
return;
|
||||
bool itemize_output = config->itemize_changes || config->out_format != NULL ||
|
||||
(config->log_file != NULL && config->log_file_format != NULL);
|
||||
if (!itemize_output)
|
||||
return;
|
||||
if (!g_progress_dir_index_valid || g_progress_precount.dir_refs == NULL ||
|
||||
!g_progress_emitted_valid || g_progress_emitted_keys == NULL)
|
||||
return;
|
||||
for (int i = 0; i < g_progress_precount.dir_refs->size; i++) {
|
||||
DirRef* ref = (DirRef*)g_progress_precount.dir_refs->items[i];
|
||||
if (ref == NULL || ref->name == NULL || ref->name[0] == '\0')
|
||||
continue;
|
||||
if (str_hash_set_lookup(&g_progress_emitted, ref->name))
|
||||
continue;
|
||||
File* dir = ref->file;
|
||||
if (dir == NULL)
|
||||
continue;
|
||||
if (!dir->dest_state.known && !client_probe_dir_state(fd, dir))
|
||||
return;
|
||||
change_emit_dir_sent(config, dir);
|
||||
char* key = str_dup(ref->name);
|
||||
if (key != NULL && array_list_add(g_progress_emitted_keys, key))
|
||||
str_hash_set_insert_ref(&g_progress_emitted, key);
|
||||
else
|
||||
free(key);
|
||||
}
|
||||
}
|
||||
|
||||
/* rsync's --info=name/progress line for one entry: transfer-relative name (a
|
||||
* trailing slash for directories) plus the ` -> target` symlink suffix. */
|
||||
static char* progress_entry_line(const File* file, const char* rel) {
|
||||
@@ -645,6 +777,15 @@ void client_progress_file(const Config* config, const File* file) {
|
||||
void client_progress_name(const Config* config, const File* file) {
|
||||
if (!g_progress_active || file == NULL)
|
||||
return;
|
||||
/* rsync's --progress/-P name stream reports an entry only when it is created
|
||||
or (for a symlink) actually relinked: a pre-existing directory or an
|
||||
unchanged symlink is silent (protocol 2.30.0 dest-state report). */
|
||||
if (file->dest_state.known && file->dest_state.existed) {
|
||||
if (file->is_dir || (file->is_symlink && file->dest_state.target_matches)) {
|
||||
g_progress_index++;
|
||||
return;
|
||||
}
|
||||
}
|
||||
const char* rel = delete_display_path(config, file_wire_path(file));
|
||||
if (!config->itemize_changes && config->out_format == NULL) {
|
||||
char* line = progress_entry_line(file, rel ? rel : "");
|
||||
|
||||
@@ -615,14 +615,34 @@ static bool send_file_direct(File* file, int fd, bool use_metadata, int compress
|
||||
static bool send_directory_entry(const Client* client, File* file, const Config* config) {
|
||||
if (!file || !file_wire_path(file))
|
||||
return false;
|
||||
if (!send_status(client->file_descriptor, STATUS_MKDIR) ||
|
||||
!send_wire_str(client->file_descriptor, file_wire_path(file)))
|
||||
int fd = client->file_descriptor;
|
||||
if (!send_status(fd, STATUS_MKDIR))
|
||||
return false;
|
||||
if (config->use_metadata && !metadata_send(client->file_descriptor, file->metadata))
|
||||
/* Output parity (protocol 2.30.0): when report_dest_info is negotiated every
|
||||
STATUS_MKDIR body is prefixed with a probe flag (1 = probe only, 0 = a real
|
||||
create), so the receiver knows whether to expect the metadata/xattr block. */
|
||||
if (config->report_dest_info && !send_int(fd, 0))
|
||||
return false;
|
||||
if (!send_wire_str(fd, file_wire_path(file)))
|
||||
return false;
|
||||
if (config->use_metadata && !metadata_send(fd, file->metadata))
|
||||
return false;
|
||||
/* Directory xattrs/ACLs (-X/-A) ride the same trailing block as regular files
|
||||
when the xattr transport was negotiated. */
|
||||
return !config->use_xattrs || xattr_send(client->file_descriptor, file->xattrs);
|
||||
if (config->use_xattrs && !xattr_send(fd, file->xattrs))
|
||||
return false;
|
||||
/* The receiver answers with the directory's pre-transfer destination state
|
||||
BEFORE creating it, so the sender can render rsync's `.d..t......` versus
|
||||
`cd+++++++++` and suppress an unchanged directory. */
|
||||
if (config->report_dest_info) {
|
||||
Status status;
|
||||
if (!receive_status(fd, &status) || status != STATUS_DEST_INFO ||
|
||||
!format_dest_state_receive(fd, &file->dest_state)) {
|
||||
log_message(LOG_LEVEL_ERROR, "Unexpected reply to the directory destination-state report");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* P7 Wave D: transmit every captured source directory's metadata in terminal
|
||||
@@ -678,7 +698,21 @@ static bool send_symlink_entry(const Client* client, File* file, const Config* c
|
||||
return false;
|
||||
/* Symlink xattrs/ACLs (-X/-A) ride the same trailing block as regular files
|
||||
and directories when the xattr transport was negotiated. */
|
||||
return !config->use_xattrs || xattr_send(fd, file->xattrs);
|
||||
if (config->use_xattrs && !xattr_send(fd, file->xattrs))
|
||||
return false;
|
||||
/* The receiver answers with the symlink's pre-transfer destination state
|
||||
(including whether the on-disk link target already matches) BEFORE creating
|
||||
it, so the sender can render rsync's `cLc........` / `.L..t......` and
|
||||
suppress an unchanged symlink. */
|
||||
if (config->report_dest_info) {
|
||||
Status status;
|
||||
if (!receive_status(fd, &status) || status != STATUS_DEST_INFO ||
|
||||
!format_dest_state_receive(fd, &file->dest_state)) {
|
||||
log_message(LOG_LEVEL_ERROR, "Unexpected reply to the symlink destination-state report");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Send a single file directly via sendfile (non-incremental path).
|
||||
@@ -869,10 +903,12 @@ static int send_chunk_with_removal(Client* client, Chunk* chunk, Config* config,
|
||||
feed -i/--out-format its ancestor directory lines here. */
|
||||
if (config->itemize_changes || config->out_format != NULL)
|
||||
client_change_emit_ancestors(config, chunk->items[i]);
|
||||
if (chunk->items[i]->is_dir)
|
||||
if (chunk->items[i]->is_dir) {
|
||||
change_emit_dir_sent(config, chunk->items[i]);
|
||||
else
|
||||
client_change_mark_dir(config, chunk->items[i]);
|
||||
} else {
|
||||
change_emit_file_sent(config, chunk->items[i]);
|
||||
}
|
||||
if (!chunk->items[i]->is_dir)
|
||||
transfer_stats_note_transferred(stats, chunk->items[i]);
|
||||
}
|
||||
@@ -884,6 +920,11 @@ static int send_chunk_with_removal(Client* client, Chunk* chunk, Config* config,
|
||||
if (f == NULL)
|
||||
continue;
|
||||
transfer_stats_note_entry(stats, f);
|
||||
/* Output parity: probe this entry's ancestor directories' destination state
|
||||
before the entry (or the first child below them) is sent, while the
|
||||
receiver has not yet created them implicitly. */
|
||||
if (!client_change_probe_ancestors(config, f, client->file_descriptor))
|
||||
return -1;
|
||||
if (f->is_dir) {
|
||||
/* Explicit directory entry (--dirs): a MKDIR frame carrying the
|
||||
destination path (and metadata when negotiated). Directories have no
|
||||
@@ -892,6 +933,7 @@ static int send_chunk_with_removal(Client* client, Chunk* chunk, Config* config,
|
||||
return -1;
|
||||
client_change_emit_ancestors(config, f);
|
||||
change_emit_dir_sent(config, f);
|
||||
client_change_mark_dir(config, f);
|
||||
client_progress_name(config, f);
|
||||
continue;
|
||||
}
|
||||
@@ -1137,6 +1179,7 @@ static int send_chunks_multithreaded(void* pipeline_context) {
|
||||
(and all parallel workers) has been joined before scanner_done was set, so
|
||||
the list is complete and race-free; on an early stop the list may be
|
||||
incomplete and is deliberately not sent. */
|
||||
client_change_emit_pending_dirs(context->config, client->file_descriptor);
|
||||
if (!context->scan_stopped_early &&
|
||||
!send_dir_times(client, context->config, context->dir_entries))
|
||||
goto send_fail;
|
||||
@@ -1844,6 +1887,10 @@ static int send_files_finalize(const Config* config, SendFilesState* state) {
|
||||
}
|
||||
}
|
||||
}
|
||||
/* Output parity: report changed directories that had no transferred child
|
||||
before the deferred directory times are applied (so the probe still sees
|
||||
their pre-transfer state). */
|
||||
client_change_emit_pending_dirs(config, client->file_descriptor);
|
||||
/* P7 Wave D: every directory has now been traversed (or the scan stopped
|
||||
early), so transmit the captured directory times last. The receiver defers
|
||||
applying them until after its own deletion/publication phase. */
|
||||
|
||||
@@ -68,6 +68,14 @@ void client_progress_name(const Config* config, const File* file);
|
||||
/* Emit a transferred entry's ancestor directories (as -i/--out-format change
|
||||
* lines or --progress name lines) before the entry's own line. */
|
||||
void client_change_emit_ancestors(const Config* config, const File* file);
|
||||
/* Output parity (protocol 2.30.0): probe each not-yet-known ancestor directory's
|
||||
* pre-transfer destination state before the entry that first triggers it is
|
||||
* sent. Returns false on a protocol/transport error. */
|
||||
bool client_change_probe_ancestors(const Config* config, const File* file, int fd);
|
||||
/* Mark a transferred directory entry as already reported, and flush the
|
||||
* itemize lines for changed directories that had no transferred child. */
|
||||
void client_change_mark_dir(const Config* config, const File* file);
|
||||
void client_change_emit_pending_dirs(const Config* config, int fd);
|
||||
void client_progress_uptodate(const Config* config, const File* file);
|
||||
void client_progress_prepare(const Config* config, const ArrayList* plan_dirs,
|
||||
unsigned long long plan_non_dir_count);
|
||||
|
||||
Reference in New Issue
Block a user