feat: Basic git commit counter
This commit is contained in:
parent
798c1d937a
commit
9391298354
|
@ -1,2 +1,160 @@
|
||||||
__pycache__
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
cover/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
.pybuilder/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
# For a library or package, you might want to ignore these files since the code is
|
||||||
|
# intended to run in multiple environments; otherwise, check them in:
|
||||||
|
# .python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# poetry
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||||
|
#poetry.lock
|
||||||
|
|
||||||
|
# pdm
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||||
|
#pdm.lock
|
||||||
|
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||||
|
# in version control.
|
||||||
|
# https://pdm.fming.dev/#use-with-ide
|
||||||
|
.pdm.toml
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
.venv
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
.pytype/
|
||||||
|
|
||||||
|
# Cython debug symbols
|
||||||
|
cython_debug/
|
||||||
|
|
||||||
|
# PyCharm
|
||||||
|
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||||
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
|
#.idea/
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
pandas
|
pandas
|
||||||
numpy
|
numpy
|
||||||
GitPython
|
GitPython
|
||||||
|
typer[all]
|
||||||
|
calmap
|
|
@ -0,0 +1,30 @@
|
||||||
|
[metadata]
|
||||||
|
name = gitstats
|
||||||
|
version = attr: gitstats.__version__
|
||||||
|
author = Samuel Ortion
|
||||||
|
author_email = samuel+git@ortion.fr
|
||||||
|
keywords = git, contributions
|
||||||
|
license = GPLv3
|
||||||
|
license_files = LICENSE
|
||||||
|
long_description = file: README.md
|
||||||
|
long_description_content_type = text/markdown
|
||||||
|
classifiers =
|
||||||
|
Development Status :: 3 - Alpha
|
||||||
|
Intended Audience :: Developers
|
||||||
|
License :: OSI Approved :: GNU General Public License v3
|
||||||
|
Programming Language :: Python :: 3
|
||||||
|
Topic :: Scientific/Engineering :: Bio-Informatics
|
||||||
|
|
||||||
|
[options]
|
||||||
|
packages = find:
|
||||||
|
package_dir =
|
||||||
|
=src
|
||||||
|
python_require = >= 3.7 # What is the minimum version of python required?
|
||||||
|
install_required = file: requirements.txt
|
||||||
|
|
||||||
|
[options.entry_points]
|
||||||
|
console_scripts =
|
||||||
|
git.stats=gitstats.__main__:main
|
||||||
|
|
||||||
|
[options.packages.find]
|
||||||
|
where = src
|
|
@ -0,0 +1,5 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
from setuptools import setup
|
||||||
|
|
||||||
|
setup()
|
|
@ -0,0 +1 @@
|
||||||
|
__version__ = "0.1.0"
|
|
@ -0,0 +1,57 @@
|
||||||
|
import logging
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
import typer
|
||||||
|
from typing import Optional
|
||||||
|
from git import Repo
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
from . import commits
|
||||||
|
from . import plots
|
||||||
|
|
||||||
|
app = typer.Typer()
|
||||||
|
|
||||||
|
|
||||||
|
@app.command()
|
||||||
|
def count(dir_name: str, year: int = 2023, author_name: str = None, output_csv: Optional[str] = None) -> pd.DataFrame:
|
||||||
|
# Count the number of commits each days for git repositories in the folder
|
||||||
|
commit_date_counter = defaultdict(int)
|
||||||
|
for repo_dir in commits.iter_repos(dir_name):
|
||||||
|
repo = Repo(repo_dir)
|
||||||
|
if repo.heads != []:
|
||||||
|
repo_commit_date_counter = commits.repo_daily_commits(repo, year, author_name)
|
||||||
|
for key, count in repo_commit_date_counter.items():
|
||||||
|
commit_date_counter[key] += 1
|
||||||
|
# Construct a pandas DataFrame with this data
|
||||||
|
df = pd.DataFrame({"date": commit_date_counter.keys(), "count": commit_date_counter.values()})
|
||||||
|
df["date"] = pd.to_datetime(df["date"])
|
||||||
|
|
||||||
|
if output_csv is not None:
|
||||||
|
df.to_csv(output_csv, sep=";", index=False)
|
||||||
|
return df
|
||||||
|
|
||||||
|
@app.command()
|
||||||
|
def plot(input_csv: str, output_plot: Optional[str] = None):
|
||||||
|
df = pd.read_csv(input_csv, sep=";")
|
||||||
|
df["date"] = pd.to_datetime(df["date"])
|
||||||
|
plots.year_of_contributions(df)
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
|
||||||
|
@app.command()
|
||||||
|
def pipe(dir: str = ".", author: Optional[str] = None, year: Optional[int] = None, output_csv: Optional[str] = None, output_plot: Optional[str] = None):
|
||||||
|
df = count(dir_name=dir, year=year, author_name=author, output_csv=output_csv)
|
||||||
|
if output_csv:
|
||||||
|
df.to_csv(output_csv, sep=";")
|
||||||
|
plots.year_of_contributions(df)
|
||||||
|
if output_plot:
|
||||||
|
plt.savefig(output_plot)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
app()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
|
@ -2,24 +2,56 @@
|
||||||
Generate a CSV file with daily stats commit count from a set of repositories
|
Generate a CSV file with daily stats commit count from a set of repositories
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from git import Repo
|
|
||||||
import datetime
|
import datetime
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
from collections import defaultdict
|
||||||
|
from typing import Iterable, Optional
|
||||||
|
|
||||||
|
from git import Repo
|
||||||
|
|
||||||
REPO = "."
|
REPO = "."
|
||||||
|
|
||||||
repo = Repo(REPO)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
def repo_daily_commits(repo: Repo, year: int):
|
def wrapper(gen: Iterable, repo: Repo):
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
yield next(gen)
|
||||||
|
except StopIteration:
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
logger.error(f"Occured in this repo: {repo.working_dir}")
|
||||||
|
logger.error("Will probably omit anterior commits")
|
||||||
|
|
||||||
|
def repo_daily_commits(repo: Repo, year: Optional[int] = None, author_name: Optional[str] = None):
|
||||||
"""Count how many commit was performed for each days in the year"""
|
"""Count how many commit was performed for each days in the year"""
|
||||||
data = {}
|
if author_name is not None:
|
||||||
for commit in repo.iter_commits(all=True):
|
author_name = author_name.lower()
|
||||||
timestamp = commit.authored_date
|
other_author_names: set = set()
|
||||||
date = datetime.date.fromtimestamp(timestamp)
|
data = defaultdict(int)
|
||||||
if int(date.year) != year:
|
for commit in wrapper(repo.iter_commits(all=True), repo):
|
||||||
continue
|
if author_name is not None:
|
||||||
if date not in data:
|
commit_author: str = commit.author.name
|
||||||
data[date] = 0
|
if commit_author.lower() != author_name:
|
||||||
data[date] += 1
|
other_author_names.add(commit_author)
|
||||||
|
commit_timestamp: int = commit.authored_date
|
||||||
|
commit_date: datetime.date = datetime.date.fromtimestamp(
|
||||||
|
commit_timestamp)
|
||||||
|
if year is not None:
|
||||||
|
if commit_date.year != year:
|
||||||
|
continue
|
||||||
|
data[commit_date] += 1
|
||||||
|
if author_name is not None:
|
||||||
|
# Warn for all other author names
|
||||||
|
for other_author_name in other_author_names:
|
||||||
|
logger.info(f"Other commit author in this folder of repositories:\t{other_author_name}")
|
||||||
return data
|
return data
|
||||||
|
|
||||||
print(repo_daily_commits(repo, 2023))
|
def iter_repos(dir_name: str):
|
||||||
|
pys = []
|
||||||
|
for root, dirs, files in os.walk(dir_name):
|
||||||
|
for directory in dirs:
|
||||||
|
if directory == '.git':
|
||||||
|
yield os.path.join(root, directory)
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
"""Plot contribution heatmap using calmap
|
||||||
|
|
||||||
|
ref. https://builtin.com/data-science/github-contribution-plot"""
|
||||||
|
|
||||||
|
import calmap
|
||||||
|
import pandas as pd
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
|
def year_of_contributions(df: pd.DataFrame):
|
||||||
|
events = pd.Series(df["count"], index=df["date"])
|
||||||
|
fig = plt.figure(figsize=(20,8))
|
||||||
|
ax = fig.add_subplot(111)
|
||||||
|
cax = calmap.yearplot(events, # cmap='YlGn',
|
||||||
|
fillcolor='lightgrey',
|
||||||
|
daylabels="MTWTFSS",
|
||||||
|
dayticks=[0, 2, 4, 6],
|
||||||
|
linewidth=2,
|
||||||
|
ax=ax
|
||||||
|
)
|
||||||
|
# fig.colorbar(cax.get_children()[1], ax=cax, orientation='horizontal') # FIXME: this is ugly.
|
|
@ -0,0 +1,198 @@
|
||||||
|
;date;count
|
||||||
|
0;2023-04-12;5
|
||||||
|
1;2023-01-12;3
|
||||||
|
2;2023-03-28;4
|
||||||
|
3;2023-01-28;3
|
||||||
|
4;2023-01-16;1
|
||||||
|
5;2023-01-13;2
|
||||||
|
6;2023-03-29;6
|
||||||
|
7;2023-04-23;4
|
||||||
|
8;2023-02-23;2
|
||||||
|
9;2023-02-02;3
|
||||||
|
10;2023-01-15;5
|
||||||
|
11;2023-01-14;4
|
||||||
|
12;2023-08-26;4
|
||||||
|
13;2023-05-29;4
|
||||||
|
14;2023-03-25;2
|
||||||
|
15;2023-01-08;3
|
||||||
|
16;2023-01-06;4
|
||||||
|
17;2023-01-05;3
|
||||||
|
18;2023-01-04;3
|
||||||
|
19;2023-06-23;2
|
||||||
|
20;2023-06-19;2
|
||||||
|
21;2023-06-15;5
|
||||||
|
22;2023-05-31;4
|
||||||
|
23;2023-05-28;2
|
||||||
|
24;2023-05-17;3
|
||||||
|
25;2023-05-14;3
|
||||||
|
26;2023-05-08;4
|
||||||
|
27;2023-05-07;4
|
||||||
|
28;2023-05-06;2
|
||||||
|
29;2023-04-19;2
|
||||||
|
30;2023-04-18;3
|
||||||
|
31;2023-04-09;4
|
||||||
|
32;2023-04-01;3
|
||||||
|
33;2023-03-12;3
|
||||||
|
34;2023-03-11;2
|
||||||
|
35;2023-03-05;2
|
||||||
|
36;2023-01-19;1
|
||||||
|
37;2023-04-03;3
|
||||||
|
38;2023-02-07;3
|
||||||
|
39;2023-02-04;4
|
||||||
|
40;2023-02-18;3
|
||||||
|
41;2023-02-17;4
|
||||||
|
42;2023-01-23;2
|
||||||
|
43;2023-01-17;3
|
||||||
|
44;2023-01-09;2
|
||||||
|
45;2023-08-11;2
|
||||||
|
46;2023-07-25;2
|
||||||
|
47;2023-08-07;1
|
||||||
|
48;2023-08-05;2
|
||||||
|
49;2023-07-31;3
|
||||||
|
50;2023-07-19;1
|
||||||
|
51;2023-07-13;1
|
||||||
|
52;2023-07-12;1
|
||||||
|
53;2023-07-04;3
|
||||||
|
54;2023-06-14;1
|
||||||
|
55;2023-06-11;2
|
||||||
|
56;2023-06-01;2
|
||||||
|
57;2023-05-15;1
|
||||||
|
58;2023-05-12;1
|
||||||
|
59;2023-05-27;2
|
||||||
|
60;2023-05-26;3
|
||||||
|
61;2023-05-22;3
|
||||||
|
62;2023-05-20;2
|
||||||
|
63;2023-05-16;2
|
||||||
|
64;2023-05-13;1
|
||||||
|
65;2023-05-09;3
|
||||||
|
66;2023-05-01;4
|
||||||
|
67;2023-04-29;2
|
||||||
|
68;2023-04-10;3
|
||||||
|
69;2023-04-20;3
|
||||||
|
70;2023-04-25;3
|
||||||
|
71;2023-04-11;4
|
||||||
|
72;2023-04-04;4
|
||||||
|
73;2023-03-31;3
|
||||||
|
74;2023-03-23;2
|
||||||
|
75;2023-03-13;1
|
||||||
|
76;2023-03-22;1
|
||||||
|
77;2023-03-16;2
|
||||||
|
78;2023-03-15;1
|
||||||
|
79;2023-03-14;2
|
||||||
|
80;2023-03-20;2
|
||||||
|
81;2023-03-08;1
|
||||||
|
82;2023-02-28;2
|
||||||
|
83;2023-02-27;3
|
||||||
|
84;2023-02-26;3
|
||||||
|
85;2023-02-22;1
|
||||||
|
86;2023-02-20;2
|
||||||
|
87;2023-02-19;6
|
||||||
|
88;2023-02-13;3
|
||||||
|
89;2023-02-10;3
|
||||||
|
90;2023-02-08;3
|
||||||
|
91;2023-02-06;2
|
||||||
|
92;2023-02-05;8
|
||||||
|
93;2023-02-03;2
|
||||||
|
94;2023-02-01;2
|
||||||
|
95;2023-01-30;1
|
||||||
|
96;2023-01-29;3
|
||||||
|
97;2023-01-27;1
|
||||||
|
98;2023-01-22;2
|
||||||
|
99;2023-01-21;1
|
||||||
|
100;2023-01-18;2
|
||||||
|
101;2023-07-06;2
|
||||||
|
102;2023-04-21;4
|
||||||
|
103;2023-02-12;2
|
||||||
|
104;2023-01-24;1
|
||||||
|
105;2023-08-19;1
|
||||||
|
106;2023-06-29;1
|
||||||
|
107;2023-06-07;3
|
||||||
|
108;2023-08-04;2
|
||||||
|
109;2023-08-28;2
|
||||||
|
110;2023-08-25;2
|
||||||
|
111;2023-08-23;1
|
||||||
|
112;2023-04-07;4
|
||||||
|
113;2023-05-05;1
|
||||||
|
114;2023-04-26;2
|
||||||
|
115;2023-03-04;1
|
||||||
|
116;2023-03-03;3
|
||||||
|
117;2023-03-02;1
|
||||||
|
118;2023-04-16;2
|
||||||
|
119;2023-04-08;3
|
||||||
|
120;2023-04-06;2
|
||||||
|
121;2023-01-02;3
|
||||||
|
122;2023-08-01;2
|
||||||
|
123;2023-07-28;1
|
||||||
|
124;2023-07-24;3
|
||||||
|
125;2023-07-21;2
|
||||||
|
126;2023-07-17;1
|
||||||
|
127;2023-07-14;1
|
||||||
|
128;2023-07-03;2
|
||||||
|
129;2023-06-26;2
|
||||||
|
130;2023-06-22;1
|
||||||
|
131;2023-06-12;1
|
||||||
|
132;2023-06-08;1
|
||||||
|
133;2023-06-03;2
|
||||||
|
134;2023-05-23;3
|
||||||
|
135;2023-05-21;2
|
||||||
|
136;2023-05-10;1
|
||||||
|
137;2023-05-02;2
|
||||||
|
138;2023-04-28;1
|
||||||
|
139;2023-04-22;2
|
||||||
|
140;2023-04-17;2
|
||||||
|
141;2023-04-15;3
|
||||||
|
142;2023-04-05;2
|
||||||
|
143;2023-04-02;2
|
||||||
|
144;2023-03-30;2
|
||||||
|
145;2023-03-27;4
|
||||||
|
146;2023-03-26;4
|
||||||
|
147;2023-03-24;2
|
||||||
|
148;2023-03-10;1
|
||||||
|
149;2023-02-25;2
|
||||||
|
150;2023-02-24;2
|
||||||
|
151;2023-02-21;1
|
||||||
|
152;2023-02-15;2
|
||||||
|
153;2023-02-14;1
|
||||||
|
154;2023-01-26;1
|
||||||
|
155;2023-01-11;3
|
||||||
|
156;2023-01-10;2
|
||||||
|
157;2023-01-03;3
|
||||||
|
158;2023-01-01;1
|
||||||
|
159;2023-07-29;5
|
||||||
|
160;2023-09-01;4
|
||||||
|
161;2023-08-31;1
|
||||||
|
162;2023-08-29;2
|
||||||
|
163;2023-08-15;1
|
||||||
|
164;2023-09-02;3
|
||||||
|
165;2023-02-11;2
|
||||||
|
166;2023-05-19;1
|
||||||
|
167;2023-06-06;2
|
||||||
|
168;2023-07-27;1
|
||||||
|
169;2023-06-21;1
|
||||||
|
170;2023-06-05;1
|
||||||
|
171;2023-06-04;1
|
||||||
|
172;2023-05-03;1
|
||||||
|
173;2023-04-24;2
|
||||||
|
174;2023-04-14;2
|
||||||
|
175;2023-04-13;1
|
||||||
|
176;2023-02-09;2
|
||||||
|
177;2023-08-09;1
|
||||||
|
178;2023-08-08;2
|
||||||
|
179;2023-08-06;1
|
||||||
|
180;2023-08-12;1
|
||||||
|
181;2023-08-03;1
|
||||||
|
182;2023-07-30;1
|
||||||
|
183;2023-07-23;1
|
||||||
|
184;2023-07-05;1
|
||||||
|
185;2023-06-25;1
|
||||||
|
186;2023-06-18;1
|
||||||
|
187;2023-06-17;1
|
||||||
|
188;2023-05-30;1
|
||||||
|
189;2023-05-11;1
|
||||||
|
190;2023-03-09;1
|
||||||
|
191;2023-03-06;1
|
||||||
|
192;2023-02-16;1
|
||||||
|
193;2023-01-20;1
|
||||||
|
194;2023-07-16;1
|
||||||
|
195;2023-07-08;1
|
||||||
|
196;2023-08-24;2
|
|
Loading…
Reference in New Issue