diff --git a/docs/diff.rst b/docs/diff.rst index 204db22..b46d06a 100644 --- a/docs/diff.rst +++ b/docs/diff.rst @@ -23,6 +23,10 @@ Examples >>> diff = repo.diff('HEAD^', 'HEAD~3') >>> patches = [p for p in diff] + # Get the stats for a diff + >>> diff = repo.diff('HEAD^', 'HEAD~3') + >>> diff.stats + # Diffing the empty tree >>> tree = revparse_single('HEAD').tree >>> tree.diff_to_tree() @@ -89,3 +93,11 @@ The DiffHunk type .. autoattribute:: pygit2.DiffHunk.new_start .. autoattribute:: pygit2.DiffHunk.new_lines .. autoattribute:: pygit2.DiffHunk.lines + +The DiffStats type +==================== + +.. autoattribute :: pygit2.DiffStats.insertions +.. autoattribute :: pygit2.DiffStats.deletions +.. autoattribute :: pygit2.DiffStats.files_changed +.. automethod :: pygit2.DiffStats.format diff --git a/src/diff.c b/src/diff.c index c0b3043..336bc25 100644 --- a/src/diff.c +++ b/src/diff.c @@ -44,6 +44,7 @@ extern PyTypeObject DiffDeltaType; extern PyTypeObject DiffFileType; extern PyTypeObject DiffHunkType; extern PyTypeObject DiffLineType; +extern PyTypeObject DiffStatsType; extern PyTypeObject RepositoryType; PyObject * @@ -143,6 +144,28 @@ wrap_diff_hunk(git_patch *patch, size_t idx) return (PyObject *) py_hunk; } +PyObject * +wrap_diff_stats(git_diff *diff) +{ + git_diff_stats *stats; + DiffStats *py_stats; + int err; + + err = git_diff_get_stats(&stats, diff); + if (err < 0) + return Error_set(err); + + py_stats = PyObject_New(DiffStats, &DiffStatsType); + if (!py_stats) { + git_diff_stats_free(stats); + return NULL; + } + + py_stats->stats = stats; + + return (PyObject *) py_stats; +} + PyObject * wrap_diff_line(const git_diff_line *line) { @@ -558,6 +581,132 @@ PyTypeObject DiffHunkType = { 0, /* tp_new */ }; +PyDoc_STRVAR(DiffStats_insertions__doc__, "Total number of insertions"); + +PyObject * +DiffStats_insertions__get__(DiffStats *self) +{ + return PyLong_FromSize_t(git_diff_stats_insertions(self->stats)); +} + +PyDoc_STRVAR(DiffStats_deletions__doc__, "Total number of deletions"); + +PyObject * +DiffStats_deletions__get__(DiffStats *self) +{ + return PyLong_FromSize_t(git_diff_stats_deletions(self->stats)); +} + +PyDoc_STRVAR(DiffStats_files_changed__doc__, "Total number of files changed"); + +PyObject * +DiffStats_files_changed__get__(DiffStats *self) +{ + return PyLong_FromSize_t(git_diff_stats_files_changed(self->stats)); +} + +PyDoc_STRVAR(DiffStats_format__doc__, + "format(format, width)-> str\n" + "\n" + "Format the stats as a string\n" + "\n" + "Arguments:\n" + "\n" + "format\n" + " The format to use. A pygit2.GIT_DIFF_STATS_* constant\n" + "\n" + "width\n" + " The width of the output. The output will be scaled to fit."); + +PyObject * +DiffStats_format(DiffStats *self, PyObject *args, PyObject *kwds) +{ + int err, format; + git_buf buf = { 0 }; + Py_ssize_t width; + PyObject *str; + char *keywords[] = {"format", "width", NULL}; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "in", keywords, &format, &width)) + return NULL; + + if (width <= 0) { + PyErr_SetString(PyExc_ValueError, "width must be positive"); + return NULL; + } + + err = git_diff_stats_to_buf(&buf, self->stats, format, width); + if (err < 0) + return Error_set(err); + + str = to_unicode(buf.ptr, NULL, NULL); + git_buf_free(&buf); + + return str; +} + +static void +DiffStats_dealloc(DiffStats *self) +{ + git_diff_stats_free(self->stats); + PyObject_Del(self); +} + +PyMethodDef DiffStats_methods[] = { + METHOD(DiffStats, format, METH_VARARGS | METH_KEYWORDS), + {NULL} +}; + +PyGetSetDef DiffStats_getseters[] = { + GETTER(DiffStats, insertions), + GETTER(DiffStats, deletions), + GETTER(DiffStats, files_changed), + {NULL} +}; + +PyDoc_STRVAR(DiffStats__doc__, "DiffStats object."); + +PyTypeObject DiffStatsType = { + PyVarObject_HEAD_INIT(NULL, 0) + "_pygit2.DiffStats", /* tp_name */ + sizeof(DiffStats), /* tp_basicsize */ + 0, /* tp_itemsize */ + (destructor)DiffStats_dealloc, /* tp_dealloc */ + 0, /* tp_print */ + 0, /* tp_getattr */ + 0, /* tp_setattr */ + 0, /* tp_compare */ + 0, /* tp_repr */ + 0, /* tp_as_number */ + 0, /* tp_as_sequence */ + 0, /* tp_as_mapping */ + 0, /* tp_hash */ + 0, /* tp_call */ + 0, /* tp_str */ + 0, /* tp_getattro */ + 0, /* tp_setattro */ + 0, /* tp_as_buffer */ + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /* tp_flags */ + DiffStats__doc__, /* tp_doc */ + 0, /* tp_traverse */ + 0, /* tp_clear */ + 0, /* tp_richcompare */ + 0, /* tp_weaklistoffset */ + 0, /* tp_iter */ + 0, /* tp_iternext */ + DiffStats_methods, /* tp_methods */ + 0, /* tp_members */ + DiffStats_getseters, /* tp_getset */ + 0, /* tp_base */ + 0, /* tp_dict */ + 0, /* tp_descr_get */ + 0, /* tp_descr_set */ + 0, /* tp_dictoffset */ + 0, /* tp_init */ + 0, /* tp_alloc */ + 0, /* tp_new */ +}; + PyDoc_STRVAR(Diff_from_c__doc__, "Method exposed for Index to hook into"); PyObject * @@ -662,6 +811,13 @@ Diff_getitem(Diff *self, PyObject *value) return diff_get_patch_byindex(self->diff, i); } +PyDoc_STRVAR(Diff_stats__doc__, "Accumulate diff statistics for all patches"); + +PyObject * +Diff_stats__get__(Diff *self) +{ + return wrap_diff_stats(self->diff); +} static void Diff_dealloc(Diff *self) @@ -673,6 +829,7 @@ Diff_dealloc(Diff *self) PyGetSetDef Diff_getseters[] = { GETTER(Diff, patch), + GETTER(Diff, stats), {NULL} }; diff --git a/src/pygit2.c b/src/pygit2.c index decd901..eedff26 100644 --- a/src/pygit2.c +++ b/src/pygit2.c @@ -48,6 +48,7 @@ extern PyTypeObject DiffDeltaType; extern PyTypeObject DiffFileType; extern PyTypeObject DiffHunkType; extern PyTypeObject DiffLineType; +extern PyTypeObject DiffStatsType; extern PyTypeObject PatchType; extern PyTypeObject TreeType; extern PyTypeObject TreeBuilderType; @@ -294,12 +295,14 @@ moduleinit(PyObject* m) INIT_TYPE(DiffFileType, NULL, NULL) INIT_TYPE(DiffHunkType, NULL, NULL) INIT_TYPE(DiffLineType, NULL, NULL) + INIT_TYPE(DiffStatsType, NULL, NULL) INIT_TYPE(PatchType, NULL, NULL) ADD_TYPE(m, Diff) ADD_TYPE(m, DiffDelta) ADD_TYPE(m, DiffFile) ADD_TYPE(m, DiffHunk) ADD_TYPE(m, DiffLine) + ADD_TYPE(m, DiffStats) ADD_TYPE(m, Patch) ADD_CONSTANT_INT(m, GIT_DIFF_NORMAL) ADD_CONSTANT_INT(m, GIT_DIFF_REVERSE) @@ -322,6 +325,11 @@ moduleinit(PyObject* m) ADD_CONSTANT_INT(m, GIT_DIFF_INCLUDE_TYPECHANGE) ADD_CONSTANT_INT(m, GIT_DIFF_INCLUDE_TYPECHANGE_TREES) ADD_CONSTANT_INT(m, GIT_DIFF_RECURSE_IGNORED_DIRS) + ADD_CONSTANT_INT(m, GIT_DIFF_STATS_NONE) + ADD_CONSTANT_INT(m, GIT_DIFF_STATS_FULL) + ADD_CONSTANT_INT(m, GIT_DIFF_STATS_SHORT) + ADD_CONSTANT_INT(m, GIT_DIFF_STATS_NUMBER) + ADD_CONSTANT_INT(m, GIT_DIFF_STATS_INCLUDE_SUMMARY) /* Flags for diff find similar */ /* --find-renames */ ADD_CONSTANT_INT(m, GIT_DIFF_FIND_RENAMES) diff --git a/src/types.h b/src/types.h index 7e0cfa5..f82ad47 100644 --- a/src/types.h +++ b/src/types.h @@ -146,6 +146,8 @@ typedef struct { PyObject *content; } DiffLine; +SIMPLE_TYPE(DiffStats, git_diff_stats, stats); + /* git_tree_walk , git_treebuilder*/ SIMPLE_TYPE(TreeBuilder, git_treebuilder, bld) diff --git a/test/test_diff.py b/test/test_diff.py index 0862b9f..b17d67c 100644 --- a/test/test_diff.py +++ b/test/test_diff.py @@ -101,6 +101,11 @@ HUNK_EXPECTED = """- a contents 2 + a contents """ +STATS_EXPECTED = """ a | 2 +- + c/d | 1 - + 2 files changed, 1 insertion(+), 2 deletions(-) + delete mode 100644 c/d +""" class DiffDirtyTest(utils.DirtyRepoTestCase): def test_diff_empty_index(self): @@ -289,5 +294,19 @@ class DiffTest(utils.BareRepoTestCase): self.assertAny(lambda x: x.delta.status == GIT_DELTA_RENAMED, diff) self.assertAny(lambda x: x.delta.status_char() == 'R', diff) + def test_diff_stats(self): + commit_a = self.repo[COMMIT_SHA1_1] + commit_b = self.repo[COMMIT_SHA1_2] + + diff = commit_a.tree.diff_to_tree(commit_b.tree) + stats = diff.stats + self.assertEqual(1, stats.insertions) + self.assertEqual(2, stats.deletions) + self.assertEqual(2, stats.files_changed) + formatted = stats.format(format=pygit2.GIT_DIFF_STATS_FULL | + pygit2.GIT_DIFF_STATS_INCLUDE_SUMMARY, + width=80) + self.assertEqual(STATS_EXPECTED, formatted) + if __name__ == '__main__': unittest.main()