diff options
Diffstat (limited to 'src/cli/progress.c')
-rw-r--r-- | src/cli/progress.c | 346 |
1 files changed, 346 insertions, 0 deletions
diff --git a/src/cli/progress.c b/src/cli/progress.c new file mode 100644 index 0000000..ddfbafb --- /dev/null +++ b/src/cli/progress.c @@ -0,0 +1,346 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include <stdio.h> +#include <stdarg.h> +#include <stdint.h> + +#include "progress.h" +#include "error.h" + +/* + * Show updates to the percentage and number of objects received + * separately from the throughput to give an accurate progress while + * avoiding too much noise on the screen. (In milliseconds.) + */ +#define PROGRESS_UPDATE_TIME 60 +#define THROUGHPUT_UPDATE_TIME 500 + +#define is_nl(c) ((c) == '\r' || (c) == '\n') + +#define return_os_error(msg) do { \ + git_error_set(GIT_ERROR_OS, "%s", msg); return -1; } while(0) + +GIT_INLINE(size_t) no_nl_len(const char *str, size_t len) +{ + size_t i = 0; + + while (i < len && !is_nl(str[i])) + i++; + + return i; +} + +GIT_INLINE(size_t) nl_len(bool *has_nl, const char *str, size_t len) +{ + size_t i = no_nl_len(str, len); + + *has_nl = false; + + while (i < len && is_nl(str[i])) { + *has_nl = true; + i++; + } + + return i; +} + +static int progress_write(cli_progress *progress, bool force, git_str *line) +{ + bool has_nl; + size_t no_nl = no_nl_len(line->ptr, line->size); + size_t nl = nl_len(&has_nl, line->ptr + no_nl, line->size - no_nl); + uint64_t now = git_time_monotonic(); + size_t i; + + /* Avoid spamming the console with progress updates */ + if (!force && line->ptr[line->size - 1] != '\n' && progress->last_update) { + if (now - progress->last_update < PROGRESS_UPDATE_TIME) { + git_str_clear(&progress->deferred); + git_str_put(&progress->deferred, line->ptr, line->size); + return git_str_oom(&progress->deferred) ? -1 : 0; + } + } + + /* + * If there's something on this line already (eg, a progress line + * with only a trailing `\r` that we'll print over) then we need + * to really print over it in case we're writing a shorter line. + */ + if (printf("%.*s", (int)no_nl, line->ptr) < 0) + return_os_error("could not print status"); + + if (progress->onscreen.size) { + for (i = no_nl; i < progress->onscreen.size; i++) { + if (printf(" ") < 0) + return_os_error("could not print status"); + } + } + + if (printf("%.*s", (int)nl, line->ptr + no_nl) < 0 || + fflush(stdout) != 0) + return_os_error("could not print status"); + + git_str_clear(&progress->onscreen); + + if (line->ptr[line->size - 1] == '\n') { + progress->last_update = 0; + } else { + git_str_put(&progress->onscreen, line->ptr, line->size); + progress->last_update = now; + } + + git_str_clear(&progress->deferred); + return git_str_oom(&progress->onscreen) ? -1 : 0; +} + +static int progress_printf(cli_progress *progress, bool force, const char *fmt, ...) + GIT_FORMAT_PRINTF(3, 4); + +int progress_printf(cli_progress *progress, bool force, const char *fmt, ...) +{ + git_str buf = GIT_BUF_INIT; + va_list ap; + int error; + + va_start(ap, fmt); + error = git_str_vprintf(&buf, fmt, ap); + va_end(ap); + + if (error < 0) + return error; + + error = progress_write(progress, force, &buf); + + git_str_dispose(&buf); + return error; +} + +static int progress_complete(cli_progress *progress) +{ + if (progress->deferred.size) + progress_write(progress, true, &progress->deferred); + + if (progress->onscreen.size) + if (printf("\n") < 0) + return_os_error("could not print status"); + + git_str_clear(&progress->deferred); + git_str_clear(&progress->onscreen); + progress->last_update = 0; + progress->action_start = 0; + progress->action_finish = 0; + + return 0; +} + +GIT_INLINE(int) percent(size_t completed, size_t total) +{ + if (total == 0) + return (completed == 0) ? 100 : 0; + + return (int)(((double)completed / (double)total) * 100); +} + +int cli_progress_fetch_sideband(const char *str, int len, void *payload) +{ + cli_progress *progress = (cli_progress *)payload; + size_t remain; + + if (len <= 0) + return 0; + + /* Accumulate the sideband data, then print it line-at-a-time. */ + if (git_str_put(&progress->sideband, str, len) < 0) + return -1; + + str = progress->sideband.ptr; + remain = progress->sideband.size; + + while (remain) { + bool has_nl; + size_t line_len = nl_len(&has_nl, str, remain); + + if (!has_nl) + break; + + if (line_len < INT_MAX) { + int error = progress_printf(progress, true, + "remote: %.*s", (int)line_len, str); + + if (error < 0) + return error; + } + + str += line_len; + remain -= line_len; + } + + git_str_consume_bytes(&progress->sideband, (progress->sideband.size - remain)); + + return 0; +} + +static int fetch_receiving( + cli_progress *progress, + const git_indexer_progress *stats) +{ + char *recv_units[] = { "B", "KiB", "MiB", "GiB", "TiB", NULL }; + char *rate_units[] = { "B/s", "KiB/s", "MiB/s", "GiB/s", "TiB/s", NULL }; + uint64_t now, elapsed; + + double recv_len, rate; + size_t recv_unit_idx = 0, rate_unit_idx = 0; + bool done = (stats->received_objects == stats->total_objects); + + if (!progress->action_start) + progress->action_start = git_time_monotonic(); + + if (done && progress->action_finish) + now = progress->action_finish; + else if (done) + progress->action_finish = now = git_time_monotonic(); + else + now = git_time_monotonic(); + + if (progress->throughput_update && + now - progress->throughput_update < THROUGHPUT_UPDATE_TIME) { + elapsed = progress->throughput_update - + progress->action_start; + recv_len = progress->throughput_bytes; + } else { + elapsed = now - progress->action_start; + recv_len = (double)stats->received_bytes; + + progress->throughput_update = now; + progress->throughput_bytes = recv_len; + } + + rate = elapsed ? recv_len / elapsed : 0; + + while (recv_len > 1024 && recv_units[recv_unit_idx+1]) { + recv_len /= 1024; + recv_unit_idx++; + } + + while (rate > 1024 && rate_units[rate_unit_idx+1]) { + rate /= 1024; + rate_unit_idx++; + } + + return progress_printf(progress, false, + "Receiving objects: %3d%% (%d/%d), %.2f %s | %.2f %s%s\r", + percent(stats->received_objects, stats->total_objects), + stats->received_objects, + stats->total_objects, + recv_len, recv_units[recv_unit_idx], + rate, rate_units[rate_unit_idx], + done ? ", done." : ""); +} + +static int fetch_resolving( + cli_progress *progress, + const git_indexer_progress *stats) +{ + bool done = (stats->indexed_deltas == stats->total_deltas); + + return progress_printf(progress, false, + "Resolving deltas: %3d%% (%d/%d)%s\r", + percent(stats->indexed_deltas, stats->total_deltas), + stats->indexed_deltas, stats->total_deltas, + done ? ", done." : ""); +} + +int cli_progress_fetch_transfer(const git_indexer_progress *stats, void *payload) +{ + cli_progress *progress = (cli_progress *)payload; + int error = 0; + + switch (progress->action) { + case CLI_PROGRESS_NONE: + progress->action = CLI_PROGRESS_RECEIVING; + /* fall through */ + + case CLI_PROGRESS_RECEIVING: + if ((error = fetch_receiving(progress, stats)) < 0) + break; + + /* + * Upgrade from receiving to resolving; do this after the + * final call to cli_progress_fetch_receiving (above) to + * ensure that we've printed a final "done" string after + * any sideband data. + */ + if (!stats->indexed_deltas) + break; + + progress_complete(progress); + progress->action = CLI_PROGRESS_RESOLVING; + /* fall through */ + + case CLI_PROGRESS_RESOLVING: + error = fetch_resolving(progress, stats); + break; + + default: + /* should not be reached */ + GIT_ASSERT(!"unexpected progress state"); + } + + return error; +} + +void cli_progress_checkout( + const char *path, + size_t completed_steps, + size_t total_steps, + void *payload) +{ + cli_progress *progress = (cli_progress *)payload; + bool done = (completed_steps == total_steps); + + GIT_UNUSED(path); + + if (progress->action != CLI_PROGRESS_CHECKING_OUT) { + progress_complete(progress); + progress->action = CLI_PROGRESS_CHECKING_OUT; + } + + progress_printf(progress, false, + "Checking out files: %3d%% (%" PRIuZ "/%" PRIuZ ")%s\r", + percent(completed_steps, total_steps), + completed_steps, total_steps, + done ? ", done." : ""); +} + +int cli_progress_abort(cli_progress *progress) +{ + if (progress->onscreen.size > 0 && printf("\n") < 0) + return_os_error("could not print status"); + + return 0; +} + +int cli_progress_finish(cli_progress *progress) +{ + int error = progress->action ? progress_complete(progress) : 0; + + progress->action = 0; + return error; +} + +void cli_progress_dispose(cli_progress *progress) +{ + if (progress == NULL) + return; + + git_str_dispose(&progress->sideband); + git_str_dispose(&progress->onscreen); + git_str_dispose(&progress->deferred); + + memset(progress, 0, sizeof(cli_progress)); +} |