Unverified Commit 13502d9e by Edward Thomson Committed by GitHub

Merge pull request #6274 from libgit2/ethomson/cli_clone

cli: clone
parents a1c94e94 8a757ae2
......@@ -15,5 +15,6 @@
#include "error.h"
#include "opt.h"
#include "opt_usage.h"
#include "sighandler.h"
#endif /* CLI_cli_h__ */
......@@ -26,6 +26,7 @@ extern const cli_cmd_spec *cli_cmd_spec_byname(const char *name);
/* Commands */
extern int cmd_cat_file(int argc, char **argv);
extern int cmd_clone(int argc, char **argv);
extern int cmd_hash_object(int argc, char **argv);
extern int cmd_help(int argc, char **argv);
......
/*
* 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 <git2.h>
#include "cli.h"
#include "cmd.h"
#include "error.h"
#include "sighandler.h"
#include "progress.h"
#include "fs_path.h"
#include "futils.h"
#define COMMAND_NAME "clone"
static char *branch, *remote_path, *local_path;
static int show_help, quiet, checkout = 1, bare;
static bool local_path_exists;
static cli_progress progress = CLI_PROGRESS_INIT;
static const cli_opt_spec opts[] = {
{ CLI_OPT_TYPE_SWITCH, "help", 0, &show_help, 1,
CLI_OPT_USAGE_HIDDEN | CLI_OPT_USAGE_STOP_PARSING, NULL,
"display help about the " COMMAND_NAME " command" },
{ CLI_OPT_TYPE_SWITCH, "quiet", 'q', &quiet, 1,
CLI_OPT_USAGE_DEFAULT, NULL, "display the type of the object" },
{ CLI_OPT_TYPE_SWITCH, "no-checkout", 'n', &checkout, 0,
CLI_OPT_USAGE_DEFAULT, NULL, "don't checkout HEAD" },
{ CLI_OPT_TYPE_SWITCH, "bare", 0, &bare, 1,
CLI_OPT_USAGE_DEFAULT, NULL, "don't create a working directory" },
{ CLI_OPT_TYPE_VALUE, "branch", 'b', &branch, 0,
CLI_OPT_USAGE_DEFAULT, "name", "branch to check out" },
{ CLI_OPT_TYPE_LITERAL },
{ CLI_OPT_TYPE_ARG, "repository", 0, &remote_path, 0,
CLI_OPT_USAGE_REQUIRED, "repository", "repository path" },
{ CLI_OPT_TYPE_ARG, "directory", 0, &local_path, 0,
CLI_OPT_USAGE_DEFAULT, "directory", "directory to clone into" },
{ 0 }
};
static void print_help(void)
{
cli_opt_usage_fprint(stdout, PROGRAM_NAME, COMMAND_NAME, opts);
printf("\n");
printf("Clone a repository into a new directory.\n");
printf("\n");
printf("Options:\n");
cli_opt_help_fprint(stdout, opts);
}
static char *compute_local_path(const char *orig_path)
{
const char *slash;
char *local_path;
if ((slash = strrchr(orig_path, '/')) == NULL &&
(slash = strrchr(orig_path, '\\')) == NULL)
local_path = git__strdup(orig_path);
else
local_path = git__strdup(slash + 1);
return local_path;
}
static bool validate_local_path(const char *path)
{
if (!git_fs_path_exists(path))
return false;
if (!git_fs_path_isdir(path) || !git_fs_path_is_empty_dir(path)) {
fprintf(stderr, "fatal: destination path '%s' already exists and is not an empty directory.\n",
path);
exit(128);
}
return true;
}
static void cleanup(void)
{
int rmdir_flags = GIT_RMDIR_REMOVE_FILES;
cli_progress_abort(&progress);
if (local_path_exists)
rmdir_flags |= GIT_RMDIR_SKIP_ROOT;
if (!git_fs_path_isdir(local_path))
return;
git_futils_rmdir_r(local_path, NULL, rmdir_flags);
}
static void interrupt_cleanup(void)
{
cleanup();
exit(130);
}
int cmd_clone(int argc, char **argv)
{
git_clone_options clone_opts = GIT_CLONE_OPTIONS_INIT;
git_repository *repo = NULL;
cli_opt invalid_opt;
char *computed_path = NULL;
int ret = 0;
if (cli_opt_parse(&invalid_opt, opts, argv + 1, argc - 1, CLI_OPT_PARSE_GNU))
return cli_opt_usage_error(COMMAND_NAME, opts, &invalid_opt);
if (show_help) {
print_help();
return 0;
}
if (!remote_path) {
ret = cli_error_usage("you must specify a repository to clone");
goto done;
}
if (bare)
clone_opts.bare = 1;
if (branch)
clone_opts.checkout_branch = branch;
if (!checkout)
clone_opts.checkout_opts.checkout_strategy = GIT_CHECKOUT_NONE;
if (!local_path)
local_path = computed_path = compute_local_path(remote_path);
local_path_exists = validate_local_path(local_path);
cli_sighandler_set_interrupt(interrupt_cleanup);
if (!local_path_exists &&
git_futils_mkdir(local_path, 0777, 0) < 0) {
ret = cli_error_git();
goto done;
}
if (!quiet) {
clone_opts.fetch_opts.callbacks.sideband_progress = cli_progress_fetch_sideband;
clone_opts.fetch_opts.callbacks.transfer_progress = cli_progress_fetch_transfer;
clone_opts.fetch_opts.callbacks.payload = &progress;
clone_opts.checkout_opts.progress_cb = cli_progress_checkout;
clone_opts.checkout_opts.progress_payload = &progress;
printf("Cloning into '%s'...\n", local_path);
}
if (git_clone(&repo, remote_path, local_path, &clone_opts) < 0) {
cleanup();
ret = cli_error_git();
goto done;
}
cli_progress_finish(&progress);
done:
cli_progress_dispose(&progress);
git__free(computed_path);
git_repository_free(repo);
return ret;
}
......@@ -29,6 +29,7 @@ const cli_opt_spec cli_common_opts[] = {
const cli_cmd_spec cli_cmds[] = {
{ "cat-file", cmd_cat_file, "Display an object in the repository" },
{ "clone", cmd_clone, "Clone a repository into a new directory" },
{ "hash-object", cmd_hash_object, "Hash a raw object and product its object ID" },
{ "help", cmd_help, "Display help information" },
{ NULL }
......
/*
* 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"
#define PROGRESS_UPDATE_TIME 0.05
#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);
double now = git__timer();
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 };
double now, recv_len, rate, elapsed;
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__timer();
if (done && progress->action_finish)
now = progress->action_finish;
else if (done)
progress->action_finish = now = git__timer();
else
now = git__timer();
recv_len = (double)stats->received_bytes;
elapsed = now - progress->action_start;
rate = elapsed ? recv_len / elapsed : 0;
done = (stats->received_objects == stats->total_objects);
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%% (%lu/%lu)%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));
}
/*
* 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.
*/
#ifndef CLI_progress_h__
#define CLI_progress_h__
#include <git2.h>
#include "str.h"
/*
* A general purpose set of progress printing functions. An individual
* `cli_progress` object is capable of displaying progress for a single
* function, even if that function displays multiple pieces of progress
* (like `git_clone`). `cli_progress_finish` should be called after
* any function invocation to re-set state.
*/
typedef enum {
CLI_PROGRESS_NONE,
CLI_PROGRESS_RECEIVING,
CLI_PROGRESS_RESOLVING,
CLI_PROGRESS_CHECKING_OUT
} cli_progress_t;
typedef struct {
cli_progress_t action;
/* Actions may time themselves (eg fetch) but are not required to */
double action_start;
double action_finish;
/* Last console update, avoid too frequent updates. */
double last_update;
/* Accumulators for partial output and deferred updates. */
git_str sideband;
git_str onscreen;
git_str deferred;
} cli_progress;
#define CLI_PROGRESS_INIT { 0 }
/**
* Prints sideband data from fetch to the console. Suitable for a
* `sideband_progress` callback for `git_fetch_options`.
*
* @param str The sideband string
* @param len The length of the sideband string
* @param payload A pointer to the cli_progress
* @return 0 on success, -1 on failure
*/
extern int cli_progress_fetch_sideband(
const char *str,
int len,
void *payload);
/**
* Prints fetch transfer statistics to the console. Suitable for a
* `transfer_progress` callback for `git_fetch_options`.
*
* @param stats The indexer stats
* @param payload A pointer to the cli_progress
* @return 0 on success, -1 on failure
*/
extern int cli_progress_fetch_transfer(
const git_indexer_progress *stats,
void *payload);
/**
* Prints checkout progress to the console. Suitable for a
* `progress_cb` callback for `git_checkout_options`.
*
* @param path The path being written
* @param completed_steps The completed checkout steps
* @param total_steps The total number of checkout steps
* @param payload A pointer to the cli_progress
*/
extern void cli_progress_checkout(
const char *path,
size_t completed_steps,
size_t total_steps,
void *payload);
/**
* Stop displaying progress quickly; suitable for stopping an application
* quickly. Does not display any lines that were buffered, just gets the
* console back to a sensible place.
*
* @param progress The progress information
* @return 0 on success, -1 on failure
*/
extern int cli_progress_abort(cli_progress *progress);
/**
* Finishes displaying progress; flushes any buffered output.
*
* @param progress The progress information
* @return 0 on success, -1 on failure
*/
extern int cli_progress_finish(cli_progress *progress);
/**
* Disposes the progress information.
*
* @param progress The progress information
*/
extern void cli_progress_dispose(cli_progress *progress);
#endif /* CLI_progress_h__ */
/*
* 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.
*/
#ifndef CLI_sighandler_h__
#define CLI_sighandler_h__
/**
* Sets up a signal handler that will run when the process is interrupted
* (via SIGINT on POSIX or Control-C or Control-Break on Windows).
*
* @param handler The function to run on interrupt
* @return 0 on success, -1 on failure
*/
int cli_sighandler_set_interrupt(void (*handler)(void));
#endif /* CLI_sighandler_h__ */
/*
* 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 <stdint.h>
#include <signal.h>
#include "git2_util.h"
#include "cli.h"
static void (*interrupt_handler)(void) = NULL;
static void interrupt_proxy(int signal)
{
GIT_UNUSED(signal);
interrupt_handler();
}
int cli_sighandler_set_interrupt(void (*handler)(void))
{
void (*result)(int);
if ((interrupt_handler = handler) != NULL)
result = signal(SIGINT, interrupt_proxy);
else
result = signal(SIGINT, SIG_DFL);
if (result == SIG_ERR) {
git_error_set(GIT_ERROR_OS, "could not set signal handler");
return -1;
}
return 0;
}
/*
* 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 "git2_util.h"
#include <windows.h>
#include "cli.h"
static void (*interrupt_handler)(void) = NULL;
static BOOL WINAPI interrupt_proxy(DWORD signal)
{
GIT_UNUSED(signal);
interrupt_handler();
return TRUE;
}
int cli_sighandler_set_interrupt(void (*handler)(void))
{
BOOL result;
if ((interrupt_handler = handler) != NULL)
result = SetConsoleCtrlHandler(interrupt_proxy, FALSE);
else
result = SetConsoleCtrlHandler(NULL, FALSE);
if (!result) {
git_error_set(GIT_ERROR_OS, "could not set control control handler");
return -1;
}
return 0;
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment