aboutsummaryrefslogtreecommitdiffstats
path: root/contrib/git-stats-graph.py
diff options
context:
space:
mode:
Diffstat (limited to 'contrib/git-stats-graph.py')
-rwxr-xr-xcontrib/git-stats-graph.py120
1 files changed, 120 insertions, 0 deletions
diff --git a/contrib/git-stats-graph.py b/contrib/git-stats-graph.py
new file mode 100755
index 00000000..7632d566
--- /dev/null
+++ b/contrib/git-stats-graph.py
@@ -0,0 +1,120 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: MIT
+# Copyright (c) 2023 Bence Ferdinandy <bence@ferdinandy.com>
+
+"""
+Create graphs about development statistics of releases.
+"""
+
+from datetime import date
+from subprocess import check_output
+
+from matplotlib import pyplot as plt
+
+
+def git(*args):
+ return check_output(["git"] + list(args)).decode("utf-8").strip()
+
+
+def stats():
+ """
+ Returns statistics from the git repo:
+
+ tags: sorted list of minor version tags (assumes semver)
+ The first element is the hash of the first commit, last element is HEAD. All
+ the other return values are one shorter as, there's no statistics returned for
+ the first commit.
+ counts: number of commits (between this and the previous release)
+ dates: dates of the releases
+ files: number of files changed (between this and the previous release)
+ inserts: number of lines inserted (between this and the previous release)
+ deletions: number of lines deleted (between this and the previous release)
+ """
+
+ tags = git("tag").split("\n")
+ tags = [t for t in tags if t.split(".")[-1] == "0"] # drop patch versions
+ tags = sorted(tags, key=lambda x: [int(t) for t in x.split(".")])
+ first_commit = git("rev-list", "--max-parents=0", "HEAD")
+ tags = [first_commit] + tags + ["HEAD"]
+ counts = []
+ dates = []
+ files = []
+ inserts = []
+ deletions = []
+ for i, t in enumerate(tags[:-1]):
+ counts.append(int(git("rev-list", f"{t}..{tags[i+1]}", "--count")))
+ dates.append(
+ date.fromisoformat(git("show", "-s", "--format=%cs", t).split("\n")[-1])
+ )
+ statline = git("diff", "--stat", t, tags[i + 1]).split("\n")[-1]
+ fnum, _, _, ins, _, dels, _ = statline.split()
+ files.append(int(fnum))
+ inserts.append(int(ins))
+ deletions.append(int(dels))
+ return tags, counts, dates, files, inserts, deletions
+
+
+def main(output):
+ tags, counts, dates, files, inserts, deletions = stats()
+
+ fig, (ax1, ax2) = plt.subplots(2, figsize=(8, 11))
+ fig.suptitle("aerc release statistics", fontweight="bold")
+ # commit counts subplot
+ ax1.plot(dates, counts, "o-")
+
+ # alternate placement of text above and below for readability
+ text_y = []
+ for i, t in enumerate(tags[1:]):
+ downpad = 25 if len(t) == 5 else 30
+ p = counts[i] + (-1) ** (i + 1) * 10 - (i + 1) % 2 * downpad
+ if p < 5:
+ p = counts[i] + 10
+ text_y.append(p)
+ for i, t in enumerate(tags[1:]):
+ ax1.text(
+ dates[i],
+ text_y[i],
+ t,
+ horizontalalignment="center",
+ rotation="vertical",
+ )
+ ax1.set_ylabel("# of commits")
+ ax1.set_ylim(bottom=0)
+ ax1.set_title("commits per release")
+
+ # lines added/deleted subplot
+ #
+ ax2.plot(dates, inserts, "o-", label="insertions(+)", color="green")
+ ax2.plot(dates, deletions, "o-.", label="deletions(-)", color="red")
+ ax2.set_ylabel("# of lines")
+ ax2.legend(loc="upper left")
+ ax2.set_ylim(top=max(max(inserts), max(deletions)) + 2000)
+ for i, t in enumerate(tags[1:]):
+ ax2.text(
+ dates[i],
+ max(inserts[i], deletions[i]) + 500,
+ t,
+ horizontalalignment="center",
+ rotation="vertical",
+ )
+ ax2.set_xlabel("date")
+ ax2.set_title("insertion/deletions per release")
+ plt.tight_layout()
+ plt.savefig(output, dpi=300)
+
+
+if __name__ == "__main__":
+ import argparse
+
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument(
+ "-o",
+ "--output",
+ default="aerc-release-stats.png",
+ help="""
+ Path to output image (defaults to 'aerc-release-stats.png',
+ respects file extensions via matplotlib)
+ """,
+ )
+ args = parser.parse_args()
+ main(args.output)