smart-commit-tool 3.0.2__tar.gz → 3.0.3__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: smart-commit-tool
3
- Version: 3.0.2
3
+ Version: 3.0.3
4
4
  Summary: Automated pre-push workflow manager with built-in code quality enforcement and smart branch protection.
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.11
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "smart-commit-tool"
7
- version = "3.0.2"
7
+ version = "3.0.3"
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"
@@ -17,9 +17,10 @@ dependencies = [
17
17
  smart-commit = "smart_commit.cli:main"
18
18
 
19
19
  [tool.smart_commit]
20
+ language = "ru"
20
21
  repository_url = "https://github.com/mokinprokin/SmartCommit.git"
21
22
  commands = ["ruff check ."]
22
- protected_branches = ["main", "master", "prod", "release"]
23
+ protected_branches = ["master", "prod", "release"]
23
24
 
24
25
  [tool.hatch.build.targets.wheel]
25
26
  packages = ["src/smart_commit"]
@@ -0,0 +1,76 @@
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
+ from .services.i18n import i18n
11
+
12
+
13
+ def interactive_input(prompt: str, default: str = "") -> str:
14
+ """Запрашивает ввод у пользователя через i18n."""
15
+ suffix = f" [{default}]" if default else ""
16
+ value = input(f"{prompt}{suffix}: ").strip()
17
+ return value if value else default
18
+
19
+
20
+ def main():
21
+ # 1. Сначала загружаем конфиг, чтобы узнать язык
22
+ config = config_service.load_config()
23
+
24
+ # 2. Настройка парсера аргументов
25
+ parser = argparse.ArgumentParser(description="Smart Git Commit Tool")
26
+ parser.add_argument("-b", "--branch", help="Имя ветки")
27
+ parser.add_argument("-m", "--message", help="Сообщение коммита")
28
+ parser.add_argument("-r", "--remote", help="Remote (origin)")
29
+
30
+ args = parser.parse_args()
31
+
32
+ # 3. Начало работы
33
+ logger.info(i18n.t("init"))
34
+
35
+ # Сбор параметров (Fallback на интерактивный ввод с переводами)
36
+ branch = args.branch or interactive_input(i18n.t("branch_prompt"))
37
+ message = args.message or interactive_input(i18n.t("msg_prompt"))
38
+ remote = args.remote or interactive_input(i18n.t("remote_prompt"), default="origin")
39
+
40
+ if not branch or not message:
41
+ # Используем новый ключ для ошибки валидации
42
+ logger.error(i18n.t("err_required"))
43
+ sys.exit(1)
44
+
45
+ # 4. Валидация и подготовка ветки
46
+ # Передаем список защищенных веток в сервис
47
+ git_service.check_protected(branch, config.get("protected_branches", []))
48
+ git_service.ensure_branch(branch)
49
+
50
+ # 5. Индексация файлов
51
+ git_service.add_all()
52
+
53
+ # 6. Проверка изменений и безопасность
54
+ staged_files = git_service.get_staged_files()
55
+ if not staged_files:
56
+ logger.warning(i18n.t("no_changes"))
57
+ sys.exit(0)
58
+
59
+ # Сканирование на секреты
60
+ if security_service.check_secrets(staged_files):
61
+ # Если секреты найдены и добавлены в .gitignore, пересобираем индекс
62
+ git_service.run_cmd(["git", "rm", "-r", "--cached", "."])
63
+ git_service.add_all()
64
+
65
+ # 7. Запуск кастомных команд из pyproject.toml (например, ruff)
66
+ runner_service.run_pre_commands(config.get("commands", []))
67
+
68
+ # 8. Финализация
69
+ git_service.commit(message)
70
+ git_service.push(remote, branch)
71
+
72
+ logger.success(i18n.t("success"))
73
+
74
+
75
+ if __name__ == "__main__":
76
+ main()
@@ -1,6 +1,7 @@
1
1
  import os
2
2
  from pathlib import Path
3
3
  from .logger import logger
4
+ from .i18n import i18n
4
5
 
5
6
  try:
6
7
  import tomllib
@@ -13,19 +14,25 @@ def load_config() -> dict:
13
14
  config_path = Path(os.getcwd()) / "pyproject.toml"
14
15
 
15
16
  default_config = {
17
+ "language": "en",
16
18
  "repository_url": "",
17
19
  "commands": [],
18
- "protected_branches": ["main", "master"],
20
+ "protected_branches": ["main", "master", "prod", "release"],
19
21
  }
20
22
 
21
23
  if not config_path.exists():
22
- logger.warning("pyproject.toml не найден. Используются настройки по умолчанию.")
24
+ i18n.lang = default_config["language"]
25
+ logger.warning(i18n.t("config_not_found"))
23
26
  return default_config
24
27
 
25
28
  try:
26
29
  with open(config_path, "rb") as f:
27
30
  data = tomllib.load(f)
28
- return data.get("tool", {}).get("smart_commit", default_config)
31
+ config = data.get("tool", {}).get("smart_commit", default_config)
32
+
33
+ i18n.lang = config.get("language", "en")
34
+ return config
29
35
  except Exception as e:
30
- logger.error(f"Ошибка чтения конфига: {e}")
36
+ i18n.lang = "en"
37
+ logger.error(i18n.t("config_read_err", e=e))
31
38
  return default_config
@@ -1,13 +1,15 @@
1
1
  import subprocess
2
2
  import sys
3
3
  from .logger import logger
4
+ from .i18n import i18n
4
5
 
5
6
 
6
7
  def run_cmd(cmd: list[str], check=True) -> str:
7
8
  """Выполняет bash-команду и возвращает вывод."""
8
9
  result = subprocess.run(cmd, capture_output=True, text=True)
9
10
  if check and result.returncode != 0:
10
- logger.error(f"Ошибка Git: {result.stderr.strip()}")
11
+ error_msg = result.stderr.strip()
12
+ logger.error(i18n.t("git_error", error=error_msg))
11
13
  sys.exit(1)
12
14
  return result.stdout.strip()
13
15
 
@@ -15,7 +17,7 @@ def run_cmd(cmd: list[str], check=True) -> str:
15
17
  def check_protected(branch: str, protected_branches: list[str]):
16
18
  """Блокирует пуш в защищенные ветки."""
17
19
  if branch in protected_branches:
18
- logger.error(f"Прямой коммит в защищенную ветку '{branch}' запрещен!")
20
+ logger.error(i18n.t("protected_err", branch=branch))
19
21
  sys.exit(1)
20
22
 
21
23
 
@@ -23,15 +25,15 @@ def ensure_branch(branch: str):
23
25
  """Проверяет наличие ветки. Если нет - создает."""
24
26
  result = subprocess.run(["git", "checkout", branch], capture_output=True, text=True)
25
27
  if result.returncode != 0:
26
- logger.info(f"Ветка '{branch}' не найдена. Создаю новую...")
28
+ logger.info(i18n.t("branch_created", branch=branch))
27
29
  run_cmd(["git", "checkout", "-b", branch])
28
30
  else:
29
- logger.info(f"Переключено на ветку '{branch}'.")
31
+ logger.info(i18n.t("branch_switched", branch=branch))
30
32
 
31
33
 
32
34
  def add_all():
33
35
  run_cmd(["git", "add", "."])
34
- logger.info("Файлы добавлены в индекс (git add).")
36
+ logger.info(i18n.t("git_add"))
35
37
 
36
38
 
37
39
  def get_staged_files() -> list[str]:
@@ -41,13 +43,11 @@ def get_staged_files() -> list[str]:
41
43
 
42
44
 
43
45
  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}'")
