diff --git a/examples/common.c b/examples/common.c index a066c153c..0f25f3787 100644 --- a/examples/common.c +++ b/examples/common.c @@ -53,6 +53,33 @@ size_t is_prefixed(const char *str, const char *pfx) return strncmp(str, pfx, len) ? 0 : len; } +int optional_str_arg( + const char **out, struct args_info *args, const char *opt, const char *def) +{ + const char *found = args->argv[args->pos]; + size_t len = is_prefixed(found, opt); + + if (!len) + return 0; + + if (!found[len]) { + if (args->pos + 1 == args->argc) { + *out = def; + return 1; + } + args->pos += 1; + *out = args->argv[args->pos]; + return 1; + } + + if (found[len] == '=') { + *out = found + len + 1; + return 1; + } + + return 0; +} + int match_str_arg( const char **out, struct args_info *args, const char *opt) { diff --git a/examples/common.h b/examples/common.h index 2fd7d579f..b9fa37ce9 100644 --- a/examples/common.h +++ b/examples/common.h @@ -46,6 +46,15 @@ struct args_info { }; #define ARGS_INFO_INIT { argc, argv, 0 } +/** + * Check current `args` entry against `opt` string. If it matches + * exactly, take the next arg as a string; if it matches as a prefix with + * an equal sign, take the remainder as a string; if value not supplied, + * default value `def` will be given. otherwise return 0. + */ +extern int optional_str_arg( + const char **out, struct args_info *args, const char *opt, const char *def); + /** * Check current `args` entry against `opt` string. If it matches * exactly, take the next arg as a string; if it matches as a prefix with diff --git a/examples/describe.c b/examples/describe.c new file mode 100644 index 000000000..cbf73b012 --- /dev/null +++ b/examples/describe.c @@ -0,0 +1,162 @@ +/* + * libgit2 "describe" example - shows how to describe commits + * + * Written by the libgit2 contributors + * + * To the extent possible under law, the author(s) have dedicated all copyright + * and related and neighboring rights to this software to the public domain + * worldwide. This software is distributed without any warranty. + * + * You should have received a copy of the CC0 Public Domain Dedication along + * with this software. If not, see + * . + */ + +#include "common.h" + +/** + * The following example partially reimplements the `git describe` command + * and some of its options. + * + * These commands should work: + + * - Describe HEAD with default options (`describe`) + * - Describe specified revision (`describe master~2`) + * - Describe specified revisions (`describe master~2 HEAD~3`) + * - Describe HEAD with dirty state suffix (`describe --dirty=*`) + * - Describe consider all refs (`describe --all master`) + * - Describe consider lightweight tags (`describe --tags temp-tag`) + * - Describe show non-default abbreviated size (`describe --abbrev=10`) + * - Describe always output the long format if matches a tag (`describe --long v1.0`) + * - Describe consider only tags of specified pattern (`describe --match v*-release`) + * - Describe show the fallback result (`describe --always`) + * - Describe follow only the first parent commit (`describe --first-parent`) + * + * The command line parsing logic is simplified and doesn't handle + * all of the use cases. + */ + +/** describe_options represents the parsed command line options */ +typedef struct { + const char **commits; + size_t commit_count; + git_describe_options describe_options; + git_describe_format_options format_options; +} describe_options; + +typedef struct args_info args_info; + +static void do_describe_single(git_repository *repo, describe_options *opts, const char *rev) +{ + git_object *commit; + git_describe_result *describe_result; + git_buf buf = { 0 }; + + if (rev) { + check_lg2(git_revparse_single(&commit, repo, rev), + "Failed to lookup rev", rev); + + check_lg2(git_describe_commit(&describe_result, commit, &opts->describe_options), + "Failed to describe rev", rev); + } + else + check_lg2(git_describe_workdir(&describe_result, repo, &opts->describe_options), + "Failed to describe workdir", NULL); + + check_lg2(git_describe_format(&buf, describe_result, &opts->format_options), + "Failed to format describe rev", rev); + + printf("%s\n", buf.ptr); +} + +static void do_describe(git_repository *repo, describe_options *opts) +{ + if (opts->commit_count == 0) + do_describe_single(repo, opts, NULL); + else + { + size_t i; + for (i = 0; i < opts->commit_count; i++) + do_describe_single(repo, opts, opts->commits[i]); + } +} + +static void print_usage(void) +{ + fprintf(stderr, "usage: see `git help describe`\n"); + exit(1); +} + +/** Parse command line arguments */ +static void parse_options(describe_options *opts, int argc, char **argv) +{ + args_info args = ARGS_INFO_INIT; + + for (args.pos = 1; args.pos < argc; ++args.pos) { + const char *curr = argv[args.pos]; + + if (curr[0] != '-') { + opts->commits = (const char **)realloc((void *)opts->commits, ++opts->commit_count); + opts->commits[opts->commit_count - 1] = curr; + } else if (!strcmp(curr, "--all")) { + opts->describe_options.describe_strategy = GIT_DESCRIBE_ALL; + } else if (!strcmp(curr, "--tags")) { + opts->describe_options.describe_strategy = GIT_DESCRIBE_TAGS; + } else if (!strcmp(curr, "--exact-match")) { + opts->describe_options.max_candidates_tags = 0; + } else if (!strcmp(curr, "--long")) { + opts->format_options.always_use_long_format = 1; + } else if (!strcmp(curr, "--always")) { + opts->describe_options.show_commit_oid_as_fallback = 1; + } else if (!strcmp(curr, "--first-parent")) { + opts->describe_options.only_follow_first_parent = 1; + } else if (optional_str_arg(&opts->format_options.dirty_suffix, &args, "--dirty", "-dirty")) { + } else if (match_int_arg((int *)&opts->format_options.abbreviated_size, &args, "--abbrev", 0)) { + } else if (match_int_arg((int *)&opts->describe_options.max_candidates_tags, &args, "--candidates", 0)) { + } else if (match_str_arg(&opts->describe_options.pattern, &args, "--match")) { + } + } + + if (opts->commit_count > 0) { + if (opts->format_options.dirty_suffix) + fatal("--dirty is incompatible with commit-ishes", NULL); + } + else { + if (!opts->format_options.dirty_suffix || !opts->format_options.dirty_suffix[0]) { + opts->commits = (const char **)malloc(++opts->commit_count); + opts->commits[0] = "HEAD"; + } + } +} + +/** Initialize describe_options struct */ +static void describe_options_init(describe_options *opts) +{ + memset(opts, 0, sizeof(*opts)); + + opts->commits = NULL; + opts->commit_count = 0; + git_describe_init_options(&opts->describe_options, GIT_DESCRIBE_OPTIONS_VERSION); + git_describe_init_format_options(&opts->format_options, GIT_DESCRIBE_FORMAT_OPTIONS_VERSION); +} + +int main(int argc, char **argv) +{ + git_repository *repo; + describe_options opts; + + git_threads_init(); + + check_lg2(git_repository_open_ext(&repo, ".", 0, NULL), + "Could not open repository", NULL); + + describe_options_init(&opts); + parse_options(&opts, argc, argv); + + do_describe(repo, &opts); + + git_repository_free(repo); + git_threads_shutdown(); + + return 0; +}