Commit 8a757ae24c8c4fdb3e2112de541aee8e7812ebda

Edward Thomson 2020-04-04T18:31:00

cli: introduce a clone command

diff --git a/src/cli/cmd.h b/src/cli/cmd.h
index 664b502..8b1a1b3 100644
--- a/src/cli/cmd.h
+++ b/src/cli/cmd.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);
 
diff --git a/src/cli/cmd_clone.c b/src/cli/cmd_clone.c
new file mode 100644
index 0000000..a382b58
--- /dev/null
+++ b/src/cli/cmd_clone.c
@@ -0,0 +1,176 @@
+/*
+ * 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;
+}
diff --git a/src/cli/main.c b/src/cli/main.c
index 08abb32..cbfc50e 100644
--- a/src/cli/main.c
+++ b/src/cli/main.c
@@ -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 }
diff --git a/src/cli/progress.c b/src/cli/progress.c
index 99c46d8..39b5953 100644
--- a/src/cli/progress.c
+++ b/src/cli/progress.c
@@ -43,7 +43,7 @@ GIT_INLINE(size_t) nl_len(bool *has_nl, const char *str, size_t len)
 	return i;
 }
 
-static int progress_write(cli_progress *progress, bool force, git_buf *line)
+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);
@@ -54,9 +54,9 @@ static int progress_write(cli_progress *progress, bool force, git_buf *line)
 	/* 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_buf_clear(&progress->deferred);
-			git_buf_put(&progress->deferred, line->ptr, line->size);
-			return git_buf_oom(&progress->deferred) ? -1 : 0;
+			git_str_clear(&progress->deferred);
+			git_str_put(&progress->deferred, line->ptr, line->size);
+			return git_str_oom(&progress->deferred) ? -1 : 0;
 		}
 	}
 
@@ -79,17 +79,17 @@ static int progress_write(cli_progress *progress, bool force, git_buf *line)
 	    fflush(stdout) != 0)
 		return_os_error("could not print status");
 
-	git_buf_clear(&progress->onscreen);
+	git_str_clear(&progress->onscreen);
 
 	if (line->ptr[line->size - 1] == '\n') {
 		progress->last_update = 0;
 	} else {
-		git_buf_put(&progress->onscreen, line->ptr, line->size);
+		git_str_put(&progress->onscreen, line->ptr, line->size);
 		progress->last_update = now;
 	}
 
-	git_buf_clear(&progress->deferred);
-	return git_buf_oom(&progress->onscreen) ? -1 : 0;
+	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, ...)
@@ -97,12 +97,12 @@ static int progress_printf(cli_progress *progress, bool force, const char *fmt, 
 
 int progress_printf(cli_progress *progress, bool force, const char *fmt, ...)
 {
-	git_buf buf = GIT_BUF_INIT;
+	git_str buf = GIT_BUF_INIT;
 	va_list ap;
 	int error;
 
 	va_start(ap, fmt);
-	error = git_buf_vprintf(&buf, fmt, ap);
+	error = git_str_vprintf(&buf, fmt, ap);
 	va_end(ap);
 
 	if (error < 0)
@@ -110,7 +110,7 @@ int progress_printf(cli_progress *progress, bool force, const char *fmt, ...)
 
 	error = progress_write(progress, force, &buf);
 
-	git_buf_dispose(&buf);
+	git_str_dispose(&buf);
 	return error;
 }
 
@@ -123,8 +123,8 @@ static int progress_complete(cli_progress *progress)
 		if (printf("\n") < 0)
 			return_os_error("could not print status");
 
-	git_buf_clear(&progress->deferred);
-	git_buf_clear(&progress->onscreen);
+	git_str_clear(&progress->deferred);
+	git_str_clear(&progress->onscreen);
 	progress->last_update = 0;
 	progress->action_start = 0;
 	progress->action_finish = 0;
@@ -149,7 +149,7 @@ int cli_progress_fetch_sideband(const char *str, int len, void *payload)
 		return 0;
 
 	/* Accumulate the sideband data, then print it line-at-a-time. */
-	if (git_buf_put(&progress->sideband, str, len) < 0)
+	if (git_str_put(&progress->sideband, str, len) < 0)
 		return -1;
 
 	str = progress->sideband.ptr;
@@ -174,7 +174,7 @@ int cli_progress_fetch_sideband(const char *str, int len, void *payload)
 		remain -= line_len;
 	}
 
-	git_buf_consume_bytes(&progress->sideband, (progress->sideband.size - remain));
+	git_str_consume_bytes(&progress->sideband, (progress->sideband.size - remain));
 
 	return 0;
 }
@@ -272,7 +272,7 @@ int cli_progress_fetch_transfer(const git_indexer_progress *stats, void *payload
 
 	default:
 		/* should not be reached */
-		cli_die("unexpected progress state");
+		GIT_ASSERT(!"unexpected progress state");
 	}
 
 	return error;
@@ -322,9 +322,9 @@ void cli_progress_dispose(cli_progress *progress)
 	if (progress == NULL)
 		return;
 
-	git_buf_dispose(&progress->sideband);
-	git_buf_dispose(&progress->onscreen);
-	git_buf_dispose(&progress->deferred);
+	git_str_dispose(&progress->sideband);
+	git_str_dispose(&progress->onscreen);
+	git_str_dispose(&progress->deferred);
 
 	memset(progress, 0, sizeof(cli_progress));
 }
diff --git a/src/cli/progress.h b/src/cli/progress.h
index 3eebcf8..fab2e97 100644
--- a/src/cli/progress.h
+++ b/src/cli/progress.h
@@ -9,7 +9,7 @@
 #define CLI_progress_h__
 
 #include <git2.h>
-#include "buffer.h"
+#include "str.h"
 
 /*
  * A general purpose set of progress printing functions.  An individual
@@ -37,9 +37,9 @@ typedef struct {
 	double last_update;
 
 	/* Accumulators for partial output and deferred updates. */
-	git_buf sideband;
-	git_buf onscreen;
-	git_buf deferred;
+	git_str sideband;
+	git_str onscreen;
+	git_str deferred;
 } cli_progress;
 
 #define CLI_PROGRESS_INIT { 0 }