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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173
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);
+}