46
+ run_cmd(["git", "commit", "-m", message], check=False)
47
+ logger.info(i18n.t("commit_created", message=message))
48
48
 
49
49
 
50
50
  def push(remote: str, branch: str):
51
- logger.info(f"Отправка изменений в {remote}/{branch}...")
51
+ logger.info(i18n.t("push_start", remote=remote, branch=branch))
52
52
  run_cmd(["git", "push", "-u", remote, branch])
53
- logger.success("Push успешно завершен!")
53
+ logger.success(i18n.t("push_success"))
@@ -0,0 +1,62 @@
1
+ class I18nService:
2
+ LOCALES = {
3
+ "en": {
4
+ "init": "Initializing Smart Commit...",
5
+ "branch_prompt": "Enter branch name",
6
+ "msg_prompt": "Enter commit message",
7
+ "remote_prompt": "Enter remote",
8
+ "err_required": "Branch and commit message are required!",
9
+ "protected_err": "Direct commit to protected branch '{branch}' is forbidden!",
10
+ "branch_created": "Branch '{branch}' not found. Creating new one...",
11
+ "git_add": "Files added to index (git add).",
12
+ "no_changes": "No changes to commit.",
13
+ "secret_warn": "POSSIBLE SECRET LEAKS DETECTED!",
14
+ "gitignore_ask": "Add these files to .gitignore? [y/N]: ",
15
+ "pre_cmd_start": "Running pre-commit commands...",
16
+ "push_start": "Pushing changes to {remote}/{branch}...",
17
+ "success": "Smart Commit finished successfully!",
18
+ "git_error": "Git Error: {error}",
19
+ "branch_switched": "Switched to branch '{branch}'.",
20
+ "commit_created": "Commit created: '{message}'",
21
+ "push_success": "Push completed successfully!",
22
+ "pre_cmd_start": "Running pre-commit commands...",
23
+ "pre_cmd_exec": "Executing: {cmd}",
24
+ "pre_cmd_err": "Command '{cmd}' failed with error!",
25
+ "pre_cmd_success": "All pre-commit commands executed successfully.",
26
+ },
27
+ "ru": {
28
+ "init": "Инициализация Smart Commit...",
29
+ "branch_prompt": "Введите имя ветки",
30
+ "msg_prompt": "Введите сообщение коммита",
31
+ "remote_prompt": "Введите remote",
32
+ "err_required": "Ветка и сообщение коммита обязательны!",
33
+ "protected_err": "Прямой коммит в защищенную ветку '{branch}' запрещен!",
34
+ "branch_created": "Ветка '{branch}' не найдена. Создаю новую...",
35
+ "git_add": "Файлы добавлены в индекс (git add).",
36
+ "no_changes": "Нет изменений для коммита.",
37
+ "secret_warn": "ОБНАРУЖЕНЫ ВОЗМОЖНЫЕ УТЕЧКИ СЕКРЕТОВ!",
38
+ "gitignore_ask": "Добавить эти файлы в .gitignore? [y/N]: ",
39
+ "pre_cmd_start": "Запуск pre-commit команд...",
40
+ "push_start": "Отправка изменений в {remote}/{branch}...",
41
+ "success": "Smart Commit успешно завершил работу!",
42
+ "git_error": "Ошибка Git: {error}",
43
+ "branch_switched": "Переключено на ветку '{branch}'.",
44
+ "commit_created": "Коммит создан: '{message}'",
45
+ "push_success": "Push успешно завершен!",
46
+ "pre_cmd_start": "Запуск pre-commit команд...",
47
+ "pre_cmd_exec": "Выполнение: {cmd}",
48
+ "pre_cmd_err": "Команда '{cmd}' завершилась с ошибкой!",
49
+ "pre_cmd_success": "Все pre-commit команды выполнены успешно.",
50
+ },
51
+ }
52
+
53
+ def __init__(self, lang="en"):
54
+ self.lang = lang if lang in self.LOCALES else "en"
55
+
56
+ def t(self, key, **kwargs):
57
+ """Translate method"""
58
+ text = self.LOCALES[self.lang].get(key, key)
59
+ return text.format(**kwargs)
60
+
61
+
62
+ i18n = I18nService()
@@ -1,6 +1,7 @@
1
1
  import subprocess
