Skip to content

feat: add merge_base to check the branch is rebased #192

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 30 commits into from
Nov 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
da1bf27
feat: check merge base (WIP)
shenxianpeng Nov 6, 2024
4a46848
feat: check merge base (WIP)
shenxianpeng Nov 7, 2024
f7aacc1
ci: auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 7, 2024
ec2c791
fix pre-commit check issues
shenxianpeng Nov 7, 2024
00da816
Update commit_check/branch.py
shenxianpeng Nov 8, 2024
b6d0e25
Update .commit-check.yml
shenxianpeng Nov 8, 2024
ceccc4c
feat: add noxfile.py
shenxianpeng Nov 8, 2024
432434e
ci: auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 8, 2024
be6d753
fix pre-commit check
shenxianpeng Nov 8, 2024
aacf5b1
Update noxfile.py
shenxianpeng Nov 8, 2024
fd7deb8
fix: update merge_base feature
shenxianpeng Nov 8, 2024
9cdecd4
feat: refactor print error message
shenxianpeng Nov 8, 2024
5fb9ac0
fix: update noxfile.py to fix workflow
shenxianpeng Nov 8, 2024
a743a2b
fix: update noxfile.py to fix workflow
shenxianpeng Nov 8, 2024
5fe3167
fix: update noxfile.py to fix finding wheel
shenxianpeng Nov 8, 2024
30761bb
fix: update noxfile.py
shenxianpeng Nov 9, 2024
f196cb9
test: disable run commit-check
shenxianpeng Nov 9, 2024
b3fd87b
fix: revert main.yml
shenxianpeng Nov 9, 2024
1c5df69
ci: auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 9, 2024
2825ff0
Merge branch 'main' into feature/check-rebase
shenxianpeng Nov 9, 2024
4b53208
fix: removed does work test case
shenxianpeng Nov 9, 2024
d07ae51
fix: update merge_base regex
shenxianpeng Nov 9, 2024
26438ee
fix: refactor code based on review
shenxianpeng Nov 9, 2024
5b30018
fix: update noxfile.py to fix lint
shenxianpeng Nov 10, 2024
336407d
refactor: update commit-check.yml
shenxianpeng Nov 11, 2024
a73238a
test: add test for git_merge_base()
shenxianpeng Nov 11, 2024
8460825
refactor: update util_test.py
shenxianpeng Nov 11, 2024
9a6d794
feat: add new tests
shenxianpeng Nov 11, 2024
f04cd23
refactor: update test
shenxianpeng Nov 11, 2024
2081ec1
test: add tests for main and branch
shenxianpeng Nov 11, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .commit-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,8 @@ checks:
regex: Signed-off-by:.*[A-Za-z0-9]\s+<.+@.+>
error: Signed-off-by not found in latest commit
suggest: run command `git commit -m "conventional commit message" --signoff`

- check: merge_base
regex: main # it can be master, develop, devel etc based on your project.
error: Current branch is not up to date with target branch
suggest: please ensure your branch is rebased with the target branch
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@ __pycache__
.mypy_cache
.vscode
venv
.venv
UNKNOWN.egg-info
dist
build
tests/__pycache__
.coverage
coverage.xml
.nox
_build/

# docs
docs/_build
Expand Down
6 changes: 6 additions & 0 deletions commit_check/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@
'error': 'Signed-off-by not found in latest commit',
'suggest': 'run command `git commit -m "conventional commit message" --signoff`',
},
{
'check': 'merge_base',
'regex': r'main', # it can be master, develop, devel etc based on your project.
'error': 'Current branch is not up to date with target branch',
'suggest': 'please ensure your branch is rebased with the target branch',
},
],
}

Expand Down
4 changes: 3 additions & 1 deletion commit_check/author.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Check git author name and email"""
import re
from commit_check import YELLOW, RESET_COLOR, PASS, FAIL
from commit_check.util import get_commit_info, print_error_message, print_suggestion
from commit_check.util import get_commit_info, print_error_header, print_error_message, print_suggestion


def check_author(checks: list, check_type: str) -> int:
Expand All @@ -19,6 +19,8 @@ def check_author(checks: list, check_type: str) -> int:
config_value = str(get_commit_info(format_str))
result = re.match(check['regex'], config_value)
if result is None:
if not print_error_header.has_been_called:
print_error_header()
print_error_message(
check['check'], check['regex'],
check['error'], config_value,
Expand Down
32 changes: 31 additions & 1 deletion commit_check/branch.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Check git branch naming convention."""
import re
from commit_check import YELLOW, RESET_COLOR, PASS, FAIL
from commit_check.util import get_branch_name, print_error_message, print_suggestion
from commit_check.util import get_branch_name, git_merge_base, print_error_header, print_error_message, print_suggestion


