diff --git a/include/git2/diff.h b/include/git2/diff.h index c052f34f8..81c41df04 100644 --- a/include/git2/diff.h +++ b/include/git2/diff.h @@ -703,6 +703,28 @@ GIT_EXTERN(const git_diff_delta *) git_diff_patch_delta( 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 * diff --git a/src/buffer.h b/src/buffer.h index 4efd240b5..6e73895b4 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 8a7a7a2a1..4f1064b7f 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 4e85ab883..ea643e881 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); +}