smart-commit-tool 1.1.0__tar.gz → 2.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {smart_commit_tool-1.1.0 → smart_commit_tool-2.0.1}/PKG-INFO +1 -1
- {smart_commit_tool-1.1.0 → smart_commit_tool-2.0.1}/pyproject.toml +1 -1
- smart_commit_tool-2.0.1/src/smart_commit/cli.py +105 -0
- smart_commit_tool-2.0.1/src/smart_commit/config.py +41 -0
- smart_commit_tool-2.0.1/src/smart_commit/constants.py +116 -0
- smart_commit_tool-2.0.1/src/smart_commit/services/ci.py +66 -0
- {smart_commit_tool-1.1.0 → smart_commit_tool-2.0.1}/src/smart_commit/services/security.py +12 -35
- {smart_commit_tool-1.1.0 → smart_commit_tool-2.0.1}/src/smart_commit/services/validator.py +2 -2
- smart_commit_tool-1.1.0/src/smart_commit/cli.py +0 -65
- smart_commit_tool-1.1.0/src/smart_commit/config.py +0 -50
- {smart_commit_tool-1.1.0 → smart_commit_tool-2.0.1}/.gitignore +0 -0
- {smart_commit_tool-1.1.0 → smart_commit_tool-2.0.1}/LICENSE +0 -0
- {smart_commit_tool-1.1.0 → smart_commit_tool-2.0.1}/README.md +0 -0
- {smart_commit_tool-1.1.0 → smart_commit_tool-2.0.1}/README.ru.md +0 -0
- {smart_commit_tool-1.1.0 → smart_commit_tool-2.0.1}/assets/demonstration.gif +0 -0
- {smart_commit_tool-1.1.0 → smart_commit_tool-2.0.1}/src/smart_commit/exceptions.py +0 -0
- {smart_commit_tool-1.1.0 → smart_commit_tool-2.0.1}/src/smart_commit/logger.py +0 -0
- {smart_commit_tool-1.1.0 → smart_commit_tool-2.0.1}/src/smart_commit/services/git.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "smart-commit-tool"
|
|
7
|
-
version = "
|
|
7
|
+
version = "2.0.1"
|
|
8
8
|
description = "Automated pre-push workflow manager with built-in code quality enforcement and smart branch protection."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import sys
|
|
3
|
+
from .exceptions import SmartCommitError
|
|
4
|
+
from .config import ConfigService
|
|
5
|
+
from .services.security import SecurityService
|
|
6
|
+
from .services.git import GitService
|
|
7
|
+
from .services.validator import ValidatorService
|
|
8
|
+
from .services.ci import CIService
|
|
9
|
+
from .logger import Console, logger
|
|
10
|
+
from .constants import TOOL_NAME, DEFAULT_PROTECTED_BRANCHES
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def run_ci_generation_flow():
|
|
14
|
+
"""Dedicated flow for 'smart-commit generate-action' command."""
|
|
15
|
+
logger.info("=== CI Generation Flow Started ===")
|
|
16
|
+
config = ConfigService.load_or_create()
|
|
17
|
+
commands = config.get("commands", [])
|
|
18
|
+
protected_branches = config.get("protected_branches", DEFAULT_PROTECTED_BRANCHES)
|
|
19
|
+
|
|
20
|
+
GitService.ensure_repo()
|
|
21
|
+
|
|
22
|
+
CIService.generate_github_action(commands, protected_branches)
|
|
23
|
+
CIService.prompt_and_push()
|
|
24
|
+
|
|
25
|
+
sys.exit(0)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def main():
|
|
29
|
+
if len(sys.argv) > 1 and sys.argv[1] == "generate-action":
|
|
30
|
+
try:
|
|
31
|
+
run_ci_generation_flow()
|
|
32
|
+
except SmartCommitError as e:
|
|
33
|
+
Console.error(str(e))
|
|
34
|
+
sys.exit(1)
|
|
35
|
+
except KeyboardInterrupt:
|
|
36
|
+
Console.warning("\nProcess interrupted by user.")
|
|
37
|
+
sys.exit(0)
|
|
38
|
+
|
|
39
|
+
parser = argparse.ArgumentParser(description=TOOL_NAME)
|
|
40
|
+
parser.add_argument("-b", "--branch", help="Specify target branch")
|
|
41
|
+
parser.add_argument("-m", "--message", help="Commit message")
|
|
42
|
+
args = parser.parse_args()
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
logger.info("=== Smart Commit Process Started ===")
|
|
46
|
+
|
|
47
|
+
config = ConfigService.load_or_create()
|
|
48
|
+
protected_branches = config.get(
|
|
49
|
+
"protected_branches", DEFAULT_PROTECTED_BRANCHES
|
|
50
|
+
)
|
|
51
|
+
commands = config.get("commands", [])
|
|
52
|
+
|
|
53
|
+
GitService.ensure_repo()
|
|
54
|
+
SecurityService.ensure_gitignore()
|
|
55
|
+
SecurityService.check_env_leaks()
|
|
56
|
+
|
|
57
|
+
current_branch = GitService.get_current_branch()
|
|
58
|
+
branch = args.branch or current_branch
|
|
59
|
+
GitService.switch_branch(branch)
|
|
60
|
+
|
|
61
|
+
if branch in protected_branches:
|
|
62
|
+
Console.warning(
|
|
63
|
+
f"You are pushing directly to a protected branch: '{branch}'."
|
|
64
|
+
)
|
|
65
|
+
ans = input("Are you sure you want to continue? [y/N]: ").strip().lower()
|
|
66
|
+
if ans != "y":
|
|
67
|
+
raise SmartCommitError("Operation cancelled by user.")
|
|
68
|
+
|
|
69
|
+
has_changes = GitService.has_any_changes()
|
|
70
|
+
message = args.message
|
|
71
|
+
|
|
72
|
+
if has_changes:
|
|
73
|
+
if not message:
|
|
74
|
+
message = input("📝 Enter commit message: ").strip()
|
|
75
|
+
if not message:
|
|
76
|
+
raise SmartCommitError("Commit message cannot be empty.")
|
|
77
|
+
|
|
78
|
+
ValidatorService.check_conventional_commit(message)
|
|
79
|
+
|
|
80
|
+
if commands:
|
|
81
|
+
Console.info("--- 🛠 RUNNING PIPELINE (from pyproject.toml) ---")
|
|
82
|
+
ValidatorService.run_commands(commands)
|
|
83
|
+
|
|
84
|
+
Console.info("--- 🚀 FINALIZING WORKFLOW ---")
|
|
85
|
+
|
|
86
|
+
if has_changes:
|
|
87
|
+
GitService.smart_stage()
|
|
88
|
+
GitService.commit(message)
|
|
89
|
+
else:
|
|
90
|
+
Console.info(
|
|
91
|
+
"Working directory is clean. Skipping commit phase and proceeding to push."
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
GitService.push_with_retry(branch)
|
|
95
|
+
|
|
96
|
+
except SmartCommitError as e:
|
|
97
|
+
Console.error(str(e))
|
|
98
|
+
sys.exit(1)
|
|
99
|
+
except KeyboardInterrupt:
|
|
100
|
+
Console.warning("\nProcess interrupted by user.")
|
|
101
|
+
sys.exit(0)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
if __name__ == "__main__":
|
|
105
|
+
main()
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from .exceptions import SmartCommitError
|
|
3
|
+
from .logger import Console, logger
|
|
4
|
+
from .constants import PYPROJECT_FILE, DEFAULT_TOML_CONFIG
|
|
5
|
+
|
|
6
|
+
if sys.version_info >= (3, 11):
|
|
7
|
+
import tomllib
|
|
8
|
+
else:
|
|
9
|
+
import tomli as tomllib
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ConfigService:
|
|
13
|
+
@classmethod
|
|
14
|
+
def load_or_create(cls) -> dict:
|
|
15
|
+
"""Reads configuration. If the section is missing, appends default settings."""
|
|
16
|
+
if not PYPROJECT_FILE.exists():
|
|
17
|
+
Console.warning(f"{PYPROJECT_FILE.name} not found. Creating a base file...")
|
|
18
|
+
PYPROJECT_FILE.touch()
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
with open(PYPROJECT_FILE, "rb") as f:
|
|
22
|
+
data = tomllib.load(f)
|
|
23
|
+
except Exception as e:
|
|
24
|
+
raise SmartCommitError(f"Failed to parse {PYPROJECT_FILE.name}: {e}")
|
|
25
|
+
|
|
26
|
+
config = data.get("tool", {}).get("smart_commit")
|
|
27
|
+
|
|
28
|
+
if config is None:
|
|
29
|
+
Console.info(
|
|
30
|
+
"Section [tool.smart_commit] not found. Generating default pipeline..."
|
|
31
|
+
)
|
|
32
|
+
logger.info(f"Appending default tool.smart_commit to {PYPROJECT_FILE.name}")
|
|
33
|
+
|
|
34
|
+
with open(PYPROJECT_FILE, "a", encoding="utf-8") as f:
|
|
35
|
+
f.write(DEFAULT_TOML_CONFIG)
|
|
36
|
+
|
|
37
|
+
with open(PYPROJECT_FILE, "rb") as f:
|
|
38
|
+
data = tomllib.load(f)
|
|
39
|
+
config = data.get("tool", {}).get("smart_commit", {})
|
|
40
|
+
|
|
41
|
+
return config
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
TOOL_NAME = "Smart Commit & Push Tool"
|
|
4
|
+
PYPROJECT_FILE = Path("pyproject.toml")
|
|
5
|
+
LOG_FILE = ".smart_commit.log"
|
|
6
|
+
|
|
7
|
+
DEFAULT_PROTECTED_BRANCHES = ["main", "master", "prod", "release"]
|
|
8
|
+
|
|
9
|
+
DEFAULT_TOML_CONFIG = """
|
|
10
|
+
# Auto-generated by Smart Commit
|
|
11
|
+
[tool.smart_commit]
|
|
12
|
+
repository_url = ""
|
|
13
|
+
commands = ["echo 'Running pre-commit pipeline...'"]
|
|
14
|
+
protected_branches = ["main", "master", "prod", "release"]
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
GITIGNORE_FILE = Path(".gitignore")
|
|
18
|
+
EXCLUDED_SCAN_DIRS = ["venv", ".venv", "env", "node_modules", ".git", "__pycache__"]
|
|
19
|
+
|
|
20
|
+
BASE_GITIGNORE = """
|
|
21
|
+
# Universal .gitignore generated by Smart Commit
|
|
22
|
+
__pycache__/
|
|
23
|
+
*.py[cod]
|
|
24
|
+
*$py.class
|
|
25
|
+
.venv/
|
|
26
|
+
venv/
|
|
27
|
+
env/
|
|
28
|
+
.env*
|
|
29
|
+
*.env
|
|
30
|
+
!.env.example
|
|
31
|
+
.vscode/
|
|
32
|
+
.idea/
|
|
33
|
+
dist/
|
|
34
|
+
build/
|
|
35
|
+
*.egg-info/
|
|
36
|
+
node_modules/
|
|
37
|
+
.DS_Store
|
|
38
|
+
.smart_commit.log
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
WORKFLOW_DIR = Path(".github/workflows")
|
|
42
|
+
WORKFLOW_FILE = WORKFLOW_DIR / "smart_commit_ci.yml"
|
|
43
|
+
CI_COMMIT_MESSAGE = "ci: auto-generate GitHub Actions workflow via Smart Commit"
|
|
44
|
+
|
|
45
|
+
GITHUB_ACTION_TEMPLATE = """name: Smart Commit CI Pipeline
|
|
46
|
+
|
|
47
|
+
on:
|
|
48
|
+
push:
|
|
49
|
+
branches: [ __BRANCHES__ ]
|
|
50
|
+
pull_request:
|
|
51
|
+
branches: [ __BRANCHES__ ]
|
|
52
|
+
|
|
53
|
+
jobs:
|
|
54
|
+
validate:
|
|
55
|
+
runs-on: ubuntu-latest
|
|
56
|
+
strategy:
|
|
57
|
+
fail-fast: false
|
|
58
|
+
matrix:
|
|
59
|
+
# Runs the pipeline on multiple Python versions automatically
|
|
60
|
+
python-version: ["3.10", "3.11", "3.12"]
|
|
61
|
+
|
|
62
|
+
steps:
|
|
63
|
+
- name: Checkout Repository
|
|
64
|
+
uses: actions/checkout@v4
|
|
65
|
+
|
|
66
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
67
|
+
uses: actions/setup-python@v5
|
|
68
|
+
with:
|
|
69
|
+
python-version: ${{ matrix.python-version }}
|
|
70
|
+
cache: 'pip' # Speeds up consecutive runs
|
|
71
|
+
|
|
72
|
+
- name: Install Dependencies (Adaptive Smart Installer)
|
|
73
|
+
run: |
|
|
74
|
+
python -m pip install --upgrade pip
|
|
75
|
+
|
|
76
|
+
echo "🔍 Detecting package manager..."
|
|
77
|
+
|
|
78
|
+
if [ -f "poetry.lock" ] || [ -f "pyproject.toml" ] && grep -q "tool.poetry" "pyproject.toml"; then
|
|
79
|
+
echo "🚀 Poetry detected!"
|
|
80
|
+
pip install poetry
|
|
81
|
+
# Force poetry to install globally so custom commands work without 'poetry run'
|
|
82
|
+
poetry config virtualenvs.create false
|
|
83
|
+
poetry install --no-interaction --no-ansi
|
|
84
|
+
|
|
85
|
+
elif [ -f "Pipfile.lock" ] || [ -f "Pipfile" ]; then
|
|
86
|
+
echo "🚀 Pipenv detected!"
|
|
87
|
+
pip install pipenv
|
|
88
|
+
pipenv install --system --dev
|
|
89
|
+
|
|
90
|
+
elif [ -f "pdm.lock" ] || [ -f "pyproject.toml" ] && grep -q "tool.pdm" "pyproject.toml"; then
|
|
91
|
+
echo "🚀 PDM detected!"
|
|
92
|
+
pip install pdm
|
|
93
|
+
pdm config python.use_venv false
|
|
94
|
+
pdm install
|
|
95
|
+
|
|
96
|
+
elif [ -f "requirements.txt" ]; then
|
|
97
|
+
echo "🚀 Standard requirements.txt detected!"
|
|
98
|
+
pip install -r requirements.txt
|
|
99
|
+
# Install package itself if setup.py exists
|
|
100
|
+
if [ -f "setup.py" ]; then pip install -e .; fi
|
|
101
|
+
|
|
102
|
+
elif [ -f "pyproject.toml" ]; then
|
|
103
|
+
echo "🚀 Standard PEP 621 pyproject.toml detected (Hatch, Flit, Setuptools)!"
|
|
104
|
+
# Attempt to install with dev dependencies if available, otherwise regular install
|
|
105
|
+
pip install -e .[dev] || pip install -e .
|
|
106
|
+
|
|
107
|
+
else
|
|
108
|
+
echo "⚠️ No known package manager found. Skipping dependency installation."
|
|
109
|
+
fi
|
|
110
|
+
|
|
111
|
+
__COMMANDS__
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
CONVENTIONAL_COMMIT_PATTERN = (
|
|
115
|
+
r"^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\(.+\))?:\s.+"
|
|
116
|
+
)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from ..logger import Console, logger
|
|
2
|
+
from ..exceptions import SmartCommitError
|
|
3
|
+
from .git import GitService
|
|
4
|
+
from ..constants import (
|
|
5
|
+
WORKFLOW_DIR,
|
|
6
|
+
WORKFLOW_FILE,
|
|
7
|
+
GITHUB_ACTION_TEMPLATE,
|
|
8
|
+
CI_COMMIT_MESSAGE,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CIService:
|
|
13
|
+
@classmethod
|
|
14
|
+
def generate_github_action(cls, commands: list[str], protected_branches: list[str]):
|
|
15
|
+
"""Generates a GitHub Actions workflow file dynamically based on pyproject.toml commands."""
|
|
16
|
+
Console.info("⚙️ Generating GitHub Actions workflow...")
|
|
17
|
+
|
|
18
|
+
branches_yaml = ", ".join(f'"{b}"' for b in protected_branches)
|
|
19
|
+
|
|
20
|
+
if not commands:
|
|
21
|
+
commands_yaml = (
|
|
22
|
+
' - run: echo "No commands defined in pyproject.toml"'
|
|
23
|
+
)
|
|
24
|
+
else:
|
|
25
|
+
commands_yaml = "\n".join(
|
|
26
|
+
f" - name: Run '{cmd}'\n run: {cmd}" for cmd in commands
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
workflow_content = GITHUB_ACTION_TEMPLATE.replace("__BRANCHES__", branches_yaml)
|
|
30
|
+
workflow_content = workflow_content.replace("__COMMANDS__", commands_yaml)
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
WORKFLOW_DIR.mkdir(parents=True, exist_ok=True)
|
|
34
|
+
with open(WORKFLOW_FILE, "w", encoding="utf-8") as f:
|
|
35
|
+
f.write(workflow_content)
|
|
36
|
+
|
|
37
|
+
Console.success(f"GitHub Action successfully created at: {WORKFLOW_FILE}")
|
|
38
|
+
logger.info("Generated GitHub Action workflow file.")
|
|
39
|
+
|
|
40
|
+
except Exception as e:
|
|
41
|
+
raise SmartCommitError(f"Failed to generate CI workflow: {e}")
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
def prompt_and_push(cls):
|
|
45
|
+
"""Asks the user if they want to instantly commit and push the new Action."""
|
|
46
|
+
ans = (
|
|
47
|
+
input("🚀 Do you want to commit and push this GitHub Action now? [y/N]: ")
|
|
48
|
+
.strip()
|
|
49
|
+
.lower()
|
|
50
|
+
)
|
|
51
|
+
if ans != "y":
|
|
52
|
+
Console.info("Action saved locally. You can commit it manually later.")
|
|
53
|
+
return
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
branch = GitService.get_current_branch()
|
|
57
|
+
|
|
58
|
+
GitService._run(["git", "add", str(WORKFLOW_FILE)])
|
|
59
|
+
|
|
60
|
+
GitService.commit(CI_COMMIT_MESSAGE)
|
|
61
|
+
Console.info(f"Committed: '{CI_COMMIT_MESSAGE}'")
|
|
62
|
+
|
|
63
|
+
GitService.push_with_retry(branch)
|
|
64
|
+
|
|
65
|
+
except Exception as e:
|
|
66
|
+
raise SmartCommitError(f"Failed to push CI configuration: {e}")
|
|
@@ -1,40 +1,19 @@
|
|
|
1
1
|
import subprocess
|
|
2
2
|
from pathlib import Path
|
|
3
3
|
from ..logger import Console, logger
|
|
4
|
+
from ..constants import GITIGNORE_FILE, BASE_GITIGNORE, EXCLUDED_SCAN_DIRS
|
|
4
5
|
|
|
5
6
|
|
|
6
7
|
class SecurityService:
|
|
7
|
-
BASE_GITIGNORE = """
|
|
8
|
-
# Universal .gitignore generated by Smart Commit
|
|
9
|
-
__pycache__/
|
|
10
|
-
*.py[cod]
|
|
11
|
-
*$py.class
|
|
12
|
-
.venv/
|
|
13
|
-
venv/
|
|
14
|
-
env/
|
|
15
|
-
.env*
|
|
16
|
-
*.env
|
|
17
|
-
!.env.example
|
|
18
|
-
.vscode/
|
|
19
|
-
.idea/
|
|
20
|
-
dist/
|
|
21
|
-
build/
|
|
22
|
-
*.egg-info/
|
|
23
|
-
node_modules/
|
|
24
|
-
.DS_Store
|
|
25
|
-
.smart_commit.log
|
|
26
|
-
"""
|
|
27
|
-
|
|
28
8
|
@classmethod
|
|
29
9
|
def ensure_gitignore(cls):
|
|
30
10
|
"""Ensures .gitignore exists or creates a secure default."""
|
|
31
|
-
|
|
32
|
-
if not path.exists():
|
|
11
|
+
if not GITIGNORE_FILE.exists():
|
|
33
12
|
Console.warning(
|
|
34
|
-
".
|
|
13
|
+
f"{GITIGNORE_FILE.name} not found. Creating a secure default configuration..."
|
|
35
14
|
)
|
|
36
|
-
with open(
|
|
37
|
-
f.write(
|
|
15
|
+
with open(GITIGNORE_FILE, "w", encoding="utf-8") as f:
|
|
16
|
+
f.write(BASE_GITIGNORE.lstrip())
|
|
38
17
|
logger.info("Created default .gitignore")
|
|
39
18
|
|
|
40
19
|
@classmethod
|
|
@@ -51,11 +30,7 @@ node_modules/
|
|
|
51
30
|
env_files = [
|
|
52
31
|
f
|
|
53
32
|
for f in raw_files
|
|
54
|
-
if f.is_file()
|
|
55
|
-
and not any(
|
|
56
|
-
part in ["venv", ".venv", "env", "node_modules", ".git", "__pycache__"]
|
|
57
|
-
for part in f.parts
|
|
58
|
-
)
|
|
33
|
+
if f.is_file() and not any(part in EXCLUDED_SCAN_DIRS for part in f.parts)
|
|
59
34
|
]
|
|
60
35
|
|
|
61
36
|
leaked_files = []
|
|
@@ -69,22 +44,24 @@ node_modules/
|
|
|
69
44
|
|
|
70
45
|
if leaked_files:
|
|
71
46
|
Console.error(
|
|
72
|
-
"CRITICAL SECURITY RISK: .env files found that are NOT in .
|
|
47
|
+
f"CRITICAL SECURITY RISK: .env files found that are NOT in {GITIGNORE_FILE.name}!"
|
|
73
48
|
)
|
|
74
49
|
for lf in leaked_files:
|
|
75
50
|
print(f" 👉 {lf}")
|
|
76
51
|
|
|
77
52
|
ans = (
|
|
78
|
-
input(
|
|
53
|
+
input(
|
|
54
|
+
f"🛑 Add these files to {GITIGNORE_FILE.name} automatically? [Y/n]: "
|
|
55
|
+
)
|
|
79
56
|
.strip()
|
|
80
57
|
.lower()
|
|
81
58
|
)
|
|
82
59
|
if ans != "n":
|
|
83
|
-
with open(
|
|
60
|
+
with open(GITIGNORE_FILE, "a", encoding="utf-8") as f:
|
|
84
61
|
f.write("\n# Auto-added by Smart Commit to prevent leaks\n")
|
|
85
62
|
for lf in leaked_files:
|
|
86
63
|
f.write(f"{lf}\n")
|
|
87
|
-
Console.success("Files successfully added to .
|
|
64
|
+
Console.success(f"Files successfully added to {GITIGNORE_FILE.name}.")
|
|
88
65
|
else:
|
|
89
66
|
Console.warning(
|
|
90
67
|
"Action declined. Please be careful: secrets might be pushed!"
|
|
@@ -2,14 +2,14 @@ import subprocess
|
|
|
2
2
|
import re
|
|
3
3
|
from ..exceptions import ValidationError
|
|
4
4
|
from ..logger import Console, logger
|
|
5
|
+
from ..constants import CONVENTIONAL_COMMIT_PATTERN
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
class ValidatorService:
|
|
8
9
|
@classmethod
|
|
9
10
|
def check_conventional_commit(cls, message: str):
|
|
10
11
|
"""Validates message against Conventional Commits standard."""
|
|
11
|
-
|
|
12
|
-
if not re.match(pattern, message):
|
|
12
|
+
if not re.match(CONVENTIONAL_COMMIT_PATTERN, message):
|
|
13
13
|
logger.warning(f"Non-conventional commit message: {message}")
|
|
14
14
|
Console.warning(
|
|
15
15
|
"Commit message does not follow Conventional Commits (e.g., 'feat: add auth')."
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import argparse
|
|
2
|
-
import sys
|
|
3
|
-
from .exceptions import SmartCommitError
|
|
4
|
-
from .config import ConfigService
|
|
5
|
-
from .services.security import SecurityService
|
|
6
|
-
from .services.git import GitService
|
|
7
|
-
from .services.validator import ValidatorService
|
|
8
|
-
from .logger import Console, logger
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def main():
|
|
12
|
-
parser = argparse.ArgumentParser(description="Smart Commit & Push Tool")
|
|
13
|
-
parser.add_argument("-b", "--branch", help="Specify target branch")
|
|
14
|
-
parser.add_argument("-m", "--message", help="Commit message")
|
|
15
|
-
args = parser.parse_args()
|
|
16
|
-
|
|
17
|
-
try:
|
|
18
|
-
logger.info("=== Smart Commit Process Started ===")
|
|
19
|
-
|
|
20
|
-
config = ConfigService.load_or_create()
|
|
21
|
-
protected_branches = config.get("protected_branches", ["main", "master"])
|
|
22
|
-
commands = config.get("commands", [])
|
|
23
|
-
|
|
24
|
-
GitService.ensure_repo()
|
|
25
|
-
SecurityService.ensure_gitignore()
|
|
26
|
-
SecurityService.check_env_leaks()
|
|
27
|
-
|
|
28
|
-
current_branch = GitService.get_current_branch()
|
|
29
|
-
branch = args.branch or current_branch
|
|
30
|
-
GitService.switch_branch(branch)
|
|
31
|
-
if branch in protected_branches:
|
|
32
|
-
Console.warning(
|
|
33
|
-
f"You are pushing directly to a protected branch: '{branch}'."
|
|
34
|
-
)
|
|
35
|
-
ans = input("Are you sure you want to continue? [y/N]: ").strip().lower()
|
|
36
|
-
if ans != "y":
|
|
37
|
-
raise SmartCommitError("Operation cancelled by user.")
|
|
38
|
-
|
|
39
|
-
message = args.message
|
|
40
|
-
if not message:
|
|
41
|
-
message = input("📝 Enter commit message: ").strip()
|
|
42
|
-
|
|
43
|
-
if not message:
|
|
44
|
-
raise SmartCommitError("Commit message cannot be empty.")
|
|
45
|
-
|
|
46
|
-
ValidatorService.check_conventional_commit(message)
|
|
47
|
-
if commands:
|
|
48
|
-
Console.info("--- 🛠 RUNNING PIPELINE (from pyproject.toml) ---")
|
|
49
|
-
ValidatorService.run_commands(commands)
|
|
50
|
-
|
|
51
|
-
Console.info("--- 🚀 FINALIZING COMMIT ---")
|
|
52
|
-
GitService.smart_stage()
|
|
53
|
-
GitService.commit(message)
|
|
54
|
-
GitService.push_with_retry(branch)
|
|
55
|
-
|
|
56
|
-
except SmartCommitError as e:
|
|
57
|
-
Console.error(str(e))
|
|
58
|
-
sys.exit(1)
|
|
59
|
-
except KeyboardInterrupt:
|
|
60
|
-
Console.warning("\nProcess interrupted by user.")
|
|
61
|
-
sys.exit(0)
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if __name__ == "__main__":
|
|
65
|
-
main()
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import sys
|
|
2
|
-
from pathlib import Path
|
|
3
|
-
from .exceptions import SmartCommitError
|
|
4
|
-
from .logger import Console, logger
|
|
5
|
-
|
|
6
|
-
if sys.version_info >= (3, 11):
|
|
7
|
-
import tomllib
|
|
8
|
-
else:
|
|
9
|
-
import tomli as tomllib
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class ConfigService:
|
|
13
|
-
DEFAULT_CONFIG = """
|
|
14
|
-
# Auto-generated by Smart Commit
|
|
15
|
-
[tool.smart_commit]
|
|
16
|
-
repository_url = ""
|
|
17
|
-
commands = ["echo 'Running pre-commit pipeline...'"]
|
|
18
|
-
protected_branches = ["main", "master", "prod", "release"]
|
|
19
|
-
"""
|
|
20
|
-
|
|
21
|
-
@classmethod
|
|
22
|
-
def load_or_create(cls) -> dict:
|
|
23
|
-
"""Reads configuration. If the section is missing, appends default settings."""
|
|
24
|
-
path = Path("pyproject.toml")
|
|
25
|
-
|
|
26
|
-
if not path.exists():
|
|
27
|
-
Console.warning("pyproject.toml not found. Creating a base file...")
|
|
28
|
-
path.touch()
|
|
29
|
-
|
|
30
|
-
try:
|
|
31
|
-
with open(path, "rb") as f:
|
|
32
|
-
data = tomllib.load(f)
|
|
33
|
-
except Exception as e:
|
|
34
|
-
raise SmartCommitError(f"Failed to parse pyproject.toml: {e}")
|
|
35
|
-
|
|
36
|
-
config = data.get("tool", {}).get("smart_commit")
|
|
37
|
-
|
|
38
|
-
if config is None:
|
|
39
|
-
Console.info(
|
|
40
|
-
"Section [tool.smart_commit] not found. Generating default pipeline..."
|
|
41
|
-
)
|
|
42
|
-
logger.info("Appending default tool.smart_commit to pyproject.toml")
|
|
43
|
-
with open(path, "a", encoding="utf-8") as f:
|
|
44
|
-
f.write(cls.DEFAULT_CONFIG)
|
|
45
|
-
|
|
46
|
-
with open(path, "rb") as f:
|
|
47
|
-
data = tomllib.load(f)
|
|
48
|
-
config = data.get("tool", {}).get("smart_commit", {})
|
|
49
|
-
|
|
50
|
-
return config
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|