Commit 855c66de66fe39fe219c6dab9d614c6e9d633d9c

Edward Thomson 2014-03-27T16:34:20

Introduce core.safecrlf handling

diff --git a/src/config_cache.c b/src/config_cache.c
index ec75d15..4bcbf02 100644
--- a/src/config_cache.c
+++ b/src/config_cache.c
@@ -68,6 +68,7 @@ static struct map_data _cvar_maps[] = {
 	{"core.trustctime", NULL, 0, GIT_TRUSTCTIME_DEFAULT },
 	{"core.abbrev", _cvar_map_int, 1, GIT_ABBREV_DEFAULT },
 	{"core.precomposeunicode", NULL, 0, GIT_PRECOMPOSE_DEFAULT },
+	{"core.safecrlf", NULL, 0, GIT_SAFE_CRLF_DEFAULT},
 };
 
 int git_repository__cvar(int *out, git_repository *repo, git_cvar_cached cvar)
diff --git a/src/crlf.c b/src/crlf.c
index 2480cc9..645ada2 100644
--- a/src/crlf.c
+++ b/src/crlf.c
@@ -21,6 +21,7 @@ struct crlf_attrs {
 	int crlf_action;
 	int eol;
 	int auto_crlf;
+	int safe_crlf;
 };
 
 struct crlf_filter {
@@ -123,6 +124,9 @@ static int crlf_apply_to_odb(
 	const git_buf *from,
 	const git_filter_source *src)
 {
+	git_buf safe = GIT_BUF_INIT;
+	int error = 0;
+
 	/* Empty file? Nothing to do */
 	if (!git_buf_len(from))
 		return 0;
@@ -154,12 +158,31 @@ static int crlf_apply_to_odb(
 				return GIT_PASSTHROUGH;
 		}
 
-		if (!stats.cr)
+		if (!stats.cr && !ca->safe_crlf)
 			return GIT_PASSTHROUGH;
 	}
 
 	/* Actually drop the carriage returns */
-	return git_buf_text_crlf_to_lf(to, from);
+	if ((error = git_buf_text_crlf_to_lf(to, from)) < 0)
+		return error;
+
+	/* If safecrlf is enabled, sanity-check the result. */
+	if (ca->safe_crlf) {
+		if ((error = git_buf_grow(&safe, max(from->size, to->size))) < 0 ||
+			(error = git_buf_text_lf_to_crlf(&safe, to)) < 0)
+			goto done;
+
+		if (git_buf_cmp(from, &safe) != 0) {
+			giterr_set(GITERR_FILTER, "LF would be replaced by CRLF in '%s'",
+				git_filter_source_path(src));
+			error = -1;
+		}
+	}
+
+done:
+	git_buf_free(&safe);
+
+	return error;
 }
 
 static const char *line_ending(struct crlf_attrs *ca)
@@ -272,6 +295,13 @@ static int crlf_check(
 			return GIT_PASSTHROUGH;
 	}
 
+	if (git_filter_source_mode(src) == GIT_FILTER_CLEAN) {
+		error = git_repository__cvar(
+			&ca.safe_crlf, git_filter_source_repo(src), GIT_CVAR_SAFE_CRLF);
+		if (error < 0)
+			return error;
+	}
+
 	*payload = git__malloc(sizeof(ca));
 	GITERR_CHECK_ALLOC(*payload);
 	memcpy(*payload, &ca, sizeof(ca));
diff --git a/src/repository.h b/src/repository.h
index 99923b6..e401a82 100644
--- a/src/repository.h
+++ b/src/repository.h
@@ -38,6 +38,7 @@ typedef enum {
 	GIT_CVAR_TRUSTCTIME,    /* core.trustctime */
 	GIT_CVAR_ABBREV,        /* core.abbrev */
 	GIT_CVAR_PRECOMPOSE,    /* core.precomposeunicode */
+	GIT_CVAR_SAFE_CRLF,		/* core.safecrlf */
 	GIT_CVAR_CACHE_MAX
 } git_cvar_cached;
 
@@ -89,7 +90,8 @@ typedef enum {
 	GIT_ABBREV_DEFAULT = 7,
 	/* core.precomposeunicode */
 	GIT_PRECOMPOSE_DEFAULT = GIT_CVAR_FALSE,
-
+	/* core.safecrlf */
+	GIT_SAFE_CRLF_DEFAULT = GIT_CVAR_FALSE,
 } git_cvar_value;
 
 /* internal repository init flags */
