diff --git a/include/git2/diff.h b/include/git2/diff.h index f855f52ba..571f0c887 100644 --- a/include/git2/diff.h +++ b/include/git2/diff.h @@ -1072,6 +1072,91 @@ GIT_EXTERN(int) git_diff_buffers( git_diff_line_cb line_cb, void *payload); +/** + * This is an opaque structure which is allocated by `git_diff_get_stats`. + * You are responsible for releasing the object memory when done, using the + * `git_diff_stats_free()` function. + */ +typedef struct git_diff_stats git_diff_stats; + +/** + * Formatting options for diff stats + */ +typedef enum { + /** No stats*/ + GIT_DIFF_STATS_NONE = 0, + + /** Full statistics, equivalent of `--stat` */ + GIT_DIFF_STATS_FULL = (1u << 0), + + /** Short statistics, equivalent of `--shortstat` */ + GIT_DIFF_STATS_SHORT = (1u << 1), + + /** Number statistics, equivalent of `--numstat` */ + GIT_DIFF_STATS_NUMBER = (1u << 2), + + /** Extended header information such as creations, renames and mode changes, equivalent of `--summary` */ + GIT_DIFF_STATS_INCLUDE_SUMMARY = (1u << 3), +} git_diff_stats_format_t; + +/** + * Accumlate diff statistics for all patches. + * + * @param out Structure containg the diff statistics. + * @param diff A git_diff generated by one of the above functions. + * @return 0 on success; non-zero on error + */ +GIT_EXTERN(int) git_diff_get_stats( + git_diff_stats **out, + git_diff *diff); + +/** + * Get the total number of files changed in a diff + * + * @param stats A `git_diff_stats` generated by one of the above functions. + * @return total number of files changed in the diff + */ +GIT_EXTERN(size_t) git_diff_stats_files_changed( + const git_diff_stats *stats); + +/** + * Get the total number of insertions in a diff + * + * @param stats A `git_diff_stats` generated by one of the above functions. + * @return total number of insertions in the diff + */ +GIT_EXTERN(size_t) git_diff_stats_insertions( + const git_diff_stats *stats); + +/** + * Get the total number of deletions in a diff + * + * @param stats A `git_diff_stats` generated by one of the above functions. + * @return total number of deletions in the diff + */ +GIT_EXTERN(size_t) git_diff_stats_deletions( + const git_diff_stats *stats); + +/** + * Print diff statistics to a `git_buf`. + * + * @param out buffer to store the formatted diff statistics in. + * @param stats A `git_diff_stats` generated by one of the above functions. + * @param format Formatting option. + * @return 0 on success; non-zero on error + */ +GIT_EXTERN(int) git_diff_stats_to_buf( + git_buf *out, + const git_diff_stats *stats, + git_diff_stats_format_t format); + +/** + * Deallocate a `git_diff_stats`. + * + * @param stats The previously created statistics object; + * cannot be used after free. + */ +GIT_EXTERN(void) git_diff_stats_free(git_diff_stats *stats); GIT_END_DECL diff --git a/src/diff_stats.c b/src/diff_stats.c new file mode 100644 index 000000000..bb436bf7b --- /dev/null +++ b/src/diff_stats.c @@ -0,0 +1,343 @@ +/* + * 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 "common.h" +#include "vector.h" +#include "diff.h" +#include "diff_patch.h" + +#define DIFF_RENAME_FILE_SEPARATOR " => " + +struct git_diff_stats { + git_vector patches; + + size_t files_changed; + size_t insertions; + size_t deletions; +}; + +static size_t diff_get_filename_padding( + int has_renames, + const git_diff_stats *stats) +{ + const git_patch *patch = NULL; + size_t i, max_padding = 0; + + if (has_renames) { + git_vector_foreach(&stats->patches, i, patch) { + const git_diff_delta *delta = NULL; + size_t len; + + delta = git_patch_get_delta(patch); + if (strcmp(delta->old_file.path, delta->new_file.path) == 0) + continue; + + if ((len = strlen(delta->old_file.path) + strlen(delta->new_file.path)) > max_padding) + max_padding = len; + } + } + + git_vector_foreach(&stats->patches, i, patch) { + const git_diff_delta *delta = NULL; + size_t len; + + delta = git_patch_get_delta(patch); + len = strlen(delta->new_file.path); + + if (strcmp(delta->old_file.path, delta->new_file.path) != 0) + continue; + + if (len > max_padding) + max_padding = len; + } + + return max_padding; +} + +int git_diff_file_stats__full_to_buf( + git_buf *out, + size_t max_padding, + int has_renames, + const git_patch *patch) +{ + const char *old_path = NULL, *new_path = NULL; + const git_diff_delta *delta = NULL; + size_t padding, old_size, new_size; + int error; + + delta = git_patch_get_delta(patch); + + old_path = delta->old_file.path; + new_path = delta->new_file.path; + old_size = delta->old_file.size; + new_size = delta->new_file.size; + + if ((error = git_buf_printf(out, " %s", old_path)) < 0) + goto on_error; + + if (strcmp(old_path, new_path) != 0) { + padding = max_padding - strlen(old_path) - strlen(new_path); + + if ((error = git_buf_printf(out, DIFF_RENAME_FILE_SEPARATOR "%s", new_path)) < 0) + goto on_error; + } + else { + padding = max_padding - strlen(old_path); + + if (has_renames) + padding += strlen(DIFF_RENAME_FILE_SEPARATOR); + } + + if ((error = git_buf_putcn(out, ' ', padding)) < 0 || + (error = git_buf_puts(out, " | ")) < 0) + goto on_error; + + if (delta->flags & GIT_DIFF_FLAG_BINARY) { + if ((error = git_buf_printf(out, "Bin %" PRIuZ " -> %" PRIuZ " bytes", old_size, new_size)) < 0) + goto on_error; + } + else { + size_t insertions, deletions; + + if ((error = git_patch_line_stats(NULL, &insertions, &deletions, patch)) < 0) + goto on_error; + + if ((error = git_buf_printf(out, "%" PRIuZ, insertions + deletions)) < 0) + goto on_error; + + if (insertions || deletions) { + if ((error = git_buf_putc(out, ' ')) < 0 || + (error = git_buf_putcn(out, '+', insertions)) < 0 || + (error = git_buf_putcn(out, '-', deletions)) < 0) + goto on_error; + } + } + + error = git_buf_putc(out, '\n'); + +on_error: + return error; +} + +int git_diff_file_stats__number_to_buf( + git_buf *out, + const git_patch *patch) +{ + const git_diff_delta *delta = NULL; + const char *path = NULL; + size_t insertions, deletions; + int error; + + delta = git_patch_get_delta(patch); + path = delta->new_file.path; + + if ((error = git_patch_line_stats(NULL, &insertions, &deletions, patch)) < 0) + return error; + + if (delta->flags & GIT_DIFF_FLAG_BINARY) + error = git_buf_printf(out, "%-8c" "%-8c" "%s\n", '-', '-', path); + else + error = git_buf_printf(out, "%-8" PRIuZ "%-8" PRIuZ "%s\n", insertions, deletions, path); + + return error; +} + +int git_diff_file_stats__summary_to_buf( + git_buf *out, + const git_patch *patch) +{ + const git_diff_delta *delta = NULL; + + delta = git_patch_get_delta(patch); + + if (delta->old_file.mode != delta->new_file.mode) { + if (delta->old_file.mode == 0) { + git_buf_printf(out, " create mode %06o %s\n", + delta->new_file.mode, delta->new_file.path); + } + else if (delta->new_file.mode == 0) { + git_buf_printf(out, " delete mode %06o %s\n", + delta->old_file.mode, delta->old_file.path); + } + else { + git_buf_printf(out, " mode change %06o => %06o %s\n", + delta->old_file.mode, delta->new_file.mode, delta->new_file.path); + } + } + + return 0; +} + +int git_diff_stats__has_renames( + const git_diff_stats *stats) +{ + git_patch *patch = NULL; + size_t i; + + git_vector_foreach(&stats->patches, i, patch) { + const git_diff_delta *delta = git_patch_get_delta(patch); + + if (strcmp(delta->old_file.path, delta->new_file.path) != 0) { + return 1; + } + } + + return 0; +} + +int git_diff_stats__add_file_stats( + git_diff_stats *stats, + git_patch *patch) +{ + const git_diff_delta *delta = NULL; + int error = 0; + + if ((delta = git_patch_get_delta(patch)) == NULL) + return -1; + + if ((error = git_vector_insert(&stats->patches, patch)) < 0) + return error; + + return error; +} + +int git_diff_get_stats( + git_diff_stats **out, + git_diff *diff) +{ + size_t i, deltas; + size_t total_insertions = 0, total_deletions = 0; + git_diff_stats *stats = NULL; + int error = 0; + + assert(out && diff); + + stats = git__calloc(1, sizeof(git_diff_stats)); + GITERR_CHECK_ALLOC(stats); + + deltas = git_diff_num_deltas(diff); + + for (i = 0; i < deltas; ++i) { + git_patch *patch = NULL; + size_t add, remove; + + if ((error = git_patch_from_diff(&patch, diff, i)) < 0) + goto on_error; + + if ((error = git_patch_line_stats(NULL, &add, &remove, patch)) < 0 || + (error = git_diff_stats__add_file_stats(stats, patch)) < 0) { + git_patch_free(patch); + goto on_error; + } + + total_insertions += add; + total_deletions += remove; + } + + stats->files_changed = deltas; + stats->insertions = total_insertions; + stats->deletions = total_deletions; + + *out = stats; + + goto done; + +on_error: + git_diff_stats_free(stats); + +done: + return error; +} + +size_t git_diff_stats_files_changed( + const git_diff_stats *stats) +{ + assert(stats); + + return stats->files_changed; +} + +size_t git_diff_stats_insertions( + const git_diff_stats *stats) +{ + assert(stats); + + return stats->insertions; +} + +size_t git_diff_stats_deletions( + const git_diff_stats *stats) +{ + assert(stats); + + return stats->deletions; +} + +int git_diff_stats_to_buf( + git_buf *out, + const git_diff_stats *stats, + git_diff_stats_format_t format) +{ + git_patch *patch = NULL; + size_t i; + int has_renames = 0, error = 0; + + assert(out && stats); + + /* check if we have renames, it affects the padding */ + has_renames = git_diff_stats__has_renames(stats); + + git_vector_foreach(&stats->patches, i, patch) { + if (format & GIT_DIFF_STATS_FULL) { + size_t max_padding = diff_get_filename_padding(has_renames, stats); + + error = git_diff_file_stats__full_to_buf(out, max_padding, has_renames, patch); + } + else if (format & GIT_DIFF_STATS_NUMBER) { + error = git_diff_file_stats__number_to_buf(out, patch); + } + + if (error < 0) + return error; + } + + if (format & GIT_DIFF_STATS_FULL || format & GIT_DIFF_STATS_SHORT) { + error = git_buf_printf(out, " %" PRIuZ " file%s changed, %" PRIuZ " insertions(+), %" PRIuZ " deletions(-)\n", + stats->files_changed, stats->files_changed > 1 ? "s" : "", + stats->insertions, stats->deletions); + + if (error < 0) + return error; + } + + if (format & GIT_DIFF_STATS_INCLUDE_SUMMARY) { + git_vector_foreach(&stats->patches, i, patch) { + if ((error = git_diff_file_stats__summary_to_buf(out, patch)) < 0) + return error; + } + + if (git_vector_length(&stats->patches) > 0) + git_buf_putc(out, '\n'); + } + + return error; +} + +void git_diff_stats_free(git_diff_stats *stats) +{ + size_t i; + git_patch *patch; + + if (stats == NULL) + return; + + git_vector_foreach(&stats->patches, i, patch) + git_patch_free(patch); + + git_vector_free(&stats->patches); + git__free(stats); +} + diff --git a/tests/diff/stats.c b/tests/diff/stats.c new file mode 100644 index 000000000..131b7681d --- /dev/null +++ b/tests/diff/stats.c @@ -0,0 +1,428 @@ +#include "clar.h" +#include "clar_libgit2.h" + +#include "buffer.h" +#include "commit.h" +#include "diff.h" + +static git_repository *repo; + +void test_diff_stats__initialize(void) +{ + repo = cl_git_sandbox_init("diff_format_email"); +} + +void test_diff_stats__cleanup(void) +{ + cl_git_sandbox_cleanup(); +} + +void test_diff_stats__stat(void) +{ + git_oid oid; + git_commit *commit = NULL; + git_diff *diff = NULL; + git_diff_stats *stats = NULL; + git_buf buf = GIT_BUF_INIT; + + const char *stat = + " file1.txt | 8 +++++---\n" \ + " 1 file changed, 5 insertions(+), 3 deletions(-)\n"; + + git_oid_fromstr(&oid, "9264b96c6d104d0e07ae33d3007b6a48246c6f92"); + + cl_git_pass(git_commit_lookup(&commit, repo, &oid)); + cl_git_pass(git_diff__commit(&diff, repo, commit, NULL)); + + cl_git_pass(git_diff_get_stats(&stats, diff)); + cl_assert(git_diff_stats_files_changed(stats) == 1); + cl_assert(git_diff_stats_insertions(stats) == 5); + cl_assert(git_diff_stats_deletions(stats) == 3); + + cl_git_pass(git_diff_stats_to_buf(&buf, stats, GIT_DIFF_STATS_FULL)); + cl_assert(strcmp(git_buf_cstr(&buf), stat) == 0); + + git_diff_stats_free(stats); + git_diff_free(diff); + git_commit_free(commit); + git_buf_free(&buf); +} + +void test_diff_stats__multiple_hunks(void) +{ + git_oid oid; + git_commit *commit = NULL; + git_diff *diff = NULL; + git_diff_stats *stats = NULL; + git_buf buf = GIT_BUF_INIT; + + const char *stat = + " file2.txt | 5 +++--\n" \ + " file3.txt | 6 ++++--\n" \ + " 2 files changed, 7 insertions(+), 4 deletions(-)\n"; + + git_oid_fromstr(&oid, "cd471f0d8770371e1bc78bcbb38db4c7e4106bd2"); + + cl_git_pass(git_commit_lookup(&commit, repo, &oid)); + cl_git_pass(git_diff__commit(&diff, repo, commit, NULL)); + + cl_git_pass(git_diff_get_stats(&stats, diff)); + cl_assert(git_diff_stats_files_changed(stats) == 2); + cl_assert(git_diff_stats_insertions(stats) == 7); + cl_assert(git_diff_stats_deletions(stats) == 4); + + cl_git_pass(git_diff_stats_to_buf(&buf, stats, GIT_DIFF_STATS_FULL)); + cl_assert(strcmp(git_buf_cstr(&buf), stat) == 0); + + git_diff_stats_free(stats); + git_diff_free(diff); + git_commit_free(commit); + git_buf_free(&buf); +} + +void test_diff_stats__numstat(void) +{ + git_oid oid; + git_commit *commit = NULL; + git_diff *diff = NULL; + git_diff_stats *stats = NULL; + git_buf buf = GIT_BUF_INIT; + + const char *stat = + "3 2 file2.txt\n" + "4 2 file3.txt\n"; + + git_oid_fromstr(&oid, "cd471f0d8770371e1bc78bcbb38db4c7e4106bd2"); + + cl_git_pass(git_commit_lookup(&commit, repo, &oid)); + cl_git_pass(git_diff__commit(&diff, repo, commit, NULL)); + + cl_git_pass(git_diff_get_stats(&stats, diff)); + + cl_git_pass(git_diff_stats_to_buf(&buf, stats, GIT_DIFF_STATS_NUMBER)); + cl_assert(strcmp(git_buf_cstr(&buf), stat) == 0); + + git_diff_stats_free(stats); + git_diff_free(diff); + git_commit_free(commit); + git_buf_free(&buf); +} + +void test_diff_stats__shortstat(void) +{ + git_oid oid; + git_commit *commit = NULL; + git_diff *diff = NULL; + git_diff_stats *stats = NULL; + git_buf buf = GIT_BUF_INIT; + + const char *stat = + " 1 file changed, 5 insertions(+), 3 deletions(-)\n"; + + git_oid_fromstr(&oid, "9264b96c6d104d0e07ae33d3007b6a48246c6f92"); + + cl_git_pass(git_commit_lookup(&commit, repo, &oid)); + cl_git_pass(git_diff__commit(&diff, repo, commit, NULL)); + + cl_git_pass(git_diff_get_stats(&stats, diff)); + cl_assert(git_diff_stats_files_changed(stats) == 1); + cl_assert(git_diff_stats_insertions(stats) == 5); + cl_assert(git_diff_stats_deletions(stats) == 3); + + cl_git_pass(git_diff_stats_to_buf(&buf, stats, GIT_DIFF_STATS_SHORT)); + cl_assert(strcmp(git_buf_cstr(&buf), stat) == 0); + + git_diff_stats_free(stats); + git_diff_free(diff); + git_commit_free(commit); + git_buf_free(&buf); +} + +void test_diff_stats__rename(void) +{ + git_oid oid; + git_commit *commit = NULL; + git_diff *diff = NULL; + git_diff_stats *stats = NULL; + git_buf buf = GIT_BUF_INIT; + + const char *stat = + " file2.txt => file2.txt.renamed | 1 +\n" + " file3.txt => file3.txt.renamed | 4 +++-\n" + " 2 files changed, 4 insertions(+), 1 deletions(-)\n"; + + git_oid_fromstr(&oid, "8947a46e2097638ca6040ad4877246f4186ec3bd"); + + cl_git_pass(git_commit_lookup(&commit, repo, &oid)); + cl_git_pass(git_diff__commit(&diff, repo, commit, NULL)); + cl_git_pass(git_diff_find_similar(diff, NULL)); + + cl_git_pass(git_diff_get_stats(&stats, diff)); + cl_assert(git_diff_stats_files_changed(stats) == 2); + cl_assert(git_diff_stats_insertions(stats) == 4); + cl_assert(git_diff_stats_deletions(stats) == 1); + + cl_git_pass(git_diff_stats_to_buf(&buf, stats, GIT_DIFF_STATS_FULL)); + cl_assert(strcmp(git_buf_cstr(&buf), stat) == 0); + + git_diff_stats_free(stats); + git_diff_free(diff); + git_commit_free(commit); + git_buf_free(&buf); +} + +void test_diff_stats__rename_nochanges(void) +{ + git_oid oid; + git_commit *commit = NULL; + git_diff *diff = NULL; + git_diff_stats *stats = NULL; + git_buf buf = GIT_BUF_INIT; + + const char *stat = + " file2.txt.renamed => file2.txt.renamed2 | 0\n" + " file3.txt.renamed => file3.txt.renamed2 | 0\n" + " 2 files changed, 0 insertions(+), 0 deletions(-)\n"; + + git_oid_fromstr(&oid, "3991dce9e71a0641ca49a6a4eea6c9e7ff402ed4"); + + cl_git_pass(git_commit_lookup(&commit, repo, &oid)); + cl_git_pass(git_diff__commit(&diff, repo, commit, NULL)); + cl_git_pass(git_diff_find_similar(diff, NULL)); + + cl_git_pass(git_diff_get_stats(&stats, diff)); + cl_assert(git_diff_stats_files_changed(stats) == 2); + cl_assert(git_diff_stats_insertions(stats) == 0); + cl_assert(git_diff_stats_deletions(stats) == 0); + + cl_git_pass(git_diff_stats_to_buf(&buf, stats, GIT_DIFF_STATS_FULL)); + cl_assert(strcmp(git_buf_cstr(&buf), stat) == 0); + + git_diff_stats_free(stats); + git_diff_free(diff); + git_commit_free(commit); + git_buf_free(&buf); +} + +void test_diff_stats__rename_and_modifiy(void) +{ + git_oid oid; + git_commit *commit = NULL; + git_diff *diff = NULL; + git_diff_stats *stats = NULL; + git_buf buf = GIT_BUF_INIT; + + const char *stat = + " file2.txt.renamed2 | 2 +-\n" + " file3.txt.renamed2 => file3.txt.renamed | 0\n" + " 2 files changed, 1 insertions(+), 1 deletions(-)\n"; + + git_oid_fromstr(&oid, "4ca10087e696d2ba78d07b146a118e9a7096ed4f"); + + cl_git_pass(git_commit_lookup(&commit, repo, &oid)); + cl_git_pass(git_diff__commit(&diff, repo, commit, NULL)); + cl_git_pass(git_diff_find_similar(diff, NULL)); + + cl_git_pass(git_diff_get_stats(&stats, diff)); + cl_assert(git_diff_stats_files_changed(stats) == 2); + cl_assert(git_diff_stats_insertions(stats) == 1); + cl_assert(git_diff_stats_deletions(stats) == 1); + + cl_git_pass(git_diff_stats_to_buf(&buf, stats, GIT_DIFF_STATS_FULL)); + cl_assert(strcmp(git_buf_cstr(&buf), stat) == 0); + + git_diff_stats_free(stats); + git_diff_free(diff); + git_commit_free(commit); + git_buf_free(&buf); +} + +void test_diff_stats__rename_no_find(void) +{ + git_oid oid; + git_commit *commit = NULL; + git_diff *diff = NULL; + git_diff_stats *stats = NULL; + git_buf buf = GIT_BUF_INIT; + + const char *stat = + " file2.txt | 5 -----\n" + " file2.txt.renamed | 6 ++++++\n" + " file3.txt | 5 -----\n" + " file3.txt.renamed | 7 +++++++\n" + " 4 files changed, 13 insertions(+), 10 deletions(-)\n"; + + git_oid_fromstr(&oid, "8947a46e2097638ca6040ad4877246f4186ec3bd"); + + cl_git_pass(git_commit_lookup(&commit, repo, &oid)); + cl_git_pass(git_diff__commit(&diff, repo, commit, NULL)); + + cl_git_pass(git_diff_get_stats(&stats, diff)); + cl_assert(git_diff_stats_files_changed(stats) == 4); + cl_assert(git_diff_stats_insertions(stats) == 13); + cl_assert(git_diff_stats_deletions(stats) == 10); + + cl_git_pass(git_diff_stats_to_buf(&buf, stats, GIT_DIFF_STATS_FULL)); + cl_assert(strcmp(git_buf_cstr(&buf), stat) == 0); + + git_diff_stats_free(stats); + git_diff_free(diff); + git_commit_free(commit); + git_buf_free(&buf); +} + +void test_diff_stats__rename_nochanges_no_find(void) +{ + git_oid oid; + git_commit *commit = NULL; + git_diff *diff = NULL; + git_diff_stats *stats = NULL; + git_buf buf = GIT_BUF_INIT; + + const char *stat = + " file2.txt.renamed | 6 ------\n" + " file2.txt.renamed2 | 6 ++++++\n" + " file3.txt.renamed | 7 -------\n" + " file3.txt.renamed2 | 7 +++++++\n" + " 4 files changed, 13 insertions(+), 13 deletions(-)\n"; + + git_oid_fromstr(&oid, "3991dce9e71a0641ca49a6a4eea6c9e7ff402ed4"); + + cl_git_pass(git_commit_lookup(&commit, repo, &oid)); + cl_git_pass(git_diff__commit(&diff, repo, commit, NULL)); + + cl_git_pass(git_diff_get_stats(&stats, diff)); + cl_assert(git_diff_stats_files_changed(stats) == 4); + cl_assert(git_diff_stats_insertions(stats) == 13); + cl_assert(git_diff_stats_deletions(stats) == 13); + + cl_git_pass(git_diff_stats_to_buf(&buf, stats, GIT_DIFF_STATS_FULL)); + cl_assert(strcmp(git_buf_cstr(&buf), stat) == 0); + + git_diff_stats_free(stats); + git_diff_free(diff); + git_commit_free(commit); + git_buf_free(&buf); +} + +void test_diff_stats__rename_and_modifiy_no_find(void) +{ + git_oid oid; + git_commit *commit = NULL; + git_diff *diff = NULL; + git_diff_stats *stats = NULL; + git_buf buf = GIT_BUF_INIT; + + const char *stat = + " file2.txt.renamed2 | 2 +-\n" + " file3.txt.renamed | 7 +++++++\n" + " file3.txt.renamed2 | 7 -------\n" + " 3 files changed, 8 insertions(+), 8 deletions(-)\n"; + + git_oid_fromstr(&oid, "4ca10087e696d2ba78d07b146a118e9a7096ed4f"); + + cl_git_pass(git_commit_lookup(&commit, repo, &oid)); + cl_git_pass(git_diff__commit(&diff, repo, commit, NULL)); + + cl_git_pass(git_diff_get_stats(&stats, diff)); + cl_assert(git_diff_stats_files_changed(stats) == 3); + cl_assert(git_diff_stats_insertions(stats) == 8); + cl_assert(git_diff_stats_deletions(stats) == 8); + + cl_git_pass(git_diff_stats_to_buf(&buf, stats, GIT_DIFF_STATS_FULL)); + cl_assert(strcmp(git_buf_cstr(&buf), stat) == 0); + + git_diff_stats_free(stats); + git_diff_free(diff); + git_commit_free(commit); + git_buf_free(&buf); +} + +void test_diff_stats__binary(void) +{ + git_oid oid; + git_commit *commit = NULL; + git_diff *diff = NULL; + git_diff_stats *stats = NULL; + git_buf buf = GIT_BUF_INIT; + + /* TODO: Actually 0 bytes here should be 5!. Seems like we don't load the new content for binary files? */ + const char *stat = + " binary.bin | Bin 3 -> 0 bytes\n" + " 1 file changed, 0 insertions(+), 0 deletions(-)\n"; + + git_oid_fromstr(&oid, "8d7523f6fcb2404257889abe0d96f093d9f524f9"); + + cl_git_pass(git_commit_lookup(&commit, repo, &oid)); + cl_git_pass(git_diff__commit(&diff, repo, commit, NULL)); + + cl_git_pass(git_diff_get_stats(&stats, diff)); + cl_assert(git_diff_stats_files_changed(stats) == 1); + cl_assert(git_diff_stats_insertions(stats) == 0); + cl_assert(git_diff_stats_deletions(stats) == 0); + + cl_git_pass(git_diff_stats_to_buf(&buf, stats, GIT_DIFF_STATS_FULL)); + cl_assert(strcmp(git_buf_cstr(&buf), stat) == 0); + + git_diff_stats_free(stats); + git_diff_free(diff); + git_commit_free(commit); + git_buf_free(&buf); +} + +void test_diff_stats__binary_numstat(void) +{ + git_oid oid; + git_commit *commit = NULL; + git_diff *diff = NULL; + git_diff_stats *stats = NULL; + git_buf buf = GIT_BUF_INIT; + + const char *stat = + "- - binary.bin\n"; + + git_oid_fromstr(&oid, "8d7523f6fcb2404257889abe0d96f093d9f524f9"); + + cl_git_pass(git_commit_lookup(&commit, repo, &oid)); + cl_git_pass(git_diff__commit(&diff, repo, commit, NULL)); + + cl_git_pass(git_diff_get_stats(&stats, diff)); + + cl_git_pass(git_diff_stats_to_buf(&buf, stats, GIT_DIFF_STATS_NUMBER)); + cl_assert(strcmp(git_buf_cstr(&buf), stat) == 0); + + git_diff_stats_free(stats); + git_diff_free(diff); + git_commit_free(commit); + git_buf_free(&buf); +} + +void test_diff_stats__mode_change(void) +{ + git_oid oid; + git_commit *commit = NULL; + git_diff *diff = NULL; + git_diff_stats *stats = NULL; + git_buf buf = GIT_BUF_INIT; + + const char *stat = + " file1.txt.renamed | 0\n" \ + " 1 file changed, 0 insertions(+), 0 deletions(-)\n" \ + " mode change 100644 => 100755 file1.txt.renamed\n" \ + "\n"; + + git_oid_fromstr(&oid, "7ade76dd34bba4733cf9878079f9fd4a456a9189"); + + cl_git_pass(git_commit_lookup(&commit, repo, &oid)); + cl_git_pass(git_diff__commit(&diff, repo, commit, NULL)); + + cl_git_pass(git_diff_get_stats(&stats, diff)); + + cl_git_pass(git_diff_stats_to_buf(&buf, stats, GIT_DIFF_STATS_FULL | GIT_DIFF_STATS_INCLUDE_SUMMARY)); + cl_assert(strcmp(git_buf_cstr(&buf), stat) == 0); + + git_diff_stats_free(stats); + git_diff_free(diff); + git_commit_free(commit); + git_buf_free(&buf); +}