2
2
  import sys
3
3
  from .logger import logger
4
+ from .i18n import i18n
4
5
 
5
6
 
6
7
  def run_pre_commands(commands: list[str]):
@@ -8,13 +9,13 @@ def run_pre_commands(commands: list[str]):
8
9
  if not commands:
9
10
  return
10
11
 
11
- logger.info("Запуск pre-commit команд...")
12
+ logger.info(i18n.t("pre_cmd_start"))
12
13
  for cmd in commands:
13
- logger.info(f"Выполнение: {cmd}")
14
+ logger.info(i18n.t("pre_cmd_exec", cmd=cmd))
14
15
  result = subprocess.run(cmd, shell=True)
15
16
 
16
17
  if result.returncode != 0:
17
- logger.error(f"Команда '{cmd}' завершилась с ошибкой!")
18
+ logger.error(i18n.t("pre_cmd_err", cmd=cmd))
18
19
  sys.exit(1)
19
20
 
20
- logger.success("Все pre-commit команды выполнены успешно.")
21
+ logger.success(i18n.t("pre_cmd_success"))
@@ -2,6 +2,7 @@ import os
2
2
  import re
3
3
  import sys
4
4
  from .logger import logger
5
+ from .i18n import i18n
5
6
 
6
7
  SECRET_PATTERNS = [
7
8
  re.compile(
@@ -10,7 +11,14 @@ SECRET_PATTERNS = [
10
11
  re.compile(r"-----BEGIN (RSA|OPENSSH|PRIVATE) KEY-----"),
11
12
  ]
12
13
 
13
- SUSPICIOUS_FILES = [".env", ".env.local", "secrets.json"]
14
+ SUSPICIOUS_FILES = [
15
+ ".env",
16
+ ".env.local",
17
+ "secrets.json",
18
+ ".env.test",
19
+ ".test.env",
20
+ "credentials.json",
21
+ ]
14
22
 
15
23
 
16
24
  def check_secrets(staged_files: list[str]) -> bool:
@@ -41,19 +49,20 @@ def check_secrets(staged_files: list[str]) -> bool:
41
49
  if not secrets_found:
42
50
  return False
43
51
 
44
- logger.warning("ОБНАРУЖЕНЫ ВОЗМОЖНЫЕ УТЕЧКИ СЕКРЕТОВ!")
52
+ logger.warning(i18n.t("secret_warn"))
45
53
  for s in secrets_found:
46
54
  logger.warning(f" -> {s}")
47
55
 
48
- answer = input("Добавить эти файлы в .gitignore? [y/N]: ").strip().lower()
56
+ answer = input(i18n.t("gitignore_ask")).strip().lower()
49
57
  if answer == "y":
50
58
  with open(".gitignore", "a", encoding="utf-8") as f:
51
- f.write("\n# Auto-added by Smart Commit\n")
59
+ comment = i18n.t("gitignore_comment")
60
+ f.write(f"\n# {comment}\n")
52
61
  for s in secrets_found:
53
62
  f.write(f"{s}\n")
54
63
 
55
- logger.success("Файлы добавлены в .gitignore.")
64
+ logger.success(i18n.t("gitignore_success"))
56
65
  return True
57
66
 
58
- logger.error("Коммит прерван из-за угрозы безопасности. Очистите секреты вручную.")
67
+ logger.error(i18n.t("security_abort"))
59
68
  sys.exit(1)
@@ -1,70 +0,0 @@
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()