diff --git a/tests/filter/crlf.c b/tests/filter/crlf.c
index c9fb9cd..75320ef 100644
--- a/tests/filter/crlf.c
+++ b/tests/filter/crlf.c
@@ -1,5 +1,6 @@
 #include "clar_libgit2.h"
 #include "git2/sys/filter.h"
+#include "buffer.h"
 
 static git_repository *g_repo = NULL;
 
@@ -69,3 +70,82 @@ void test_filter_crlf__to_odb(void)
 	git_filter_list_free(fl);
 	git_buf_free(&out);
 }
+
+void test_filter_crlf__with_safecrlf(void)
+{
+	git_filter_list *fl;
+	git_filter *crlf;
+	git_buf in = {0}, out = GIT_BUF_INIT;
+
+	cl_repo_set_bool(g_repo, "core.safecrlf", true);
+
+	cl_git_pass(git_filter_list_new(&fl, g_repo, GIT_FILTER_TO_ODB));
+
+	crlf = git_filter_lookup(GIT_FILTER_CRLF);
+	cl_assert(crlf != NULL);
+
+	cl_git_pass(git_filter_list_push(fl, crlf, NULL));
+
+	/* Normalized \r\n succeeds with safecrlf */
+	in.ptr = "Normal\r\nCRLF\r\nline-endings.\r\n";
+	in.size = strlen(in.ptr);
+
+	cl_git_pass(git_filter_list_apply_to_data(&out, fl, &in));
+	cl_assert_equal_s("Normal\nCRLF\nline-endings.\n", out.ptr);
+
+	/* Mix of line endings fails with safecrlf */
+	in.ptr = "Mixed\nup\r\nLF\nand\r\nCRLF\nline-endings.\r\n";
+	in.size = strlen(in.ptr);
+
+	cl_git_fail(git_filter_list_apply_to_data(&out, fl, &in));
+	cl_assert_equal_i(giterr_last()->klass, GITERR_FILTER);
+
+	/* Normalized \n fails with safecrlf */
+	in.ptr = "Normal\nLF\nonly\nline-endings.\n";
+	in.size = strlen(in.ptr);
+
+	cl_git_fail(git_filter_list_apply_to_data(&out, fl, &in));
+	cl_assert_equal_i(giterr_last()->klass, GITERR_FILTER);
+
+	git_filter_list_free(fl);
+	git_buf_free(&out);
+}
+
+void test_filter_crlf__no_safecrlf(void)
+{
+	git_filter_list *fl;
+	git_filter *crlf;
+	git_buf in = {0}, out = GIT_BUF_INIT;
+
+	cl_git_pass(git_filter_list_new(&fl, g_repo, GIT_FILTER_TO_ODB));
+
+	crlf = git_filter_lookup(GIT_FILTER_CRLF);
+	cl_assert(crlf != NULL);
+
+	cl_git_pass(git_filter_list_push(fl, crlf, NULL));
+
+	/* Normalized \r\n succeeds with safecrlf */
+	in.ptr = "Normal\r\nCRLF\r\nline-endings.\r\n";
+	in.size = strlen(in.ptr);
+
+	cl_git_pass(git_filter_list_apply_to_data(&out, fl, &in));
+	cl_assert_equal_s("Normal\nCRLF\nline-endings.\n", out.ptr);
+
+	/* Mix of line endings fails with safecrlf */
+	in.ptr = "Mixed\nup\r\nLF\nand\r\nCRLF\nline-endings.\r\n";
+	in.size = strlen(in.ptr);
+
+	cl_git_pass(git_filter_list_apply_to_data(&out, fl, &in));
+	cl_assert_equal_s("Mixed\nup\nLF\nand\nCRLF\nline-endings.\n", out.ptr);
+
+	/* Normalized \n fails with safecrlf */
+	in.ptr = "Normal\nLF\nonly\nline-endings.\n";
+	in.size = strlen(in.ptr);
+
+	cl_git_pass(git_filter_list_apply_to_data(&out, fl, &in));
+	cl_assert_equal_s("Normal\nLF\nonly\nline-endings.\n", out.ptr);
+
+	git_filter_list_free(fl);
+	git_buf_free(&out);
+}
+