Commit f1e2735c74d03105592a282e2c32f45033db0e8d

Russell Belfer 2013-01-30T11:10:39

Add helper for diff line stats This adds a `git_diff_patch_line_stats()` API that gets the total number of adds, deletes, and context lines in a patch. This will make it a little easier to emulate `git diff --stat` and the like. Right now, this relies on generating the `git_diff_patch` object, which is a pretty heavyweight way to get stat information. At some future point, it would probably be nice to be able to get this information without allocating the entire `git_diff_patch`, but that's a much larger project.

diff --git a/include/git2/diff.h b/include/git2/diff.h
index c052f34..81c41df 100644
--- a/include/git2/diff.h
+++ b/include/git2/diff.h
@@ -704,6 +704,28 @@ GIT_EXTERN(size_t) git_diff_patch_num_hunks(
 	git_diff_patch *patch);
 
 /**
+ * Get line counts of each type in a patch.
+ *
+ * This helps imitate a diff --numstat type of output.  For that purpose,
+ * you only need the `total_additions` and `total_deletions` values, but we
+ * include the `total_context` line count in case you want the total number
+ * of lines of diff output that will be generated.
+ *
+ * All outputs are optional. Pass NULL if you don't need a particular count.
+ *
+ * @param total_context Count of context lines in output, can be NULL.
+ * @param total_additions Count of addition lines in output, can be NULL.
+ * @param total_deletions Count of deletion lines in output, can be NULL.
+ * @param patch The git_diff_patch object
+ * @return Number of lines in hunk or -1 if invalid hunk index
+ */
+GIT_EXTERN(int) git_diff_patch_line_stats(
+	size_t *total_context,
+	size_t *total_additions,
+	size_t *total_deletions,
+	const git_diff_patch *patch);
+
+/**
  * Get the information about a hunk in a patch
  *
  * Given a patch and a hunk index into the patch, this returns detailed
diff --git a/src/buffer.h b/src/buffer.h
index 4efd240..6e73895 100644
--- a/src/buffer.h
+++ b/src/buffer.h
@@ -134,6 +134,13 @@ GIT_INLINE(ssize_t) git_buf_rfind(git_buf *buf, char ch)
 	return idx;
 }
 
+GIT_INLINE(ssize_t) git_buf_find(git_buf *buf, char ch)
+{
+	size_t idx = 0;
+	while (idx < buf->size && buf->ptr[idx] != ch) idx++;
+	return (idx == buf->size) ? -1 : (ssize_t)idx;
+}
+
 /* Remove whitespace from the end of the buffer */
 void git_buf_rtrim(git_buf *buf);
 
diff --git a/src/diff_output.c b/src/diff_output.c
index 8a7a7a2..4f1064b 100644
--- a/src/diff_output.c
+++ b/src/diff_output.c
@@ -1501,6 +1501,39 @@ size_t git_diff_patch_num_hunks(git_diff_patch *patch)
 	return patch->hunks_size;
 }
 
+int git_diff_patch_line_stats(
+	size_t *total_ctxt,
+	size_t *total_adds,
+	size_t *total_dels,
+	const git_diff_patch *patch)
+{
+	size_t totals[3], idx;
+
+	memset(totals, 0, sizeof(totals));
+
+	for (idx = 0; idx < patch->lines_size; ++idx) {
+		switch (patch->lines[idx].origin) {
+		case GIT_DIFF_LINE_CONTEXT:  totals[0]++; break;
+		case GIT_DIFF_LINE_ADDITION: totals[1]++; break;
+		case GIT_DIFF_LINE_DELETION: totals[2]++; break;
+		default:
+			/* diff --stat and --numstat don't count EOFNL marks because
+			 * they will always be paired with a ADDITION or DELETION line.
+			 */
+			break;
+		}
+	}
+
+	if (total_ctxt)
+		*total_ctxt = totals[0];
+	if (total_adds)
+		*total_adds = totals[1];
+	if (total_dels)
+		*total_dels = totals[2];
+
+	return 0;
+}
+
 int git_diff_patch_get_hunk(
 	const git_diff_range **range,
 	const char **header,
@@ -1706,4 +1739,3 @@ int git_diff__paired_foreach(
 
 	return 0;
 }
-
diff --git a/tests-clar/diff/patch.c b/tests-clar/diff/patch.c
index 4e85ab8..ea643e8 100644
--- a/tests-clar/diff/patch.c
+++ b/tests-clar/diff/patch.c
@@ -235,3 +235,68 @@ void test_diff_patch__hunks_have_correct_line_numbers(void)
 	git_diff_list_free(diff);
 	git_tree_free(head);
 }
+
+static void check_single_patch_stats(
+	git_repository *repo, size_t hunks, size_t adds, size_t dels)
+{
+	git_diff_list *diff;
+	git_diff_patch *patch;
+	const git_diff_delta *delta;
+	size_t actual_adds, actual_dels;
+
+	cl_git_pass(git_diff_index_to_workdir(&diff, repo, NULL, NULL));
+
+	cl_assert_equal_i(1, (int)git_diff_num_deltas(diff));
+
+	cl_git_pass(git_diff_get_patch(&patch, &delta, diff, 0));
+	cl_assert_equal_i(GIT_DELTA_MODIFIED, (int)delta->status);
+
+	cl_assert_equal_i(hunks, (int)git_diff_patch_num_hunks(patch));
+
+	cl_git_pass(
+		git_diff_patch_line_stats(NULL, &actual_adds, &actual_dels, patch));
+
+	cl_assert_equal_i(adds, actual_adds);
+	cl_assert_equal_i(dels, actual_dels);
+
+	git_diff_patch_free(patch);
+	git_diff_list_free(diff);
+}
+
+void test_diff_patch__line_counts_with_eofnl(void)
+{
+	git_buf content = GIT_BUF_INIT;
+	const char *end;
+	git_index *index;
+
+	g_repo = cl_git_sandbox_init("renames");
+
+	cl_git_pass(git_futils_readbuffer(&content, "renames/songofseven.txt"));
+
+	/* remove first line */
+
+	end = git_buf_cstr(&content) + git_buf_find(&content, '\n') + 1;
+	git_buf_consume(&content, end);
+	cl_git_rewritefile("renames/songofseven.txt", content.ptr);
+
+	check_single_patch_stats(g_repo, 1, 0, 1);
+
+	/* remove trailing whitespace */
+
+	git_buf_rtrim(&content);
+	cl_git_rewritefile("renames/songofseven.txt", content.ptr);
+
+	check_single_patch_stats(g_repo, 2, 1, 2);
+
+	/* add trailing whitespace */
+
+	cl_git_pass(git_repository_index(&index, g_repo));
+	cl_git_pass(git_index_add_bypath(index, "songofseven.txt"));
+	cl_git_pass(git_index_write(index));
+	git_index_free(index);
+
+	cl_git_pass(git_buf_putc(&content, '\n'));
+	cl_git_rewritefile("renames/songofseven.txt", content.ptr);
+
+	check_single_patch_stats(g_repo, 1, 1, 1);
+}