diff --git a/Dockerfile b/Dockerfile index fc917ad..55a8667 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,13 +1,12 @@ FROM python:3.9-slim-buster -ADD git-quality-check.py / +ADD git_quality_check /git_quality_check +ADD example /example +ADD setup.py / +ADD README.md / -# RUN pip install pystrich - -RUN apt-get upgrade -y -RUN apt-get update -RUN apt-get -y install software-properties-common -RUN apt-get upgrade -y RUN apt-get update -RUN apt-add-repository ppa:git-core/ppa -y RUN apt-get -y install git -ENTRYPOINT [ "python", "/git-quality-check.py" ] \ No newline at end of file + +RUN python setup.py develop + +ENTRYPOINT [ "python", "/example/git-quality-check.py" ] \ No newline at end of file diff --git a/example/git-quality-check.py b/example/git-quality-check.py new file mode 100644 index 0000000..57000c7 --- /dev/null +++ b/example/git-quality-check.py @@ -0,0 +1,50 @@ +from git_quality_check.utils import ( + git_logs, + git_all_branches, + set_output, + format_number, + parse_inputs, +) + +from git_quality_check.indicators.counters import ( + count_old_branches, + count_coupled, + process_logs, +) + + +from git_quality_check.indicators.commits import ( + is_empty_body, + not_a_squashed_commit, + count_bad_words, + is_test_commit, +) + +from git_quality_check.scoring import compute_score + + +if __name__ == "__main__": + + bad_words, main_branches = parse_inputs() + + logs = git_logs() + branches = git_all_branches() + + bad_commit_index = process_logs( + logs, [not_a_squashed_commit, is_empty_body, count_bad_words(bad_words)] + ) + test_index = process_logs(logs, [is_test_commit]) + + old_branches_index = count_old_branches(branches) + coupling_index = count_coupled(branches, main_branches) + + print(bad_commit_index) + print(test_index) + print(old_branches_index) + print(coupling_index) + + overall = compute_score( + bad_commit_index, test_index, old_branches_index, coupling_index + ) + + set_output(format_number(overall)) diff --git a/git-quality-check.py b/git-quality-check.py deleted file mode 100644 index 7e36d20..0000000 --- a/git-quality-check.py +++ /dev/null @@ -1,220 +0,0 @@ -from multiprocessing.spawn import old_main_modules -import random -import subprocess -from datetime import datetime -import os - -global bad_words, main_branches - - -def run_git(command: list[str]): - command.insert(0, "--no-pager") - command.insert(0, "git") - return subprocess.check_output(command).decode() - - -def git_logs(): - return run_git(["log"]).split("commit ") - - -def remove_first_line(log: str): - if is_valid_log: - try: - eol = log.index("\n") - log = log[eol + 1 :] - except ValueError: - # commit is empty or just contains one line - return "" - return log - - -def is_valid_log(log: str): - return not log == "" - - -def process_logs(logs: list[str], functions: list[str:int]): - counter = 0 - count = len(logs) * len(functions) - for i in range(len(logs)): - log = logs[i] - for function in functions: - counter += function(log) - return counter / count * 100 - - -def remove_header(log: str): - log = remove_first_line(log) # Hash - log = remove_first_line(log) # Author - log = remove_first_line(log) # Date - return log - - -def is_empty_body(log: str): - if not is_valid_log(log): - return 1 - log = remove_header(log) - if not is_valid_log(log): - return 1 - return 0 - - -def not_a_squashed_commit(log): - if "(#" not in log: - return 1 - return 0 - - -def count_bad_words(log: str): - counter = 0 - for word in bad_words: - if word in log.lower().split(): - counter += 1 - return counter - - -def is_test_commit(log: str): - for word in ["test", "testing"]: - if word in log.lower().split(): - return 1 - return 0 - - -def strip(s: str): - return s.strip().lstrip() - - -def git_all_branches(): - ret = run_git(["branch", "-r"]).split("\n") - return [strip(r) for r in ret if not strip(r) == ""] - - -def is_well_formed_branch(branch: str): - return not "->" in branch - - -def git_get_branch_date(branch: str): - if not is_well_formed_branch(branch): - return None - ret = run_git(["log", "-n", "1", '--date=format:"%Y-%m-%d"', branch]).split( - "Date: " - )[1] - ret = ret.replace('"', "").split("-") - year = int(strip(ret[0])) - month = int(ret[1]) - day = int(ret[2].split("\n")[0]) - return datetime(year, month, day) - - -def get_date(): - return datetime.today() - - -def diff_month(d1, d2): - return (d1.year - d2.year) * 12 + d1.month - d2.month - - -def is_old(branch): - branch_date = git_get_branch_date(branch) - if not branch_date: - return False - date = get_date() - return diff_month(date, branch_date) > 2 - - -def sample(li: list[str], min: int): - count = len(li) - if count > min: - li = random.sample(branches, min) - count = min - return (li, count) - - -def count_old_branches(branches): - counter = 0 - branches, count = sample(branches, 10) - for branch in branches: - counter += 1 if is_old(branch) else 0 - return counter / count * 100 - - -def are_coupled(branchA: str, branchB: str): - if not is_well_formed_branch(branchA) or not is_well_formed_branch(branchB): - return False - if branchA == branchB: - return False - try: - ret = run_git(["branch", "--contains", branchA, "-r"]).split("\n") - except: - print("Git `branch --contains failed with: ", branchA) - return False - for r in ret: - if branchB == r.strip().lstrip(): - return True - return False - - -def count_coupled(branches): - branches, count = sample(branches, 10) - branches.extend(main_branches) - count += len(main_branches) - counter = 0 - for bA in branches: - for bB in branches: - counter += 1 if are_coupled(bA, bB) else 0 - return counter / count * 100 - - -def format_number(number: float): - return "{0:.2f}".format(number) - - -def set_output(output: str): - print(f"::set-output name=score::{output}") - - -def compute_score(bad_commit_index, test_index, old_branches_index, coupling_index): - return ( - (100 - bad_commit_index) - + test_index - + (100 - old_branches_index) - + (100 - coupling_index) - ) / 4 - - -def parse_inputs(): - global bad_words, main_branches - try: - bad_words = os.environ["INPUT_BADWORDS"].split(", ") - except: - bad_words = ["WIP", "work in progress", "in progress", "TODO"] - try: - main_branches = os.environ["INPUT_MAINBRANCHES"].split(", ") - except: - main_branches = ["origin/develop", "origin/master"] - - -if __name__ == "__main__": - - parse_inputs() - - logs = git_logs() - branches = git_all_branches() - - bad_commit_index = process_logs( - logs, [not_a_squashed_commit, is_empty_body, count_bad_words] - ) - test_index = process_logs(logs, [is_test_commit]) - - old_branches_index = count_old_branches(branches) - coupling_index = count_coupled(branches) - - print(bad_commit_index) - print(test_index) - print(old_branches_index) - print(coupling_index) - - overall = compute_score( - bad_commit_index, test_index, old_branches_index, coupling_index - ) - - set_output(format_number(overall)) diff --git a/git_quality_check/__init__.py b/git_quality_check/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/git_quality_check/_version.py b/git_quality_check/_version.py new file mode 100644 index 0000000..9d7ccf0 --- /dev/null +++ b/git_quality_check/_version.py @@ -0,0 +1 @@ +__version__ = "v0.0-beta" diff --git a/git_quality_check/indicators/commits/__init__.py b/git_quality_check/indicators/commits/__init__.py new file mode 100644 index 0000000..6dac9b2 --- /dev/null +++ b/git_quality_check/indicators/commits/__init__.py @@ -0,0 +1,4 @@ +from .is_empty_body import is_empty_body +from .not_a_squashed_commit import not_a_squashed_commit +from .count_bad_words import count_bad_words +from .is_test_commit import is_test_commit diff --git a/git_quality_check/indicators/commits/count_bad_words.py b/git_quality_check/indicators/commits/count_bad_words.py new file mode 100644 index 0000000..59da32e --- /dev/null +++ b/git_quality_check/indicators/commits/count_bad_words.py @@ -0,0 +1,9 @@ +def count_bad_words(bad_words: list[str]): + def _count_bad_words(log: str): + counter = 0 + for word in bad_words: + if word in log.lower().split(): + counter += 1 + return counter + + return _count_bad_words diff --git a/git_quality_check/indicators/commits/is_empty_body.py b/git_quality_check/indicators/commits/is_empty_body.py new file mode 100644 index 0000000..21b62cf --- /dev/null +++ b/git_quality_check/indicators/commits/is_empty_body.py @@ -0,0 +1,13 @@ +from git_quality_check.utils import ( + is_valid_log, + remove_header, +) + + +def is_empty_body(log: str): + if not is_valid_log(log): + return 1 + log = remove_header(log) + if not is_valid_log(log): + return 1 + return 0 diff --git a/git_quality_check/indicators/commits/is_test_commit.py b/git_quality_check/indicators/commits/is_test_commit.py new file mode 100644 index 0000000..50058e3 --- /dev/null +++ b/git_quality_check/indicators/commits/is_test_commit.py @@ -0,0 +1,5 @@ +def is_test_commit(log: str): + for word in ["test", "testing"]: + if word in log.lower().split(): + return 1 + return 0 diff --git a/git_quality_check/indicators/commits/not_a_squashed_commit.py b/git_quality_check/indicators/commits/not_a_squashed_commit.py new file mode 100644 index 0000000..bce21a8 --- /dev/null +++ b/git_quality_check/indicators/commits/not_a_squashed_commit.py @@ -0,0 +1,4 @@ +def not_a_squashed_commit(log: str): + if "(#" not in log: + return 1 + return 0 diff --git a/git_quality_check/indicators/counters/__init__.py b/git_quality_check/indicators/counters/__init__.py new file mode 100644 index 0000000..3ca3f36 --- /dev/null +++ b/git_quality_check/indicators/counters/__init__.py @@ -0,0 +1,3 @@ +from .count_old_branches import count_old_branches +from .count_coupled import count_coupled +from .process_logs import process_logs diff --git a/git_quality_check/indicators/counters/count_coupled.py b/git_quality_check/indicators/counters/count_coupled.py new file mode 100644 index 0000000..0b7b665 --- /dev/null +++ b/git_quality_check/indicators/counters/count_coupled.py @@ -0,0 +1,12 @@ +from git_quality_check.utils import sample, are_coupled + + +def count_coupled(branches, main_branches): + branches, count = sample(branches, 10) + branches.extend(main_branches) + count += len(main_branches) + counter = 0 + for bA in branches: + for bB in branches: + counter += 1 if are_coupled(bA, bB) else 0 + return counter / count * 100 diff --git a/git_quality_check/indicators/counters/count_old_branches.py b/git_quality_check/indicators/counters/count_old_branches.py new file mode 100644 index 0000000..b27c5f2 --- /dev/null +++ b/git_quality_check/indicators/counters/count_old_branches.py @@ -0,0 +1,9 @@ +from git_quality_check.utils import sample, is_old + + +def count_old_branches(branches): + counter = 0 + branches, count = sample(branches, 10) + for branch in branches: + counter += 1 if is_old(branch) else 0 + return counter / count * 100 diff --git a/git_quality_check/indicators/counters/process_logs.py b/git_quality_check/indicators/counters/process_logs.py new file mode 100644 index 0000000..b8963bf --- /dev/null +++ b/git_quality_check/indicators/counters/process_logs.py @@ -0,0 +1,8 @@ +def process_logs(logs: list[str], functions: list[str:int]): + counter = 0 + count = len(logs) * len(functions) + for i in range(len(logs)): + log = logs[i] + for function in functions: + counter += function(log) + return counter / count * 100 diff --git a/git_quality_check/scoring/__init__.py b/git_quality_check/scoring/__init__.py new file mode 100644 index 0000000..12672e1 --- /dev/null +++ b/git_quality_check/scoring/__init__.py @@ -0,0 +1 @@ +from .overall import compute_score diff --git a/git_quality_check/scoring/overall.py b/git_quality_check/scoring/overall.py new file mode 100644 index 0000000..906a153 --- /dev/null +++ b/git_quality_check/scoring/overall.py @@ -0,0 +1,7 @@ +def compute_score(bad_commit_index, test_index, old_branches_index, coupling_index): + return ( + (100 - bad_commit_index) + + test_index + + (100 - old_branches_index) + + (100 - coupling_index) + ) / 4 diff --git a/git_quality_check/utils/__init__.py b/git_quality_check/utils/__init__.py new file mode 100644 index 0000000..808a56d --- /dev/null +++ b/git_quality_check/utils/__init__.py @@ -0,0 +1,10 @@ +from .git import ( + is_valid_log, + remove_header, + is_old, + are_coupled, + git_logs, + git_all_branches, +) + +from .common import sample, set_output, format_number, parse_inputs diff --git a/git_quality_check/utils/common.py b/git_quality_check/utils/common.py new file mode 100644 index 0000000..68a2ec4 --- /dev/null +++ b/git_quality_check/utils/common.py @@ -0,0 +1,45 @@ +import random +import os +from datetime import datetime + + +def parse_inputs(): + bad_words = [] + main_branches = [] + try: + bad_words = os.environ["INPUT_BADWORDS"].split(", ") + except: + bad_words = ["WIP", "work in progress", "in progress", "TODO"] + try: + main_branches = os.environ["INPUT_MAINBRANCHES"].split(", ") + except: + main_branches = ["origin/develop", "origin/master"] + return bad_words, main_branches + + +def strip(s: str): + return s.strip().lstrip() + + +def get_date(): + return datetime.today() + + +def sample(li: list[str], min: int): + count = len(li) + if count > min: + li = random.sample(li, min) + count = min + return (li, count) + + +def format_number(number: float): + return "{0:.2f}".format(number) + + +def set_output(output: str): + print(f"::set-output name=score::{output}") + + +def diff_month(d1, d2): + return (d1.year - d2.year) * 12 + d1.month - d2.month diff --git a/git_quality_check/utils/git.py b/git_quality_check/utils/git.py new file mode 100644 index 0000000..447fb5a --- /dev/null +++ b/git_quality_check/utils/git.py @@ -0,0 +1,81 @@ +import subprocess +from datetime import datetime +from .common import get_date, diff_month, strip + + +def is_valid_log(log: str): + return not log == "" + + +def run_git(command: list[str]): + command.insert(0, "--no-pager") + command.insert(0, "git") + return subprocess.check_output(command).decode() + + +def git_logs(): + return run_git(["log"]).split("commit ") + + +def is_old(branch): + branch_date = git_get_branch_date(branch) + if not branch_date: + return False + date = get_date() + return diff_month(date, branch_date) > 2 + + +def git_all_branches(): + ret = run_git(["branch", "-r"]).split("\n") + return [strip(r) for r in ret if not strip(r) == ""] + + +def are_coupled(branchA: str, branchB: str): + if not is_well_formed_branch(branchA) or not is_well_formed_branch(branchB): + return False + if branchA == branchB: + return False + try: + ret = run_git(["branch", "--contains", branchA, "-r"]).split("\n") + except: + print("Git `branch --contains failed with: ", branchA) + return False + for r in ret: + if branchB == r.strip().lstrip(): + return True + return False + + +def is_well_formed_branch(branch: str): + return not "->" in branch + + +def git_get_branch_date(branch: str): + if not is_well_formed_branch(branch): + return None + ret = run_git(["log", "-n", "1", '--date=format:"%Y-%m-%d"', branch]).split( + "Date: " + )[1] + ret = ret.replace('"', "").split("-") + year = int(strip(ret[0])) + month = int(ret[1]) + day = int(ret[2].split("\n")[0]) + return datetime(year, month, day) + + +def remove_first_line(log: str): + if is_valid_log: + try: + eol = log.index("\n") + log = log[eol + 1 :] + except ValueError: + # commit is empty or just contains one line + return "" + return log + + +def remove_header(log: str): + log = remove_first_line(log) # Hash + log = remove_first_line(log) # Author + log = remove_first_line(log) # Date + return log diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..ccb32c5 --- /dev/null +++ b/setup.py @@ -0,0 +1,41 @@ +import os.path as op + +from setuptools import setup, find_packages + + +# get the version (don't import mne here, so dependencies are not needed) +version = None +with open(op.join("git_quality_check", "_version.py"), "r") as fid: + for line in (line.strip() for line in fid): + if line.startswith("__version__"): + version = line.split("=")[1].strip().strip("'") + break +if version is None: + raise RuntimeError("Could not determine version") + +with open("README.md", "r", encoding="utf8") as fid: + long_description = fid.read() + +setup( + name="git-quality-check", + version=version, + description="A simple tool to evalute the quality of a git repository.", + url="https://github.com/gcattan/git-quality-check", + author="Gregoire Cattan", + author_email="gcattan@hotmail.com", + license="BSD (3-clause)", + packages=find_packages(), + long_description=long_description, + long_description_content_type="text/markdown", + project_urls={ + "Documentation": "https://github.com/gcattan/git-quality-check", + "Source": "https://github.com/gcattan/git-quality-check", + "Tracker": "https://github.com/gcattan/git-quality-check/issues/", + }, + platforms="any", + python_requires=">=3.9", + install_requires=[], + # extras_require={'docs': ['sphinx-gallery', 'sphinx-bootstrap_theme', 'numpydoc', 'mne', 'seaborn'], + # 'tests': ['pytest', 'seaborn', 'flake8', 'mne', 'pooch', 'tqdm']}, + zip_safe=False, +)