def check_branch(checks: list) -> int:
Expand All @@ -15,6 +15,8 @@ def check_branch(checks: list) -> int:
branch_name = get_branch_name()
result = re.match(check['regex'], branch_name)
if result is None:
if not print_error_header.has_been_called:
print_error_header()
print_error_message(
check['check'], check['regex'],
check['error'], branch_name,
Expand All @@ -23,3 +25,31 @@ def check_branch(checks: list) -> int:
print_suggestion(check['suggest'])
return FAIL
return PASS


def check_merge_base(checks: list) -> int:
"""Check if the current branch is based on the latest target branch.
params checks: List of check configurations containing merge_base rules

:returns PASS(0) if merge base check succeeds, FAIL(1) otherwise
"""
for check in checks:
if check['check'] == 'merge_base':
if check['regex'] == "":
print(
f"{YELLOW}Not found target branch for checking merge base. skip checking.{RESET_COLOR}",
)
return PASS
current_branch = get_branch_name()
result = git_merge_base(check['regex'], current_branch)
if result != 0:
if not print_error_header.has_been_called:
print_error_header()
print_error_message(
check['check'], check['regex'],
check['error'], current_branch,
)
if check['suggest']:
print_suggestion(check['suggest'])
return FAIL
return PASS
6 changes: 5 additions & 1 deletion commit_check/commit.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import re
from pathlib import PurePath
from commit_check import YELLOW, RESET_COLOR, PASS, FAIL
from commit_check.util import cmd_output, get_commit_info, print_error_message, print_suggestion
from commit_check.util import cmd_output, get_commit_info, print_error_header, print_error_message, print_suggestion


def get_default_commit_msg_file() -> str:
Expand Down Expand Up @@ -37,6 +37,8 @@ def check_commit_msg(checks: list, commit_msg_file: str = "") -> int:
if check['check'] == 'message':
result = re.match(check['regex'], commit_msg)
if result is None:
if not print_error_header.has_been_called:
print_error_header()
print_error_message(
check['check'], check['regex'],
check['error'], commit_msg,
Expand Down Expand Up @@ -64,6 +66,8 @@ def check_commit_signoff(checks: list, commit_msg_file: str = "") -> int:
commit_hash = get_commit_info("H")
result = re.search(check['regex'], commit_msg)
if result is None:
if not print_error_header.has_been_called:
print_error_header()
print_error_message(
check['check'], check['regex'],
check['error'], commit_hash,
Expand Down
10 changes: 10 additions & 0 deletions commit_check/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,14 @@ def get_parser() -> argparse.ArgumentParser:
required=False,
)

parser.add_argument(
'-mb',
'--merge-base',
help='check common ancestors',
action="store_true",
required=False,
)

parser.add_argument(
'-d',
'--dry-run',
Expand Down Expand Up @@ -108,6 +116,8 @@ def main() -> int:
retval = branch.check_branch(checks)
if args.commit_signoff:
retval = commit.check_commit_signoff(checks)
if args.merge_base:
retval = branch.check_merge_base(checks)

if args.dry_run:
retval = PASS
Expand Down
47 changes: 39 additions & 8 deletions commit_check/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,23 @@ def get_commit_info(format_string: str, sha: str = "HEAD") -> str:
return output


def git_merge_base(target_branch: str, current_branch: str) -> int:
"""Check ancestors for a given commit.
:param target_branch: target branch
:param current_branch: default is HEAD

:returns: 0 if ancestor exists, 1 if not, 128 if git command fails.
"""
try:
commands = ['git', 'merge-base', '--is-ancestor', f'{target_branch}', f'{current_branch}']
result = subprocess.run(
commands, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='utf-8'
)
return result.returncode
except CalledProcessError:
return 128


def cmd_output(commands: list) -> str:
"""Run command
:param commands: list of commands
Expand Down Expand Up @@ -83,14 +100,18 @@ def validate_config(path_to_config: str) -> dict:
return configuration


def print_error_message(check_type: str, regex: str, error: str, reason: str):
"""Print error message.
:param check_type:
:param regex:
:param error:
:param reason:
def track_print_call(func):
def wrapper(*args, **kwargs):
wrapper.has_been_called = True
return func(*args, **kwargs)
wrapper.has_been_called = False # Initialize as False
return wrapper

:returns: Give error messages to user

@track_print_call
def print_error_header():
"""Print error message.
:returns: Print error head to user
"""
print("Commit rejected by Commit-Check. ")
print(" ")
Expand All @@ -105,10 +126,20 @@ def print_error_message(check_type: str, regex: str, error: str, reason: str):
print(" ")
print("Commit rejected. ")
print(" ")


def print_error_message(check_type: str, regex: str, error: str, reason: str):
"""Print error message.
:param check_type:
:param regex:
:param error:
:param reason:

:returns: Give error messages to user
"""
print(f"Type {YELLOW}{check_type}{RESET_COLOR} check failed => {RED}{reason}{RESET_COLOR} ", end='',)
print("")
print(f"It doesn't match regex: {regex}")
print("")
print(error)


Expand Down
71 changes: 71 additions & 0 deletions noxfile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import nox
import glob

nox.options.reuse_existing_virtualenvs = True
nox.options.reuse_venv = True
nox.options.sessions = ["lint"]

REQUIREMENTS = {
"dev": "requirements-dev.txt",
"docs": "docs/requirements.txt",
}

# -----------------------------------------------------------------------------
# Development Commands
# -----------------------------------------------------------------------------


@nox.session()
def lint(session):
session.install("pre-commit")
# only need pre-commit hook for local development
session.run("pre-commit", "install", "--hook-type", "pre-commit")
if session.posargs:
args = session.posargs + ["--all-files"]
else:
args = ["--all-files", "--show-diff-on-failure"]

session.run("pre-commit", "run", *args)


@nox.session(name="test-hook")
def test_hook(session):
session.install("-e", ".")
session.install("pre-commit")
session.run("pre-commit", "try-repo", ".")


@nox.session()
def build(session):
session.run("python3", "-m", "pip", "wheel", "--no-deps", "-w", "dist", ".")


@nox.session(name="install-wheel", requires=["build"])
def install_wheel(session):
whl_file = glob.glob("dist/*.whl")
session.install(str(whl_file[0]))


# @nox.session(name="commit-check", requires=["install-wheel"])
@nox.session(name="commit-check", requires=["install-wheel"])
def commit_check(session):
session.run(
"commit-check",
"--message",
"--branch",
"--author-email",
)


@nox.session(requires=["install-wheel"])
def coverage(session):
session.run("coverage", "run", "--source", "commit_check", "-m", "pytest")
session.run("coverage", "report")
session.run("coverage", "xml")


@nox.session()
def docs(session):
session.install("-e", ".")
session.install("-r", REQUIREMENTS["docs"])
session.run("sphinx-build", "-E", "-W", "-b", "html", "docs", "_build/html")
1 change: 1 addition & 0 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
coverage
nox
pre-commit
pytest
pytest-mock
40 changes: 37 additions & 3 deletions tests/branch_test.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
from commit_check import PASS, FAIL
from commit_check.branch import check_branch
from commit_check.branch import check_branch, check_merge_base

# used by get_branch_name mock
FAKE_BRANCH_NAME = "fake_branch_name"
# The location of check_branch()
LOCATION = "commit_check.branch"


class TestBranch:
class TestCheckBranch:
def test_check_branch(self, mocker):
# Must call get_branch_name, re.match at once.
checks = [{
Expand Down Expand Up @@ -113,3 +112,38 @@ def test_check_branch_with_result_none(self, mocker):
assert m_re_match.call_count == 1
assert m_print_error_message.call_count == 1
assert m_print_suggestion.call_count == 1


class TestCheckMergeBase:
def test_check_merge_base_with_empty_checks(self, mocker):
checks = []
m_check_merge = mocker.patch(f"{LOCATION}.check_merge_base")
retval = check_merge_base(checks)
assert retval == PASS
assert m_check_merge.call_count == 0

def test_check_merge_base_with_different_check(self, mocker):
checks = [{
"check": "branch",
"regex": "main"
}]
m_check_merge = mocker.patch(f"{LOCATION}.check_merge_base")
retval = check_merge_base(checks)
assert retval == PASS
assert m_check_merge.call_count == 0

def test_check_merge_base_fail_with_messages(self, mocker, capfd):
checks = [{
"check": "merge_base",
"regex": "develop",
"error": "Current branch is not",
"suggest": "Please rebase"
}]
mocker.patch(f"{LOCATION}.check_merge_base", return_value=1)
m_print_error = mocker.patch(f"{LOCATION}.print_error_message")
m_print_suggest = mocker.patch(f"{LOCATION}.print_suggestion")

retval = check_merge_base(checks)
assert retval == FAIL
assert "Current branch is not" in m_print_error.call_args[0][2]
assert "Please rebase" in m_print_suggest.call_args[0][0]
Loading
Loading