smart-commit-tool 2.0.3__tar.gz → 3.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-2.0.3 → smart_commit_tool-3.0.1}/.github/workflows/smart_commit_ci.yml +5 -6
- {smart_commit_tool-2.0.3 → smart_commit_tool-3.0.1}/PKG-INFO +1 -1
- smart_commit_tool-3.0.1/assets/demo.gif +0 -0
- {smart_commit_tool-2.0.3 → smart_commit_tool-3.0.1}/pyproject.toml +1 -1
- smart_commit_tool-3.0.1/smart_commit/__init__.py +0 -0
- smart_commit_tool-3.0.1/smart_commit/cli.py +70 -0
- smart_commit_tool-3.0.1/smart_commit/services/config.py +31 -0
- smart_commit_tool-3.0.1/smart_commit/services/git.py +53 -0
- smart_commit_tool-3.0.1/smart_commit/services/logger.py +26 -0
- smart_commit_tool-3.0.1/smart_commit/services/runner.py +20 -0
- smart_commit_tool-3.0.1/smart_commit/services/security.py +59 -0
- smart_commit_tool-2.0.3/assets/demonstration.gif +0 -0
- smart_commit_tool-2.0.3/src/smart_commit/cli.py +0 -105
- smart_commit_tool-2.0.3/src/smart_commit/config.py +0 -41
- smart_commit_tool-2.0.3/src/smart_commit/constants.py +0 -115
- smart_commit_tool-2.0.3/src/smart_commit/exceptions.py +0 -14
- smart_commit_tool-2.0.3/src/smart_commit/logger.py +0 -32
- smart_commit_tool-2.0.3/src/smart_commit/services/ci.py +0 -66
- smart_commit_tool-2.0.3/src/smart_commit/services/git.py +0 -129
- smart_commit_tool-2.0.3/src/smart_commit/services/security.py +0 -68
- smart_commit_tool-2.0.3/src/smart_commit/services/validator.py +0 -28
- {smart_commit_tool-2.0.3 → smart_commit_tool-3.0.1}/.gitignore +0 -0
- {smart_commit_tool-2.0.3 → smart_commit_tool-3.0.1}/LICENSE +0 -0
- {smart_commit_tool-2.0.3 → smart_commit_tool-3.0.1}/README.md +0 -0
- {smart_commit_tool-2.0.3 → smart_commit_tool-3.0.1}/README.ru.md +0 -0
|
@@ -12,7 +12,6 @@ jobs:
|
|
|
12
12
|
strategy:
|
|
13
13
|
fail-fast: false
|
|
14
14
|
matrix:
|
|
15
|
-
# Runs the pipeline on multiple Python versions automatically
|
|
16
15
|
python-version: ["3.10", "3.11", "3.12"]
|
|
17
16
|
|
|
18
17
|
steps:
|
|
@@ -23,18 +22,20 @@ jobs:
|
|
|
23
22
|
uses: actions/setup-python@v5
|
|
24
23
|
with:
|
|
25
24
|
python-version: ${{ matrix.python-version }}
|
|
26
|
-
cache: 'pip'
|
|
25
|
+
cache: 'pip'
|
|
27
26
|
|
|
28
27
|
- name: Install Dependencies (Adaptive Smart Installer)
|
|
29
28
|
run: |
|
|
30
29
|
python -m pip install --upgrade pip
|
|
31
30
|
|
|
31
|
+
# Ensure local bin is in PATH for all subsequent steps
|
|
32
|
+
echo "$HOME/.local/bin" >> $GITHUB_PATH
|
|
33
|
+
|
|
32
34
|
echo "🔍 Detecting package manager..."
|
|
33
35
|
|
|
34
36
|
if [ -f "poetry.lock" ] || [ -f "pyproject.toml" ] && grep -q "tool.poetry" "pyproject.toml"; then
|
|
35
37
|
echo "🚀 Poetry detected!"
|
|
36
38
|
pip install poetry
|
|
37
|
-
# Force poetry to install globally so custom commands work without 'poetry run'
|
|
38
39
|
poetry config virtualenvs.create false
|
|
39
40
|
poetry install --no-interaction --no-ansi
|
|
40
41
|
|
|
@@ -52,12 +53,10 @@ jobs:
|
|
|
52
53
|
elif [ -f "requirements.txt" ]; then
|
|
53
54
|
echo "🚀 Standard requirements.txt detected!"
|
|
54
55
|
pip install -r requirements.txt
|
|
55
|
-
# Install package itself if setup.py exists
|
|
56
56
|
if [ -f "setup.py" ]; then pip install -e .; fi
|
|
57
57
|
|
|
58
58
|
elif [ -f "pyproject.toml" ]; then
|
|
59
|
-
echo "🚀 Standard PEP 621 pyproject.toml detected
|
|
60
|
-
# Attempt to install with dev dependencies if available, otherwise regular install
|
|
59
|
+
echo "🚀 Standard PEP 621 pyproject.toml detected!"
|
|
61
60
|
pip install -e .[dev] || pip install -e .
|
|
62
61
|
|
|
63
62
|
else
|
|
Binary file
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "smart-commit-tool"
|
|
7
|
-
version = "
|
|
7
|
+
version = "3.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"
|
|
File without changes
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import sys
|
|
3
|
+
from .services import (
|
|
4
|
+
config as config_service,
|
|
5
|
+
git as git_service,
|
|
6
|
+
security as security_service,
|
|
7
|
+
runner as runner_service,
|
|
8
|
+
)
|
|
9
|
+
from .services.logger import logger
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def interactive_input(prompt: str, default: str = "") -> str:
|
|
13
|
+
"""Запрашивает ввод у пользователя, если флаг не передан."""
|
|
14
|
+
suffix = f" [{default}]" if default else ""
|
|
15
|
+
value = input(f"{prompt}{suffix}: ").strip()
|
|
16
|
+
return value if value else default
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def main():
|
|
20
|
+
parser = argparse.ArgumentParser(description="Smart Git Commit Tool")
|
|
21
|
+
parser.add_argument("-b", "--branch", help="Имя ветки")
|
|
22
|
+
parser.add_argument("-m", "--message", help="Сообщение коммита")
|
|
23
|
+
parser.add_argument("-r", "--remote", help="Remote (например, origin)")
|
|
24
|
+
|
|
25
|
+
args = parser.parse_args()
|
|
26
|
+
|
|
27
|
+
logger.info("Инициализация Smart Commit...")
|
|
28
|
+
|
|
29
|
+
# 1. Сбор параметров (Fallback на интерактивный ввод)
|
|
30
|
+
branch = args.branch or interactive_input("Введите имя ветки")
|
|
31
|
+
message = args.message or interactive_input("Введите сообщение коммита")
|
|
32
|
+
remote = args.remote or interactive_input("Введите remote", default="origin")
|
|
33
|
+
|
|
34
|
+
if not branch or not message:
|
|
35
|
+
logger.error("Ветка и сообщение коммита обязательны!")
|
|
36
|
+
sys.exit(1)
|
|
37
|
+
|
|
38
|
+
# 2. Загрузка конфига
|
|
39
|
+
config = config_service.load_config()
|
|
40
|
+
|
|
41
|
+
# 3. Валидация ветки
|
|
42
|
+
git_service.check_protected(branch, config.get("protected_branches", []))
|
|
43
|
+
git_service.ensure_branch(branch)
|
|
44
|
+
|
|
45
|
+
# 4. Добавление файлов в индекс (чтобы сканер секретов увидел их)
|
|
46
|
+
git_service.add_all()
|
|
47
|
+
|
|
48
|
+
# 5. Секьюрити чек
|
|
49
|
+
staged_files = git_service.get_staged_files()
|
|
50
|
+
if not staged_files:
|
|
51
|
+
logger.warning("Нет изменений для коммита.")
|
|
52
|
+
sys.exit(0)
|
|
53
|
+
|
|
54
|
+
if security_service.check_secrets(staged_files):
|
|
55
|
+
# Удаляем из индекса файлы, которые только что попали в gitignore
|
|
56
|
+
git_service.run_cmd(["git", "rm", "-r", "--cached", "."])
|
|
57
|
+
git_service.add_all()
|
|
58
|
+
|
|
59
|
+
# 6. Запуск кастомных команд (ruff, тесты и т.д.)
|
|
60
|
+
runner_service.run_pre_commands(config.get("commands", []))
|
|
61
|
+
|
|
62
|
+
# 7. Финализация (Commit & Push)
|
|
63
|
+
git_service.commit(message)
|
|
64
|
+
git_service.push(remote, branch)
|
|
65
|
+
|
|
66
|
+
logger.success("Smart Commit завершил работу!")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
if __name__ == "__main__":
|
|
70
|
+
main()
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from .logger import logger
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
import tomllib
|
|
7
|
+
except ModuleNotFoundError:
|
|
8
|
+
import tomli as tomllib
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def load_config() -> dict:
|
|
12
|
+
"""Загружает секцию [tool.smart_commit] из pyproject.toml."""
|
|
13
|
+
config_path = Path(os.getcwd()) / "pyproject.toml"
|
|
14
|
+
|
|
15
|
+
default_config = {
|
|
16
|
+
"repository_url": "",
|
|
17
|
+
"commands": [],
|
|
18
|
+
"protected_branches": ["main", "master"],
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if not config_path.exists():
|
|
22
|
+
logger.warning("pyproject.toml не найден. Используются настройки по умолчанию.")
|
|
23
|
+
return default_config
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
with open(config_path, "rb") as f:
|
|
27
|
+
data = tomllib.load(f)
|
|
28
|
+
return data.get("tool", {}).get("smart_commit", default_config)
|
|
29
|
+
except Exception as e:
|
|
30
|
+
logger.error(f"Ошибка чтения конфига: {e}")
|
|
31
|
+
return default_config
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import sys
|
|
3
|
+
from .logger import logger
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def run_cmd(cmd: list[str], check=True) -> str:
|
|
7
|
+
"""Выполняет bash-команду и возвращает вывод."""
|
|
8
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
9
|
+
if check and result.returncode != 0:
|
|
10
|
+
logger.error(f"Ошибка Git: {result.stderr.strip()}")
|
|
11
|
+
sys.exit(1)
|
|
12
|
+
return result.stdout.strip()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def check_protected(branch: str, protected_branches: list[str]):
|
|
16
|
+
"""Блокирует пуш в защищенные ветки."""
|
|
17
|
+
if branch in protected_branches:
|
|
18
|
+
logger.error(f"Прямой коммит в защищенную ветку '{branch}' запрещен!")
|
|
19
|
+
sys.exit(1)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def ensure_branch(branch: str):
|
|
23
|
+
"""Проверяет наличие ветки. Если нет - создает."""
|
|
24
|
+
result = subprocess.run(["git", "checkout", branch], capture_output=True, text=True)
|
|
25
|
+
if result.returncode != 0:
|
|
26
|
+
logger.info(f"Ветка '{branch}' не найдена. Создаю новую...")
|
|
27
|
+
run_cmd(["git", "checkout", "-b", branch])
|
|
28
|
+
else:
|
|
29
|
+
logger.info(f"Переключено на ветку '{branch}'.")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def add_all():
|
|
33
|
+
run_cmd(["git", "add", "."])
|
|
34
|
+
logger.info("Файлы добавлены в индекс (git add).")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def get_staged_files() -> list[str]:
|
|
38
|
+
"""Возвращает список файлов, готовых к коммиту."""
|
|
39
|
+
output = run_cmd(["git", "diff", "--cached", "--name-only"])
|
|
40
|
+
return [f for f in output.split("\n") if f]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def commit(message: str):
|
|
44
|
+
run_cmd(
|
|
45
|
+
["git", "commit", "-m", message], check=False
|
|
46
|
+
) # check=False, чтобы обработать "nothing to commit"
|
|
47
|
+
logger.info(f"Коммит создан: '{message}'")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def push(remote: str, branch: str):
|
|
51
|
+
logger.info(f"Отправка изменений в {remote}/{branch}...")
|
|
52
|
+
run_cmd(["git", "push", "-u", remote, branch])
|
|
53
|
+
logger.success("Push успешно завершен!")
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from colorama import Fore, Style, init
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
init(autoreset=True)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Logger:
|
|
9
|
+
@staticmethod
|
|
10
|
+
def info(msg: str):
|
|
11
|
+
print(f"{Fore.CYAN}[INFO]{Style.RESET_ALL} {msg}")
|
|
12
|
+
|
|
13
|
+
@staticmethod
|
|
14
|
+
def success(msg: str):
|
|
15
|
+
print(f"{Fore.GREEN}[SUCCESS]{Style.RESET_ALL} {msg}")
|
|
16
|
+
|
|
17
|
+
@staticmethod
|
|
18
|
+
def warning(msg: str):
|
|
19
|
+
print(f"{Fore.YELLOW}[WARNING]{Style.RESET_ALL} {msg}")
|
|
20
|
+
|
|
21
|
+
@staticmethod
|
|
22
|
+
def error(msg: str):
|
|
23
|
+
print(f"{Fore.RED}[ERROR]{Style.RESET_ALL} {msg}", file=sys.stderr)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
logger = Logger()
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import sys
|
|
3
|
+
from .logger import logger
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def run_pre_commands(commands: list[str]):
|
|
7
|
+
"""Запускает команды из конфига перед коммитом."""
|
|
8
|
+
if not commands:
|
|
9
|
+
return
|
|
10
|
+
|
|
11
|
+
logger.info("Запуск pre-commit команд...")
|
|
12
|
+
for cmd in commands:
|
|
13
|
+
logger.info(f"Выполнение: {cmd}")
|
|
14
|
+
result = subprocess.run(cmd, shell=True)
|
|
15
|
+
|
|
16
|
+
if result.returncode != 0:
|
|
17
|
+
logger.error(f"Команда '{cmd}' завершилась с ошибкой!")
|
|
18
|
+
sys.exit(1)
|
|
19
|
+
|
|
20
|
+
logger.success("Все pre-commit команды выполнены успешно.")
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
import sys
|
|
4
|
+
from .logger import logger
|
|
5
|
+
|
|
6
|
+
SECRET_PATTERNS = [
|
|
7
|
+
re.compile(
|
|
8
|
+
r'(?i)(api[_-]?key|secret|password|token)\s*[:=]\s*["\'][a-zA-Z0-9\-_]{10,}["\']'
|
|
9
|
+
),
|
|
10
|
+
re.compile(r"-----BEGIN (RSA|OPENSSH|PRIVATE) KEY-----"),
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
SUSPICIOUS_FILES = [".env", ".env.local", "secrets.json"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def check_secrets(staged_files: list[str]) -> bool:
|
|
17
|
+
"""
|
|
18
|
+
Проверяет файлы на наличие секретов.
|
|
19
|
+
Возвращает True, если был изменен .gitignore (требуется повторный git add).
|
|
20
|
+
"""
|
|
21
|
+
secrets_found = []
|
|
22
|
+
|
|
23
|
+
for file_path in staged_files:
|
|
24
|
+
if not os.path.exists(file_path):
|
|
25
|
+
continue
|
|
26
|
+
|
|
27
|
+
file_name = os.path.basename(file_path)
|
|
28
|
+
|
|
29
|
+
if file_name in SUSPICIOUS_FILES:
|
|
30
|
+
secrets_found.append(file_path)
|
|
31
|
+
continue
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
35
|
+
content = f.read()
|
|
36
|
+
if any(pattern.search(content) for pattern in SECRET_PATTERNS):
|
|
37
|
+
secrets_found.append(file_path)
|
|
38
|
+
except UnicodeDecodeError:
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
if not secrets_found:
|
|
42
|
+
return False
|
|
43
|
+
|
|
44
|
+
logger.warning("ОБНАРУЖЕНЫ ВОЗМОЖНЫЕ УТЕЧКИ СЕКРЕТОВ!")
|
|
45
|
+
for s in secrets_found:
|
|
46
|
+
logger.warning(f" -> {s}")
|
|
47
|
+
|
|
48
|
+
answer = input("Добавить эти файлы в .gitignore? [y/N]: ").strip().lower()
|
|
49
|
+
if answer == "y":
|
|
50
|
+
with open(".gitignore", "a", encoding="utf-8") as f:
|
|
51
|
+
f.write("\n# Auto-added by Smart Commit\n")
|
|
52
|
+
for s in secrets_found:
|
|
53
|
+
f.write(f"{s}\n")
|
|
54
|
+
|
|
55
|
+
logger.success("Файлы добавлены в .gitignore.")
|
|
56
|
+
return True
|
|
57
|
+
|
|
58
|
+
logger.error("Коммит прерван из-за угрозы безопасности. Очистите секреты вручную.")
|
|
59
|
+
sys.exit(1)
|
|
Binary file
|
|
@@ -1,105 +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 .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()
|
|
@@ -1,41 +0,0 @@
|
|
|
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
|
|
@@ -1,115 +0,0 @@
|
|
|
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
|
-
python-version: ["3.10", "3.11", "3.12"]
|
|
60
|
-
|
|
61
|
-
steps:
|
|
62
|
-
- name: Checkout Repository
|
|
63
|
-
uses: actions/checkout@v4
|
|
64
|
-
|
|
65
|
-
- name: Set up Python ${{ matrix.python-version }}
|
|
66
|
-
uses: actions/setup-python@v5
|
|
67
|
-
with:
|
|
68
|
-
python-version: ${{ matrix.python-version }}
|
|
69
|
-
cache: 'pip'
|
|
70
|
-
|
|
71
|
-
- name: Install Dependencies (Adaptive Smart Installer)
|
|
72
|
-
run: |
|
|
73
|
-
python -m pip install --upgrade pip
|
|
74
|
-
|
|
75
|
-
# Ensure local bin is in PATH for all subsequent steps
|
|
76
|
-
echo "$HOME/.local/bin" >> $GITHUB_PATH
|
|
77
|
-
|
|
78
|
-
echo "🔍 Detecting package manager..."
|
|
79
|
-
|
|
80
|
-
if [ -f "poetry.lock" ] || [ -f "pyproject.toml" ] && grep -q "tool.poetry" "pyproject.toml"; then
|
|
81
|
-
echo "🚀 Poetry detected!"
|
|
82
|
-
pip install poetry
|
|
83
|
-
poetry config virtualenvs.create false
|
|
84
|
-
poetry install --no-interaction --no-ansi
|
|
85
|
-
|
|
86
|
-
elif [ -f "Pipfile.lock" ] || [ -f "Pipfile" ]; then
|
|
87
|
-
echo "🚀 Pipenv detected!"
|
|
88
|
-
pip install pipenv
|
|
89
|
-
pipenv install --system --dev
|
|
90
|
-
|
|
91
|
-
elif [ -f "pdm.lock" ] || [ -f "pyproject.toml" ] && grep -q "tool.pdm" "pyproject.toml"; then
|
|
92
|
-
echo "🚀 PDM detected!"
|
|
93
|
-
pip install pdm
|
|
94
|
-
pdm config python.use_venv false
|
|
95
|
-
pdm install
|
|
96
|
-
|
|
97
|
-
elif [ -f "requirements.txt" ]; then
|
|
98
|
-
echo "🚀 Standard requirements.txt detected!"
|
|
99
|
-
pip install -r requirements.txt
|
|
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!"
|
|
104
|
-
pip install -e .[dev] || pip install -e .
|
|
105
|
-
|
|
106
|
-
else
|
|
107
|
-
echo "⚠️ No known package manager found. Skipping dependency installation."
|
|
108
|
-
fi
|
|
109
|
-
|
|
110
|
-
__COMMANDS__
|
|
111
|
-
"""
|
|
112
|
-
|
|
113
|
-
CONVENTIONAL_COMMIT_PATTERN = (
|
|
114
|
-
r"^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\(.+\))?:\s.+"
|
|
115
|
-
)
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
class SmartCommitError(Exception):
|
|
2
|
-
detail = "Unknown error"
|
|
3
|
-
|
|
4
|
-
def __init__(self, *args):
|
|
5
|
-
message = args[0] if args else self.detail
|
|
6
|
-
super().__init__(message)
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class GitOperationError(SmartCommitError):
|
|
10
|
-
detail = "Git execution failed"
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class ValidationError(SmartCommitError):
|
|
14
|
-
detail = "Validation check failed"
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
from colorama import init, Fore, Style
|
|
3
|
-
|
|
4
|
-
init(autoreset=True)
|
|
5
|
-
|
|
6
|
-
logging.basicConfig(
|
|
7
|
-
level=logging.DEBUG,
|
|
8
|
-
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
9
|
-
)
|
|
10
|
-
logger = logging.getLogger("SmartCommit")
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class Console:
|
|
14
|
-
@staticmethod
|
|
15
|
-
def info(msg: str):
|
|
16
|
-
print(f"{Fore.CYAN}ℹ️ {msg}")
|
|
17
|
-
logger.info(msg)
|
|
18
|
-
|
|
19
|
-
@staticmethod
|
|
20
|
-
def success(msg: str):
|
|
21
|
-
print(f"{Fore.GREEN}{Style.BRIGHT}✅ {msg}")
|
|
22
|
-
logger.info(f"SUCCESS: {msg}")
|
|
23
|
-
|
|
24
|
-
@staticmethod
|
|
25
|
-
def warning(msg: str):
|
|
26
|
-
print(f"{Fore.YELLOW}⚠️ {msg}")
|
|
27
|
-
logger.warning(msg)
|
|
28
|
-
|
|
29
|
-
@staticmethod
|
|
30
|
-
def error(msg: str):
|
|
31
|
-
print(f"{Fore.RED}{Style.BRIGHT}❌ {msg}")
|
|
32
|
-
logger.error(msg)
|
|
@@ -1,66 +0,0 @@
|
|
|
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,129 +0,0 @@
|
|
|
1
|
-
import subprocess
|
|
2
|
-
from pathlib import Path
|
|
3
|
-
from ..exceptions import GitOperationError
|
|
4
|
-
from ..logger import Console, logger
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class GitService:
|
|
8
|
-
@classmethod
|
|
9
|
-
def _run(cls, cmd: list[str], silent: bool = True) -> subprocess.CompletedProcess:
|
|
10
|
-
"""Internal helper for executing Git commands with combined output."""
|
|
11
|
-
logger.debug(f"Git CMD: {' '.join(cmd)}")
|
|
12
|
-
return subprocess.run(cmd, capture_output=True, text=True)
|
|
13
|
-
|
|
14
|
-
@classmethod
|
|
15
|
-
def ensure_repo(cls):
|
|
16
|
-
"""Checks for repository initialization, Detached HEAD, and ongoing merges/rebases."""
|
|
17
|
-
git_dir = Path(".git")
|
|
18
|
-
if not git_dir.exists():
|
|
19
|
-
Console.warning("Git not initialized. Initializing now...")
|
|
20
|
-
cls._run(["git", "init"])
|
|
21
|
-
is_rebase = (git_dir / "rebase-apply").exists() or (
|
|
22
|
-
git_dir / "rebase-merge"
|
|
23
|
-
).exists()
|
|
24
|
-
is_merge = (git_dir / "MERGE_HEAD").exists()
|
|
25
|
-
|
|
26
|
-
if is_rebase or is_merge:
|
|
27
|
-
raise GitOperationError(
|
|
28
|
-
"🛑 Git is currently in the middle of a REBASE or MERGE.\n"
|
|
29
|
-
"Please resolve conflicts manually or run 'git rebase --abort', then try again."
|
|
30
|
-
)
|
|
31
|
-
|
|
32
|
-
res = cls._run(["git", "branch", "--show-current"])
|
|
33
|
-
if not res.stdout.strip():
|
|
34
|
-
raise GitOperationError(
|
|
35
|
-
"You are in a DETACHED HEAD state. Please switch to a branch (e.g., 'git checkout main')."
|
|
36
|
-
)
|
|
37
|
-
|
|
38
|
-
@classmethod
|
|
39
|
-
def get_current_branch(cls) -> str:
|
|
40
|
-
return cls._run(["git", "branch", "--show-current"]).stdout.strip()
|
|
41
|
-
|
|
42
|
-
@classmethod
|
|
43
|
-
def switch_branch(cls, branch: str):
|
|
44
|
-
"""Safely switches to the target branch or creates it."""
|
|
45
|
-
current = cls.get_current_branch()
|
|
46
|
-
if current != branch:
|
|
47
|
-
Console.info(f"🌿 Switching branch: {current} ➔ {branch}")
|
|
48
|
-
has_commits = cls._run(["git", "rev-parse", "HEAD"]).returncode == 0
|
|
49
|
-
|
|
50
|
-
if not has_commits:
|
|
51
|
-
res = cls._run(["git", "branch", "-M", branch])
|
|
52
|
-
else:
|
|
53
|
-
res = cls._run(["git", "checkout", "-B", branch])
|
|
54
|
-
|
|
55
|
-
if res.returncode != 0:
|
|
56
|
-
error_msg = res.stderr.strip() or res.stdout.strip()
|
|
57
|
-
raise GitOperationError(f"Failed to switch branch: {error_msg}")
|
|
58
|
-
|
|
59
|
-
@classmethod
|
|
60
|
-
def has_any_changes(cls) -> bool:
|
|
61
|
-
"""Checks if there are any staged, unstaged, or untracked changes."""
|
|
62
|
-
res = cls._run(["git", "status", "--porcelain"])
|
|
63
|
-
return bool(res.stdout.strip())
|
|
64
|
-
|
|
65
|
-
@classmethod
|
|
66
|
-
def has_staged_changes(cls) -> bool:
|
|
67
|
-
"""Checks if there are files already in the staging area."""
|
|
68
|
-
res = cls._run(["git", "diff", "--cached", "--quiet"])
|
|
69
|
-
return res.returncode != 0
|
|
70
|
-
|
|
71
|
-
@classmethod
|
|
72
|
-
def smart_stage(cls):
|
|
73
|
-
"""Intelligent file staging."""
|
|
74
|
-
if cls.has_staged_changes():
|
|
75
|
-
Console.info("Staged files detected. Committing current index only.")
|
|
76
|
-
else:
|
|
77
|
-
Console.warning("No files in the staging area.")
|
|
78
|
-
ans = (
|
|
79
|
-
input("Would you like to stage all changes (git add .)? [y/N]: ")
|
|
80
|
-
.strip()
|
|
81
|
-
.lower()
|
|
82
|
-
)
|
|
83
|
-
if ans == "y":
|
|
84
|
-
cls._run(["git", "add", "."])
|
|
85
|
-
else:
|
|
86
|
-
raise GitOperationError("Nothing to commit. Operation cancelled.")
|
|
87
|
-
|
|
88
|
-
@classmethod
|
|
89
|
-
def commit(cls, message: str):
|
|
90
|
-
"""Commits changes. Ignores 'nothing to commit' errors to allow push-only flows."""
|
|
91
|
-
res = cls._run(["git", "commit", "-m", message])
|
|
92
|
-
|
|
93
|
-
if res.returncode != 0:
|
|
94
|
-
output = (res.stdout + res.stderr).lower()
|
|
95
|
-
if "nothing to commit" in output or "working tree clean" in output:
|
|
96
|
-
return
|
|
97
|
-
|
|
98
|
-
raise GitOperationError(
|
|
99
|
-
f"Commit failed. Git says:\n{res.stderr or res.stdout}"
|
|
100
|
-
)
|
|
101
|
-
|
|
102
|
-
@classmethod
|
|
103
|
-
def push_with_retry(cls, branch: str):
|
|
104
|
-
"""Pushes to remote, attempting a pull --rebase if rejected or diverged."""
|
|
105
|
-
Console.info(f"Pushing to branch: {branch}...")
|
|
106
|
-
res = cls._run(["git", "push", "-u", "origin", branch])
|
|
107
|
-
|
|
108
|
-
if res.returncode == 0:
|
|
109
|
-
Console.success(f"Code successfully pushed to origin/{branch}!")
|
|
110
|
-
return
|
|
111
|
-
Console.warning("Push rejected or branches diverged. Remote changes detected.")
|
|
112
|
-
Console.info("Synchronizing via pull --rebase...")
|
|
113
|
-
|
|
114
|
-
pull_res = cls._run(["git", "pull", "origin", branch, "--rebase"])
|
|
115
|
-
|
|
116
|
-
if pull_res.returncode != 0:
|
|
117
|
-
error_msg = pull_res.stderr.strip() or pull_res.stdout.strip()
|
|
118
|
-
cls._run(["git", "rebase", "--abort"])
|
|
119
|
-
raise GitOperationError(
|
|
120
|
-
f"🛑 Rebase conflict! Conflicts were detected with remote files.\n"
|
|
121
|
-
f"Sync aborted. Please resolve manually. Git says:\n{error_msg}"
|
|
122
|
-
)
|
|
123
|
-
|
|
124
|
-
Console.success("Sync successful. Retrying final push...")
|
|
125
|
-
push_retry = cls._run(["git", "push", "-u", "origin", branch])
|
|
126
|
-
|
|
127
|
-
if push_retry.returncode != 0:
|
|
128
|
-
error_msg = push_retry.stderr.strip() or push_retry.stdout.strip()
|
|
129
|
-
raise GitOperationError(f"Final push failed after rebase:\n{error_msg}")
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import subprocess
|
|
2
|
-
from pathlib import Path
|
|
3
|
-
from ..logger import Console, logger
|
|
4
|
-
from ..constants import GITIGNORE_FILE, BASE_GITIGNORE, EXCLUDED_SCAN_DIRS
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class SecurityService:
|
|
8
|
-
@classmethod
|
|
9
|
-
def ensure_gitignore(cls):
|
|
10
|
-
"""Ensures .gitignore exists or creates a secure default."""
|
|
11
|
-
if not GITIGNORE_FILE.exists():
|
|
12
|
-
Console.warning(
|
|
13
|
-
f"{GITIGNORE_FILE.name} not found. Creating a secure default configuration..."
|
|
14
|
-
)
|
|
15
|
-
with open(GITIGNORE_FILE, "w", encoding="utf-8") as f:
|
|
16
|
-
f.write(BASE_GITIGNORE.lstrip())
|
|
17
|
-
logger.info("Created default .gitignore")
|
|
18
|
-
|
|
19
|
-
@classmethod
|
|
20
|
-
def check_env_leaks(cls):
|
|
21
|
-
"""
|
|
22
|
-
Scans for .env files (e.g. .env.local, .test.env, prod.env)
|
|
23
|
-
and uses Git to check if they are ignored.
|
|
24
|
-
Prevents accidental leaks of secrets.
|
|
25
|
-
"""
|
|
26
|
-
Console.info("🛡️ Scanning for potential secret leaks (.env files)...")
|
|
27
|
-
|
|
28
|
-
raw_files = set(Path(".").rglob(".env*")) | set(Path(".").rglob("*.env"))
|
|
29
|
-
|
|
30
|
-
env_files = [
|
|
31
|
-
f
|
|
32
|
-
for f in raw_files
|
|
33
|
-
if f.is_file() and not any(part in EXCLUDED_SCAN_DIRS for part in f.parts)
|
|
34
|
-
]
|
|
35
|
-
|
|
36
|
-
leaked_files = []
|
|
37
|
-
|
|
38
|
-
for env_file in env_files:
|
|
39
|
-
res = subprocess.run(
|
|
40
|
-
["git", "check-ignore", "-q", str(env_file)], capture_output=True
|
|
41
|
-
)
|
|
42
|
-
if res.returncode == 1:
|
|
43
|
-
leaked_files.append(str(env_file))
|
|
44
|
-
|
|
45
|
-
if leaked_files:
|
|
46
|
-
Console.error(
|
|
47
|
-
f"CRITICAL SECURITY RISK: .env files found that are NOT in {GITIGNORE_FILE.name}!"
|
|
48
|
-
)
|
|
49
|
-
for lf in leaked_files:
|
|
50
|
-
print(f" 👉 {lf}")
|
|
51
|
-
|
|
52
|
-
ans = (
|
|
53
|
-
input(
|
|
54
|
-
f"🛑 Add these files to {GITIGNORE_FILE.name} automatically? [Y/n]: "
|
|
55
|
-
)
|
|
56
|
-
.strip()
|
|
57
|
-
.lower()
|
|
58
|
-
)
|
|
59
|
-
if ans != "n":
|
|
60
|
-
with open(GITIGNORE_FILE, "a", encoding="utf-8") as f:
|
|
61
|
-
f.write("\n# Auto-added by Smart Commit to prevent leaks\n")
|
|
62
|
-
for lf in leaked_files:
|
|
63
|
-
f.write(f"{lf}\n")
|
|
64
|
-
Console.success(f"Files successfully added to {GITIGNORE_FILE.name}.")
|
|
65
|
-
else:
|
|
66
|
-
Console.warning(
|
|
67
|
-
"Action declined. Please be careful: secrets might be pushed!"
|
|
68
|
-
)
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import subprocess
|
|
2
|
-
import re
|
|
3
|
-
from ..exceptions import ValidationError
|
|
4
|
-
from ..logger import Console, logger
|
|
5
|
-
from ..constants import CONVENTIONAL_COMMIT_PATTERN
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class ValidatorService:
|
|
9
|
-
@classmethod
|
|
10
|
-
def check_conventional_commit(cls, message: str):
|
|
11
|
-
"""Validates message against Conventional Commits standard."""
|
|
12
|
-
if not re.match(CONVENTIONAL_COMMIT_PATTERN, message):
|
|
13
|
-
logger.warning(f"Non-conventional commit message: {message}")
|
|
14
|
-
Console.warning(
|
|
15
|
-
"Commit message does not follow Conventional Commits (e.g., 'feat: add auth')."
|
|
16
|
-
)
|
|
17
|
-
|
|
18
|
-
@classmethod
|
|
19
|
-
def run_commands(cls, commands: list[str]):
|
|
20
|
-
"""Runs pre-commit pipeline commands (linters, tests)."""
|
|
21
|
-
for cmd in commands:
|
|
22
|
-
Console.info(f"Executing: {cmd}")
|
|
23
|
-
logger.info(f"Running validation command: {cmd}")
|
|
24
|
-
res = subprocess.run(cmd, shell=True)
|
|
25
|
-
if res.returncode != 0:
|
|
26
|
-
raise ValidationError(
|
|
27
|
-
f"Pipeline failed at: '{cmd}'. Please fix the issues!"
|
|
28
|
-
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|