diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml index 12c5ca1..67b1574 100644 --- a/.github/workflows/examples.yml +++ b/.github/workflows/examples.yml @@ -17,7 +17,6 @@ jobs: - name: Install Dependencies run: | sudo apt-get update - sudo apt-get install -y gnuplot - name: Generate GitStats Report run: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3a32118..8a3ca92 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -44,14 +44,8 @@ jobs: with: python-version: ${{ matrix.python-version }} - - name: Install gnuplot - run: choco install gnuplot -y --no-progress - - name: Generate GitStats Report run: | - $env:PATH += ";C:\Program Files\gnuplot\bin" - gnuplot --version - python -m venv venv .\venv\Scripts\activate.bat # for testing @@ -82,12 +76,8 @@ jobs: with: python-version: ${{ matrix.python-version }} - - name: Install gnuplot - run: brew install gnuplot - - name: Generate GitStats Report run: | - gnuplot --version python3 -m venv venv source venv/bin/activate # for testing diff --git a/Dockerfile b/Dockerfile index c5aa26d..8bf41cd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,6 @@ ARG VERSION # Install git and clean up unnecessary files to reduce image size RUN apt-get update && apt-get install -y --no-install-recommends \ git \ - gnuplot \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* \ && pip3 install gitstats${VERSION:+==$VERSION} diff --git a/docs/source/TODO.md b/docs/source/TODO.md index a2a1eef..02853a7 100644 --- a/docs/source/TODO.md +++ b/docs/source/TODO.md @@ -16,7 +16,6 @@ added? [Unsorted] -- clean up after running gnuplot (option to keep .dat files around?) - show raw data in some way (the tables used currently aren't very nice) - allow multiple stylesheets - parameter --style diff --git a/docs/source/installation.rst b/docs/source/installation.rst index 04360a9..1dfb69f 100644 --- a/docs/source/installation.rst +++ b/docs/source/installation.rst @@ -1,30 +1,30 @@ -Installation -============ -.. important:: +**Requirements** - The following requirements need to be installed before using ``gitstats`` +- Git (obviously) +- Python 3.9+ +- Pip or Pipx - - Python 3.9+ (https://www.python.org/downloads/) - - Git (http://git-scm.com/) - - Gnuplot (http://www.gnuplot.info): You can install Gnuplot on - - - Ubuntu with ``sudo apt install gnuplot`` - - macOS with ``brew install gnuplot`` - - Windows with ``choco install gnuplot`` +**From source** +You can install from source by cloning the repository and running: +.. code-block:: bash -You can install gitstats with pip: + pip install . +**From PyPI** +You can install from PyPI by running: .. code-block:: bash pip install gitstats -Or you can also get gitstats Docker image. - -.. tip:: +Or if you want to install with ``pipx``: +.. code-block:: bash - The Docker image has all the dependencies (Python, Git, Gnuplot and gitstats) already installed. + pipx install gitstats +**Docker** +There is a Docker image available at https://ghcr.io/shenxianpeng/gitstats. +To use it, you can run: .. code-block:: bash - docker run ghcr.io/shenxianpeng/gitstats:latest + docker run -it --rm -v /path/to/your/repo:/repo -v /path/to/output:/output ghcr.io/shenxianpeng/gitstats:latest /repo /output diff --git a/docs/source/integration.rst b/docs/source/integration.rst index 312f350..af25fc5 100644 --- a/docs/source/integration.rst +++ b/docs/source/integration.rst @@ -37,7 +37,6 @@ Use gitstats in GitHub Actions to generate reports and deploy them to GitHub Pag - name: Install Dependencies run: | sudo apt-get update - sudo apt-get install -y gnuplot - name: Generate GitStats Report run: | diff --git a/gitstats/__init__.py b/gitstats/__init__.py index 80bdce1..451a75a 100644 --- a/gitstats/__init__.py +++ b/gitstats/__init__.py @@ -7,7 +7,6 @@ exectime_external = 0.0 time_start = time.time() -GNUPLOT_COMMON = "set terminal png transparent size 640,240\nset size 1.0,1.0\n" ON_LINUX = platform.system() == "Linux" WEEKDAYS = ("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun") diff --git a/gitstats/main.py b/gitstats/main.py index 7a7b603..c948c38 100755 --- a/gitstats/main.py +++ b/gitstats/main.py @@ -15,7 +15,6 @@ from gitstats.report_creator import HTMLReportCreator, get_keys_sorted_by_value_key from gitstats.utils import ( get_version, - get_gnuplot_version, get_pipe_output, get_commit_range, get_log_range, @@ -708,10 +707,6 @@ def run(gitpath, outputpath, extra_fmt=None) -> int: print("FATAL: Output path is not a directory or does not exist") return 1 - if get_gnuplot_version is None: - print("gnuplot not found") - return 1 - print("Output path: %s" % outputpath) cachefile = os.path.join(outputpath, "gitstats.cache") diff --git a/gitstats/report_creator.py b/gitstats/report_creator.py index e005584..9093ed8 100644 --- a/gitstats/report_creator.py +++ b/gitstats/report_creator.py @@ -3,18 +3,16 @@ # Copyright (c) 2024-present Xianpeng Shen . # GPLv2 / GPLv3 import os -import glob import shutil import datetime import time import math -from gitstats import load_config, GNUPLOT_COMMON, WEEKDAYS +import matplotlib.pyplot as plt +from gitstats import load_config, WEEKDAYS from gitstats.utils import ( get_version, get_git_version, - get_gnuplot_version, get_pipe_output, - gnuplot_cmd, ) conf = load_config() @@ -79,8 +77,8 @@ def create_index_html(self, data, path): ) ) f.write( - 'Generated Bygitstats %s, %s, %s' - % (get_version(), get_git_version(), get_gnuplot_version()) + 'Generated Bygitstats %s, %s' + % (get_version(), get_git_version()) ) f.write( "Report Period%s to %s" @@ -739,234 +737,186 @@ def create_graphs(self, path): self.create_graph_lines_of_code(path) self.create_graph_lines_of_code_by_author(path) self.create_graph_commits_by_author(path) - self.create_graph_by_gnuplot(path) def create_graph_hour_of_day(self, path): # hour of day - f = open(path + "/hour_of_day.plot", "w", encoding="utf-8") - f.write(GNUPLOT_COMMON) - f.write( - """ -set output 'hour_of_day.png' -unset key -set xrange [0.5:24.5] -set yrange [0:] -set xtics 4 -set grid y -set ylabel "Commits" -plot 'hour_of_day.dat' using 1:2:(0.5) w boxes fs solid -""" - ) - f.close() + with open(path + "/hour_of_day.dat") as f: + data = [line.split() for line in f.readlines()] + x = [int(row[0]) for row in data] + y = [int(row[1]) for row in data] + + plt.figure() + plt.bar(x, y, width=0.5) + plt.xlabel("Hour of Day") + plt.ylabel("Commits") + plt.grid(True) + plt.xticks(range(0, 25, 4)) + plt.xlim(0.5, 24.5) + plt.ylim(0, max(y) * 1.1) + plt.savefig(path + "/hour_of_day.png") + plt.close() def create_graph_day_of_week(self, path): # day of week - f = open(path + "/day_of_week.plot", "w", encoding="utf-8") - f.write(GNUPLOT_COMMON) - f.write( - """ -set output 'day_of_week.png' -unset key -set xrange [0.5:7.5] -set yrange [0:] -set xtics 1 -set grid y -set ylabel "Commits" -plot 'day_of_week.dat' using 1:3:(0.5):xtic(2) w boxes fs solid -""" - ) - f.close() + with open(path + "/day_of_week.dat") as f: + data = [line.split() for line in f.readlines()] + x = [row[1] for row in data] + y = [int(row[2]) for row in data] + + plt.figure() + plt.bar(x, y, width=0.5) + plt.xlabel("Day of Week") + plt.ylabel("Commits") + plt.grid(True) + plt.xticks(rotation=45) + plt.ylim(0, max(y) * 1.1) + plt.savefig(path + "/day_of_week.png") + plt.close() def create_graph_domains(self, path): # Domains - f = open(path + "/domains.plot", "w", encoding="utf-8") - f.write(GNUPLOT_COMMON) - f.write( - """ -set output 'domains.png' -unset key -unset xtics -set yrange [0:] -set grid y -set ylabel "Commits" -plot 'domains.dat' using 2:3:(0.5) with boxes fs solid, '' using 2:3:1 with labels rotate by 45 offset 0,1 -""" - ) - f.close() + with open(path + "/domains.dat") as f: + data = [line.split() for line in f.readlines()] + x = [row[0] for row in data] + y = [int(row[2]) for row in data] + + plt.figure() + plt.bar(x, y, width=0.5) + plt.xlabel("Domains") + plt.ylabel("Commits") + plt.grid(True) + plt.xticks(rotation=45) + plt.ylim(0, max(y) * 1.1) + plt.savefig(path + "/domains.png") + plt.close() def create_graph_month_of_year(self, path): # Month of Year - f = open(path + "/month_of_year.plot", "w", encoding="utf-8") - f.write(GNUPLOT_COMMON) - f.write( - """ -set output 'month_of_year.png' -unset key -set xrange [0.5:12.5] -set yrange [0:] -set xtics 1 -set grid y -set ylabel "Commits" -plot 'month_of_year.dat' using 1:2:(0.5) w boxes fs solid -""" - ) - f.close() + with open(path + "/month_of_year.dat") as f: + data = [line.split() for line in f.readlines()] + x = [int(row[0]) for row in data] + y = [int(row[1]) for row in data] + + plt.figure() + plt.bar(x, y, width=0.5) + plt.xlabel("Month of Year") + plt.ylabel("Commits") + plt.grid(True) + plt.xticks(range(1, 13)) + plt.xlim(0.5, 12.5) + plt.ylim(0, max(y) * 1.1) + plt.savefig(path + "/month_of_year.png") + plt.close() def create_graph_commits_by_year_month(self, path): # commits_by_year_month - f = open(path + "/commits_by_year_month.plot", "w", encoding="utf-8") - f.write(GNUPLOT_COMMON) - f.write( - """ -set output 'commits_by_year_month.png' -unset key -set yrange [0:] -set xdata time -set timefmt "%Y-%m" -set format x "%Y-%m" -set xtics rotate -set bmargin 5 -set grid y -set ylabel "Commits" -plot 'commits_by_year_month.dat' using 1:2:(0.5) w boxes fs solid -""" - ) - f.close() + with open(path + "/commits_by_year_month.dat") as f: + data = [line.split() for line in f.readlines()] + x = [row[0] for row in data] + y = [int(row[1]) for row in data] + + plt.figure() + plt.bar(x, y, width=0.5) + plt.xlabel("Year-Month") + plt.ylabel("Commits") + plt.grid(True) + plt.xticks(rotation=45) + plt.ylim(0, max(y) * 1.1) + plt.savefig(path + "/commits_by_year_month.png") + plt.close() def create_graph_commits_by_year(self, path): # commits_by_year - f = open(path + "/commits_by_year.plot", "w", encoding="utf-8") - f.write(GNUPLOT_COMMON) - f.write( - """ -set output 'commits_by_year.png' -unset key -set yrange [0:] -set xtics 1 rotate -set grid y -set ylabel "Commits" -set yrange [0:] -plot 'commits_by_year.dat' using 1:2:(0.5) w boxes fs solid -""" - ) - f.close() + with open(path + "/commits_by_year.dat") as f: + data = [line.split() for line in f.readlines()] + x = [int(row[0]) for row in data] + y = [int(row[1]) for row in data] + + plt.figure() + plt.bar(x, y, width=0.5) + plt.xlabel("Year") + plt.ylabel("Commits") + plt.grid(True) + plt.xticks(x, rotation=45) + plt.ylim(0, max(y) * 1.1) + plt.savefig(path + "/commits_by_year.png") + plt.close() def create_graph_files_by_date(self, path): # Files by date - f = open(path + "/files_by_date.plot", "w", encoding="utf-8") - f.write(GNUPLOT_COMMON) - f.write( - """ -set output 'files_by_date.png' -unset key -set yrange [0:] -set xdata time -set timefmt "%Y-%m-%d" -set format x "%Y-%m-%d" -set grid y -set ylabel "Files" -set xtics rotate -set ytics autofreq -set bmargin 6 -plot 'files_by_date.dat' using 1:2 w steps -""" - ) - f.close() + with open(path + "/files_by_date.dat") as f: + data = [line.split() for line in f.readlines()] + x = [row[0] for row in data] + y = [int(row[1]) for row in data] + + plt.figure() + plt.plot(x, y, drawstyle="steps-post") + plt.xlabel("Date") + plt.ylabel("Files") + plt.grid(True) + plt.xticks(rotation=45) + plt.ylim(0, max(y) * 1.1) + plt.savefig(path + "/files_by_date.png") + plt.close() def create_graph_lines_of_code(self, path): # Lines of Code - f = open(path + "/lines_of_code.plot", "w", encoding="utf-8") - f.write(GNUPLOT_COMMON) - f.write( - """ -set output 'lines_of_code.png' -unset key -set yrange [0:] -set xdata time -set timefmt "%s" -set format x "%Y-%m-%d" -set grid y -set ylabel "Lines" -set xtics rotate -set bmargin 6 -plot 'lines_of_code.dat' using 1:2 w lines -""" - ) - f.close() + with open(path + "/lines_of_code.dat") as f: + data = [line.split() for line in f.readlines()] + x = [datetime.datetime.fromtimestamp(int(row[0])) for row in data] + y = [int(row[1]) for row in data] + + plt.figure() + plt.plot(x, y) + plt.xlabel("Date") + plt.ylabel("Lines") + plt.grid(True) + plt.xticks(rotation=45) + plt.ylim(0, max(y) * 1.1) + plt.savefig(path + "/lines_of_code.png") + plt.close() def create_graph_lines_of_code_by_author(self, path): # Lines of Code Added per author - f = open(path + "/lines_of_code_by_author.plot", "w", encoding="utf-8") - f.write(GNUPLOT_COMMON) - f.write( - """ -set terminal png transparent size 640,480 -set output 'lines_of_code_by_author.png' -set key left top -set yrange [0:] -set xdata time -set timefmt "%s" -set format x "%Y-%m-%d" -set grid y -set ylabel "Lines" -set xtics rotate -set bmargin 6 -plot """ - ) - i = 1 - plots = [] - for a in self.authors_to_plot: - i = i + 1 - author = a.replace('"', '\\"').replace("`", "") - plots.append( - """'lines_of_code_by_author.dat' using 1:%d title "%s" w lines""" - % (i, author) - ) - f.write(", ".join(plots)) - f.write("\n") - - f.close() + with open(path + "/lines_of_code_by_author.dat") as f: + data = [list(map(int, line.split())) for line in f.readlines()] + timestamps = [row[0] for row in data] + dates = [datetime.datetime.fromtimestamp(ts) for ts in timestamps] + + plt.figure() + for i, author in enumerate(self.authors_to_plot): + y = [row[i + 1] for row in data] + plt.plot(dates, y, label=author) + + plt.xlabel("Date") + plt.ylabel("Lines") + plt.grid(True) + plt.xticks(rotation=45) + plt.legend() + plt.ylim(bottom=0) + plt.savefig(path + "/lines_of_code_by_author.png") + plt.close() def create_graph_commits_by_author(self, path): # Commits per author - f = open(path + "/commits_by_author.plot", "w", encoding="utf-8") - f.write(GNUPLOT_COMMON) - f.write( - """ -set terminal png transparent size 640,480 -set output 'commits_by_author.png' -set key left top -set yrange [0:] -set xdata time -set timefmt "%s" -set format x "%Y-%m-%d" -set grid y -set ylabel "Commits" -set xtics rotate -set bmargin 6 -plot """ - ) - i = 1 - plots = [] - for a in self.authors_to_plot: - i = i + 1 - author = a.replace('"', '\\"').replace("`", "") - plots.append( - """'commits_by_author.dat' using 1:%d title "%s" w lines""" - % (i, author) - ) - f.write(", ".join(plots)) - f.write("\n") - - f.close() - - def create_graph_by_gnuplot(self, path): - os.chdir(path) - files = glob.glob(path + "/*.plot") - for f in files: - out = get_pipe_output([gnuplot_cmd + ' "%s"' % f]) - if len(out) > 0: - print(out) + with open(path + "/commits_by_author.dat") as f: + data = [list(map(int, line.split())) for line in f.readlines()] + timestamps = [row[0] for row in data] + dates = [datetime.datetime.fromtimestamp(ts) for ts in timestamps] + + plt.figure() + for i, author in enumerate(self.authors_to_plot): + y = [row[i + 1] for row in data] + plt.plot(dates, y, label=author) + + plt.xlabel("Date") + plt.ylabel("Commits") + plt.grid(True) + plt.xticks(rotation=45) + plt.legend() + plt.ylim(bottom=0) + plt.savefig(path + "/commits_by_author.png") + plt.close() def print_header(self, file) -> None: file.write( diff --git a/gitstats/utils.py b/gitstats/utils.py index 69dca50..c4281c9 100644 --- a/gitstats/utils.py +++ b/gitstats/utils.py @@ -38,11 +38,6 @@ def get_git_version(): return get_pipe_output(["git --version"]).split("\n")[0] -def get_gnuplot_version(): - output = get_pipe_output(["%s --version" % gnuplot_cmd]).split("\n")[0] - return output if output else None - - def get_pipe_output(cmds, quiet=False): global exectime_external start = time.time() @@ -165,10 +160,3 @@ def get_log_range(defaultrange="HEAD", end_only=True): if len(conf["start_date"]) > 0: return '--since="%s" "%s"' % (conf["start_date"], commit_range) return commit_range - - -# By default, gnuplot is searched from path, but can be overridden with the -# environment variable "GNUPLOT" -gnuplot_cmd = "gnuplot" -if "GNUPLOT" in os.environ: - gnuplot_cmd = os.environ["GNUPLOT"] diff --git a/noxfile.py b/noxfile.py index 005ae38..b11145f 100644 --- a/noxfile.py +++ b/noxfile.py @@ -33,13 +33,6 @@ def publish_image(session: nox.Session) -> None: session.run("docker", "push", f"ghcr.io/shenxianpeng/gitstats:{TAG}", external=True) -@nox.session(name="install-deps") -def install_deps(session: nox.Session) -> None: - """Install gnuplot""" - session.run("sudo", "apt", "update", "-y", external=True) - session.run("sudo", "apt", "install", "gnuplot", "-y", external=True) - - @nox.session def build(session: nox.Session) -> None: """Generate gitstats report and json file""" diff --git a/pyproject.toml b/pyproject.toml index 5cbc199..06613d4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,7 @@ authors = [ ] requires-python = ">=3.9" +dependencies = ["matplotlib"] dynamic = ["version"] classifiers = [