smart-commit-tool 0.1.0__tar.gz → 1.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.0.1/LICENSE +21 -0
- smart_commit_tool-1.0.1/PKG-INFO +91 -0
- smart_commit_tool-1.0.1/README.md +81 -0
- smart_commit_tool-1.0.1/README.ru.md +80 -0
- smart_commit_tool-1.0.1/assets/demonstration.gif +0 -0
- {smart_commit_tool-0.1.0 → smart_commit_tool-1.0.1}/pyproject.toml +2 -2
- smart_commit_tool-1.0.1/src/smart_commit/cli.py +65 -0
- smart_commit_tool-1.0.1/src/smart_commit/config.py +50 -0
- smart_commit_tool-1.0.1/src/smart_commit/exceptions.py +14 -0
- smart_commit_tool-1.0.1/src/smart_commit/logger.py +32 -0
- smart_commit_tool-1.0.1/src/smart_commit/services/git.py +104 -0
- smart_commit_tool-1.0.1/src/smart_commit/services/security.py +86 -0
- smart_commit_tool-1.0.1/src/smart_commit/services/validator.py +28 -0
- smart_commit_tool-0.1.0/PKG-INFO +0 -78
- smart_commit_tool-0.1.0/README.md +0 -69
- smart_commit_tool-0.1.0/src/smart_commit/__init__.py +0 -0
- smart_commit_tool-0.1.0/src/smart_commit/cli.py +0 -214
- {smart_commit_tool-0.1.0 → smart_commit_tool-1.0.1}/.gitignore +0 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Makar Pavlovich
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: smart-commit-tool
|
|
3
|
+
Version: 1.0.1
|
|
4
|
+
Summary: Automated pre-push workflow manager with built-in code quality enforcement and smart branch protection.
|
|
5
|
+
License-File: LICENSE
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Requires-Dist: colorama
|
|
8
|
+
Requires-Dist: tomli; python_version < '3.11'
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# 🚀 Smart Commit
|
|
12
|
+
|
|
13
|
+
[English](README.md) | [Русский](README.ru.md)
|
|
14
|
+
|
|
15
|
+
**Smart Commit** is a universal CLI tool designed to streamline and secure your Git workflow. It orchestrates linting, testing, committing, and pushing into a single, bulletproof operation.
|
|
16
|
+
|
|
17
|
+
**The Golden Rule:** If your tests fail, your code doesn't ship.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## ✨ Key Features
|
|
22
|
+
|
|
23
|
+
* 🌍 **Language Agnostic**: Works seamlessly with Python, JS, Go, Rust, Dart, or any other stack.
|
|
24
|
+
* ⚡ **All-in-One Command**: Replaces the tedious `lint -> test -> add -> commit -> push` routine.
|
|
25
|
+
* 🛡️ **Branch Protection**: Safeguards your `main`, `master`, or `prod` branches from accidental direct pushes.
|
|
26
|
+
* 🔧 **Zero-Setup**: Install via `pip` and configure in under a minute using a single file.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 📦 Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install smart-commit-tool
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
> **Note:** Once installed, the tool is available via the simple and concise `smart-commit` command.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## 🔧 Configuration (`pyproject.toml`)
|
|
42
|
+
|
|
43
|
+
Smart Commit leverages `pyproject.toml` for its configuration. Simply create this file in your project root (even if you aren't using Python for your main project).
|
|
44
|
+
|
|
45
|
+
```toml
|
|
46
|
+
[tool.smart_commit]
|
|
47
|
+
repository_url = "https://github.com/youruser/yourrepo.git"
|
|
48
|
+
protected_branches = ["main", "master", "prod"]
|
|
49
|
+
|
|
50
|
+
# List any shell commands you want to run BEFORE the push.
|
|
51
|
+
# If any command returns a non-zero exit code, the process aborts.
|
|
52
|
+
commands = [
|
|
53
|
+
"ruff check .", # Python Linter
|
|
54
|
+
"pytest -v", # Python Tests
|
|
55
|
+
"npm run lint", # Node.js Linter
|
|
56
|
+
"go test ./..." # Go Tests
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## 🚀 Usage
|
|
64
|
+
|
|
65
|
+
Navigate to your project root (where your `pyproject.toml` is located) and run:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
smart-commit
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### CLI Arguments
|
|
73
|
+
|
|
74
|
+
| Flag | Description |
|
|
75
|
+
| --- | --- |
|
|
76
|
+
| `-b, --branch` | Specify target branch (skips interactive prompt) |
|
|
77
|
+
| `-m, --message` | Set commit message (skips interactive prompt) |
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## 🔄 How It Works (The Algorithm)
|
|
82
|
+
|
|
83
|
+
1. **Environment Sanity Check**: Verifies Git initialization and remote repository connectivity.
|
|
84
|
+
2. **Branch Guard**: If you attempt to push to a protected branch, the tool prompts you to create a new feature branch instead.
|
|
85
|
+
3. **Pre-push Validation**: Sequentially executes your defined `commands`. If any step fails, the entire process stops immediately to keep your remote clean.
|
|
86
|
+
4. **Optimized Push**: Automatically handles `add` and `commit`. If the remote branch has moved forward, it helps you `rebase` to ensure a linear, clean history.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## 📄 License
|
|
91
|
+
Distributed under the MIT License. Feel free to use, modify, and share!
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# 🚀 Smart Commit
|
|
2
|
+
|
|
3
|
+
[English](README.md) | [Русский](README.ru.md)
|
|
4
|
+
|
|
5
|
+
**Smart Commit** is a universal CLI tool designed to streamline and secure your Git workflow. It orchestrates linting, testing, committing, and pushing into a single, bulletproof operation.
|
|
6
|
+
|
|
7
|
+
**The Golden Rule:** If your tests fail, your code doesn't ship.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## ✨ Key Features
|
|
12
|
+
|
|
13
|
+
* 🌍 **Language Agnostic**: Works seamlessly with Python, JS, Go, Rust, Dart, or any other stack.
|
|
14
|
+
* ⚡ **All-in-One Command**: Replaces the tedious `lint -> test -> add -> commit -> push` routine.
|
|
15
|
+
* 🛡️ **Branch Protection**: Safeguards your `main`, `master`, or `prod` branches from accidental direct pushes.
|
|
16
|
+
* 🔧 **Zero-Setup**: Install via `pip` and configure in under a minute using a single file.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 📦 Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install smart-commit-tool
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
> **Note:** Once installed, the tool is available via the simple and concise `smart-commit` command.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## 🔧 Configuration (`pyproject.toml`)
|
|
32
|
+
|
|
33
|
+
Smart Commit leverages `pyproject.toml` for its configuration. Simply create this file in your project root (even if you aren't using Python for your main project).
|
|
34
|
+
|
|
35
|
+
```toml
|
|
36
|
+
[tool.smart_commit]
|
|
37
|
+
repository_url = "https://github.com/youruser/yourrepo.git"
|
|
38
|
+
protected_branches = ["main", "master", "prod"]
|
|
39
|
+
|
|
40
|
+
# List any shell commands you want to run BEFORE the push.
|
|
41
|
+
# If any command returns a non-zero exit code, the process aborts.
|
|
42
|
+
commands = [
|
|
43
|
+
"ruff check .", # Python Linter
|
|
44
|
+
"pytest -v", # Python Tests
|
|
45
|
+
"npm run lint", # Node.js Linter
|
|
46
|
+
"go test ./..." # Go Tests
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## 🚀 Usage
|
|
54
|
+
|
|
55
|
+
Navigate to your project root (where your `pyproject.toml` is located) and run:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
smart-commit
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### CLI Arguments
|
|
63
|
+
|
|
64
|
+
| Flag | Description |
|
|
65
|
+
| --- | --- |
|
|
66
|
+
| `-b, --branch` | Specify target branch (skips interactive prompt) |
|
|
67
|
+
| `-m, --message` | Set commit message (skips interactive prompt) |
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## 🔄 How It Works (The Algorithm)
|
|
72
|
+
|
|
73
|
+
1. **Environment Sanity Check**: Verifies Git initialization and remote repository connectivity.
|
|
74
|
+
2. **Branch Guard**: If you attempt to push to a protected branch, the tool prompts you to create a new feature branch instead.
|
|
75
|
+
3. **Pre-push Validation**: Sequentially executes your defined `commands`. If any step fails, the entire process stops immediately to keep your remote clean.
|
|
76
|
+
4. **Optimized Push**: Automatically handles `add` and `commit`. If the remote branch has moved forward, it helps you `rebase` to ensure a linear, clean history.
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## 📄 License
|
|
81
|
+
Distributed under the MIT License. Feel free to use, modify, and share!
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# 🚀 Smart Commit
|
|
2
|
+
[English](README.md) | [Русский](README.ru.md)
|
|
3
|
+
|
|
4
|
+
**Smart Commit** — это универсальный CLI-инструмент для автоматизации Git-воркфлоу. Он объединяет проверки, тесты, коммит и пуш в одну безопасную операцию.
|
|
5
|
+
|
|
6
|
+
**Главный принцип:** если тесты не прошли — код не улетит в репозиторий.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## ✨ Почему это удобно?
|
|
11
|
+
|
|
12
|
+
* 🌍 **Языковой агностик**: Работает с Python, JS, Go, Rust, Dart и любым другим стеком.
|
|
13
|
+
* ⚡ **Всё в одной команде**: Заменяет рутину `lint -> test -> add -> commit -> push`.
|
|
14
|
+
* 🛡️ **Защита веток**: Больше никаких случайных пушей в `main` или `prod`.
|
|
15
|
+
* 🔧 **Zero-Setup**: Устанавливается через `pip`, настраивается за 1 минуту в одном файле.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 📦 Установка
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install smart-commit-tool
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
> **Важно:** После установки вам доступна короткая и удобная команда `smart-commit`.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 🔧 Настройка (pyproject.toml)
|
|
31
|
+
|
|
32
|
+
Инструмент использует файл `pyproject.toml` для конфигурации. Создайте его в корне вашего проекта (даже если вы не пишете на Python).
|
|
33
|
+
|
|
34
|
+
```toml
|
|
35
|
+
[tool.smart_commit]
|
|
36
|
+
repository_url = "https://github.com/youruser/yourrepo.git"
|
|
37
|
+
protected_branches = ["main", "master", "prod"]
|
|
38
|
+
|
|
39
|
+
# Здесь вы просто перечисляете через запятую любые консольные команды,
|
|
40
|
+
# которые должны успешно выполниться ПЕРЕД тем, как код уйдет в Git.
|
|
41
|
+
commands = [
|
|
42
|
+
"ruff check .", # Линтер
|
|
43
|
+
"pytest -v", # Тесты
|
|
44
|
+
"npm run lint", # Можно запускать команды любого языка!
|
|
45
|
+
"go test ./..." # Инструмент универсален
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## 🚀 Как пользоваться
|
|
53
|
+
|
|
54
|
+
Перейдите в папку проекта, где лежит ваш `pyproject.toml`, и запустите:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
smart-commit
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Параметры запуска
|
|
62
|
+
|
|
63
|
+
| Флаг | Описание |
|
|
64
|
+
| --- | --- |
|
|
65
|
+
| `-b, --branch` | Сразу указать ветку (пропустит интерактивный выбор) |
|
|
66
|
+
| `-m, --message` | Сразу указать сообщение коммита |
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## 🔄 Как это работает (Алгоритм)
|
|
71
|
+
|
|
72
|
+
1. **Проверка окружения**: Инструмент проверяет наличие Git и связь с удаленным репозиторием.
|
|
73
|
+
2. **Branch Guard**: Если вы пытаетесь пушить в защищенную ветку (`main`), скрипт предложит создать новую ветку.
|
|
74
|
+
3. **Валидация (Pre-push)**: Последовательно запускаются ваши `commands`. Если любая из них вернет ошибку — процесс мгновенно прерывается.
|
|
75
|
+
4. **Умный Push**: Инструмент сам сделает `add`, `commit` и отправит код. Если на сервере появились новые изменения, он предложит сделать `rebase`, чтобы история оставалась чистой.
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## 📄 Лицензия
|
|
80
|
+
Распространяется под лицензией **MIT**. Пользуйтесь, меняйте и делитесь с друзьями!
|
|
Binary file
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "smart-commit-tool"
|
|
7
|
-
version = "0.1
|
|
7
|
+
version = "1.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"
|
|
@@ -17,7 +17,7 @@ dependencies = [
|
|
|
17
17
|
smart-commit = "smart_commit.cli:main"
|
|
18
18
|
|
|
19
19
|
[tool.smart_commit]
|
|
20
|
-
repository_url = "https://github.com/mokinprokin/
|
|
20
|
+
repository_url = "https://github.com/mokinprokin/SmartCommit.git"
|
|
21
21
|
commands = ["ruff check ."]
|
|
22
22
|
protected_branches = ["main", "master", "prod", "release"]
|
|
23
23
|
|
|
@@ -0,0 +1,65 @@
|
|
|
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()
|
|
@@ -0,0 +1,50 @@
|
|
|
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
|
|
@@ -0,0 +1,14 @@
|
|
|
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"
|
|
@@ -0,0 +1,32 @@
|
|
|
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)
|
|
@@ -0,0 +1,104 @@
|
|
|
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."""
|
|
11
|
+
logger.debug(f"Git CMD: {' '.join(cmd)}")
|
|
12
|
+
return subprocess.run(cmd, capture_output=silent, text=True)
|
|
13
|
+
|
|
14
|
+
@classmethod
|
|
15
|
+
def ensure_repo(cls):
|
|
16
|
+
"""Checks for repository initialization and Detached HEAD state."""
|
|
17
|
+
if not Path(".git").exists():
|
|
18
|
+
Console.warning("Git not initialized. Initializing now...")
|
|
19
|
+
cls._run(["git", "init"])
|
|
20
|
+
|
|
21
|
+
res = cls._run(["git", "branch", "--show-current"])
|
|
22
|
+
if not res.stdout.strip():
|
|
23
|
+
raise GitOperationError(
|
|
24
|
+
"You are in a DETACHED HEAD state. Process stopped for safety."
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def get_current_branch(cls) -> str:
|
|
29
|
+
return cls._run(["git", "branch", "--show-current"]).stdout.strip()
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
def switch_branch(cls, branch: str):
|
|
33
|
+
"""Safely switches to the target branch or creates it."""
|
|
34
|
+
current = cls.get_current_branch()
|
|
35
|
+
if current != branch:
|
|
36
|
+
Console.info(f"🌿 Switching branch: {current} ➔ {branch}")
|
|
37
|
+
has_commits = cls._run(["git", "rev-parse", "HEAD"]).returncode == 0
|
|
38
|
+
|
|
39
|
+
if not has_commits:
|
|
40
|
+
res = cls._run(["git", "branch", "-M", branch])
|
|
41
|
+
else:
|
|
42
|
+
res = cls._run(["git", "checkout", "-B", branch])
|
|
43
|
+
|
|
44
|
+
if res.returncode != 0:
|
|
45
|
+
error_msg = res.stderr.strip() or res.stdout.strip()
|
|
46
|
+
raise GitOperationError(f"Failed to switch branch: {error_msg}")
|
|
47
|
+
|
|
48
|
+
@classmethod
|
|
49
|
+
def has_staged_changes(cls) -> bool:
|
|
50
|
+
"""Checks if there are files already in the staging area."""
|
|
51
|
+
res = cls._run(["git", "diff", "--cached", "--quiet"])
|
|
52
|
+
return res.returncode != 0
|
|
53
|
+
|
|
54
|
+
@classmethod
|
|
55
|
+
def smart_stage(cls):
|
|
56
|
+
"""Intelligent file staging."""
|
|
57
|
+
if cls.has_staged_changes():
|
|
58
|
+
Console.info("Staged files detected. Committing current index only.")
|
|
59
|
+
else:
|
|
60
|
+
Console.warning("No files in the staging area.")
|
|
61
|
+
ans = (
|
|
62
|
+
input("Would you like to stage all changes (git add .)? [y/N]: ")
|
|
63
|
+
.strip()
|
|
64
|
+
.lower()
|
|
65
|
+
)
|
|
66
|
+
if ans == "y":
|
|
67
|
+
cls._run(["git", "add", "."])
|
|
68
|
+
else:
|
|
69
|
+
raise GitOperationError("Nothing to commit. Operation cancelled.")
|
|
70
|
+
|
|
71
|
+
@classmethod
|
|
72
|
+
def commit(cls, message: str):
|
|
73
|
+
"""Commits changes and extracts Git output if it fails."""
|
|
74
|
+
res = cls._run(["git", "commit", "-m", message])
|
|
75
|
+
if res.returncode != 0:
|
|
76
|
+
error_msg = res.stderr.strip() or res.stdout.strip()
|
|
77
|
+
raise GitOperationError(f"Commit failed. Git says:\n{error_msg}")
|
|
78
|
+
|
|
79
|
+
@classmethod
|
|
80
|
+
def push_with_retry(cls, branch: str):
|
|
81
|
+
"""Pushes to remote, attempting a pull --rebase if rejected."""
|
|
82
|
+
Console.info(f"Pushing to branch: {branch}...")
|
|
83
|
+
res = cls._run(["git", "push", "-u", "origin", branch])
|
|
84
|
+
|
|
85
|
+
if res.returncode == 0:
|
|
86
|
+
Console.success(f"Code successfully pushed to origin/{branch}!")
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
Console.warning("Push rejected. Remote changes detected.")
|
|
90
|
+
Console.info("Synchronizing (pull --rebase)...")
|
|
91
|
+
pull_res = cls._run(["git", "pull", "origin", branch, "--rebase"])
|
|
92
|
+
|
|
93
|
+
if pull_res.returncode != 0:
|
|
94
|
+
error_msg = pull_res.stderr.strip() or pull_res.stdout.strip()
|
|
95
|
+
raise GitOperationError(
|
|
96
|
+
f"🛑 Rebase conflict detected!\nGit says: {error_msg}"
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
Console.success("Sync successful. Retrying push...")
|
|
100
|
+
push_retry = cls._run(["git", "push", "-u", "origin", branch])
|
|
101
|
+
|
|
102
|
+
if push_retry.returncode != 0:
|
|
103
|
+
error_msg = push_retry.stderr.strip() or push_retry.stdout.strip()
|
|
104
|
+
raise GitOperationError(f"Final push failed after rebase:\n{error_msg}")
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from ..logger import Console, logger
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
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.example
|
|
17
|
+
.vscode/
|
|
18
|
+
.idea/
|
|
19
|
+
dist/
|
|
20
|
+
build/
|
|
21
|
+
*.egg-info/
|
|
22
|
+
node_modules/
|
|
23
|
+
.DS_Store
|
|
24
|
+
.smart_commit.log
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def ensure_gitignore(cls):
|
|
29
|
+
"""Ensures .gitignore exists or creates a secure default."""
|
|
30
|
+
path = Path(".gitignore")
|
|
31
|
+
if not path.exists():
|
|
32
|
+
Console.warning(
|
|
33
|
+
".gitignore not found. Creating a secure default configuration..."
|
|
34
|
+
)
|
|
35
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
36
|
+
f.write(cls.BASE_GITIGNORE.lstrip())
|
|
37
|
+
logger.info("Created default .gitignore")
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
def check_env_leaks(cls):
|
|
41
|
+
"""
|
|
42
|
+
Scans for .env* files and uses Git to check if they are ignored.
|
|
43
|
+
Prevents accidental leaks of secrets.
|
|
44
|
+
"""
|
|
45
|
+
Console.info("🛡️ Scanning for potential secret leaks (.env files)...")
|
|
46
|
+
|
|
47
|
+
env_files = [
|
|
48
|
+
f
|
|
49
|
+
for f in Path(".").rglob(".env*")
|
|
50
|
+
if not any(
|
|
51
|
+
part in ["venv", ".venv", "env", "node_modules", ".git"]
|
|
52
|
+
for part in f.parts
|
|
53
|
+
)
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
leaked_files = []
|
|
57
|
+
|
|
58
|
+
for env_file in env_files:
|
|
59
|
+
res = subprocess.run(
|
|
60
|
+
["git", "check-ignore", "-q", str(env_file)], capture_output=True
|
|
61
|
+
)
|
|
62
|
+
if res.returncode == 1:
|
|
63
|
+
leaked_files.append(str(env_file))
|
|
64
|
+
|
|
65
|
+
if leaked_files:
|
|
66
|
+
Console.error(
|
|
67
|
+
"CRITICAL SECURITY RISK: .env files found that are NOT in .gitignore!"
|
|
68
|
+
)
|
|
69
|
+
for lf in leaked_files:
|
|
70
|
+
print(f" 👉 {lf}")
|
|
71
|
+
|
|
72
|
+
ans = (
|
|
73
|
+
input("🛑 Add these files to .gitignore automatically? [Y/n]: ")
|
|
74
|
+
.strip()
|
|
75
|
+
.lower()
|
|
76
|
+
)
|
|
77
|
+
if ans != "n":
|
|
78
|
+
with open(".gitignore", "a", encoding="utf-8") as f:
|
|
79
|
+
f.write("\n# Auto-added by Smart Commit to prevent leaks\n")
|
|
80
|
+
for lf in leaked_files:
|
|
81
|
+
f.write(f"{lf}\n")
|
|
82
|
+
Console.success("Files successfully added to .gitignore.")
|
|
83
|
+
else:
|
|
84
|
+
Console.warning(
|
|
85
|
+
"Action declined. Please be careful: secrets might be pushed!"
|
|
86
|
+
)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import re
|
|
3
|
+
from ..exceptions import ValidationError
|
|
4
|
+
from ..logger import Console, logger
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ValidatorService:
|
|
8
|
+
@classmethod
|
|
9
|
+
def check_conventional_commit(cls, message: str):
|
|
10
|
+
"""Validates message against Conventional Commits standard."""
|
|
11
|
+
pattern = r"^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\(.+\))?:\s.+"
|
|
12
|
+
if not re.match(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
|
+
)
|
smart_commit_tool-0.1.0/PKG-INFO
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: smart-commit-tool
|
|
3
|
-
Version: 0.1.0
|
|
4
|
-
Summary: Automated pre-push workflow manager with built-in code quality enforcement and smart branch protection.
|
|
5
|
-
Requires-Python: >=3.11
|
|
6
|
-
Requires-Dist: colorama
|
|
7
|
-
Requires-Dist: tomli; python_version < '3.11'
|
|
8
|
-
Description-Content-Type: text/markdown
|
|
9
|
-
|
|
10
|
-
# 🚀 Smart Commit Tool
|
|
11
|
-
|
|
12
|
-
Smart Commit Tool is a robust Git wrapper designed for developers who value code quality. It automates the "Pre-check → Commit → Sync → Push" workflow, ensuring that no broken code ever reaches your remote repository.
|
|
13
|
-
|
|
14
|
-
## ✨ Features
|
|
15
|
-
|
|
16
|
-
- ⚡ **Automated Validations**: Runs your test suites (Ruff, Pytest, etc.) before every push.
|
|
17
|
-
- 🛡️ **Branch Protection**: Prevents accidental pushes to sensitive branches like `main` or `prod`.
|
|
18
|
-
- 🔄 **Optimistic Sync**: Automatically handles `pull --rebase` only when conflicts occur, keeping your history clean.
|
|
19
|
-
- 🎨 **Terminal UX**: Full color-coded feedback and interactive prompts for a better developer experience.
|
|
20
|
-
- ⚙️ **Configurable**: Managed entirely via your existing `pyproject.toml`.
|
|
21
|
-
|
|
22
|
-
## 📦 Installation
|
|
23
|
-
|
|
24
|
-
Since the project uses `hatchling`, you can install it locally in editable mode (perfect for development):
|
|
25
|
-
|
|
26
|
-
```bash
|
|
27
|
-
pip install smart-commit
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
After installation, the `smart-commit` command will be available in your terminal.
|
|
31
|
-
|
|
32
|
-
## 🚀 Usage
|
|
33
|
-
|
|
34
|
-
Simply run the command in your project root:
|
|
35
|
-
|
|
36
|
-
```bash
|
|
37
|
-
smart-commit
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
### CLI Arguments
|
|
41
|
-
|
|
42
|
-
| Argument | Description |
|
|
43
|
-
|---------------|---------------------------------------------------|
|
|
44
|
-
| `-b, --branch`| Specify the target branch (skips interactive prompt) |
|
|
45
|
-
| `-m, --message`| Set the commit message (skips interactive prompt) |
|
|
46
|
-
|
|
47
|
-
## 🔧 Configuration
|
|
48
|
-
|
|
49
|
-
Add the following section to your `pyproject.toml` to customize the behavior:
|
|
50
|
-
|
|
51
|
-
```toml
|
|
52
|
-
[tool.smart_commit]
|
|
53
|
-
repository_url = "https://github.com/youruser/yourrepo.git"
|
|
54
|
-
protected_branches = ["main", "master", "prod", "release"]
|
|
55
|
-
commands = [
|
|
56
|
-
"ruff check .",
|
|
57
|
-
"pytest",
|
|
58
|
-
...
|
|
59
|
-
]
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
### How it works:
|
|
63
|
-
|
|
64
|
-
1. **Environment Check**: Ensures Git is initialized and the remote URL is correct.
|
|
65
|
-
2. **Branch Guard**: If you are on a protected branch, it prompts you to switch to a new one.
|
|
66
|
-
3. **Validation**: Runs every command in your `commands` list. If one fails, the process stops.
|
|
67
|
-
4. **Smart Push**: Attempts a push. If the remote has changed, it performs a rebase and retries automatically.
|
|
68
|
-
|
|
69
|
-
## 🛠 Tech Stack
|
|
70
|
-
|
|
71
|
-
- Python 3.11+
|
|
72
|
-
- **Colorama**: For cross-platform terminal styling.
|
|
73
|
-
- **Tomli/Tomllib**: For seamless configuration parsing.
|
|
74
|
-
- **Hatchling**: Modern build backend.
|
|
75
|
-
|
|
76
|
-
## 📄 License
|
|
77
|
-
|
|
78
|
-
Distributed under the MIT License. See `LICENSE` for more information.
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
# 🚀 Smart Commit Tool
|
|
2
|
-
|
|
3
|
-
Smart Commit Tool is a robust Git wrapper designed for developers who value code quality. It automates the "Pre-check → Commit → Sync → Push" workflow, ensuring that no broken code ever reaches your remote repository.
|
|
4
|
-
|
|
5
|
-
## ✨ Features
|
|
6
|
-
|
|
7
|
-
- ⚡ **Automated Validations**: Runs your test suites (Ruff, Pytest, etc.) before every push.
|
|
8
|
-
- 🛡️ **Branch Protection**: Prevents accidental pushes to sensitive branches like `main` or `prod`.
|
|
9
|
-
- 🔄 **Optimistic Sync**: Automatically handles `pull --rebase` only when conflicts occur, keeping your history clean.
|
|
10
|
-
- 🎨 **Terminal UX**: Full color-coded feedback and interactive prompts for a better developer experience.
|
|
11
|
-
- ⚙️ **Configurable**: Managed entirely via your existing `pyproject.toml`.
|
|
12
|
-
|
|
13
|
-
## 📦 Installation
|
|
14
|
-
|
|
15
|
-
Since the project uses `hatchling`, you can install it locally in editable mode (perfect for development):
|
|
16
|
-
|
|
17
|
-
```bash
|
|
18
|
-
pip install smart-commit
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
After installation, the `smart-commit` command will be available in your terminal.
|
|
22
|
-
|
|
23
|
-
## 🚀 Usage
|
|
24
|
-
|
|
25
|
-
Simply run the command in your project root:
|
|
26
|
-
|
|
27
|
-
```bash
|
|
28
|
-
smart-commit
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
### CLI Arguments
|
|
32
|
-
|
|
33
|
-
| Argument | Description |
|
|
34
|
-
|---------------|---------------------------------------------------|
|
|
35
|
-
| `-b, --branch`| Specify the target branch (skips interactive prompt) |
|
|
36
|
-
| `-m, --message`| Set the commit message (skips interactive prompt) |
|
|
37
|
-
|
|
38
|
-
## 🔧 Configuration
|
|
39
|
-
|
|
40
|
-
Add the following section to your `pyproject.toml` to customize the behavior:
|
|
41
|
-
|
|
42
|
-
```toml
|
|
43
|
-
[tool.smart_commit]
|
|
44
|
-
repository_url = "https://github.com/youruser/yourrepo.git"
|
|
45
|
-
protected_branches = ["main", "master", "prod", "release"]
|
|
46
|
-
commands = [
|
|
47
|
-
"ruff check .",
|
|
48
|
-
"pytest",
|
|
49
|
-
...
|
|
50
|
-
]
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
### How it works:
|
|
54
|
-
|
|
55
|
-
1. **Environment Check**: Ensures Git is initialized and the remote URL is correct.
|
|
56
|
-
2. **Branch Guard**: If you are on a protected branch, it prompts you to switch to a new one.
|
|
57
|
-
3. **Validation**: Runs every command in your `commands` list. If one fails, the process stops.
|
|
58
|
-
4. **Smart Push**: Attempts a push. If the remote has changed, it performs a rebase and retries automatically.
|
|
59
|
-
|
|
60
|
-
## 🛠 Tech Stack
|
|
61
|
-
|
|
62
|
-
- Python 3.11+
|
|
63
|
-
- **Colorama**: For cross-platform terminal styling.
|
|
64
|
-
- **Tomli/Tomllib**: For seamless configuration parsing.
|
|
65
|
-
- **Hatchling**: Modern build backend.
|
|
66
|
-
|
|
67
|
-
## 📄 License
|
|
68
|
-
|
|
69
|
-
Distributed under the MIT License. See `LICENSE` for more information.
|
|
File without changes
|
|
@@ -1,214 +0,0 @@
|
|
|
1
|
-
import subprocess
|
|
2
|
-
import sys
|
|
3
|
-
import argparse
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
from typing import Any
|
|
6
|
-
from colorama import init, Fore, Style
|
|
7
|
-
|
|
8
|
-
# Initialize colorama for cross-platform colored output
|
|
9
|
-
init(autoreset=True)
|
|
10
|
-
|
|
11
|
-
if sys.version_info >= (3, 11):
|
|
12
|
-
import tomllib
|
|
13
|
-
else:
|
|
14
|
-
import tomli as tomllib
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
class SmartCommitError(Exception):
|
|
18
|
-
"""Base class for script-related errors."""
|
|
19
|
-
|
|
20
|
-
pass
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def run_cmd(
|
|
24
|
-
cmd: list[str] | str, silent: bool = False, use_shell: bool = False
|
|
25
|
-
) -> subprocess.CompletedProcess[str]:
|
|
26
|
-
"""
|
|
27
|
-
Executes a shell command.
|
|
28
|
-
Prefer passing a list of arguments (use_shell=False) for internal git commands.
|
|
29
|
-
"""
|
|
30
|
-
if not silent:
|
|
31
|
-
display_cmd = cmd if isinstance(cmd, str) else " ".join(cmd)
|
|
32
|
-
print(f"{Fore.CYAN}🛠 Executing: {Style.BRIGHT}{display_cmd}")
|
|
33
|
-
|
|
34
|
-
result = subprocess.run(cmd, shell=use_shell, capture_output=silent, text=True)
|
|
35
|
-
return result
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def ensure_gitignore() -> None:
|
|
39
|
-
"""Creates a standard .gitignore for Python if it doesn't exist."""
|
|
40
|
-
if not Path(".gitignore").exists():
|
|
41
|
-
print(f"{Fore.YELLOW}📝 .gitignore not found. Creating a standard one...")
|
|
42
|
-
content = (
|
|
43
|
-
"__pycache__/\n*.py[cod]\n*$py.class\n.venv/\nvenv/\n"
|
|
44
|
-
".env\n.vscode/\ndist/\nbuild/\n*.egg-info/\n"
|
|
45
|
-
)
|
|
46
|
-
with open(".gitignore", "w", encoding="utf-8") as f:
|
|
47
|
-
f.write(content)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def get_config() -> dict[str, Any]:
|
|
51
|
-
"""Reads configuration from pyproject.toml."""
|
|
52
|
-
path = Path("pyproject.toml")
|
|
53
|
-
if not path.exists():
|
|
54
|
-
raise SmartCommitError(
|
|
55
|
-
"pyproject.toml not found. Please run the command in the project root."
|
|
56
|
-
)
|
|
57
|
-
|
|
58
|
-
with open(path, "rb") as f:
|
|
59
|
-
data = tomllib.load(f)
|
|
60
|
-
return data.get("tool", {}).get("smart_commit", {})
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
def ensure_git_setup(expected_url: str) -> None:
|
|
64
|
-
"""Checks and configures Git repository and remote."""
|
|
65
|
-
if not Path(".git").exists():
|
|
66
|
-
print(f"{Fore.YELLOW}📁 Git not initialized. Initializing repository...")
|
|
67
|
-
run_cmd(["git", "init"])
|
|
68
|
-
|
|
69
|
-
res = run_cmd(["git", "remote", "get-url", "origin"], silent=True)
|
|
70
|
-
current_url = res.stdout.strip()
|
|
71
|
-
|
|
72
|
-
if res.returncode != 0:
|
|
73
|
-
print(f"{Fore.CYAN}🔗 Adding remote origin: {expected_url}")
|
|
74
|
-
run_cmd(["git", "remote", "add", "origin", expected_url])
|
|
75
|
-
elif expected_url not in current_url:
|
|
76
|
-
print(f"{Fore.YELLOW}🔄 Updating repository URL to: {expected_url}")
|
|
77
|
-
run_cmd(["git", "remote", "set-url", "origin", expected_url])
|
|
78
|
-
else:
|
|
79
|
-
print(f"{Fore.GREEN}✅ Git repository is correctly configured.")
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
def switch_branch(branch: str) -> None:
|
|
83
|
-
"""Safely switches branches, handling unborn (empty) repositories."""
|
|
84
|
-
has_commits = run_cmd(["git", "rev-parse", "HEAD"], silent=True).returncode == 0
|
|
85
|
-
|
|
86
|
-
if not has_commits:
|
|
87
|
-
run_cmd(["git", "branch", "-M", branch], silent=True)
|
|
88
|
-
else:
|
|
89
|
-
run_cmd(["git", "checkout", "-B", branch], silent=True)
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
def main() -> None:
|
|
93
|
-
parser = argparse.ArgumentParser(description="Smart Commit & Push Tool")
|
|
94
|
-
parser.add_argument("-b", "--branch", help="Specify branch (skips prompt)")
|
|
95
|
-
parser.add_argument("-m", "--message", help="Commit message (skips prompt)")
|
|
96
|
-
args = parser.parse_args()
|
|
97
|
-
|
|
98
|
-
try:
|
|
99
|
-
config = get_config()
|
|
100
|
-
repo_url = config.get("repository_url")
|
|
101
|
-
commands: list[str] = config.get("commands", [])
|
|
102
|
-
protected_branches = config.get(
|
|
103
|
-
"protected_branches", ["main", "master", "prod", "production"]
|
|
104
|
-
)
|
|
105
|
-
|
|
106
|
-
if not repo_url:
|
|
107
|
-
raise SmartCommitError(
|
|
108
|
-
"In pyproject.toml, [tool.smart_commit].repository_url is missing."
|
|
109
|
-
)
|
|
110
|
-
|
|
111
|
-
ensure_git_setup(repo_url)
|
|
112
|
-
ensure_gitignore()
|
|
113
|
-
|
|
114
|
-
print(f"\n{Fore.MAGENTA}{Style.BRIGHT}--- 🚀 SMART COMMIT PRE-CHECK ---")
|
|
115
|
-
|
|
116
|
-
branch = args.branch
|
|
117
|
-
if not branch:
|
|
118
|
-
current_branch = run_cmd(
|
|
119
|
-
["git", "branch", "--show-current"], silent=True
|
|
120
|
-
).stdout.strip()
|
|
121
|
-
|
|
122
|
-
if current_branch:
|
|
123
|
-
user_input = input(f"{Fore.BLUE}🌿 Branch [{current_branch}]: ").strip()
|
|
124
|
-
branch = user_input if user_input else current_branch
|
|
125
|
-
else:
|
|
126
|
-
branch = input(f"{Fore.BLUE}🌿 Branch name (e.g., main): ").strip()
|
|
127
|
-
|
|
128
|
-
message = args.message
|
|
129
|
-
if not message:
|
|
130
|
-
message = input(f"{Fore.BLUE}📝 Commit message: ").strip()
|
|
131
|
-
|
|
132
|
-
if not branch or not message:
|
|
133
|
-
raise SmartCommitError("Branch and message cannot be empty.")
|
|
134
|
-
|
|
135
|
-
if branch in protected_branches:
|
|
136
|
-
print(
|
|
137
|
-
f"\n{Fore.RED}{Style.BRIGHT}⚠️ WARNING: You are pushing directly to a protected branch: '{branch}'."
|
|
138
|
-
)
|
|
139
|
-
choice = (
|
|
140
|
-
input(
|
|
141
|
-
f"{Fore.YELLOW}Continue? [y (yes) / n (cancel) / b (create new branch)]: "
|
|
142
|
-
)
|
|
143
|
-
.lower()
|
|
144
|
-
.strip()
|
|
145
|
-
)
|
|
146
|
-
|
|
147
|
-
if choice == "n":
|
|
148
|
-
raise SmartCommitError("Operation cancelled by user.")
|
|
149
|
-
elif choice == "b":
|
|
150
|
-
new_branch = input(f"{Fore.BLUE}Enter new branch name: ").strip()
|
|
151
|
-
if not new_branch:
|
|
152
|
-
raise SmartCommitError("Branch name cannot be empty.")
|
|
153
|
-
branch = new_branch
|
|
154
|
-
print(f"{Fore.CYAN}🌿 Switching to new branch '{branch}'...")
|
|
155
|
-
elif choice != "y":
|
|
156
|
-
raise SmartCommitError("Invalid input. Operation cancelled.")
|
|
157
|
-
|
|
158
|
-
switch_branch(branch)
|
|
159
|
-
|
|
160
|
-
print(f"\n{Fore.MAGENTA}{Style.BRIGHT}--- 🛠 RUNNING VALIDATIONS ---")
|
|
161
|
-
for cmd in commands:
|
|
162
|
-
if run_cmd(cmd, use_shell=True).returncode != 0:
|
|
163
|
-
raise SmartCommitError(
|
|
164
|
-
f"Validation failed for: '{cmd}'. Please fix the issues and try again!"
|
|
165
|
-
)
|
|
166
|
-
|
|
167
|
-
print(f"\n{Fore.MAGENTA}{Style.BRIGHT}--- ✅ ALL CHECKS PASSED. PUSHING... ---")
|
|
168
|
-
|
|
169
|
-
if run_cmd(["git", "add", "."]).returncode != 0:
|
|
170
|
-
raise SmartCommitError("Error occurred during 'git add'.")
|
|
171
|
-
|
|
172
|
-
if run_cmd(["git", "commit", "-m", message]).returncode != 0:
|
|
173
|
-
print(f"{Fore.YELLOW}ℹ️ No changes to commit or commit error occurred.")
|
|
174
|
-
|
|
175
|
-
print(f"{Fore.CYAN}📤 Pushing changes to {Style.BRIGHT}{branch}...")
|
|
176
|
-
push_res = run_cmd(["git", "push", "-u", "origin", branch], silent=True)
|
|
177
|
-
|
|
178
|
-
if push_res.returncode == 0:
|
|
179
|
-
print(
|
|
180
|
-
f"\n{Fore.GREEN}{Style.BRIGHT}🎉 Success! Code validated and pushed to '{branch}'."
|
|
181
|
-
)
|
|
182
|
-
return
|
|
183
|
-
|
|
184
|
-
print(f"{Fore.YELLOW}⚠️ Push rejected. Remote changes detected.")
|
|
185
|
-
print(f"{Fore.CYAN}📥 Synchronizing (pull --rebase)...")
|
|
186
|
-
|
|
187
|
-
pull_res = run_cmd(["git", "pull", "origin", branch, "--rebase"], silent=True)
|
|
188
|
-
|
|
189
|
-
if pull_res.returncode != 0:
|
|
190
|
-
raise SmartCommitError(
|
|
191
|
-
"🛑 Conflict detected during pull!\n"
|
|
192
|
-
"Please resolve conflicts manually (git rebase --continue) and run the script again."
|
|
193
|
-
)
|
|
194
|
-
|
|
195
|
-
print(f"{Fore.GREEN}🔄 Sync successful. Retrying push...")
|
|
196
|
-
push_res_retry = run_cmd(["git", "push", "-u", "origin", branch])
|
|
197
|
-
|
|
198
|
-
if push_res_retry.returncode == 0:
|
|
199
|
-
print(
|
|
200
|
-
f"\n{Fore.GREEN}{Style.BRIGHT}🎉 Success! Changes synchronized and pushed to '{branch}'."
|
|
201
|
-
)
|
|
202
|
-
else:
|
|
203
|
-
raise SmartCommitError("Failed to push changes after rebase.")
|
|
204
|
-
|
|
205
|
-
except SmartCommitError as e:
|
|
206
|
-
print(f"\n{Fore.RED}{Style.BRIGHT}❌ Error: {e}")
|
|
207
|
-
sys.exit(1)
|
|
208
|
-
except KeyboardInterrupt:
|
|
209
|
-
print(f"\n{Fore.YELLOW}Process interrupted by user.")
|
|
210
|
-
sys.exit(0)
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
if __name__ == "__main__":
|
|
214
|
-
main()
|
|
File without changes
|