diff --git a/include/git2.h b/include/git2.h index 3457e5f0476..1cb77d4a54a 100644 --- a/include/git2.h +++ b/include/git2.h @@ -40,6 +40,7 @@ #include "git2/message.h" #include "git2/net.h" #include "git2/notes.h" +#include "git2/notification.h" #include "git2/object.h" #include "git2/odb.h" #include "git2/odb_backend.h" diff --git a/include/git2/common.h b/include/git2/common.h index 0be84fa77bd..fe102e0db48 100644 --- a/include/git2/common.h +++ b/include/git2/common.h @@ -257,7 +257,9 @@ typedef enum { GIT_OPT_GET_SERVER_TIMEOUT, GIT_OPT_SET_USER_AGENT_PRODUCT, GIT_OPT_GET_USER_AGENT_PRODUCT, - GIT_OPT_ADD_SSL_X509_CERT + GIT_OPT_ADD_SSL_X509_CERT, + GIT_OPT_SET_NOTIFICATION_CALLBACK, + GIT_OPT_GET_NOTIFICATION_CALLBACK } git_libgit2_opt_t; /** @@ -563,6 +565,32 @@ typedef enum { * > Sets the timeout (in milliseconds) for reading from and writing * > to a remote server. Set to 0 to use the system default. * + * opts(GIT_OPT_SET_NOTIFICATION_CALLBACK, int (*cb)(git_notification_level_t, git_notification_t, const char *, void *, ...), void *data) + * > Sets the notification callback, which will be invoked when + * > notifications occur that the calling program can display or + * > otherwise act on. + * > + * > The callback will be invoked for all informational messages, + * > warnings, and non-fatal errors, as well as continuable errors + * > that libgit2 would otherwise treat as fatal. + * > + * > Users should examine the notification level (which is the first + * > argument) and the notification type (the second argument) to + * > understand whether they want to act and how. The third argument + * > is the default message, so that callers can display warning + * > messages without needing to create them. The fourth argument is + * > the callback data, and the remainder of arguments are the + * > per-notification data; see the notifications for information + * > about what is returned for each notification type. + * > + * > - `cb` the callback to invoke when a warning occurs + * > - `data` data to be provided to warning callbacks, or NULL + * + * opts(GIT_OPT_GET_NOTIFICATION_CALLBACK, int *(*cb)(git_notification_level_t, git_notification_t, const char *, void *, ...), void **data) + * > Gets the current notification callback and callback data, which + * > will be invoked when notifications occur that the calling program + * > can display or otherwise act on. + * * @param option Option key * @return 0 on success, <0 on failure */ diff --git a/include/git2/notification.h b/include/git2/notification.h new file mode 100644 index 00000000000..197b7462f18 --- /dev/null +++ b/include/git2/notification.h @@ -0,0 +1,100 @@ +/* + * 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 INCLUDE_git_notification_h__ +#define INCLUDE_git_notification_h__ + +#include "common.h" + +/** + * @file git2/notification.h + * @brief Git notification routines + * @defgroup git_notification Git notification routines + * @ingroup Git + * @{ + */ +GIT_BEGIN_DECL + +/** + * The notification level. Most of these notifications are "informational"; + * by default, the notification levels below `GIT_NOTIFICATION_FATAL` will + * be raised but continue program execution. For these informational + * notifications, an application _may_ decide to stop processing (by + * returning a non-zero code from the notification callback). An example of + * an informational notification is a line ending misconfiguration when + * `core.safecrlf=warn` is configured. + * + * However, the notification `GIT_NOTIFICATION_FATAL` has different + * behavior; these notifications are raised before libgit2 stops processing + * and gives callers the ability to continue anyway. + */ +typedef enum { + /** + * An informational message; by default, libgit2 will continue + * function execution. + */ + GIT_NOTIFICATION_INFO = 0, + + /** + * A warning; by default, libgit2 will continue function execution + * and will not return an error code. A notification callback can + * override this behavior and cause libgit2 to return immediately. + * + * For example, when line-ending issues are encountered and + * `core.safecrlf=warn`, a warning notification is raised, but + * function execution otherwise continues. + */ + GIT_NOTIFICATION_WARN = 1, + + /** + * An error where, by default, libgit2 would continue function + * execution but return an error code at the end of execution. + * A notification callback can override this behavior and cause + * libgit2 to return immediately. + * + * For example, during checkout, non-fatal errors may be raised + * while trying to write an individual file (perhaps due to + * platform filename limitations). In this case, an error-level + * notification will be raised, checkout will continue to put files + * on disk, but the function will return an error code upon + * completion. + */ + GIT_NOTIFICATION_ERROR = 2, + + /** + * A severe error where, by default, libgit2 would stop function + * execution immediately and return an error code. A caller may + * wish to get additional insight into the error in the structured + * notification content. + * + * For example, a `safe.directory` is a fatal error. + */ + GIT_NOTIFICATION_FATAL = 3 +} git_notification_level_t; + +/** + * The notification type. Any notification that is sent by libgit2 will + * be a unique type, potentially with detailed information about the + * state of the notification. + */ +typedef enum { + /** + * A notification provided when `core.safecrlf` is configured and a + * file has line-ending reversability problems. The level will be + * `WARN` (when `core.safecrlf=warn`) or `FATAL` (when + * `core.safecrlf=on`). + * + * The data will be: + * + * - `const char *path`: the path to the file + * - `const char *message`: the notification message + */ + GIT_NOTIFICATION_CRLF = 1 +} git_notification_t; + +/** @} */ +GIT_END_DECL +#endif diff --git a/src/cli/common.c b/src/cli/common.c index dbeefea48ed..e14862acba2 100644 --- a/src/cli/common.c +++ b/src/cli/common.c @@ -15,6 +15,55 @@ #include "common.h" #include "error.h" +static int notification_cb( + git_notification_level_t level, + git_notification_t notification, + const char *message, + void *data) +{ + const char *level_string; + + GIT_UNUSED(notification); + GIT_UNUSED(data); + + /* + * Don't display fatal notifications; we'll get an error back from + * functions for those. + */ + if (level == GIT_NOTIFICATION_FATAL) + return 0; + + switch (level) { + case GIT_NOTIFICATION_ERROR: + level_string = "error"; + break; + case GIT_NOTIFICATION_INFO: + level_string = "info"; + break; + default: + level_string = "warning"; + } + + fprintf(stderr, "%s: %s\n", level_string, message); + fflush(stderr); + + return 0; +} + +void cli_init(void) +{ + if (git_libgit2_init() < 0 || + git_libgit2_opts(GIT_OPT_SET_NOTIFICATION_CALLBACK, notification_cb, NULL) < 0) { + cli_error("failed to initialize libgit2"); + exit(CLI_EXIT_GIT); + } +} + +void cli_shutdown(void) +{ + git_libgit2_shutdown(); +} + static int parse_option(cli_opt *opt, void *data) { git_str kv = GIT_STR_INIT, env = GIT_STR_INIT; diff --git a/src/cli/common.h b/src/cli/common.h index 330f776c91e..ac46a8a7769 100644 --- a/src/cli/common.h +++ b/src/cli/common.h @@ -51,6 +51,9 @@ typedef struct { int args_len; } cli_repository_open_options; +extern void cli_init(void); +extern void cli_shutdown(void); + extern int cli_repository_open( git_repository **out, cli_repository_open_options *opts); diff --git a/src/cli/main.c b/src/cli/main.c index 4716d6ddee9..a2dcb82c654 100644 --- a/src/cli/main.c +++ b/src/cli/main.c @@ -90,11 +90,7 @@ int main(int argc, char **argv) cli_opt opt; int ret = 0; - if (git_libgit2_init() < 0) { - cli_error("failed to initialize libgit2"); - exit(CLI_EXIT_GIT); - } - + cli_init(); 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 */ @@ -137,6 +133,6 @@ int main(int argc, char **argv) ret = cmd->fn(argc - 1, &argv[1]); done: - git_libgit2_shutdown(); + cli_shutdown(); return ret; } diff --git a/src/libgit2/crlf.c b/src/libgit2/crlf.c index 1e1f1e84558..e2087f940e2 100644 --- a/src/libgit2/crlf.c +++ b/src/libgit2/crlf.c @@ -17,6 +17,7 @@ #include "hash.h" #include "filter.h" #include "repository.h" +#include "notification.h" typedef enum { GIT_CRLF_UNDEFINED, @@ -146,10 +147,46 @@ static git_configmap_value output_eol(struct crlf_attrs *ca) return text_eol_is_crlf(ca) ? GIT_EOL_CRLF : GIT_EOL_LF; } - /* TODO: warn when available */ + GIT_ASSERT(!"unknown line ending configuration"); return ca->core_eol; } +static int warn_safecrlf(int direction, const char *filename) +{ + git_str message = GIT_STR_INIT; + int error; + + if (filename && !*filename) + filename = NULL; + + git_str_puts(&message, "in the working copy"); + + if (filename) { + git_str_puts(&message, " of '"); + git_str_puts(&message, filename); + git_str_puts(&message, "'"); + } + + if (direction == GIT_EOL_LF) + git_str_puts(&message, ", CRLF will be replaced by LF"); + else if (direction == GIT_EOL_CRLF) + git_str_puts(&message, ", LF will be replaced by CRLF"); + else + GIT_ASSERT(false); + + git_str_printf(&message, " the next time git touches it"); + + if (git_str_oom(&message)) + error = -1; + else + error = git_notification(GIT_NOTIFICATION_WARN, + GIT_NOTIFICATION_CRLF, + message.ptr, filename); + + git_str_dispose(&message); + return error; +} + GIT_INLINE(int) check_safecrlf( struct crlf_attrs *ca, const git_filter_source *src, @@ -167,7 +204,10 @@ GIT_INLINE(int) check_safecrlf( */ if (stats->crlf) { if (ca->safe_crlf == GIT_SAFE_CRLF_WARN) { - /* TODO: issue a warning when available */ + int error = warn_safecrlf(GIT_EOL_LF, filename); + + if (error != 0) + return error; } else { if (filename && *filename) git_error_set( @@ -187,7 +227,10 @@ GIT_INLINE(int) check_safecrlf( */ if (stats->crlf != stats->lf) { if (ca->safe_crlf == GIT_SAFE_CRLF_WARN) { - /* TODO: issue a warning when available */ + int error = warn_safecrlf(GIT_EOL_CRLF, filename); + + if (error != 0) + return error; } else { if (filename && *filename) git_error_set( diff --git a/src/libgit2/notification.c b/src/libgit2/notification.c new file mode 100644 index 00000000000..65b6ae354be --- /dev/null +++ b/src/libgit2/notification.c @@ -0,0 +1,17 @@ +/* + * 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 "notification.h" + +int GIT_CALLBACK(git_notification__callback)( + git_notification_level_t, + git_notification_t, + const char *, + void *, + ...) = NULL; + +void *git_notification__data = NULL; diff --git a/src/libgit2/notification.h b/src/libgit2/notification.h new file mode 100644 index 00000000000..e12810a2a6b --- /dev/null +++ b/src/libgit2/notification.h @@ -0,0 +1,26 @@ +/* + * 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 INCLUDE_notification_h__ +#define INCLUDE_notification_h__ + +#include "common.h" +#include "git2/notification.h" + +extern int GIT_CALLBACK(git_notification__callback)( + git_notification_level_t, + git_notification_t, + const char *, + void *, + ...); +extern void *git_notification__data; + +#define git_notification(level, notification, message, ...) \ + ((git_notification__callback == NULL) ? 0 : \ + git_notification__callback(level, notification, message, \ + git_notification__data, __VA_ARGS__)) + +#endif diff --git a/src/libgit2/settings.c b/src/libgit2/settings.c index 2e000f3c69f..ab6635d6b8d 100644 --- a/src/libgit2/settings.c +++ b/src/libgit2/settings.c @@ -26,6 +26,7 @@ #include "runtime.h" #include "sysdir.h" #include "thread.h" +#include "notification.h" #include "git2/global.h" #include "streams/registry.h" #include "streams/mbedtls.h" @@ -459,6 +460,26 @@ int git_libgit2_opts(int key, ...) } break; + case GIT_OPT_SET_NOTIFICATION_CALLBACK: + git_notification__callback = va_arg(ap, + int GIT_CALLBACK()(git_notification_level_t, + git_notification_t, + const char *, + void *, + ...)); + git_notification__data = va_arg(ap, void *); + break; + + case GIT_OPT_GET_NOTIFICATION_CALLBACK: + *(va_arg(ap, int GIT_CALLBACK(*)(git_notification_level_t, + git_notification_t, + const char *, + void *, + ...))) = + git_notification__callback; + *(va_arg(ap, void **)) = git_notification__data; + break; + default: git_error_set(GIT_ERROR_INVALID, "invalid option key"); error = -1; diff --git a/tests/libgit2/filter/crlf.c b/tests/libgit2/filter/crlf.c index 925ea58d2ec..38a0e20989d 100644 --- a/tests/libgit2/filter/crlf.c +++ b/tests/libgit2/filter/crlf.c @@ -15,6 +15,7 @@ void test_filter_crlf__initialize(void) void test_filter_crlf__cleanup(void) { + cl_git_pass(git_libgit2_opts(GIT_OPT_SET_NOTIFICATION_CALLBACK, NULL, NULL)); cl_git_sandbox_cleanup(); } @@ -72,15 +73,34 @@ void test_filter_crlf__to_odb(void) git_buf_dispose(&out); } +static int notification_cb( + git_notification_level_t notification_level, + git_notification_t notification_type, + const char *message, + void *data, + ...) +{ + GIT_UNUSED(message); + + cl_assert_equal_i(notification_level, GIT_NOTIFICATION_WARN); + cl_assert_equal_i(notification_type, GIT_NOTIFICATION_CRLF); + + (*((int *)data))++; + + return 0; +} + void test_filter_crlf__with_safecrlf(void) { git_filter_list *fl; git_filter *crlf; git_buf out = GIT_BUF_INIT; + int notification_count = 0; const char *in; size_t in_len; cl_repo_set_bool(g_repo, "core.safecrlf", true); + cl_git_pass(git_libgit2_opts(GIT_OPT_SET_NOTIFICATION_CALLBACK, notification_cb, ¬ification_count)); cl_git_pass(git_filter_list_new( &fl, g_repo, GIT_FILTER_TO_ODB, 0)); @@ -96,6 +116,7 @@ void test_filter_crlf__with_safecrlf(void) cl_git_pass(git_filter_list_apply_to_buffer(&out, fl, in, in_len)); cl_assert_equal_s("Normal\nCRLF\nline-endings.\n", out.ptr); + cl_assert_equal_i(0, notification_count); /* Mix of line endings fails with safecrlf */ in = "Mixed\nup\r\nLF\nand\r\nCRLF\nline-endings.\r\n"; @@ -103,6 +124,7 @@ void test_filter_crlf__with_safecrlf(void) cl_git_fail(git_filter_list_apply_to_buffer(&out, fl, in, in_len)); cl_assert_equal_i(git_error_last()->klass, GIT_ERROR_FILTER); + cl_assert_equal_i(0, notification_count); /* Normalized \n fails for autocrlf=true when safecrlf=true */ in = "Normal\nLF\nonly\nline-endings.\n"; @@ -110,6 +132,7 @@ void test_filter_crlf__with_safecrlf(void) cl_git_fail(git_filter_list_apply_to_buffer(&out, fl, in, in_len)); cl_assert_equal_i(git_error_last()->klass, GIT_ERROR_FILTER); + cl_assert_equal_i(0, notification_count); /* String with \r but without \r\n does not fail with safecrlf */ in = "Normal\nCR only\rand some more\nline-endings.\n"; @@ -117,6 +140,7 @@ void test_filter_crlf__with_safecrlf(void) cl_git_pass(git_filter_list_apply_to_buffer(&out, fl, in, in_len)); cl_assert_equal_s("Normal\nCR only\rand some more\nline-endings.\n", out.ptr); + cl_assert_equal_i(0, notification_count); git_filter_list_free(fl); git_buf_dispose(&out); @@ -127,10 +151,12 @@ void test_filter_crlf__with_safecrlf_and_unsafe_allowed(void) git_filter_list *fl; git_filter *crlf; git_buf out = GIT_BUF_INIT; + int notification_count = 0; const char *in; size_t in_len; cl_repo_set_bool(g_repo, "core.safecrlf", true); + cl_git_pass(git_libgit2_opts(GIT_OPT_SET_NOTIFICATION_CALLBACK, notification_cb, ¬ification_count)); cl_git_pass(git_filter_list_new( &fl, g_repo, GIT_FILTER_TO_ODB, GIT_FILTER_ALLOW_UNSAFE)); @@ -146,22 +172,23 @@ void test_filter_crlf__with_safecrlf_and_unsafe_allowed(void) cl_git_pass(git_filter_list_apply_to_buffer(&out, fl, in, in_len)); cl_assert_equal_s("Normal\nCRLF\nline-endings.\n", out.ptr); + cl_assert_equal_i(0, notification_count); /* Mix of line endings fails with safecrlf, but allowed to pass */ in = "Mixed\nup\r\nLF\nand\r\nCRLF\nline-endings.\r\n"; in_len = strlen(in); cl_git_pass(git_filter_list_apply_to_buffer(&out, fl, in, in_len)); - /* TODO: check for warning */ cl_assert_equal_s("Mixed\nup\nLF\nand\nCRLF\nline-endings.\n", out.ptr); + cl_assert_equal_i(1, notification_count); /* Normalized \n fails with safecrlf, but allowed to pass */ in = "Normal\nLF\nonly\nline-endings.\n"; in_len = strlen(in); cl_git_pass(git_filter_list_apply_to_buffer(&out, fl, in, in_len)); - /* TODO: check for warning */ cl_assert_equal_s("Normal\nLF\nonly\nline-endings.\n", out.ptr); + cl_assert_equal_i(2, notification_count); git_filter_list_free(fl); git_buf_dispose(&out); @@ -172,9 +199,12 @@ void test_filter_crlf__no_safecrlf(void) git_filter_list *fl; git_filter *crlf; git_buf out = GIT_BUF_INIT; + int notification_count = 0; const char *in; size_t in_len; + cl_git_pass(git_libgit2_opts(GIT_OPT_SET_NOTIFICATION_CALLBACK, notification_cb, ¬ification_count)); + cl_git_pass(git_filter_list_new( &fl, g_repo, GIT_FILTER_TO_ODB, 0)); @@ -189,6 +219,7 @@ void test_filter_crlf__no_safecrlf(void) cl_git_pass(git_filter_list_apply_to_buffer(&out, fl, in, in_len)); cl_assert_equal_s("Normal\nCRLF\nline-endings.\n", out.ptr); + cl_assert_equal_i(0, notification_count); /* Mix of line endings fails with safecrlf */ in = "Mixed\nup\r\nLF\nand\r\nCRLF\nline-endings.\r\n"; @@ -196,6 +227,7 @@ void test_filter_crlf__no_safecrlf(void) cl_git_pass(git_filter_list_apply_to_buffer(&out, fl, in, in_len)); cl_assert_equal_s("Mixed\nup\nLF\nand\nCRLF\nline-endings.\n", out.ptr); + cl_assert_equal_i(0, notification_count); /* Normalized \n fails with safecrlf */ in = "Normal\nLF\nonly\nline-endings.\n"; @@ -203,6 +235,7 @@ void test_filter_crlf__no_safecrlf(void) cl_git_pass(git_filter_list_apply_to_buffer(&out, fl, in, in_len)); cl_assert_equal_s("Normal\nLF\nonly\nline-endings.\n", out.ptr); + cl_assert_equal_i(0, notification_count); git_filter_list_free(fl); git_buf_dispose(&out); @@ -213,10 +246,12 @@ void test_filter_crlf__safecrlf_warn(void) git_filter_list *fl; git_filter *crlf; git_buf out = GIT_BUF_INIT; + int notification_count = 0; const char *in; size_t in_len; cl_repo_set_string(g_repo, "core.safecrlf", "warn"); + cl_git_pass(git_libgit2_opts(GIT_OPT_SET_NOTIFICATION_CALLBACK, notification_cb, ¬ification_count)); cl_git_pass(git_filter_list_new( &fl, g_repo, GIT_FILTER_TO_ODB, 0)); @@ -232,14 +267,15 @@ void test_filter_crlf__safecrlf_warn(void) cl_git_pass(git_filter_list_apply_to_buffer(&out, fl, in, in_len)); cl_assert_equal_s("Normal\nCRLF\nline-endings.\n", out.ptr); + cl_assert_equal_i(0, notification_count); /* Mix of line endings succeeds with safecrlf=warn */ in = "Mixed\nup\r\nLF\nand\r\nCRLF\nline-endings.\r\n"; in_len = strlen(in); cl_git_pass(git_filter_list_apply_to_buffer(&out, fl, in, in_len)); - /* TODO: check for warning */ cl_assert_equal_s("Mixed\nup\nLF\nand\nCRLF\nline-endings.\n", out.ptr); + cl_assert_equal_i(1, notification_count); /* Normalized \n is reversible, so does not fail with safecrlf=warn */ in = "Normal\nLF\nonly\nline-endings.\n"; @@ -247,6 +283,7 @@ void test_filter_crlf__safecrlf_warn(void) cl_git_pass(git_filter_list_apply_to_buffer(&out, fl, in, in_len)); cl_assert_equal_s(in, out.ptr); + cl_assert_equal_i(2, notification_count); git_filter_list_free(fl); git_buf_dispose(&out);