/* * 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 "patch_generate.h" #define DIFF_RENAME_FILE_SEPARATOR " => " #define STATS_FULL_MIN_SCALE 7 typedef struct { size_t insertions; size_t deletions; } diff_file_stats; struct git_diff_stats { git_diff *diff; diff_file_stats *filestats; size_t files_changed; size_t insertions; size_t deletions; size_t renames; size_t max_name; size_t max_filestat; int max_digits; }; static int digits_for_value(size_t val) { int count = 1; size_t placevalue = 10; while (val >= placevalue) { ++count; placevalue *= 10; } return count; } int git_diff_file_stats__full_to_buf( git_buf *out, const git_diff_delta *delta, const diff_file_stats *filestat, const git_diff_stats *stats, size_t width) { const char *old_path = NULL, *new_path = NULL; size_t padding, old_size, new_size; 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 (git_buf_printf(out, " %s", old_path) < 0) goto on_error; if (strcmp(old_path, new_path) != 0) { padding = stats->max_name - strlen(old_path) - strlen(new_path); if (git_buf_printf(out, DIFF_RENAME_FILE_SEPARATOR "%s", new_path) < 0) goto on_error; } else { padding = stats->max_name - strlen(old_path); if (stats->renames > 0) padding += strlen(DIFF_RENAME_FILE_SEPARATOR); } if (git_buf_putcn(out, ' ', padding) < 0 || git_buf_puts(out, " | ") < 0) goto on_error; if (delta->flags & GIT_DIFF_FLAG_BINARY) { if (git_buf_printf(out, "Bin %" PRIuZ " -> %" PRIuZ " bytes", old_size, new_size) < 0) goto on_error; } else { if (git_buf_printf(out, "%*" PRIuZ, stats->max_digits, filestat->insertions + filestat->deletions) < 0) goto on_error; if (filestat->insertions || filestat->deletions) { if (git_buf_putc(out, ' ') < 0) goto on_error; if (!width) { if (git_buf_putcn(out, '+', filestat->insertions) < 0 || git_buf_putcn(out, '-', filestat->deletions) < 0) goto on_error; } else { size_t total = filestat->insertions + filestat->deletions; size_t full = (total * width + stats->max_filestat / 2) / stats->max_filestat; size_t plus = full * filestat->insertions / total; size_t minus = full - plus; if (git_buf_putcn(out, '+', max(plus, 1)) < 0 || git_buf_putcn(out, '-', max(minus, 1)) < 0) goto on_error; } } } git_buf_putc(out, '\n'); on_error: return (git_buf_oom(out) ? -1 : 0); } int git_diff_file_stats__number_to_buf( git_buf *out, const git_diff_delta *delta, const diff_file_stats *filestats) { int error; const char *path = delta->new_file.path; 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", filestats->insertions, filestats->deletions, path); return error; } int git_diff_file_stats__summary_to_buf( git_buf *out, const git_diff_delta *delta) { 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_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); stats->filestats = git__calloc(deltas, sizeof(diff_file_stats)); if (!stats->filestats) { git__free(stats); return -1; } stats->diff = diff; GIT_REFCOUNT_INC(diff); for (i = 0; i < deltas && !error; ++i) { git_patch *patch = NULL; size_t add = 0, remove = 0, namelen; const git_diff_delta *delta; if ((error = git_patch_from_diff(&patch, diff, i)) < 0) break; /* keep a count of renames because it will affect formatting */ delta = patch->delta; /* TODO ugh */ namelen = strlen(delta->new_file.path); if (strcmp(delta->old_file.path, delta->new_file.path) != 0) { namelen += strlen(delta->old_file.path); stats->renames++; } /* and, of course, count the line stats */ error = git_patch_line_stats(NULL, &add, &remove, patch); git_patch_free(patch); stats->filestats[i].insertions = add; stats->filestats[i].deletions = remove; total_insertions += add; total_deletions += remove; if (stats->max_name < namelen) stats->max_name = namelen; if (stats->max_filestat < add + remove) stats->max_filestat = add + remove; } stats->files_changed = deltas; stats->insertions = total_insertions; stats->deletions = total_deletions; stats->max_digits = digits_for_value(stats->max_filestat + 1); if (error < 0) { git_diff_stats_free(stats); stats = NULL; } *out = stats; 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, size_t width) { int error = 0; size_t i; const git_diff_delta *delta; assert(out && stats); if (format & GIT_DIFF_STATS_NUMBER) { for (i = 0; i < stats->files_changed; ++i) { if ((delta = git_diff_get_delta(stats->diff, i)) == NULL) continue; error = git_diff_file_stats__number_to_buf( out, delta, &stats->filestats[i]); if (error < 0) return error; } } if (format & GIT_DIFF_STATS_FULL) { if (width > 0) { if (width > stats->max_name + stats->max_digits + 5) width -= (stats->max_name + stats->max_digits + 5); if (width < STATS_FULL_MIN_SCALE) width = STATS_FULL_MIN_SCALE; } if (width > stats->max_filestat) width = 0; for (i = 0; i < stats->files_changed; ++i) { if ((delta = git_diff_get_delta(stats->diff, i)) == NULL) continue; error = git_diff_file_stats__full_to_buf( out, delta, &stats->filestats[i], stats, width); if (error < 0) return error; } } if (format & GIT_DIFF_STATS_FULL || format & GIT_DIFF_STATS_SHORT) { git_buf_printf( out, " %" PRIuZ " file%s changed", stats->files_changed, stats->files_changed != 1 ? "s" : ""); if (stats->insertions || stats->deletions == 0) git_buf_printf( out, ", %" PRIuZ " insertion%s(+)", stats->insertions, stats->insertions != 1 ? "s" : ""); if (stats->deletions || stats->insertions == 0) git_buf_printf( out, ", %" PRIuZ " deletion%s(-)", stats->deletions, stats->deletions != 1 ? "s" : ""); git_buf_putc(out, '\n'); if (git_buf_oom(out)) return -1; } if (format & GIT_DIFF_STATS_INCLUDE_SUMMARY) { for (i = 0; i < stats->files_changed; ++i) { if ((delta = git_diff_get_delta(stats->diff, i)) == NULL) continue; error = git_diff_file_stats__summary_to_buf(out, delta); if (error < 0) return error; } } return error; } void git_diff_stats_free(git_diff_stats *stats) { if (stats == NULL) return; git_diff_free(stats->diff); /* bumped refcount in constructor */ git__free(stats->filestats); git__free(stats); }