smart-commit-tool 0.1.0__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.
|
@@ -0,0 +1,78 @@
|
|
|
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.
|
|
@@ -0,0 +1,69 @@
|
|
|
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.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "smart-commit-tool"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Automated pre-push workflow manager with built-in code quality enforcement and smart branch protection."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"tomli; python_version < '3.11'",
|
|
13
|
+
"colorama"
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[project.scripts]
|
|
17
|
+
smart-commit = "smart_commit.cli:main"
|
|
18
|
+
|
|
19
|
+
[tool.smart_commit]
|
|
20
|
+
repository_url = "https://github.com/mokinprokin/smart_commit.git"
|
|
21
|
+
commands = ["ruff check ."]
|
|
22
|
+
protected_branches = ["main", "master", "prod", "release"]
|
|
23
|
+
|
|
24
|
+
[tool.hatch.build.targets.wheel]
|
|
25
|
+
packages = ["src/smart_commit"]
|
|
File without changes
|
|
@@ -0,0 +1,214 @@
|
|
|
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()
|