devcommit 0.1.4.7__tar.gz → 0.1.4.9__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.
- {devcommit-0.1.4.7 → devcommit-0.1.4.9}/PKG-INFO +73 -1
- {devcommit-0.1.4.7 → devcommit-0.1.4.9}/README.md +72 -0
- {devcommit-0.1.4.7 → devcommit-0.1.4.9}/devcommit/main.py +483 -39
- {devcommit-0.1.4.7 → devcommit-0.1.4.9}/devcommit/utils/git.py +2 -7
- {devcommit-0.1.4.7 → devcommit-0.1.4.9}/pyproject.toml +1 -1
- {devcommit-0.1.4.7 → devcommit-0.1.4.9}/COPYING +0 -0
- {devcommit-0.1.4.7 → devcommit-0.1.4.9}/devcommit/__init__.py +0 -0
- {devcommit-0.1.4.7 → devcommit-0.1.4.9}/devcommit/app/__init__.py +0 -0
- {devcommit-0.1.4.7 → devcommit-0.1.4.9}/devcommit/app/ai_providers.py +0 -0
- {devcommit-0.1.4.7 → devcommit-0.1.4.9}/devcommit/app/gemini_ai.py +0 -0
- {devcommit-0.1.4.7 → devcommit-0.1.4.9}/devcommit/app/prompt.py +0 -0
- {devcommit-0.1.4.7 → devcommit-0.1.4.9}/devcommit/utils/__init__.py +0 -0
- {devcommit-0.1.4.7 → devcommit-0.1.4.9}/devcommit/utils/logger.py +0 -0
- {devcommit-0.1.4.7 → devcommit-0.1.4.9}/devcommit/utils/parser.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devcommit
|
|
3
|
-
Version: 0.1.4.
|
|
3
|
+
Version: 0.1.4.9
|
|
4
4
|
Summary: AI-powered git commit message generator
|
|
5
5
|
License: GNU GENERAL PUBLIC LICENSE
|
|
6
6
|
Version 3, 29 June 2007
|
|
@@ -713,6 +713,9 @@ A command-line AI tool for autocommits.
|
|
|
713
713
|
- 🚀 Automatic commit generation using AI
|
|
714
714
|
- 📁 Directory-based commits - create separate commits for each root directory
|
|
715
715
|
- 🎯 Interactive mode to choose between global or directory-based commits
|
|
716
|
+
- 📄 **Commit specific files or folders** - Stage and commit only selected files/directories
|
|
717
|
+
- 🔄 **Regenerate commit messages** - Don't like the suggestions? Regenerate with one click
|
|
718
|
+
- 🚀 **Push to remote** - Automatically push commits after committing
|
|
716
719
|
- ⚙️ Flexible configuration - use environment variables or .dcommit file
|
|
717
720
|
- 🏠 Self-hosted model support - use your own AI infrastructure
|
|
718
721
|
- 🆓 Multiple free tier options available
|
|
@@ -865,6 +868,9 @@ devcommit --stageAll --files file1.py file2.py
|
|
|
865
868
|
git add src/ tests/
|
|
866
869
|
devcommit --files src/ tests/
|
|
867
870
|
|
|
871
|
+
# Stage and commit multiple directories
|
|
872
|
+
devcommit -s -f src/core src/modules/account/ src/modules/auth/
|
|
873
|
+
|
|
868
874
|
# Short form
|
|
869
875
|
devcommit -s -f file1.py file2.py
|
|
870
876
|
```
|
|
@@ -875,6 +881,66 @@ When using `--files` or `-f`:
|
|
|
875
881
|
- With `--stageAll`: Stages the specified files/folders and then commits them
|
|
876
882
|
- AI generates commit messages based on changes in those files
|
|
877
883
|
- Works with both individual files and entire directories
|
|
884
|
+
- Files with no changes are automatically filtered out
|
|
885
|
+
|
|
886
|
+
#### Commit Mode Behavior with `--files`
|
|
887
|
+
|
|
888
|
+
The `--files` flag respects your `COMMIT_MODE` setting:
|
|
889
|
+
|
|
890
|
+
- **`COMMIT_MODE=directory`** with `--files`:
|
|
891
|
+
- **Individual files**: Each file gets its own separate commit
|
|
892
|
+
- Example: `devcommit -f src/test1.py src/test2.py` creates 2 separate commits
|
|
893
|
+
- **Directories**: Each directory gets one commit containing all its files
|
|
894
|
+
- Example: `devcommit -f src/core src/modules/account/` creates 2 commits (one per directory)
|
|
895
|
+
|
|
896
|
+
- **`COMMIT_MODE=global`** with `--files`:
|
|
897
|
+
- All specified files/directories are committed together in a single commit
|
|
898
|
+
- Example: `devcommit -f src/test1.py src/test2.py` creates 1 commit for both files
|
|
899
|
+
|
|
900
|
+
- **`COMMIT_MODE=auto`** with `--files`:
|
|
901
|
+
- Always prompts you to choose between one commit for all files or separate commits
|
|
902
|
+
- If you select directory mode: individual files get separate commits, directories get one commit each
|
|
903
|
+
- If you select global mode: everything is committed together
|
|
904
|
+
|
|
905
|
+
### Push to Remote
|
|
906
|
+
|
|
907
|
+
DevCommit can automatically push your commits to the remote repository after committing.
|
|
908
|
+
|
|
909
|
+
**Usage:**
|
|
910
|
+
|
|
911
|
+
```bash
|
|
912
|
+
# Commit all staged changes and push
|
|
913
|
+
devcommit --push
|
|
914
|
+
|
|
915
|
+
# Commit specific files and push
|
|
916
|
+
devcommit --files file1.py file2.py --push
|
|
917
|
+
|
|
918
|
+
# Stage, commit, and push in one command
|
|
919
|
+
devcommit --stageAll --push
|
|
920
|
+
|
|
921
|
+
# Short form
|
|
922
|
+
devcommit -p
|
|
923
|
+
devcommit -f file1.py -p
|
|
924
|
+
```
|
|
925
|
+
|
|
926
|
+
**Note:** The push operation will only execute if commits were successfully made. If you cancel the commit, the push will be skipped.
|
|
927
|
+
|
|
928
|
+
### Regenerate Commit Messages
|
|
929
|
+
|
|
930
|
+
Don't like the AI-generated commit messages? You can regenerate them on the fly!
|
|
931
|
+
|
|
932
|
+
When viewing commit message options, you'll see:
|
|
933
|
+
- Numbered commit message suggestions
|
|
934
|
+
- ✏️ Enter custom message
|
|
935
|
+
- 🔄 **Regenerate commit messages** (new!)
|
|
936
|
+
- ❌ Cancel
|
|
937
|
+
|
|
938
|
+
Selecting "Regenerate commit messages" will:
|
|
939
|
+
- Call the AI again to generate new suggestions
|
|
940
|
+
- Show the new messages in the same prompt
|
|
941
|
+
- Allow you to regenerate again or select a message
|
|
942
|
+
|
|
943
|
+
This works for all commit modes (global, directory, and per-file commits).
|
|
878
944
|
|
|
879
945
|
### Additional Options
|
|
880
946
|
|
|
@@ -912,6 +978,12 @@ devcommit --push
|
|
|
912
978
|
|
|
913
979
|
# Commit specific files and push
|
|
914
980
|
devcommit --files file1.py file2.py --push
|
|
981
|
+
|
|
982
|
+
# Stage and commit multiple directories with directory mode
|
|
983
|
+
devcommit -s -f src/core src/modules/account/ --directory
|
|
984
|
+
|
|
985
|
+
# Stage and commit, then push
|
|
986
|
+
devcommit -s -f src/core src/modules/account/ -p
|
|
915
987
|
```
|
|
916
988
|
|
|
917
989
|
## AI Provider Support
|
|
@@ -8,6 +8,9 @@ A command-line AI tool for autocommits.
|
|
|
8
8
|
- 🚀 Automatic commit generation using AI
|
|
9
9
|
- 📁 Directory-based commits - create separate commits for each root directory
|
|
10
10
|
- 🎯 Interactive mode to choose between global or directory-based commits
|
|
11
|
+
- 📄 **Commit specific files or folders** - Stage and commit only selected files/directories
|
|
12
|
+
- 🔄 **Regenerate commit messages** - Don't like the suggestions? Regenerate with one click
|
|
13
|
+
- 🚀 **Push to remote** - Automatically push commits after committing
|
|
11
14
|
- ⚙️ Flexible configuration - use environment variables or .dcommit file
|
|
12
15
|
- 🏠 Self-hosted model support - use your own AI infrastructure
|
|
13
16
|
- 🆓 Multiple free tier options available
|
|
@@ -160,6 +163,9 @@ devcommit --stageAll --files file1.py file2.py
|
|
|
160
163
|
git add src/ tests/
|
|
161
164
|
devcommit --files src/ tests/
|
|
162
165
|
|
|
166
|
+
# Stage and commit multiple directories
|
|
167
|
+
devcommit -s -f src/core src/modules/account/ src/modules/auth/
|
|
168
|
+
|
|
163
169
|
# Short form
|
|
164
170
|
devcommit -s -f file1.py file2.py
|
|
165
171
|
```
|
|
@@ -170,6 +176,66 @@ When using `--files` or `-f`:
|
|
|
170
176
|
- With `--stageAll`: Stages the specified files/folders and then commits them
|
|
171
177
|
- AI generates commit messages based on changes in those files
|
|
172
178
|
- Works with both individual files and entire directories
|
|
179
|
+
- Files with no changes are automatically filtered out
|
|
180
|
+
|
|
181
|
+
#### Commit Mode Behavior with `--files`
|
|
182
|
+
|
|
183
|
+
The `--files` flag respects your `COMMIT_MODE` setting:
|
|
184
|
+
|
|
185
|
+
- **`COMMIT_MODE=directory`** with `--files`:
|
|
186
|
+
- **Individual files**: Each file gets its own separate commit
|
|
187
|
+
- Example: `devcommit -f src/test1.py src/test2.py` creates 2 separate commits
|
|
188
|
+
- **Directories**: Each directory gets one commit containing all its files
|
|
189
|
+
- Example: `devcommit -f src/core src/modules/account/` creates 2 commits (one per directory)
|
|
190
|
+
|
|
191
|
+
- **`COMMIT_MODE=global`** with `--files`:
|
|
192
|
+
- All specified files/directories are committed together in a single commit
|
|
193
|
+
- Example: `devcommit -f src/test1.py src/test2.py` creates 1 commit for both files
|
|
194
|
+
|
|
195
|
+
- **`COMMIT_MODE=auto`** with `--files`:
|
|
196
|
+
- Always prompts you to choose between one commit for all files or separate commits
|
|
197
|
+
- If you select directory mode: individual files get separate commits, directories get one commit each
|
|
198
|
+
- If you select global mode: everything is committed together
|
|
199
|
+
|
|
200
|
+
### Push to Remote
|
|
201
|
+
|
|
202
|
+
DevCommit can automatically push your commits to the remote repository after committing.
|
|
203
|
+
|
|
204
|
+
**Usage:**
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
# Commit all staged changes and push
|
|
208
|
+
devcommit --push
|
|
209
|
+
|
|
210
|
+
# Commit specific files and push
|
|
211
|
+
devcommit --files file1.py file2.py --push
|
|
212
|
+
|
|
213
|
+
# Stage, commit, and push in one command
|
|
214
|
+
devcommit --stageAll --push
|
|
215
|
+
|
|
216
|
+
# Short form
|
|
217
|
+
devcommit -p
|
|
218
|
+
devcommit -f file1.py -p
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
**Note:** The push operation will only execute if commits were successfully made. If you cancel the commit, the push will be skipped.
|
|
222
|
+
|
|
223
|
+
### Regenerate Commit Messages
|
|
224
|
+
|
|
225
|
+
Don't like the AI-generated commit messages? You can regenerate them on the fly!
|
|
226
|
+
|
|
227
|
+
When viewing commit message options, you'll see:
|
|
228
|
+
- Numbered commit message suggestions
|
|
229
|
+
- ✏️ Enter custom message
|
|
230
|
+
- 🔄 **Regenerate commit messages** (new!)
|
|
231
|
+
- ❌ Cancel
|
|
232
|
+
|
|
233
|
+
Selecting "Regenerate commit messages" will:
|
|
234
|
+
- Call the AI again to generate new suggestions
|
|
235
|
+
- Show the new messages in the same prompt
|
|
236
|
+
- Allow you to regenerate again or select a message
|
|
237
|
+
|
|
238
|
+
This works for all commit modes (global, directory, and per-file commits).
|
|
173
239
|
|
|
174
240
|
### Additional Options
|
|
175
241
|
|
|
@@ -207,6 +273,12 @@ devcommit --push
|
|
|
207
273
|
|
|
208
274
|
# Commit specific files and push
|
|
209
275
|
devcommit --files file1.py file2.py --push
|
|
276
|
+
|
|
277
|
+
# Stage and commit multiple directories with directory mode
|
|
278
|
+
devcommit -s -f src/core src/modules/account/ --directory
|
|
279
|
+
|
|
280
|
+
# Stage and commit, then push
|
|
281
|
+
devcommit -s -f src/core src/modules/account/ -p
|
|
210
282
|
```
|
|
211
283
|
|
|
212
284
|
## AI Provider Support
|
|
@@ -71,7 +71,9 @@ def main(flags: CommitFlag = None):
|
|
|
71
71
|
|
|
72
72
|
# Handle staging
|
|
73
73
|
push_files_list = []
|
|
74
|
+
original_paths = [] # Keep track of original paths (files or directories) passed
|
|
74
75
|
if flags["files"] and len(flags["files"]) > 0:
|
|
76
|
+
original_paths = flags["files"]
|
|
75
77
|
|
|
76
78
|
# Get the list of files from paths first
|
|
77
79
|
try:
|
|
@@ -82,7 +84,7 @@ def main(flags: CommitFlag = None):
|
|
|
82
84
|
raise e
|
|
83
85
|
except Exception as e:
|
|
84
86
|
raise KnownError(f"Failed to get files from paths: {str(e)}")
|
|
85
|
-
|
|
87
|
+
|
|
86
88
|
if flags["stageAll"]:
|
|
87
89
|
if push_files_list:
|
|
88
90
|
# Stage specific files/folders only
|
|
@@ -158,6 +160,9 @@ def main(flags: CommitFlag = None):
|
|
|
158
160
|
# Priority: CLI flag > config (file or env) > interactive prompt
|
|
159
161
|
use_per_directory = flags.get("directory", False)
|
|
160
162
|
|
|
163
|
+
# Special handling when --files is used: check if we should use per-file commits
|
|
164
|
+
is_files_mode = push_files_list and len(push_files_list) > 0
|
|
165
|
+
|
|
161
166
|
# If not explicitly set via CLI, check config (file or environment variable)
|
|
162
167
|
if not use_per_directory:
|
|
163
168
|
commit_mode = config("COMMIT_MODE", default="auto").lower()
|
|
@@ -167,18 +172,58 @@ def main(flags: CommitFlag = None):
|
|
|
167
172
|
use_per_directory = False
|
|
168
173
|
# If "auto" or not set, fall through to interactive prompt
|
|
169
174
|
|
|
170
|
-
# If still not set, check if there are multiple directories and prompt
|
|
175
|
+
# If still not set (auto mode), check if there are multiple directories and prompt
|
|
171
176
|
if not use_per_directory and config("COMMIT_MODE", default="auto").lower() == "auto":
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
177
|
+
if is_files_mode:
|
|
178
|
+
# When --files is used with auto mode, always prompt
|
|
179
|
+
# Group files to show directory structure, but prompt for per-file vs global
|
|
180
|
+
grouped = group_files_by_directory(staged["files"])
|
|
181
|
+
console.print()
|
|
182
|
+
console.print("╭" + "─" * 60 + "╮", style="bold yellow")
|
|
183
|
+
console.print("│" + " 📂 [bold white]Files from multiple locations detected[/bold white]".ljust(70) + "│", style="bold yellow")
|
|
184
|
+
console.print("╰" + "─" * 60 + "╯", style="bold yellow")
|
|
185
|
+
console.print()
|
|
186
|
+
console.print(f" [dim]Found {len(staged['files'])} file(s) to commit[/dim]")
|
|
187
|
+
console.print()
|
|
188
|
+
# Prompt for per-file vs global commit
|
|
189
|
+
use_per_directory = prompt_commit_strategy(console, grouped, is_files_mode=True)
|
|
190
|
+
else:
|
|
191
|
+
# Regular auto mode: check directories
|
|
192
|
+
grouped = group_files_by_directory(staged["files"])
|
|
193
|
+
if len(grouped) > 1:
|
|
194
|
+
use_per_directory = prompt_commit_strategy(console, grouped, is_files_mode=False)
|
|
195
|
+
# If only one directory and not files mode, use global commit (single commit for all files)
|
|
175
196
|
|
|
176
197
|
# Track if any commits were made
|
|
177
198
|
commit_made = False
|
|
178
199
|
if use_per_directory:
|
|
179
|
-
|
|
200
|
+
# When --files is used with directory mode
|
|
201
|
+
if is_files_mode:
|
|
202
|
+
# Check if original paths were directories or individual files
|
|
203
|
+
# If directories were passed, group by those directories
|
|
204
|
+
# If individual files were passed, treat each file separately
|
|
205
|
+
has_directories = False
|
|
206
|
+
if original_paths:
|
|
207
|
+
repo_root = assert_git_repo()
|
|
208
|
+
for path in original_paths:
|
|
209
|
+
normalized_path = os.path.normpath(path)
|
|
210
|
+
full_path = os.path.join(repo_root, normalized_path) if not os.path.isabs(path) else path
|
|
211
|
+
if os.path.isdir(full_path):
|
|
212
|
+
has_directories = True
|
|
213
|
+
break
|
|
214
|
+
|
|
215
|
+
if has_directories:
|
|
216
|
+
# Group files by the original directories passed
|
|
217
|
+
commit_made = process_per_directory_commits_from_paths(console, staged, flags, original_paths)
|
|
218
|
+
else:
|
|
219
|
+
# Individual files passed, treat each file separately
|
|
220
|
+
commit_made = process_per_file_commits(console, staged, flags)
|
|
221
|
+
else:
|
|
222
|
+
commit_made = process_per_directory_commits(console, staged, flags)
|
|
180
223
|
else:
|
|
181
|
-
|
|
224
|
+
# Pass staged dict so process_global_commit knows which files to commit
|
|
225
|
+
# (important when --files is used)
|
|
226
|
+
commit_made = process_global_commit(console, flags, staged=staged)
|
|
182
227
|
|
|
183
228
|
# Handle push if requested and a commit was actually made
|
|
184
229
|
if flags.get("push", False) and commit_made:
|
|
@@ -216,7 +261,7 @@ def stage_changes(console):
|
|
|
216
261
|
spinner="dots",
|
|
217
262
|
spinner_style="cyan"
|
|
218
263
|
):
|
|
219
|
-
subprocess.run(["git", "add", "--
|
|
264
|
+
subprocess.run(["git", "add", "--all"], check=True)
|
|
220
265
|
|
|
221
266
|
|
|
222
267
|
def detect_staged_files(console, exclude_files):
|
|
@@ -241,7 +286,13 @@ def detect_staged_files(console, exclude_files):
|
|
|
241
286
|
return staged
|
|
242
287
|
|
|
243
288
|
|
|
244
|
-
def analyze_changes(console):
|
|
289
|
+
def analyze_changes(console, files=None):
|
|
290
|
+
"""Analyze changes for commit message generation.
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
console: Rich console for output
|
|
294
|
+
files: Optional list of specific files to analyze. If None, analyzes all staged files.
|
|
295
|
+
"""
|
|
245
296
|
import sys
|
|
246
297
|
|
|
247
298
|
with console.status(
|
|
@@ -249,11 +300,16 @@ def analyze_changes(console):
|
|
|
249
300
|
spinner="dots",
|
|
250
301
|
spinner_style="magenta"
|
|
251
302
|
):
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
303
|
+
if files:
|
|
304
|
+
# Analyze only specific files
|
|
305
|
+
diff = get_diff_for_files(files)
|
|
306
|
+
else:
|
|
307
|
+
# Analyze all staged files
|
|
308
|
+
diff = subprocess.run(
|
|
309
|
+
["git", "diff", "--staged"],
|
|
310
|
+
stdout=subprocess.PIPE,
|
|
311
|
+
text=True,
|
|
312
|
+
).stdout
|
|
257
313
|
|
|
258
314
|
if not diff:
|
|
259
315
|
raise KnownError(
|
|
@@ -280,7 +336,18 @@ def analyze_changes(console):
|
|
|
280
336
|
return commit_message
|
|
281
337
|
|
|
282
338
|
|
|
283
|
-
def prompt_commit_message(console, commit_message):
|
|
339
|
+
def prompt_commit_message(console, commit_message, regenerate_callback=None):
|
|
340
|
+
"""Prompt user to select a commit message.
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
console: Rich console for output
|
|
344
|
+
commit_message: List of generated commit messages
|
|
345
|
+
regenerate_callback: Optional function to call when regenerate is selected.
|
|
346
|
+
Should return a new list of commit messages.
|
|
347
|
+
|
|
348
|
+
Returns:
|
|
349
|
+
Selected commit message string, "regenerate" to regenerate, or None if cancelled
|
|
350
|
+
"""
|
|
284
351
|
tag = (
|
|
285
352
|
"Select commit message"
|
|
286
353
|
if len(commit_message) > 1
|
|
@@ -310,9 +377,14 @@ def prompt_commit_message(console, commit_message):
|
|
|
310
377
|
choices = [
|
|
311
378
|
*numbered_choices,
|
|
312
379
|
{"name": " ✏️ Enter custom message", "value": "custom"},
|
|
313
|
-
{"name": " ❌ Cancel", "value": "cancel"}
|
|
314
380
|
]
|
|
315
381
|
|
|
382
|
+
# Add regenerate option if callback is provided
|
|
383
|
+
if regenerate_callback:
|
|
384
|
+
choices.append({"name": " 🔄 Regenerate commit messages", "value": "regenerate"})
|
|
385
|
+
|
|
386
|
+
choices.append({"name": " ❌ Cancel", "value": "cancel"})
|
|
387
|
+
|
|
316
388
|
action = inquirer.fuzzy(
|
|
317
389
|
message=tag,
|
|
318
390
|
style=style,
|
|
@@ -328,6 +400,8 @@ def prompt_commit_message(console, commit_message):
|
|
|
328
400
|
return None
|
|
329
401
|
elif action == "custom":
|
|
330
402
|
return prompt_custom_message(console)
|
|
403
|
+
elif action == "regenerate":
|
|
404
|
+
return "regenerate"
|
|
331
405
|
return action
|
|
332
406
|
|
|
333
407
|
|
|
@@ -360,8 +434,21 @@ def prompt_custom_message(console):
|
|
|
360
434
|
return custom_message
|
|
361
435
|
|
|
362
436
|
|
|
363
|
-
def commit_changes(console, commit, raw_argv):
|
|
364
|
-
|
|
437
|
+
def commit_changes(console, commit, raw_argv, files=None):
|
|
438
|
+
"""Commit changes.
|
|
439
|
+
|
|
440
|
+
Args:
|
|
441
|
+
console: Rich console for output
|
|
442
|
+
commit: Commit message
|
|
443
|
+
raw_argv: Additional git commit arguments
|
|
444
|
+
files: Optional list of specific files to commit. If None, commits all staged files.
|
|
445
|
+
"""
|
|
446
|
+
if files:
|
|
447
|
+
# Commit only specific files
|
|
448
|
+
subprocess.run(["git", "commit", "-m", commit, *raw_argv, "--"] + files)
|
|
449
|
+
else:
|
|
450
|
+
# Commit all staged files
|
|
451
|
+
subprocess.run(["git", "commit", "-m", commit, *raw_argv])
|
|
365
452
|
console.print("\n[bold green]✅ Committed successfully![/bold green]")
|
|
366
453
|
|
|
367
454
|
|
|
@@ -402,8 +489,14 @@ def push_changes(console):
|
|
|
402
489
|
raise KnownError(f"Push failed: {str(e)}")
|
|
403
490
|
|
|
404
491
|
|
|
405
|
-
def prompt_commit_strategy(console, grouped):
|
|
406
|
-
"""Prompt user to choose between global or directory-based commits.
|
|
492
|
+
def prompt_commit_strategy(console, grouped, is_files_mode=False):
|
|
493
|
+
"""Prompt user to choose between global or directory-based commits.
|
|
494
|
+
|
|
495
|
+
Args:
|
|
496
|
+
console: Rich console for output
|
|
497
|
+
grouped: Dictionary of directories and their files
|
|
498
|
+
is_files_mode: If True, directory mode means per-file commits (when --files is used)
|
|
499
|
+
"""
|
|
407
500
|
console.print()
|
|
408
501
|
console.print("╭" + "─" * 60 + "╮", style="bold yellow")
|
|
409
502
|
console.print("│" + " 📂 [bold white]Multiple directories detected[/bold white]".ljust(70) + "│", style="bold yellow")
|
|
@@ -422,13 +515,23 @@ def prompt_commit_strategy(console, grouped):
|
|
|
422
515
|
"answer": "#00d7ff bold"
|
|
423
516
|
}, style_override=False)
|
|
424
517
|
|
|
518
|
+
if is_files_mode:
|
|
519
|
+
# When --files is used, directory mode means per-file commits
|
|
520
|
+
choices = [
|
|
521
|
+
{"name": " 🌐 One commit for all files", "value": False},
|
|
522
|
+
{"name": " 📄 Separate commit for each file", "value": True},
|
|
523
|
+
]
|
|
524
|
+
else:
|
|
525
|
+
# Normal mode: directory mode means per-directory commits
|
|
526
|
+
choices = [
|
|
527
|
+
{"name": " 🌐 One commit for all changes", "value": False},
|
|
528
|
+
{"name": " 📁 Separate commits per directory", "value": True},
|
|
529
|
+
]
|
|
530
|
+
|
|
425
531
|
strategy = inquirer.select(
|
|
426
532
|
message="Commit strategy",
|
|
427
533
|
style=style,
|
|
428
|
-
choices=
|
|
429
|
-
{"name": " 🌐 One commit for all changes", "value": False},
|
|
430
|
-
{"name": " 📁 Separate commits per directory", "value": True},
|
|
431
|
-
],
|
|
534
|
+
choices=choices,
|
|
432
535
|
default=None,
|
|
433
536
|
instruction="(Use arrow keys)",
|
|
434
537
|
qmark="❯"
|
|
@@ -437,15 +540,36 @@ def prompt_commit_strategy(console, grouped):
|
|
|
437
540
|
return strategy
|
|
438
541
|
|
|
439
542
|
|
|
440
|
-
def process_global_commit(console, flags):
|
|
543
|
+
def process_global_commit(console, flags, staged=None):
|
|
441
544
|
"""Process a single global commit for all changes.
|
|
545
|
+
|
|
546
|
+
Args:
|
|
547
|
+
console: Rich console for output
|
|
548
|
+
flags: Commit flags
|
|
549
|
+
staged: Optional staged dict with files. If provided, only commits those files.
|
|
550
|
+
|
|
442
551
|
Returns True if a commit was made, False otherwise."""
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
552
|
+
# If staged dict is provided (e.g., from --files), use only those files
|
|
553
|
+
files_to_commit = staged["files"] if staged and staged.get("files") else None
|
|
554
|
+
|
|
555
|
+
# Regenerate loop
|
|
556
|
+
while True:
|
|
557
|
+
commit_message = analyze_changes(console, files=files_to_commit)
|
|
558
|
+
|
|
559
|
+
# Create regenerate callback
|
|
560
|
+
def regenerate():
|
|
561
|
+
return analyze_changes(console, files=files_to_commit)
|
|
562
|
+
|
|
563
|
+
selected_commit = prompt_commit_message(console, commit_message, regenerate_callback=regenerate)
|
|
564
|
+
|
|
565
|
+
if selected_commit == "regenerate":
|
|
566
|
+
# User wants to regenerate, loop again
|
|
567
|
+
continue
|
|
568
|
+
elif selected_commit:
|
|
569
|
+
commit_changes(console, selected_commit, flags["rawArgv"], files=files_to_commit)
|
|
570
|
+
return True
|
|
571
|
+
else:
|
|
572
|
+
return False
|
|
449
573
|
|
|
450
574
|
|
|
451
575
|
def process_per_directory_commits(console, staged, flags):
|
|
@@ -546,16 +670,336 @@ def process_per_directory_commits(console, staged, flags):
|
|
|
546
670
|
console.print(f"\n[bold yellow]⚠️ No commit message generated for {directory}, skipping[/bold yellow]\n")
|
|
547
671
|
continue
|
|
548
672
|
|
|
549
|
-
# Prompt for commit message selection
|
|
550
|
-
|
|
673
|
+
# Prompt for commit message selection with regenerate option
|
|
674
|
+
while True:
|
|
675
|
+
def regenerate():
|
|
676
|
+
diff = get_diff_for_files(files, flags["excludeFiles"])
|
|
677
|
+
if not diff:
|
|
678
|
+
return []
|
|
679
|
+
import sys
|
|
680
|
+
_stderr = sys.stderr
|
|
681
|
+
_devnull = open(os.devnull, 'w')
|
|
682
|
+
sys.stderr = _devnull
|
|
683
|
+
try:
|
|
684
|
+
msg = generateCommitMessage(diff)
|
|
685
|
+
if isinstance(msg, str):
|
|
686
|
+
msg = msg.split("|")
|
|
687
|
+
return msg
|
|
688
|
+
finally:
|
|
689
|
+
sys.stderr = _stderr
|
|
690
|
+
_devnull.close()
|
|
691
|
+
|
|
692
|
+
selected_commit = prompt_commit_message(console, commit_message, regenerate_callback=regenerate)
|
|
693
|
+
|
|
694
|
+
if selected_commit == "regenerate":
|
|
695
|
+
# Regenerate commit messages
|
|
696
|
+
with console.status(
|
|
697
|
+
f"[magenta]🤖 Regenerating commit messages for {directory}...[/magenta]",
|
|
698
|
+
spinner="dots",
|
|
699
|
+
spinner_style="magenta"
|
|
700
|
+
):
|
|
701
|
+
commit_message = regenerate()
|
|
702
|
+
if not commit_message:
|
|
703
|
+
console.print(f"\n[bold yellow]⚠️ No commit message generated for {directory}, skipping[/bold yellow]\n")
|
|
704
|
+
break
|
|
705
|
+
continue
|
|
706
|
+
elif selected_commit:
|
|
707
|
+
# Commit only the files in this directory
|
|
708
|
+
subprocess.run(["git", "commit", "-m", selected_commit, *flags["rawArgv"], "--"] + files)
|
|
709
|
+
console.print(f"\n[bold green]✅ Committed {directory}[/bold green]")
|
|
710
|
+
commits_made = True
|
|
711
|
+
break
|
|
712
|
+
else:
|
|
713
|
+
console.print(f"\n[bold yellow]⊘ Skipped {directory}[/bold yellow]")
|
|
714
|
+
break
|
|
715
|
+
|
|
716
|
+
return commits_made
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
def process_per_file_commits(console, staged, flags):
|
|
720
|
+
"""Process separate commits for each file when --files is used with directory mode.
|
|
721
|
+
Returns True if at least one commit was made, False otherwise."""
|
|
722
|
+
files = staged["files"]
|
|
723
|
+
commits_made = False
|
|
724
|
+
|
|
725
|
+
# Filter out files with no diff before processing
|
|
726
|
+
files_with_changes = []
|
|
727
|
+
for file in files:
|
|
728
|
+
diff = get_diff_for_files([file], flags["excludeFiles"])
|
|
729
|
+
if diff:
|
|
730
|
+
files_with_changes.append(file)
|
|
731
|
+
|
|
732
|
+
if not files_with_changes:
|
|
733
|
+
console.print("\n[bold yellow]⚠️ No files with changes to commit[/bold yellow]\n")
|
|
734
|
+
return False
|
|
735
|
+
|
|
736
|
+
# If some files were filtered out, show a message
|
|
737
|
+
if len(files_with_changes) < len(files):
|
|
738
|
+
skipped_count = len(files) - len(files_with_changes)
|
|
739
|
+
console.print(f"\n[dim]Skipping {skipped_count} file(s) with no changes[/dim]\n")
|
|
740
|
+
|
|
741
|
+
console.print()
|
|
742
|
+
console.print("╭" + "─" * 60 + "╮", style="bold magenta")
|
|
743
|
+
console.print("│" + f" 🔮 [bold white]Processing {len(files_with_changes)} file(s)[/bold white]".ljust(71) + "│", style="bold magenta")
|
|
744
|
+
console.print("╰" + "─" * 60 + "╯", style="bold magenta")
|
|
745
|
+
console.print()
|
|
746
|
+
|
|
747
|
+
# Ask if user wants to commit all or select specific files
|
|
748
|
+
style = get_style({
|
|
749
|
+
"question": "#00d7ff bold",
|
|
750
|
+
"questionmark": "#00d7ff bold",
|
|
751
|
+
"pointer": "#00d7ff bold",
|
|
752
|
+
"instruction": "#7f7f7f",
|
|
753
|
+
"answer": "#00d7ff bold",
|
|
754
|
+
"checkbox": "#00d7ff bold"
|
|
755
|
+
}, style_override=False)
|
|
756
|
+
|
|
757
|
+
if len(files_with_changes) > 1:
|
|
758
|
+
commit_all = inquirer.confirm(
|
|
759
|
+
message="Commit all files?",
|
|
760
|
+
style=style,
|
|
761
|
+
default=True,
|
|
762
|
+
instruction="(y/n)",
|
|
763
|
+
qmark="❯"
|
|
764
|
+
).execute()
|
|
551
765
|
|
|
552
|
-
if
|
|
553
|
-
|
|
554
|
-
subprocess.run(["git", "commit", "-m", selected_commit, *flags["rawArgv"], "--"] + files)
|
|
555
|
-
console.print(f"\n[bold green]✅ Committed {directory}[/bold green]")
|
|
556
|
-
commits_made = True
|
|
766
|
+
if commit_all:
|
|
767
|
+
selected_files = files_with_changes
|
|
557
768
|
else:
|
|
558
|
-
|
|
769
|
+
# Let user select which files to commit
|
|
770
|
+
file_choices = [
|
|
771
|
+
{"name": file, "value": file}
|
|
772
|
+
for file in files_with_changes
|
|
773
|
+
]
|
|
774
|
+
|
|
775
|
+
selected_files = inquirer.checkbox(
|
|
776
|
+
message="Select files to commit",
|
|
777
|
+
style=style,
|
|
778
|
+
choices=file_choices,
|
|
779
|
+
default=files_with_changes,
|
|
780
|
+
instruction="(Space to select, Enter to confirm)",
|
|
781
|
+
qmark="❯"
|
|
782
|
+
).execute()
|
|
783
|
+
else:
|
|
784
|
+
selected_files = files_with_changes
|
|
785
|
+
|
|
786
|
+
if not selected_files:
|
|
787
|
+
console.print("\n[bold yellow]⚠️ No files selected[/bold yellow]\n")
|
|
788
|
+
return False
|
|
789
|
+
|
|
790
|
+
# Process each selected file (all should have changes since we filtered)
|
|
791
|
+
for idx, file in enumerate(selected_files, 1):
|
|
792
|
+
console.print()
|
|
793
|
+
console.print("┌" + "─" * 60 + "┐", style="bold cyan")
|
|
794
|
+
console.print("│" + f" 📄 [{idx}/{len(selected_files)}] [bold white]{file}[/bold white]".ljust(69) + "│", style="bold cyan")
|
|
795
|
+
console.print("└" + "─" * 60 + "┘", style="bold cyan")
|
|
796
|
+
console.print()
|
|
797
|
+
|
|
798
|
+
# Get diff for this file (should already have changes, but double-check)
|
|
799
|
+
with console.status(
|
|
800
|
+
f"[magenta]🤖 Analyzing {file}...[/magenta]",
|
|
801
|
+
spinner="dots",
|
|
802
|
+
spinner_style="magenta"
|
|
803
|
+
):
|
|
804
|
+
diff = get_diff_for_files([file], flags["excludeFiles"])
|
|
805
|
+
|
|
806
|
+
if not diff:
|
|
807
|
+
console.print(f"\n[bold yellow]⚠️ No diff for {file}, skipping[/bold yellow]\n")
|
|
808
|
+
continue
|
|
809
|
+
|
|
810
|
+
# Suppress stderr during AI call to hide ALTS warnings
|
|
811
|
+
import sys
|
|
812
|
+
_stderr = sys.stderr
|
|
813
|
+
_devnull = open(os.devnull, 'w')
|
|
814
|
+
sys.stderr = _devnull
|
|
815
|
+
|
|
816
|
+
try:
|
|
817
|
+
commit_message = generateCommitMessage(diff)
|
|
818
|
+
finally:
|
|
819
|
+
sys.stderr = _stderr
|
|
820
|
+
_devnull.close()
|
|
821
|
+
|
|
822
|
+
if isinstance(commit_message, str):
|
|
823
|
+
commit_message = commit_message.split("|")
|
|
824
|
+
|
|
825
|
+
if not commit_message:
|
|
826
|
+
console.print(f"\n[bold yellow]⚠️ No commit message generated for {file}, skipping[/bold yellow]\n")
|
|
827
|
+
continue
|
|
828
|
+
|
|
829
|
+
# Prompt for commit message selection with regenerate option
|
|
830
|
+
while True:
|
|
831
|
+
def regenerate():
|
|
832
|
+
diff = get_diff_for_files([file], flags["excludeFiles"])
|
|
833
|
+
if not diff:
|
|
834
|
+
return []
|
|
835
|
+
import sys
|
|
836
|
+
_stderr = sys.stderr
|
|
837
|
+
_devnull = open(os.devnull, 'w')
|
|
838
|
+
sys.stderr = _devnull
|
|
839
|
+
try:
|
|
840
|
+
msg = generateCommitMessage(diff)
|
|
841
|
+
if isinstance(msg, str):
|
|
842
|
+
msg = msg.split("|")
|
|
843
|
+
return msg
|
|
844
|
+
finally:
|
|
845
|
+
sys.stderr = _stderr
|
|
846
|
+
_devnull.close()
|
|
847
|
+
|
|
848
|
+
selected_commit = prompt_commit_message(console, commit_message, regenerate_callback=regenerate)
|
|
849
|
+
|
|
850
|
+
if selected_commit == "regenerate":
|
|
851
|
+
# Regenerate commit messages
|
|
852
|
+
with console.status(
|
|
853
|
+
f"[magenta]🤖 Regenerating commit messages for {file}...[/magenta]",
|
|
854
|
+
spinner="dots",
|
|
855
|
+
spinner_style="magenta"
|
|
856
|
+
):
|
|
857
|
+
commit_message = regenerate()
|
|
858
|
+
if not commit_message:
|
|
859
|
+
console.print(f"\n[bold yellow]⚠️ No commit message generated for {file}, skipping[/bold yellow]\n")
|
|
860
|
+
break
|
|
861
|
+
continue
|
|
862
|
+
elif selected_commit:
|
|
863
|
+
# Commit only this file
|
|
864
|
+
subprocess.run(["git", "commit", "-m", selected_commit, *flags["rawArgv"], "--", file])
|
|
865
|
+
console.print(f"\n[bold green]✅ Committed {file}[/bold green]")
|
|
866
|
+
commits_made = True
|
|
867
|
+
break
|
|
868
|
+
else:
|
|
869
|
+
console.print(f"\n[bold yellow]⊘ Skipped {file}[/bold yellow]")
|
|
870
|
+
break
|
|
871
|
+
|
|
872
|
+
return commits_made
|
|
873
|
+
|
|
874
|
+
|
|
875
|
+
def process_per_directory_commits_from_paths(console, staged, flags, original_paths):
|
|
876
|
+
"""Process separate commits for each directory/path when --files is used with directory mode.
|
|
877
|
+
Groups files by the original paths passed (directories or files).
|
|
878
|
+
Returns True if at least one commit was made, False otherwise."""
|
|
879
|
+
repo_root = assert_git_repo()
|
|
880
|
+
commits_made = False
|
|
881
|
+
|
|
882
|
+
# Group files by the original paths they came from
|
|
883
|
+
path_to_files = {}
|
|
884
|
+
for path in original_paths:
|
|
885
|
+
normalized_path = os.path.normpath(path)
|
|
886
|
+
full_path = os.path.join(repo_root, normalized_path) if not os.path.isabs(path) else path
|
|
887
|
+
|
|
888
|
+
if os.path.isdir(full_path):
|
|
889
|
+
# It's a directory - find all files that belong to this directory
|
|
890
|
+
dir_files = [f for f in staged["files"] if f.startswith(normalized_path + os.sep) or f == normalized_path]
|
|
891
|
+
if dir_files:
|
|
892
|
+
path_to_files[normalized_path] = dir_files
|
|
893
|
+
else:
|
|
894
|
+
# It's a file - add it directly
|
|
895
|
+
if normalized_path in staged["files"]:
|
|
896
|
+
path_to_files[normalized_path] = [normalized_path]
|
|
897
|
+
|
|
898
|
+
if not path_to_files:
|
|
899
|
+
console.print("\n[bold yellow]⚠️ No files found for the specified paths[/bold yellow]\n")
|
|
900
|
+
return False
|
|
901
|
+
|
|
902
|
+
# Filter out paths with no changes
|
|
903
|
+
paths_with_changes = {}
|
|
904
|
+
for path, files in path_to_files.items():
|
|
905
|
+
diff = get_diff_for_files(files, flags["excludeFiles"])
|
|
906
|
+
if diff:
|
|
907
|
+
paths_with_changes[path] = files
|
|
908
|
+
|
|
909
|
+
if not paths_with_changes:
|
|
910
|
+
console.print("\n[bold yellow]⚠️ No paths with changes to commit[/bold yellow]\n")
|
|
911
|
+
return False
|
|
912
|
+
|
|
913
|
+
console.print()
|
|
914
|
+
console.print("╭" + "─" * 60 + "╮", style="bold magenta")
|
|
915
|
+
console.print("│" + f" 🔮 [bold white]Processing {len(paths_with_changes)} path(s)[/bold white]".ljust(71) + "│", style="bold magenta")
|
|
916
|
+
console.print("╰" + "─" * 60 + "╯", style="bold magenta")
|
|
917
|
+
console.print()
|
|
918
|
+
|
|
919
|
+
# Process each path
|
|
920
|
+
for idx, (path, files) in enumerate(paths_with_changes.items(), 1):
|
|
921
|
+
console.print()
|
|
922
|
+
console.print("┌" + "─" * 60 + "┐", style="bold cyan")
|
|
923
|
+
console.print("│" + f" 📂 [{idx}/{len(paths_with_changes)}] [bold white]{path}[/bold white]".ljust(69) + "│", style="bold cyan")
|
|
924
|
+
console.print("└" + "─" * 60 + "┘", style="bold cyan")
|
|
925
|
+
console.print()
|
|
926
|
+
|
|
927
|
+
for file in files:
|
|
928
|
+
console.print(f" [cyan]▸[/cyan] [white]{file}[/white]")
|
|
929
|
+
|
|
930
|
+
# Get diff for this path's files
|
|
931
|
+
with console.status(
|
|
932
|
+
f"[magenta]🤖 Analyzing {path}...[/magenta]",
|
|
933
|
+
spinner="dots",
|
|
934
|
+
spinner_style="magenta"
|
|
935
|
+
):
|
|
936
|
+
diff = get_diff_for_files(files, flags["excludeFiles"])
|
|
937
|
+
|
|
938
|
+
if not diff:
|
|
939
|
+
console.print(f"\n[bold yellow]⚠️ No diff for {path}, skipping[/bold yellow]\n")
|
|
940
|
+
continue
|
|
941
|
+
|
|
942
|
+
# Suppress stderr during AI call to hide ALTS warnings
|
|
943
|
+
import sys
|
|
944
|
+
_stderr = sys.stderr
|
|
945
|
+
_devnull = open(os.devnull, 'w')
|
|
946
|
+
sys.stderr = _devnull
|
|
947
|
+
|
|
948
|
+
try:
|
|
949
|
+
commit_message = generateCommitMessage(diff)
|
|
950
|
+
finally:
|
|
951
|
+
sys.stderr = _stderr
|
|
952
|
+
_devnull.close()
|
|
953
|
+
|
|
954
|
+
if isinstance(commit_message, str):
|
|
955
|
+
commit_message = commit_message.split("|")
|
|
956
|
+
|
|
957
|
+
if not commit_message:
|
|
958
|
+
console.print(f"\n[bold yellow]⚠️ No commit message generated for {path}, skipping[/bold yellow]\n")
|
|
959
|
+
continue
|
|
960
|
+
|
|
961
|
+
# Prompt for commit message selection with regenerate option
|
|
962
|
+
while True:
|
|
963
|
+
def regenerate():
|
|
964
|
+
diff = get_diff_for_files(files, flags["excludeFiles"])
|
|
965
|
+
if not diff:
|
|
966
|
+
return []
|
|
967
|
+
import sys
|
|
968
|
+
_stderr = sys.stderr
|
|
969
|
+
_devnull = open(os.devnull, 'w')
|
|
970
|
+
sys.stderr = _devnull
|
|
971
|
+
try:
|
|
972
|
+
msg = generateCommitMessage(diff)
|
|
973
|
+
if isinstance(msg, str):
|
|
974
|
+
msg = msg.split("|")
|
|
975
|
+
return msg
|
|
976
|
+
finally:
|
|
977
|
+
sys.stderr = _stderr
|
|
978
|
+
_devnull.close()
|
|
979
|
+
|
|
980
|
+
selected_commit = prompt_commit_message(console, commit_message, regenerate_callback=regenerate)
|
|
981
|
+
|
|
982
|
+
if selected_commit == "regenerate":
|
|
983
|
+
# Regenerate commit messages
|
|
984
|
+
with console.status(
|
|
985
|
+
f"[magenta]🤖 Regenerating commit messages for {path}...[/magenta]",
|
|
986
|
+
spinner="dots",
|
|
987
|
+
spinner_style="magenta"
|
|
988
|
+
):
|
|
989
|
+
commit_message = regenerate()
|
|
990
|
+
if not commit_message:
|
|
991
|
+
console.print(f"\n[bold yellow]⚠️ No commit message generated for {path}, skipping[/bold yellow]\n")
|
|
992
|
+
break
|
|
993
|
+
continue
|
|
994
|
+
elif selected_commit:
|
|
995
|
+
# Commit only the files for this path
|
|
996
|
+
subprocess.run(["git", "commit", "-m", selected_commit, *flags["rawArgv"], "--"] + files)
|
|
997
|
+
console.print(f"\n[bold green]✅ Committed {path}[/bold green]")
|
|
998
|
+
commits_made = True
|
|
999
|
+
break
|
|
1000
|
+
else:
|
|
1001
|
+
console.print(f"\n[bold yellow]⊘ Skipped {path}[/bold yellow]")
|
|
1002
|
+
break
|
|
559
1003
|
|
|
560
1004
|
return commits_made
|
|
561
1005
|
|
|
@@ -56,13 +56,8 @@ def get_default_excludes() -> List[str]:
|
|
|
56
56
|
except:
|
|
57
57
|
pass
|
|
58
58
|
|
|
59
|
-
#
|
|
60
|
-
return [
|
|
61
|
-
'package-lock.json',
|
|
62
|
-
'pnpm-lock.yaml',
|
|
63
|
-
'yarn.lock',
|
|
64
|
-
'*.lock'
|
|
65
|
-
]
|
|
59
|
+
# No default exclusions; rely entirely on user configuration.
|
|
60
|
+
return []
|
|
66
61
|
|
|
67
62
|
|
|
68
63
|
# Get default files to exclude (can be overridden via config)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|