summaryrefslogtreecommitdiffstats
path: root/src/cli
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-04 12:47:08 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-04 12:47:08 +0000
commit29b5ab554790bb57337a3b6ab9dcd963cf69d22e (patch)
treebe1456d2bc6c1fb078695fad7bc8f6b212062d3c /src/cli
parentInitial commit. (diff)
downloadlibgit2-c1e1eb71b50aea7924f9091d188fa736bc813c6f.tar.xz
libgit2-c1e1eb71b50aea7924f9091d188fa736bc813c6f.zip
Adding upstream version 1.7.2+ds.upstream/1.7.2+ds
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/cli')
-rw-r--r--src/cli/CMakeLists.txt56
-rw-r--r--src/cli/README.md26
-rw-r--r--src/cli/cli.h20
-rw-r--r--src/cli/cmd.c21
-rw-r--r--src/cli/cmd.h33
-rw-r--r--src/cli/cmd_cat_file.c204
-rw-r--r--src/cli/cmd_clone.c192
-rw-r--r--src/cli/cmd_hash_object.c154
-rw-r--r--src/cli/cmd_help.c86
-rw-r--r--src/cli/error.h51
-rw-r--r--src/cli/main.c106
-rw-r--r--src/cli/opt.c669
-rw-r--r--src/cli/opt.h349
-rw-r--r--src/cli/opt_usage.c194
-rw-r--r--src/cli/opt_usage.h35
-rw-r--r--src/cli/progress.c346
-rw-r--r--src/cli/progress.h117
-rw-r--r--src/cli/sighandler.h20
-rw-r--r--src/cli/unix/sighandler.c36
-rw-r--r--src/cli/win32/precompiled.c1
-rw-r--r--src/cli/win32/precompiled.h3
-rw-r--r--src/cli/win32/sighandler.c37
22 files changed, 2756 insertions, 0 deletions
diff --git a/src/cli/CMakeLists.txt b/src/cli/CMakeLists.txt
new file mode 100644
index 0000000..84b6c19
--- /dev/null
+++ b/src/cli/CMakeLists.txt
@@ -0,0 +1,56 @@
+set(CLI_INCLUDES
+ "${libgit2_BINARY_DIR}/src/util"
+ "${libgit2_BINARY_DIR}/include"
+ "${libgit2_SOURCE_DIR}/src/util"
+ "${libgit2_SOURCE_DIR}/src/cli"
+ "${libgit2_SOURCE_DIR}/include"
+ "${LIBGIT2_DEPENDENCY_INCLUDES}")
+
+if(WIN32 AND NOT CYGWIN)
+ file(GLOB CLI_SRC_OS win32/*.c)
+ list(SORT CLI_SRC_OS)
+else()
+ file(GLOB CLI_SRC_OS unix/*.c)
+ list(SORT CLI_SRC_OS)
+endif()
+
+file(GLOB CLI_SRC_C *.c *.h)
+list(SORT CLI_SRC_C)
+
+#
+# The CLI currently needs to be statically linked against libgit2 because
+# the utility library uses libgit2's thread-local error buffers. TODO:
+# remove this dependency and allow us to dynamically link against libgit2.
+#
+
+if(BUILD_CLI STREQUAL "dynamic")
+ set(CLI_LIBGIT2_LIBRARY libgit2package)
+else()
+ set(CLI_LIBGIT2_OBJECTS $<TARGET_OBJECTS:libgit2>)
+endif()
+
+#
+# Compile and link the CLI
+#
+
+add_executable(git2_cli ${CLI_SRC_C} ${CLI_SRC_OS} ${CLI_OBJECTS}
+ $<TARGET_OBJECTS:util>
+ ${CLI_LIBGIT2_OBJECTS}
+ ${LIBGIT2_DEPENDENCY_OBJECTS})
+target_link_libraries(git2_cli ${CLI_LIBGIT2_LIBRARY} ${LIBGIT2_SYSTEM_LIBS})
+
+set_target_properties(git2_cli PROPERTIES C_STANDARD 90)
+set_target_properties(git2_cli PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${libgit2_BINARY_DIR})
+set_target_properties(git2_cli PROPERTIES OUTPUT_NAME ${LIBGIT2_FILENAME})
+
+ide_split_sources(git2_cli)
+
+target_include_directories(git2_cli PRIVATE ${CLI_INCLUDES})
+
+if(MSVC_IDE)
+ # Precompiled headers
+ set_target_properties(git2_cli PROPERTIES COMPILE_FLAGS "/Yuprecompiled.h /FIprecompiled.h")
+ set_source_files_properties(win32/precompiled.c COMPILE_FLAGS "/Ycprecompiled.h")
+endif()
+
+install(TARGETS git2_cli RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
diff --git a/src/cli/README.md b/src/cli/README.md
new file mode 100644
index 0000000..3087c39
--- /dev/null
+++ b/src/cli/README.md
@@ -0,0 +1,26 @@
+# cli
+
+A git-compatible command-line interface that uses libgit2.
+
+## Adding commands
+
+1. Individual commands have a `main`-like top-level entrypoint. For example:
+
+ ```c
+ int cmd_help(int argc, char **argv)
+ ```
+
+ Although this is the same signature as `main`, commands are not built as
+ individual standalone executables, they'll be linked into the main cli.
+ (Though there may be an option for command executables to be built as
+ standalone executables in the future.)
+
+2. Commands are prototyped in `cmd.h` and added to `main.c`'s list of
+ commands (`cli_cmds[]`). Commands should be specified with their name,
+ entrypoint and a brief description that can be printed in `git help`.
+ This is done because commands are linked into the main cli.
+
+3. Commands should accept a `--help` option that displays their help
+ information. This will be shown when a user runs `<command> --help` and
+ when a user runs `help <command>`.
+
diff --git a/src/cli/cli.h b/src/cli/cli.h
new file mode 100644
index 0000000..7dede67
--- /dev/null
+++ b/src/cli/cli.h
@@ -0,0 +1,20 @@
+/*
+ * 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_cli_h__
+#define CLI_cli_h__
+
+#define PROGRAM_NAME "git2"
+
+#include "git2_util.h"
+
+#include "error.h"
+#include "opt.h"
+#include "opt_usage.h"
+#include "sighandler.h"
+
+#endif /* CLI_cli_h__ */
diff --git a/src/cli/cmd.c b/src/cli/cmd.c
new file mode 100644
index 0000000..2a7e71c
--- /dev/null
+++ b/src/cli/cmd.c
@@ -0,0 +1,21 @@
+/*
+ * 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 "cli.h"
+#include "cmd.h"
+
+const cli_cmd_spec *cli_cmd_spec_byname(const char *name)
+{
+ const cli_cmd_spec *cmd;
+
+ for (cmd = cli_cmds; cmd->name; cmd++) {
+ if (!strcmp(cmd->name, name))
+ return cmd;
+ }
+
+ return NULL;
+}
diff --git a/src/cli/cmd.h b/src/cli/cmd.h
new file mode 100644
index 0000000..8b1a1b3
--- /dev/null
+++ b/src/cli/cmd.h
@@ -0,0 +1,33 @@
+/*
+ * 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_cmd_h__
+#define CLI_cmd_h__
+
+/* Command definitions */
+typedef struct {
+ const char *name;
+ int (*fn)(int argc, char **argv);
+ const char *desc;
+} cli_cmd_spec;
+
+/* Options that are common to all commands (eg --help, --git-dir) */
+extern const cli_opt_spec cli_common_opts[];
+
+/* All the commands supported by the CLI */
+extern const cli_cmd_spec cli_cmds[];
+
+/* Find a command by name */
+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);
+
+#endif /* CLI_cmd_h__ */
diff --git a/src/cli/cmd_cat_file.c b/src/cli/cmd_cat_file.c
new file mode 100644
index 0000000..fb53a72
--- /dev/null
+++ b/src/cli/cmd_cat_file.c
@@ -0,0 +1,204 @@
+/*
+ * 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.h>
+#include "cli.h"
+#include "cmd.h"
+
+#define COMMAND_NAME "cat-file"
+
+typedef enum {
+ DISPLAY_CONTENT = 0,
+ DISPLAY_EXISTS,
+ DISPLAY_PRETTY,
+ DISPLAY_SIZE,
+ DISPLAY_TYPE
+} display_t;
+
+static int show_help;
+static int display = DISPLAY_CONTENT;
+static char *type_name, *object_spec;
+
+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, NULL, 't', &display, DISPLAY_TYPE,
+ CLI_OPT_USAGE_REQUIRED, NULL, "display the type of the object" },
+ { CLI_OPT_TYPE_SWITCH, NULL, 's', &display, DISPLAY_SIZE,
+ CLI_OPT_USAGE_CHOICE, NULL, "display the size of the object" },
+ { CLI_OPT_TYPE_SWITCH, NULL, 'e', &display, DISPLAY_EXISTS,
+ CLI_OPT_USAGE_CHOICE, NULL, "displays nothing unless the object is corrupt" },
+ { CLI_OPT_TYPE_SWITCH, NULL, 'p', &display, DISPLAY_PRETTY,
+ CLI_OPT_USAGE_CHOICE, NULL, "pretty-print the object" },
+ { CLI_OPT_TYPE_ARG, "type", 0, &type_name, 0,
+ CLI_OPT_USAGE_CHOICE, "type", "the type of object to display" },
+ { CLI_OPT_TYPE_ARG, "object", 0, &object_spec, 0,
+ CLI_OPT_USAGE_REQUIRED, "object", "the object to display" },
+ { 0 },
+};
+
+static void print_help(void)
+{
+ cli_opt_usage_fprint(stdout, PROGRAM_NAME, COMMAND_NAME, opts);
+ printf("\n");
+
+ printf("Display the content for the given object in the repository.\n");
+ printf("\n");
+
+ printf("Options:\n");
+
+ cli_opt_help_fprint(stdout, opts);
+}
+
+static int print_odb(git_object *object, display_t display)
+{
+ git_odb *odb = NULL;
+ git_odb_object *odb_object = NULL;
+ const unsigned char *content;
+ git_object_size_t size;
+ int ret = 0;
+
+ /*
+ * Our parsed blobs retain the raw content; all other objects are
+ * parsed into a working representation. To get the raw content,
+ * we need to do an ODB lookup. (Thankfully, this should be cached
+ * in-memory from our last call.)
+ */
+ if (git_object_type(object) == GIT_OBJECT_BLOB) {
+ content = git_blob_rawcontent((git_blob *)object);
+ size = git_blob_rawsize((git_blob *)object);
+ } else {
+ if (git_repository_odb(&odb, git_object_owner(object)) < 0 ||
+ git_odb_read(&odb_object, odb, git_object_id(object)) < 0) {
+ ret = cli_error_git();
+ goto done;
+ }
+
+ content = git_odb_object_data(odb_object);
+ size = git_odb_object_size(odb_object);
+ }
+
+ switch (display) {
+ case DISPLAY_SIZE:
+ if (printf("%" PRIu64 "\n", size) < 0)
+ ret = cli_error_os();
+ break;
+ case DISPLAY_CONTENT:
+ if (p_write(fileno(stdout), content, (size_t)size) < 0)
+ ret = cli_error_os();
+ break;
+ default:
+ GIT_ASSERT(0);
+ }
+
+done:
+ git_odb_object_free(odb_object);
+ git_odb_free(odb);
+ return ret;
+}
+
+static int print_type(git_object *object)
+{
+ if (printf("%s\n", git_object_type2string(git_object_type(object))) < 0)
+ return cli_error_os();
+
+ return 0;
+}
+
+static int print_pretty(git_object *object)
+{
+ const git_tree_entry *entry;
+ size_t i, count;
+
+ /*
+ * Only trees are stored in an unreadable format and benefit from
+ * pretty-printing.
+ */
+ if (git_object_type(object) != GIT_OBJECT_TREE)
+ return print_odb(object, DISPLAY_CONTENT);
+
+ for (i = 0, count = git_tree_entrycount((git_tree *)object); i < count; i++) {
+ entry = git_tree_entry_byindex((git_tree *)object, i);
+
+ if (printf("%06o %s %s\t%s\n",
+ git_tree_entry_filemode_raw(entry),
+ git_object_type2string(git_tree_entry_type(entry)),
+ git_oid_tostr_s(git_tree_entry_id(entry)),
+ git_tree_entry_name(entry)) < 0)
+ return cli_error_os();
+ }
+
+ return 0;
+}
+
+int cmd_cat_file(int argc, char **argv)
+{
+ git_repository *repo = NULL;
+ git_object *object = NULL;
+ git_object_t type;
+ cli_opt invalid_opt;
+ int giterr, 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 (git_repository_open_ext(&repo, ".", GIT_REPOSITORY_OPEN_FROM_ENV, NULL) < 0)
+ return cli_error_git();
+
+ if ((giterr = git_revparse_single(&object, repo, object_spec)) < 0) {
+ if (display == DISPLAY_EXISTS && giterr == GIT_ENOTFOUND)
+ ret = 1;
+ else
+ ret = cli_error_git();
+
+ goto done;
+ }
+
+ if (type_name) {
+ git_object *peeled;
+
+ if ((type = git_object_string2type(type_name)) == GIT_OBJECT_INVALID) {
+ ret = cli_error_usage("invalid object type '%s'", type_name);
+ goto done;
+ }
+
+ if (git_object_peel(&peeled, object, type) < 0) {
+ ret = cli_error_git();
+ goto done;
+ }
+
+ git_object_free(object);
+ object = peeled;
+ }
+
+ switch (display) {
+ case DISPLAY_EXISTS:
+ ret = 0;
+ break;
+ case DISPLAY_TYPE:
+ ret = print_type(object);
+ break;
+ case DISPLAY_PRETTY:
+ ret = print_pretty(object);
+ break;
+ default:
+ ret = print_odb(object, display);
+ break;
+ }
+
+done:
+ git_object_free(object);
+ git_repository_free(repo);
+ return ret;
+}
diff --git a/src/cli/cmd_clone.c b/src/cli/cmd_clone.c
new file mode 100644
index 0000000..e477625
--- /dev/null
+++ b/src/cli/cmd_clone.c
@@ -0,0 +1,192 @@
+/*
+ * 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, *depth;
+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_VALUE, "depth", 0, &depth, 0,
+ CLI_OPT_USAGE_DEFAULT, "depth", "commit depth 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 int compute_depth(const char *depth)
+{
+ int64_t i;
+ const char *endptr;
+
+ if (!depth)
+ return 0;
+
+ if (git__strntol64(&i, depth, strlen(depth), &endptr, 10) < 0 || i < 0 || i > INT_MAX || *endptr) {
+ fprintf(stderr, "fatal: depth '%s' is not valid.\n", depth);
+ exit(128);
+ }
+
+ return (int)i;
+}
+
+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;
+ }
+
+ clone_opts.bare = !!bare;
+ clone_opts.checkout_branch = branch;
+ clone_opts.fetch_opts.depth = compute_depth(depth);
+
+ 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/cmd_hash_object.c b/src/cli/cmd_hash_object.c
new file mode 100644
index 0000000..93b980d
--- /dev/null
+++ b/src/cli/cmd_hash_object.c
@@ -0,0 +1,154 @@
+/*
+ * 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.h>
+#include "cli.h"
+#include "cmd.h"
+
+#include "futils.h"
+
+#define COMMAND_NAME "hash-object"
+
+static int show_help;
+static char *type_name;
+static int write_object, read_stdin, literally;
+static char **filenames;
+
+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_VALUE, NULL, 't', &type_name, 0,
+ CLI_OPT_USAGE_DEFAULT, "type", "the type of object to hash (default: \"blob\")" },
+ { CLI_OPT_TYPE_SWITCH, NULL, 'w', &write_object, 1,
+ CLI_OPT_USAGE_DEFAULT, NULL, "write the object to the object database" },
+ { CLI_OPT_TYPE_SWITCH, "literally", 0, &literally, 1,
+ CLI_OPT_USAGE_DEFAULT, NULL, "do not validate the object contents" },
+ { CLI_OPT_TYPE_SWITCH, "stdin", 0, &read_stdin, 1,
+ CLI_OPT_USAGE_REQUIRED, NULL, "read content from stdin" },
+ { CLI_OPT_TYPE_ARGS, "file", 0, &filenames, 0,
+ CLI_OPT_USAGE_CHOICE, "file", "the file (or files) to read and hash" },
+ { 0 },
+};
+
+static void print_help(void)
+{
+ cli_opt_usage_fprint(stdout, PROGRAM_NAME, COMMAND_NAME, opts);
+ printf("\n");
+
+ printf("Compute the object ID for a given file and optionally write that file\nto the object database.\n");
+ printf("\n");
+
+ printf("Options:\n");
+
+ cli_opt_help_fprint(stdout, opts);
+}
+
+static int hash_buf(
+ git_odb *odb,
+ git_str *buf,
+ git_object_t object_type,
+ git_oid_t oid_type)
+{
+ git_oid oid;
+
+ if (!literally) {
+ int valid = 0;
+
+#ifdef GIT_EXPERIMENTAL_SHA256
+ if (git_object_rawcontent_is_valid(&valid, buf->ptr, buf->size, object_type, oid_type) < 0 || !valid)
+ return cli_error_git();
+#else
+ GIT_UNUSED(oid_type);
+
+ if (git_object_rawcontent_is_valid(&valid, buf->ptr, buf->size, object_type) < 0 || !valid)
+ return cli_error_git();
+#endif
+ }
+
+ if (write_object) {
+ if (git_odb_write(&oid, odb, buf->ptr, buf->size, object_type) < 0)
+ return cli_error_git();
+ } else {
+#ifdef GIT_EXPERIMENTAL_SHA256
+ if (git_odb_hash(&oid, buf->ptr, buf->size, object_type, GIT_OID_SHA1) < 0)
+ return cli_error_git();
+#else
+ if (git_odb_hash(&oid, buf->ptr, buf->size, object_type) < 0)
+ return cli_error_git();
+#endif
+ }
+
+ if (printf("%s\n", git_oid_tostr_s(&oid)) < 0)
+ return cli_error_os();
+
+ return 0;
+}
+
+int cmd_hash_object(int argc, char **argv)
+{
+ git_repository *repo = NULL;
+ git_odb *odb = NULL;
+ git_oid_t oid_type;
+ git_str buf = GIT_STR_INIT;
+ cli_opt invalid_opt;
+ git_object_t object_type = GIT_OBJECT_BLOB;
+ char **filename;
+ 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 (type_name && (object_type = git_object_string2type(type_name)) == GIT_OBJECT_INVALID)
+ return cli_error_usage("invalid object type '%s'", type_name);
+
+ if (write_object &&
+ (git_repository_open_ext(&repo, ".", GIT_REPOSITORY_OPEN_FROM_ENV, NULL) < 0 ||
+ git_repository_odb(&odb, repo) < 0)) {
+ ret = cli_error_git();
+ goto done;
+ }
+
+ oid_type = git_repository_oid_type(repo);
+
+ /*
+ * TODO: we're reading blobs, we shouldn't pull them all into main
+ * memory, we should just stream them into the odb instead.
+ * (Or create a `git_odb_writefile` API.)
+ */
+ if (read_stdin) {
+ if (git_futils_readbuffer_fd_full(&buf, fileno(stdin)) < 0) {
+ ret = cli_error_git();
+ goto done;
+ }
+
+ if ((ret = hash_buf(odb, &buf, object_type, oid_type)) != 0)
+ goto done;
+ } else {
+ for (filename = filenames; *filename; filename++) {
+ if (git_futils_readbuffer(&buf, *filename) < 0) {
+ ret = cli_error_git();
+ goto done;
+ }
+
+ if ((ret = hash_buf(odb, &buf, object_type, oid_type)) != 0)
+ goto done;
+ }
+ }
+
+done:
+ git_str_dispose(&buf);
+ git_odb_free(odb);
+ git_repository_free(repo);
+ return ret;
+}
diff --git a/src/cli/cmd_help.c b/src/cli/cmd_help.c
new file mode 100644
index 0000000..7ee9822
--- /dev/null
+++ b/src/cli/cmd_help.c
@@ -0,0 +1,86 @@
+/*
+ * 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"
+
+#define COMMAND_NAME "help"
+
+static char *command;
+static int show_help;
+
+static const cli_opt_spec opts[] = {
+ { CLI_OPT_TYPE_SWITCH, "help", 0, &show_help, 1,
+ CLI_OPT_USAGE_HIDDEN, NULL, "display help about the help command" },
+ { CLI_OPT_TYPE_ARG, "command", 0, &command, 0,
+ CLI_OPT_USAGE_DEFAULT, "command", "the command to show help for" },
+ { 0 },
+};
+
+static int print_help(void)
+{
+ cli_opt_usage_fprint(stdout, PROGRAM_NAME, COMMAND_NAME, opts);
+ printf("\n");
+
+ printf("Display help information about %s. If a command is specified, help\n", PROGRAM_NAME);
+ printf("about that command will be shown. Otherwise, general information about\n");
+ printf("%s will be shown, including the commands available.\n", PROGRAM_NAME);
+
+ return 0;
+}
+
+static int print_commands(void)
+{
+ const cli_cmd_spec *cmd;
+
+ cli_opt_usage_fprint(stdout, PROGRAM_NAME, NULL, cli_common_opts);
+ printf("\n");
+
+ printf("These are the %s commands available:\n\n", PROGRAM_NAME);
+
+ for (cmd = cli_cmds; cmd->name; cmd++)
+ printf(" %-11s %s\n", cmd->name, cmd->desc);
+
+ printf("\nSee '%s help <command>' for more information on a specific command.\n", PROGRAM_NAME);
+
+ return 0;
+}
+
+int cmd_help(int argc, char **argv)
+{
+ char *fake_args[2];
+ const cli_cmd_spec *cmd;
+ cli_opt invalid_opt;
+
+ 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);
+
+ /* Show the meta-help */
+ if (show_help)
+ return print_help();
+
+ /* We were not asked to show help for a specific command. */
+ if (!command)
+ return print_commands();
+
+ /*
+ * If we were asked for help for a command (eg, `help <command>`),
+ * delegate back to that command's `--help` option. This lets
+ * commands own their help. Emulate the command-line arguments
+ * that would invoke `<command> --help` and invoke that command.
+ */
+ fake_args[0] = command;
+ fake_args[1] = "--help";
+
+ if ((cmd = cli_cmd_spec_byname(command)) == NULL)
+ return cli_error("'%s' is not a %s command. See '%s help'.",
+ command, PROGRAM_NAME, PROGRAM_NAME);
+
+ return cmd->fn(2, fake_args);
+}
diff --git a/src/cli/error.h b/src/cli/error.h
new file mode 100644
index 0000000..cce7a54
--- /dev/null
+++ b/src/cli/error.h
@@ -0,0 +1,51 @@
+/*
+ * 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_error_h__
+#define CLI_error_h__
+
+#include "cli.h"
+#include <stdio.h>
+
+#define CLI_EXIT_OK 0
+#define CLI_EXIT_ERROR 1
+#define CLI_EXIT_OS 128
+#define CLI_EXIT_GIT 128
+#define CLI_EXIT_USAGE 129
+
+#define cli_error__print(fmt) do { \
+ va_list ap; \
+ va_start(ap, fmt); \
+ fprintf(stderr, "%s: ", PROGRAM_NAME); \
+ vfprintf(stderr, fmt, ap); \
+ fprintf(stderr, "\n"); \
+ va_end(ap); \
+ } while(0)
+
+GIT_INLINE(int) cli_error(const char *fmt, ...)
+{
+ cli_error__print(fmt);
+ return CLI_EXIT_ERROR;
+}
+
+GIT_INLINE(int) cli_error_usage(const char *fmt, ...)
+{
+ cli_error__print(fmt);
+ return CLI_EXIT_USAGE;
+}
+
+GIT_INLINE(int) cli_error_git(void)
+{
+ const git_error *err = git_error_last();
+ fprintf(stderr, "%s: %s\n", PROGRAM_NAME,
+ err ? err->message : "unknown error");
+ return CLI_EXIT_GIT;
+}
+
+#define cli_error_os() (perror(PROGRAM_NAME), CLI_EXIT_OS)
+
+#endif /* CLI_error_h__ */
diff --git a/src/cli/main.c b/src/cli/main.c
new file mode 100644
index 0000000..cbfc50e
--- /dev/null
+++ b/src/cli/main.c
@@ -0,0 +1,106 @@
+/*
+ * 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"
+
+static int show_help = 0;
+static int show_version = 0;
+static char *command = NULL;
+static char **args = NULL;
+
+const cli_opt_spec cli_common_opts[] = {
+ { CLI_OPT_TYPE_SWITCH, "help", 0, &show_help, 1,
+ CLI_OPT_USAGE_DEFAULT, NULL, "display help information" },
+ { CLI_OPT_TYPE_SWITCH, "version", 0, &show_version, 1,
+ CLI_OPT_USAGE_DEFAULT, NULL, "display the version" },
+ { CLI_OPT_TYPE_ARG, "command", 0, &command, 0,
+ CLI_OPT_USAGE_REQUIRED, "command", "the command to run" },
+ { CLI_OPT_TYPE_ARGS, "args", 0, &args, 0,
+ CLI_OPT_USAGE_DEFAULT, "args", "arguments for the command" },
+ { 0 }
+};
+
+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 }
+};
+
+int main(int argc, char **argv)
+{
+ const cli_cmd_spec *cmd;
+ cli_opt_parser optparser;
+ cli_opt opt;
+ char *help_args[3] = { NULL };
+ int help_args_len;
+ int args_len = 0;
+ int ret = 0;
+
+ if (git_libgit2_init() < 0) {
+ cli_error("failed to initialize libgit2");
+ exit(CLI_EXIT_GIT);
+ }
+
+ cli_opt_parser_init(&optparser, cli_common_opts, argv + 1, argc - 1, CLI_OPT_PARSE_GNU);
+
+ /* Parse the top-level (common) options and command information */
+ while (cli_opt_parser_next(&opt, &optparser)) {
+ if (!opt.spec) {
+ cli_opt_status_fprint(stderr, PROGRAM_NAME, &opt);
+ cli_opt_usage_fprint(stderr, PROGRAM_NAME, NULL, cli_common_opts);
+ ret = CLI_EXIT_USAGE;
+ goto done;
+ }
+
+ /*
+ * When we see a command, stop parsing and capture the
+ * remaining arguments as args for the command itself.
+ */
+ if (command) {
+ args = &argv[optparser.idx];
+ args_len = (int)(argc - optparser.idx);
+ break;
+ }
+ }
+
+ if (show_version) {
+ printf("%s version %s\n", PROGRAM_NAME, LIBGIT2_VERSION);
+ goto done;
+ }
+
+ /*
+ * If `--help <command>` is specified, delegate to that command's
+ * `--help` option. If no command is specified, run the `help`
+ * command. Do this by updating the args to emulate that behavior.
+ */
+ if (!command || show_help) {
+ help_args[0] = command ? (char *)command : "help";
+ help_args[1] = command ? "--help" : NULL;
+ help_args_len = command ? 2 : 1;
+
+ command = help_args[0];
+ args = help_args;
+ args_len = help_args_len;
+ }
+
+ if ((cmd = cli_cmd_spec_byname(command)) == NULL) {
+ ret = cli_error("'%s' is not a %s command. See '%s help'.",
+ command, PROGRAM_NAME, PROGRAM_NAME);
+ goto done;
+ }
+
+ ret = cmd->fn(args_len, args);
+
+done:
+ git_libgit2_shutdown();
+ return ret;
+}
diff --git a/src/cli/opt.c b/src/cli/opt.c
new file mode 100644
index 0000000..62a3430
--- /dev/null
+++ b/src/cli/opt.c
@@ -0,0 +1,669 @@
+/*
+ * Copyright (c), Edward Thomson <ethomson@edwardthomson.com>
+ * All rights reserved.
+ *
+ * This file is part of adopt, distributed under the MIT license.
+ * For full terms and conditions, see the included LICENSE file.
+ *
+ * THIS FILE IS AUTOMATICALLY GENERATED; DO NOT EDIT.
+ *
+ * This file was produced by using the `rename.pl` script included with
+ * adopt. The command-line specified was:
+ *
+ * ./rename.pl cli_opt --filename=opt --include=cli.h --inline=GIT_INLINE --header-guard=CLI_opt_h__ --lowercase-status --without-usage
+ */
+
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+#include <limits.h>
+#include <assert.h>
+
+#include "cli.h"
+#include "opt.h"
+
+#ifdef _WIN32
+# include <windows.h>
+#else
+# include <fcntl.h>
+# include <sys/ioctl.h>
+#endif
+
+#ifdef _MSC_VER
+# define alloca _alloca
+#endif
+
+#define spec_is_option_type(x) \
+ ((x)->type == CLI_OPT_TYPE_BOOL || \
+ (x)->type == CLI_OPT_TYPE_SWITCH || \
+ (x)->type == CLI_OPT_TYPE_VALUE)
+
+GIT_INLINE(const cli_opt_spec *) spec_for_long(
+ int *is_negated,
+ int *has_value,
+ const char **value,
+ const cli_opt_parser *parser,
+ const char *arg)
+{
+ const cli_opt_spec *spec;
+ char *eql;
+ size_t eql_pos;
+
+ eql = strchr(arg, '=');
+ eql_pos = (eql = strchr(arg, '=')) ? (size_t)(eql - arg) : strlen(arg);
+
+ for (spec = parser->specs; spec->type; ++spec) {
+ /* Handle -- (everything after this is literal) */
+ if (spec->type == CLI_OPT_TYPE_LITERAL && arg[0] == '\0')
+ return spec;
+
+ /* Handle --no-option arguments for bool types */
+ if (spec->type == CLI_OPT_TYPE_BOOL &&
+ strncmp(arg, "no-", 3) == 0 &&
+ strcmp(arg + 3, spec->name) == 0) {
+ *is_negated = 1;
+ return spec;
+ }
+
+ /* Handle the typical --option arguments */
+ if (spec_is_option_type(spec) &&
+ spec->name &&
+ strcmp(arg, spec->name) == 0)
+ return spec;
+
+ /* Handle --option=value arguments */
+ if (spec->type == CLI_OPT_TYPE_VALUE &&
+ eql &&
+ strncmp(arg, spec->name, eql_pos) == 0 &&
+ spec->name[eql_pos] == '\0') {
+ *has_value = 1;
+ *value = arg[eql_pos + 1] ? &arg[eql_pos + 1] : NULL;
+ return spec;
+ }
+ }
+
+ return NULL;
+}
+
+GIT_INLINE(const cli_opt_spec *) spec_for_short(
+ const char **value,
+ const cli_opt_parser *parser,
+ const char *arg)
+{
+ const cli_opt_spec *spec;
+
+ for (spec = parser->specs; spec->type; ++spec) {
+ /* Handle -svalue short options with a value */
+ if (spec->type == CLI_OPT_TYPE_VALUE &&
+ arg[0] == spec->alias &&
+ arg[1] != '\0') {
+ *value = &arg[1];
+ return spec;
+ }
+
+ /* Handle typical -s short options */
+ if (arg[0] == spec->alias) {
+ *value = NULL;
+ return spec;
+ }
+ }
+
+ return NULL;
+}
+
+GIT_INLINE(const cli_opt_spec *) spec_for_arg(cli_opt_parser *parser)
+{
+ const cli_opt_spec *spec;
+ size_t args = 0;
+
+ for (spec = parser->specs; spec->type; ++spec) {
+ if (spec->type == CLI_OPT_TYPE_ARG) {
+ if (args == parser->arg_idx) {
+ parser->arg_idx++;
+ return spec;
+ }
+
+ args++;
+ }
+
+ if (spec->type == CLI_OPT_TYPE_ARGS && args == parser->arg_idx)
+ return spec;
+ }
+
+ return NULL;
+}
+
+GIT_INLINE(int) spec_is_choice(const cli_opt_spec *spec)
+{
+ return ((spec + 1)->type &&
+ ((spec + 1)->usage & CLI_OPT_USAGE_CHOICE));
+}
+
+/*
+ * If we have a choice with switches and bare arguments, and we see
+ * the switch, then we no longer expect the bare argument.
+ */
+GIT_INLINE(void) consume_choices(const cli_opt_spec *spec, cli_opt_parser *parser)
+{
+ /* back up to the beginning of the choices */
+ while (spec->type && (spec->usage & CLI_OPT_USAGE_CHOICE))
+ --spec;
+
+ if (!spec_is_choice(spec))
+ return;
+
+ do {
+ if (spec->type == CLI_OPT_TYPE_ARG)
+ parser->arg_idx++;
+ ++spec;
+ } while(spec->type && (spec->usage & CLI_OPT_USAGE_CHOICE));
+}
+
+static cli_opt_status_t parse_long(cli_opt *opt, cli_opt_parser *parser)
+{
+ const cli_opt_spec *spec;
+ char *arg = parser->args[parser->idx++];
+ const char *value = NULL;
+ int is_negated = 0, has_value = 0;
+
+ opt->arg = arg;
+
+ if ((spec = spec_for_long(&is_negated, &has_value, &value, parser, &arg[2])) == NULL) {
+ opt->spec = NULL;
+ opt->status = CLI_OPT_STATUS_UNKNOWN_OPTION;
+ goto done;
+ }
+
+ opt->spec = spec;
+
+ /* Future options parsed as literal */
+ if (spec->type == CLI_OPT_TYPE_LITERAL)
+ parser->in_literal = 1;
+
+ /* --bool or --no-bool */
+ else if (spec->type == CLI_OPT_TYPE_BOOL && spec->value)
+ *((int *)spec->value) = !is_negated;
+
+ /* --accumulate */
+ else if (spec->type == CLI_OPT_TYPE_ACCUMULATOR && spec->value)
+ *((int *)spec->value) += spec->switch_value ? spec->switch_value : 1;
+
+ /* --switch */
+ else if (spec->type == CLI_OPT_TYPE_SWITCH && spec->value)
+ *((int *)spec->value) = spec->switch_value;
+
+ /* Parse values as "--foo=bar" or "--foo bar" */
+ else if (spec->type == CLI_OPT_TYPE_VALUE) {
+ if (has_value)
+ opt->value = (char *)value;
+ else if ((parser->idx + 1) <= parser->args_len)
+ opt->value = parser->args[parser->idx++];
+
+ if (spec->value)
+ *((char **)spec->value) = opt->value;
+ }
+
+ /* Required argument was not provided */
+ if (spec->type == CLI_OPT_TYPE_VALUE &&
+ !opt->value &&
+ !(spec->usage & CLI_OPT_USAGE_VALUE_OPTIONAL))
+ opt->status = CLI_OPT_STATUS_MISSING_VALUE;
+ else
+ opt->status = CLI_OPT_STATUS_OK;
+
+ consume_choices(opt->spec, parser);
+
+done:
+ return opt->status;
+}
+
+static cli_opt_status_t parse_short(cli_opt *opt, cli_opt_parser *parser)
+{
+ const cli_opt_spec *spec;
+ char *arg = parser->args[parser->idx++];
+ const char *value;
+
+ opt->arg = arg;
+
+ if ((spec = spec_for_short(&value, parser, &arg[1 + parser->in_short])) == NULL) {
+ opt->spec = NULL;
+ opt->status = CLI_OPT_STATUS_UNKNOWN_OPTION;
+ goto done;
+ }
+
+ opt->spec = spec;
+
+ if (spec->type == CLI_OPT_TYPE_BOOL && spec->value)
+ *((int *)spec->value) = 1;
+
+ else if (spec->type == CLI_OPT_TYPE_ACCUMULATOR && spec->value)
+ *((int *)spec->value) += spec->switch_value ? spec->switch_value : 1;
+
+ else if (spec->type == CLI_OPT_TYPE_SWITCH && spec->value)
+ *((int *)spec->value) = spec->switch_value;
+
+ /* Parse values as "-ifoo" or "-i foo" */
+ else if (spec->type == CLI_OPT_TYPE_VALUE) {
+ if (value)
+ opt->value = (char *)value;
+ else if ((parser->idx + 1) <= parser->args_len)
+ opt->value = parser->args[parser->idx++];
+
+ if (spec->value)
+ *((char **)spec->value) = opt->value;
+ }
+
+ /*
+ * Handle compressed short arguments, like "-fbcd"; see if there's
+ * another character after the one we processed. If not, advance
+ * the parser index.
+ */
+ if (spec->type != CLI_OPT_TYPE_VALUE && arg[2 + parser->in_short] != '\0') {
+ parser->in_short++;
+ parser->idx--;
+ } else {
+ parser->in_short = 0;
+ }
+
+ /* Required argument was not provided */
+ if (spec->type == CLI_OPT_TYPE_VALUE && !opt->value)
+ opt->status = CLI_OPT_STATUS_MISSING_VALUE;
+ else
+ opt->status = CLI_OPT_STATUS_OK;
+
+ consume_choices(opt->spec, parser);
+
+done:
+ return opt->status;
+}
+
+static cli_opt_status_t parse_arg(cli_opt *opt, cli_opt_parser *parser)
+{
+ const cli_opt_spec *spec = spec_for_arg(parser);
+
+ opt->spec = spec;
+ opt->arg = parser->args[parser->idx];
+
+ if (!spec) {
+ parser->idx++;
+ opt->status = CLI_OPT_STATUS_UNKNOWN_OPTION;
+ } else if (spec->type == CLI_OPT_TYPE_ARGS) {
+ if (spec->value)
+ *((char ***)spec->value) = &parser->args[parser->idx];
+
+ /*
+ * We have started a list of arguments; the remainder of
+ * given arguments need not be examined.
+ */
+ parser->in_args = (parser->args_len - parser->idx);
+ parser->idx = parser->args_len;
+ opt->args_len = parser->in_args;
+ opt->status = CLI_OPT_STATUS_OK;
+ } else {
+ if (spec->value)
+ *((char **)spec->value) = parser->args[parser->idx];
+
+ parser->idx++;
+ opt->status = CLI_OPT_STATUS_OK;
+ }
+
+ return opt->status;
+}
+
+static int support_gnu_style(unsigned int flags)
+{
+ if ((flags & CLI_OPT_PARSE_FORCE_GNU) != 0)
+ return 1;
+
+ if ((flags & CLI_OPT_PARSE_GNU) == 0)
+ return 0;
+
+ /* TODO: Windows */
+#if defined(_WIN32) && defined(UNICODE)
+ if (_wgetenv(L"POSIXLY_CORRECT") != NULL)
+ return 0;
+#else
+ if (getenv("POSIXLY_CORRECT") != NULL)
+ return 0;
+#endif
+
+ return 1;
+}
+
+void cli_opt_parser_init(
+ cli_opt_parser *parser,
+ const cli_opt_spec specs[],
+ char **args,
+ size_t args_len,
+ unsigned int flags)
+{
+ assert(parser);
+
+ memset(parser, 0x0, sizeof(cli_opt_parser));
+
+ parser->specs = specs;
+ parser->args = args;
+ parser->args_len = args_len;
+ parser->flags = flags;
+
+ parser->needs_sort = support_gnu_style(flags);
+}
+
+GIT_INLINE(const cli_opt_spec *) spec_for_sort(
+ int *needs_value,
+ const cli_opt_parser *parser,
+ const char *arg)
+{
+ int is_negated, has_value = 0;
+ const char *value;
+ const cli_opt_spec *spec = NULL;
+ size_t idx = 0;
+
+ *needs_value = 0;
+
+ if (strncmp(arg, "--", 2) == 0) {
+ spec = spec_for_long(&is_negated, &has_value, &value, parser, &arg[2]);
+ *needs_value = !has_value;
+ }
+
+ else if (strncmp(arg, "-", 1) == 0) {
+ spec = spec_for_short(&value, parser, &arg[1]);
+
+ /*
+ * Advance through compressed short arguments to see if
+ * the last one has a value, eg "-xvffilename".
+ */
+ while (spec && !value && arg[1 + ++idx] != '\0')
+ spec = spec_for_short(&value, parser, &arg[1 + idx]);
+
+ *needs_value = (value == NULL);
+ }
+
+ return spec;
+}
+
+/*
+ * Some parsers allow for handling arguments like "file1 --help file2";
+ * this is done by re-sorting the arguments in-place; emulate that.
+ */
+static int sort_gnu_style(cli_opt_parser *parser)
+{
+ size_t i, j, insert_idx = parser->idx, offset;
+ const cli_opt_spec *spec;
+ char *option, *value;
+ int needs_value, changed = 0;
+
+ parser->needs_sort = 0;
+
+ for (i = parser->idx; i < parser->args_len; i++) {
+ spec = spec_for_sort(&needs_value, parser, parser->args[i]);
+
+ /* Not a "-" or "--" prefixed option. No change. */
+ if (!spec)
+ continue;
+
+ /* A "--" alone means remaining args are literal. */
+ if (spec->type == CLI_OPT_TYPE_LITERAL)
+ break;
+
+ option = parser->args[i];
+
+ /*
+ * If the argument is a value type and doesn't already
+ * have a value (eg "--foo=bar" or "-fbar") then we need
+ * to copy the next argument as its value.
+ */
+ if (spec->type == CLI_OPT_TYPE_VALUE && needs_value) {
+ /*
+ * A required value is not provided; set parser
+ * index to this value so that we fail on it.
+ */
+ if (i + 1 >= parser->args_len) {
+ parser->idx = i;
+ return 1;
+ }
+
+ value = parser->args[i + 1];
+ offset = 1;
+ } else {
+ value = NULL;
+ offset = 0;
+ }
+
+ /* Caller error if args[0] is an option. */
+ if (i == 0)
+ return 0;
+
+ /* Shift args up one (or two) and insert the option */
+ for (j = i; j > insert_idx; j--)
+ parser->args[j + offset] = parser->args[j - 1];
+
+ parser->args[insert_idx] = option;
+
+ if (value)
+ parser->args[insert_idx + 1] = value;
+
+ insert_idx += (1 + offset);
+ i += offset;
+
+ changed = 1;
+ }
+
+ return changed;
+}
+
+cli_opt_status_t cli_opt_parser_next(cli_opt *opt, cli_opt_parser *parser)
+{
+ assert(opt && parser);
+
+ memset(opt, 0x0, sizeof(cli_opt));
+
+ if (parser->idx >= parser->args_len) {
+ opt->args_len = parser->in_args;
+ return CLI_OPT_STATUS_DONE;
+ }
+
+ /* Handle options in long form, those beginning with "--" */
+ if (strncmp(parser->args[parser->idx], "--", 2) == 0 &&
+ !parser->in_short &&
+ !parser->in_literal)
+ return parse_long(opt, parser);
+
+ /* Handle options in short form, those beginning with "-" */
+ else if (parser->in_short ||
+ (strncmp(parser->args[parser->idx], "-", 1) == 0 &&
+ !parser->in_literal))
+ return parse_short(opt, parser);
+
+ /*
+ * We've reached the first "bare" argument. In POSIX mode, all
+ * remaining items on the command line are arguments. In GNU
+ * mode, there may be long or short options after this. Sort any
+ * options up to this position then re-parse the current position.
+ */
+ if (parser->needs_sort && sort_gnu_style(parser))
+ return cli_opt_parser_next(opt, parser);
+
+ return parse_arg(opt, parser);
+}
+
+GIT_INLINE(int) spec_included(const cli_opt_spec **specs, const cli_opt_spec *spec)
+{
+ const cli_opt_spec **i;
+
+ for (i = specs; *i; ++i) {
+ if (spec == *i)
+ return 1;
+ }
+
+ return 0;
+}
+
+static cli_opt_status_t validate_required(
+ cli_opt *opt,
+ const cli_opt_spec specs[],
+ const cli_opt_spec **given_specs)
+{
+ const cli_opt_spec *spec, *required;
+ int given;
+
+ /*
+ * Iterate over the possible specs to identify requirements and
+ * ensure that those have been given on the command-line.
+ * Note that we can have required *choices*, where one in a
+ * list of choices must be specified.
+ */
+ for (spec = specs, required = NULL, given = 0; spec->type; ++spec) {
+ if (!required && (spec->usage & CLI_OPT_USAGE_REQUIRED)) {
+ required = spec;
+ given = 0;
+ } else if (!required) {
+ continue;
+ }
+
+ if (!given)
+ given = spec_included(given_specs, spec);
+
+ /*
+ * Validate the requirement unless we're in a required
+ * choice. In that case, keep the required state and
+ * validate at the end of the choice list.
+ */
+ if (!spec_is_choice(spec)) {
+ if (!given) {
+ opt->spec = required;
+ opt->status = CLI_OPT_STATUS_MISSING_ARGUMENT;
+ break;
+ }
+
+ required = NULL;
+ given = 0;
+ }
+ }
+
+ return opt->status;
+}
+
+cli_opt_status_t cli_opt_parse(
+ cli_opt *opt,
+ const cli_opt_spec specs[],
+ char **args,
+ size_t args_len,
+ unsigned int flags)
+{
+ cli_opt_parser parser;
+ const cli_opt_spec **given_specs;
+ size_t given_idx = 0;
+
+ cli_opt_parser_init(&parser, specs, args, args_len, flags);
+
+ given_specs = alloca(sizeof(const cli_opt_spec *) * (args_len + 1));
+
+ while (cli_opt_parser_next(opt, &parser)) {
+ if (opt->status != CLI_OPT_STATUS_OK &&
+ opt->status != CLI_OPT_STATUS_DONE)
+ return opt->status;
+
+ if ((opt->spec->usage & CLI_OPT_USAGE_STOP_PARSING))
+ return (opt->status = CLI_OPT_STATUS_DONE);
+
+ given_specs[given_idx++] = opt->spec;
+ }
+
+ given_specs[given_idx] = NULL;
+
+ return validate_required(opt, specs, given_specs);
+}
+
+static int spec_name_fprint(FILE *file, const cli_opt_spec *spec)
+{
+ int error;
+
+ if (spec->type == CLI_OPT_TYPE_ARG)
+ error = fprintf(file, "%s", spec->value_name);
+ else if (spec->type == CLI_OPT_TYPE_ARGS)
+ error = fprintf(file, "%s", spec->value_name);
+ else if (spec->alias && !(spec->usage & CLI_OPT_USAGE_SHOW_LONG))
+ error = fprintf(file, "-%c", spec->alias);
+ else
+ error = fprintf(file, "--%s", spec->name);
+
+ return error;
+}
+
+int cli_opt_status_fprint(
+ FILE *file,
+ const char *command,
+ const cli_opt *opt)
+{
+ const cli_opt_spec *choice;
+ int error;
+
+ if (command && (error = fprintf(file, "%s: ", command)) < 0)
+ return error;
+
+ switch (opt->status) {
+ case CLI_OPT_STATUS_DONE:
+ error = fprintf(file, "finished processing arguments (no error)\n");
+ break;
+ case CLI_OPT_STATUS_OK:
+ error = fprintf(file, "no error\n");
+ break;
+ case CLI_OPT_STATUS_UNKNOWN_OPTION:
+ error = fprintf(file, "unknown option: %s\n", opt->arg);
+ break;
+ case CLI_OPT_STATUS_MISSING_VALUE:
+ if ((error = fprintf(file, "argument '")) < 0 ||
+ (error = spec_name_fprint(file, opt->spec)) < 0 ||
+ (error = fprintf(file, "' requires a value.\n")) < 0)
+ break;
+ break;
+ case CLI_OPT_STATUS_MISSING_ARGUMENT:
+ if (spec_is_choice(opt->spec)) {
+ int is_choice = 1;
+
+ if (spec_is_choice((opt->spec)+1))
+ error = fprintf(file, "one of");
+ else
+ error = fprintf(file, "either");
+
+ if (error < 0)
+ break;
+
+ for (choice = opt->spec; is_choice; ++choice) {
+ is_choice = spec_is_choice(choice);
+
+ if (!is_choice)
+ error = fprintf(file, " or");
+ else if (choice != opt->spec)
+ error = fprintf(file, ",");
+
+ if ((error < 0) ||
+ (error = fprintf(file, " '")) < 0 ||
+ (error = spec_name_fprint(file, choice)) < 0 ||
+ (error = fprintf(file, "'")) < 0)
+ break;
+
+ if (!spec_is_choice(choice))
+ break;
+ }
+
+ if ((error < 0) ||
+ (error = fprintf(file, " is required.\n")) < 0)
+ break;
+ } else {
+ if ((error = fprintf(file, "argument '")) < 0 ||
+ (error = spec_name_fprint(file, opt->spec)) < 0 ||
+ (error = fprintf(file, "' is required.\n")) < 0)
+ break;
+ }
+
+ break;
+ default:
+ error = fprintf(file, "unknown status: %d\n", opt->status);
+ break;
+ }
+
+ return error;
+}
+
diff --git a/src/cli/opt.h b/src/cli/opt.h
new file mode 100644
index 0000000..6c1d460
--- /dev/null
+++ b/src/cli/opt.h
@@ -0,0 +1,349 @@
+/*
+ * Copyright (c), Edward Thomson <ethomson@edwardthomson.com>
+ * All rights reserved.
+ *
+ * This file is part of adopt, distributed under the MIT license.
+ * For full terms and conditions, see the included LICENSE file.
+ *
+ * THIS FILE IS AUTOMATICALLY GENERATED; DO NOT EDIT.
+ *
+ * This file was produced by using the `rename.pl` script included with
+ * adopt. The command-line specified was:
+ *
+ * ./rename.pl cli_opt --filename=opt --include=cli.h --inline=GIT_INLINE --header-guard=CLI_opt_h__ --lowercase-status --without-usage
+ */
+
+#ifndef CLI_opt_h__
+#define CLI_opt_h__
+
+#include <stdio.h>
+#include <stdint.h>
+
+/**
+ * The type of argument to be parsed.
+ */
+typedef enum {
+ CLI_OPT_TYPE_NONE = 0,
+
+ /**
+ * An option that, when specified, sets a given value to true.
+ * This is useful for options like "--debug". A negation
+ * option (beginning with "no-") is implicitly specified; for
+ * example "--no-debug". The `value` pointer in the returned
+ * option will be set to `1` when this is specified, and set to
+ * `0` when the negation "no-" option is specified.
+ */
+ CLI_OPT_TYPE_BOOL,
+
+ /**
+ * An option that, when specified, sets the given `value` pointer
+ * to the specified `switch_value`. This is useful for booleans
+ * where you do not want the implicit negation that comes with an
+ * `CLI_OPT_TYPE_BOOL`, or for switches that multiplex a value, like
+ * setting a mode. For example, `--read` may set the `value` to
+ * `MODE_READ` and `--write` may set the `value` to `MODE_WRITE`.
+ */
+ CLI_OPT_TYPE_SWITCH,
+
+ /**
+ * An option that, when specified, increments the given
+ * `value` by the given `switch_value`. This can be specified
+ * multiple times to continue to increment the `value`.
+ * (For example, "-vvv" to set verbosity to 3.)
+ */
+ CLI_OPT_TYPE_ACCUMULATOR,
+
+ /**
+ * An option that takes a value, for example `-n value`,
+ * `-nvalue`, `--name value` or `--name=value`.
+ */
+ CLI_OPT_TYPE_VALUE,
+
+ /**
+ * A bare "--" that indicates that arguments following this are
+ * literal. This allows callers to specify things that might
+ * otherwise look like options, for example to operate on a file
+ * named "-rf" then you can invoke "program -- -rf" to treat
+ * "-rf" as an argument not an option.
+ */
+ CLI_OPT_TYPE_LITERAL,
+
+ /**
+ * A single argument, not an option. When options are exhausted,
+ * arguments will be matches in the order that they're specified
+ * in the spec list. For example, if two `CLI_OPT_TYPE_ARGS` are
+ * specified, `input_file` and `output_file`, then the first bare
+ * argument on the command line will be `input_file` and the
+ * second will be `output_file`.
+ */
+ CLI_OPT_TYPE_ARG,
+
+ /**
+ * A collection of arguments. This is useful when you want to take
+ * a list of arguments, for example, multiple paths. When specified,
+ * the value will be set to the first argument in the list.
+ */
+ CLI_OPT_TYPE_ARGS,
+} cli_opt_type_t;
+
+/**
+ * Additional information about an option, including parsing
+ * restrictions and usage information to be displayed to the end-user.
+ */
+typedef enum {
+ /** Defaults for the argument. */
+ CLI_OPT_USAGE_DEFAULT = 0,
+
+ /** This argument is required. */
+ CLI_OPT_USAGE_REQUIRED = (1u << 0),
+
+ /**
+ * This is a multiple choice argument, combined with the previous
+ * argument. For example, when the previous argument is `-f` and
+ * this optional is applied to an argument of type `-b` then one
+ * of `-f` or `-b` may be specified.
+ */
+ CLI_OPT_USAGE_CHOICE = (1u << 1),
+
+ /**
+ * This argument short-circuits the remainder of parsing.
+ * Useful for arguments like `--help`.
+ */
+ CLI_OPT_USAGE_STOP_PARSING = (1u << 2),
+
+ /** The argument's value is optional ("-n" or "-n foo") */
+ CLI_OPT_USAGE_VALUE_OPTIONAL = (1u << 3),
+
+ /** This argument should not be displayed in usage. */
+ CLI_OPT_USAGE_HIDDEN = (1u << 4),
+
+ /** In usage, show the long format instead of the abbreviated format. */
+ CLI_OPT_USAGE_SHOW_LONG = (1u << 5),
+} cli_opt_usage_t;
+
+typedef enum {
+ /** Default parsing behavior. */
+ CLI_OPT_PARSE_DEFAULT = 0,
+
+ /**
+ * Parse with GNU `getopt_long` style behavior, where options can
+ * be intermixed with arguments at any position (for example,
+ * "file1 --help file2".) Like `getopt_long`, this can mutate the
+ * arguments given.
+ */
+ CLI_OPT_PARSE_GNU = (1u << 0),
+
+ /**
+ * Force GNU `getopt_long` style behavior; the `POSIXLY_CORRECT`
+ * environment variable is ignored.
+ */
+ CLI_OPT_PARSE_FORCE_GNU = (1u << 1),
+} cli_opt_flag_t;
+
+/** Specification for an available option. */
+typedef struct cli_opt_spec {
+ /** Type of option expected. */
+ cli_opt_type_t type;
+
+ /** Name of the long option. */
+ const char *name;
+
+ /** The alias is the short (one-character) option alias. */
+ const char alias;
+
+ /**
+ * If this spec is of type `CLI_OPT_TYPE_BOOL`, this is a pointer
+ * to an `int` that will be set to `1` if the option is specified.
+ *
+ * If this spec is of type `CLI_OPT_TYPE_SWITCH`, this is a pointer
+ * to an `int` that will be set to the opt's `switch_value` (below)
+ * when this option is specified.
+ *
+ * If this spec is of type `CLI_OPT_TYPE_ACCUMULATOR`, this is a
+ * pointer to an `int` that will be incremented by the opt's
+ * `switch_value` (below). If no `switch_value` is provided then
+ * the value will be incremented by 1.
+ *
+ * If this spec is of type `CLI_OPT_TYPE_VALUE`,
+ * `CLI_OPT_TYPE_VALUE_OPTIONAL`, or `CLI_OPT_TYPE_ARG`, this is
+ * a pointer to a `char *` that will be set to the value
+ * specified on the command line.
+ *
+ * If this spec is of type `CLI_OPT_TYPE_ARGS`, this is a pointer
+ * to a `char **` that will be set to the remaining values
+ * specified on the command line.
+ */
+ void *value;
+
+ /**
+ * If this spec is of type `CLI_OPT_TYPE_SWITCH`, this is the value
+ * to set in the option's `value` pointer when it is specified. If
+ * this spec is of type `CLI_OPT_TYPE_ACCUMULATOR`, this is the value
+ * to increment in the option's `value` pointer when it is
+ * specified. This is ignored for other opt types.
+ */
+ int switch_value;
+
+ /**
+ * Optional usage flags that change parsing behavior and how
+ * usage information is shown to the end-user.
+ */
+ uint32_t usage;
+
+ /**
+ * The name of the value, provided when creating usage information.
+ * This is required only for the functions that display usage
+ * information and only when a spec is of type `CLI_OPT_TYPE_VALUE,
+ * `CLI_OPT_TYPE_ARG` or `CLI_OPT_TYPE_ARGS``.
+ */
+ const char *value_name;
+
+ /**
+ * Optional short description of the option to display to the
+ * end-user. This is only used when creating usage information.
+ */
+ const char *help;
+} cli_opt_spec;
+
+/** Return value for `cli_opt_parser_next`. */
+typedef enum {
+ /** Parsing is complete; there are no more arguments. */
+ CLI_OPT_STATUS_DONE = 0,
+
+ /**
+ * This argument was parsed correctly; the `opt` structure is
+ * populated and the value pointer has been set.
+ */
+ CLI_OPT_STATUS_OK = 1,
+
+ /**
+ * The argument could not be parsed correctly, it does not match
+ * any of the specifications provided.
+ */
+ CLI_OPT_STATUS_UNKNOWN_OPTION = 2,
+
+ /**
+ * The argument matched a spec of type `CLI_OPT_VALUE`, but no value
+ * was provided.
+ */
+ CLI_OPT_STATUS_MISSING_VALUE = 3,
+
+ /** A required argument was not provided. */
+ CLI_OPT_STATUS_MISSING_ARGUMENT = 4,
+} cli_opt_status_t;
+
+/** An option provided on the command-line. */
+typedef struct cli_opt {
+ /** The status of parsing the most recent argument. */
+ cli_opt_status_t status;
+
+ /**
+ * The specification that was provided on the command-line, or
+ * `NULL` if the argument did not match an `cli_opt_spec`.
+ */
+ const cli_opt_spec *spec;
+
+ /**
+ * The argument as it was specified on the command-line, including
+ * dashes, eg, `-f` or `--foo`.
+ */
+ char *arg;
+
+ /**
+ * If the spec is of type `CLI_OPT_VALUE` or `CLI_OPT_VALUE_OPTIONAL`,
+ * this is the value provided to the argument.
+ */
+ char *value;
+
+ /**
+ * If the argument is of type `CLI_OPT_ARGS`, this is the number of
+ * arguments remaining. This value is persisted even when parsing
+ * is complete and `status` == `CLI_OPT_STATUS_DONE`.
+ */
+ size_t args_len;
+} cli_opt;
+
+/* The internal parser state. Callers should not modify this structure. */
+typedef struct cli_opt_parser {
+ const cli_opt_spec *specs;
+ char **args;
+ size_t args_len;
+ unsigned int flags;
+
+ /* Parser state */
+ size_t idx;
+ size_t arg_idx;
+ size_t in_args;
+ size_t in_short;
+ int needs_sort : 1,
+ in_literal : 1;
+} cli_opt_parser;
+
+/**
+ * Parses all the command-line arguments and updates all the options using
+ * the pointers provided. Parsing stops on any invalid argument and
+ * information about the failure will be provided in the opt argument.
+ *
+ * This is the simplest way to parse options; it handles the initialization
+ * (`parser_init`) and looping (`parser_next`).
+ *
+ * @param opt The The `cli_opt` information that failed parsing
+ * @param specs A NULL-terminated array of `cli_opt_spec`s that can be parsed
+ * @param args The arguments that will be parsed
+ * @param args_len The length of arguments to be parsed
+ * @param flags The `cli_opt_flag_t flags for parsing
+ */
+cli_opt_status_t cli_opt_parse(
+ cli_opt *opt,
+ const cli_opt_spec specs[],
+ char **args,
+ size_t args_len,
+ unsigned int flags);
+
+/**
+ * Initializes a parser that parses the given arguments according to the
+ * given specifications.
+ *
+ * @param parser The `cli_opt_parser` that will be initialized
+ * @param specs A NULL-terminated array of `cli_opt_spec`s that can be parsed
+ * @param args The arguments that will be parsed
+ * @param args_len The length of arguments to be parsed
+ * @param flags The `cli_opt_flag_t flags for parsing
+ */
+void cli_opt_parser_init(
+ cli_opt_parser *parser,
+ const cli_opt_spec specs[],
+ char **args,
+ size_t args_len,
+ unsigned int flags);
+
+/**
+ * Parses the next command-line argument and places the information about
+ * the argument into the given `opt` data.
+ *
+ * @param opt The `cli_opt` information parsed from the argument
+ * @param parser An `cli_opt_parser` that has been initialized with
+ * `cli_opt_parser_init`
+ * @return true if the caller should continue iterating, or 0 if there are
+ * no arguments left to process.
+ */
+cli_opt_status_t cli_opt_parser_next(
+ cli_opt *opt,
+ cli_opt_parser *parser);
+
+/**
+ * Prints the status after parsing the most recent argument. This is
+ * useful for printing an error message when an unknown argument was
+ * specified, or when an argument was specified without a value.
+ *
+ * @param file The file to print information to
+ * @param command The name of the command to use when printing (optional)
+ * @param opt The option that failed to parse
+ * @return 0 on success, -1 on failure
+ */
+int cli_opt_status_fprint(
+ FILE *file,
+ const char *command,
+ const cli_opt *opt);
+
+#endif /* CLI_opt_h__ */
diff --git a/src/cli/opt_usage.c b/src/cli/opt_usage.c
new file mode 100644
index 0000000..478b416
--- /dev/null
+++ b/src/cli/opt_usage.c
@@ -0,0 +1,194 @@
+/*
+ * 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 "cli.h"
+#include "str.h"
+
+static int print_spec_name(git_str *out, const cli_opt_spec *spec)
+{
+ if (spec->type == CLI_OPT_TYPE_VALUE && spec->alias &&
+ !(spec->usage & CLI_OPT_USAGE_VALUE_OPTIONAL) &&
+ !(spec->usage & CLI_OPT_USAGE_SHOW_LONG))
+ return git_str_printf(out, "-%c <%s>", spec->alias, spec->value_name);
+ if (spec->type == CLI_OPT_TYPE_VALUE && spec->alias &&
+ !(spec->usage & CLI_OPT_USAGE_SHOW_LONG))
+ return git_str_printf(out, "-%c [<%s>]", spec->alias, spec->value_name);
+ if (spec->type == CLI_OPT_TYPE_VALUE &&
+ !(spec->usage & CLI_OPT_USAGE_VALUE_OPTIONAL))
+ return git_str_printf(out, "--%s[=<%s>]", spec->name, spec->value_name);
+ if (spec->type == CLI_OPT_TYPE_VALUE)
+ return git_str_printf(out, "--%s=<%s>", spec->name, spec->value_name);
+ if (spec->type == CLI_OPT_TYPE_ARG)
+ return git_str_printf(out, "<%s>", spec->value_name);
+ if (spec->type == CLI_OPT_TYPE_ARGS)
+ return git_str_printf(out, "<%s>...", spec->value_name);
+ if (spec->type == CLI_OPT_TYPE_LITERAL)
+ return git_str_printf(out, "--");
+ if (spec->alias && !(spec->usage & CLI_OPT_USAGE_SHOW_LONG))
+ return git_str_printf(out, "-%c", spec->alias);
+ if (spec->name)
+ return git_str_printf(out, "--%s", spec->name);
+
+ GIT_ASSERT(0);
+}
+
+/*
+ * This is similar to adopt's function, but modified to understand
+ * that we have a command ("git") and a "subcommand" ("checkout").
+ * It also understands a terminal's line length and wrap appropriately,
+ * using a `git_str` for storage.
+ */
+int cli_opt_usage_fprint(
+ FILE *file,
+ const char *command,
+ const char *subcommand,
+ const cli_opt_spec specs[])
+{
+ git_str usage = GIT_BUF_INIT, opt = GIT_BUF_INIT;
+ const cli_opt_spec *spec;
+ size_t i, prefixlen, linelen;
+ bool choice = false, next_choice = false, optional = false;
+ int error;
+
+ /* TODO: query actual console width. */
+ int console_width = 80;
+
+ if ((error = git_str_printf(&usage, "usage: %s", command)) < 0)
+ goto done;
+
+ if (subcommand &&
+ (error = git_str_printf(&usage, " %s", subcommand)) < 0)
+ goto done;
+
+ linelen = git_str_len(&usage);
+ prefixlen = linelen + 1;
+
+ for (spec = specs; spec->type; ++spec) {
+ if (!choice)
+ optional = !(spec->usage & CLI_OPT_USAGE_REQUIRED);
+
+ next_choice = !!((spec + 1)->usage & CLI_OPT_USAGE_CHOICE);
+
+ if (spec->usage & CLI_OPT_USAGE_HIDDEN)
+ continue;
+
+ if (choice)
+ git_str_putc(&opt, '|');
+ else
+ git_str_clear(&opt);
+
+ if (optional && !choice)
+ git_str_putc(&opt, '[');
+ if (!optional && !choice && next_choice)
+ git_str_putc(&opt, '(');
+
+ if ((error = print_spec_name(&opt, spec)) < 0)
+ goto done;
+
+ if (!optional && choice && !next_choice)
+ git_str_putc(&opt, ')');
+ else if (optional && !next_choice)
+ git_str_putc(&opt, ']');
+
+ if ((choice = next_choice))
+ continue;
+
+ if (git_str_oom(&opt)) {
+ error = -1;
+ goto done;
+ }
+
+ if (linelen > prefixlen &&
+ console_width > 0 &&
+ linelen + git_str_len(&opt) + 1 > (size_t)console_width) {
+ git_str_putc(&usage, '\n');
+
+ for (i = 0; i < prefixlen; i++)
+ git_str_putc(&usage, ' ');
+
+ linelen = prefixlen;
+ } else {
+ git_str_putc(&usage, ' ');
+ linelen += git_str_len(&opt) + 1;
+ }
+
+ git_str_puts(&usage, git_str_cstr(&opt));
+
+ if (git_str_oom(&usage)) {
+ error = -1;
+ goto done;
+ }
+ }
+
+ error = fprintf(file, "%s\n", git_str_cstr(&usage));
+
+done:
+ error = (error < 0) ? -1 : 0;
+
+ git_str_dispose(&usage);
+ git_str_dispose(&opt);
+ return error;
+}
+
+int cli_opt_usage_error(
+ const char *subcommand,
+ const cli_opt_spec specs[],
+ const cli_opt *invalid_opt)
+{
+ cli_opt_status_fprint(stderr, PROGRAM_NAME, invalid_opt);
+ cli_opt_usage_fprint(stderr, PROGRAM_NAME, subcommand, specs);
+ return CLI_EXIT_USAGE;
+}
+
+int cli_opt_help_fprint(
+ FILE *file,
+ const cli_opt_spec specs[])
+{
+ git_str help = GIT_BUF_INIT;
+ const cli_opt_spec *spec;
+ int error = 0;
+
+ /* Display required arguments first */
+ for (spec = specs; spec->type; ++spec) {
+ if (! (spec->usage & CLI_OPT_USAGE_REQUIRED) ||
+ (spec->usage & CLI_OPT_USAGE_HIDDEN))
+ continue;
+
+ git_str_printf(&help, " ");
+
+ if ((error = print_spec_name(&help, spec)) < 0)
+ goto done;
+
+ git_str_printf(&help, ": %s\n", spec->help);
+ }
+
+ /* Display the remaining arguments */
+ for (spec = specs; spec->type; ++spec) {
+ if ((spec->usage & CLI_OPT_USAGE_REQUIRED) ||
+ (spec->usage & CLI_OPT_USAGE_HIDDEN))
+ continue;
+
+ git_str_printf(&help, " ");
+
+ if ((error = print_spec_name(&help, spec)) < 0)
+ goto done;
+
+ git_str_printf(&help, ": %s\n", spec->help);
+
+ }
+
+ if (git_str_oom(&help) ||
+ p_write(fileno(file), help.ptr, help.size) < 0)
+ error = -1;
+
+done:
+ error = (error < 0) ? -1 : 0;
+
+ git_str_dispose(&help);
+ return error;
+}
+
diff --git a/src/cli/opt_usage.h b/src/cli/opt_usage.h
new file mode 100644
index 0000000..c752494
--- /dev/null
+++ b/src/cli/opt_usage.h
@@ -0,0 +1,35 @@
+/*
+ * 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_opt_usage_h__
+#define CLI_opt_usage_h__
+
+/**
+ * Prints usage information to the given file handle.
+ *
+ * @param file The file to print information to
+ * @param command The name of the command to use when printing
+ * @param subcommand The name of the subcommand (eg "checkout") to use when printing, or NULL to skip
+ * @param specs The specifications allowed by the command
+ * @return 0 on success, -1 on failure
+ */
+int cli_opt_usage_fprint(
+ FILE *file,
+ const char *command,
+ const char *subcommand,
+ const cli_opt_spec specs[]);
+
+int cli_opt_usage_error(
+ const char *subcommand,
+ const cli_opt_spec specs[],
+ const cli_opt *invalid_opt);
+
+int cli_opt_help_fprint(
+ FILE *file,
+ const cli_opt_spec specs[]);
+
+#endif /* CLI_opt_usage_h__ */
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));
+}
diff --git a/src/cli/progress.h b/src/cli/progress.h
new file mode 100644
index 0000000..886fef8
--- /dev/null
+++ b/src/cli/progress.h
@@ -0,0 +1,117 @@
+/*
+ * 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 */
+ uint64_t action_start;
+ uint64_t action_finish;
+
+ /* Last console update, avoid too frequent updates. */
+ uint64_t last_update;
+
+ /* Accumulators for partial output and deferred updates. */
+ git_str sideband;
+ git_str onscreen;
+ git_str deferred;
+
+ /* Last update about throughput */
+ uint64_t throughput_update;
+ double throughput_bytes;
+} 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__ */
diff --git a/src/cli/sighandler.h b/src/cli/sighandler.h
new file mode 100644
index 0000000..877223e
--- /dev/null
+++ b/src/cli/sighandler.h
@@ -0,0 +1,20 @@
+/*
+ * 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__ */
diff --git a/src/cli/unix/sighandler.c b/src/cli/unix/sighandler.c
new file mode 100644
index 0000000..6b4982d
--- /dev/null
+++ b/src/cli/unix/sighandler.c
@@ -0,0 +1,36 @@
+/*
+ * 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;
+}
diff --git a/src/cli/win32/precompiled.c b/src/cli/win32/precompiled.c
new file mode 100644
index 0000000..5f656a4
--- /dev/null
+++ b/src/cli/win32/precompiled.c
@@ -0,0 +1 @@
+#include "precompiled.h"
diff --git a/src/cli/win32/precompiled.h b/src/cli/win32/precompiled.h
new file mode 100644
index 0000000..b0309b8
--- /dev/null
+++ b/src/cli/win32/precompiled.h
@@ -0,0 +1,3 @@
+#include <git2.h>
+
+#include "cli.h"
diff --git a/src/cli/win32/sighandler.c b/src/cli/win32/sighandler.c
new file mode 100644
index 0000000..cc0b646
--- /dev/null
+++ b/src/cli/win32/sighandler.c
@@ -0,0 +1,37 @@
+/*
+ * 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;
+}