devcommit 0.1.4.7__tar.gz → 0.1.4.8__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devcommit
3
- Version: 0.1.4.7
3
+ Version: 0.1.4.8
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
- grouped = group_files_by_directory(staged["files"])
173
- if len(grouped) > 1:
174
- use_per_directory = prompt_commit_strategy(console, grouped)
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
- commit_made = process_per_directory_commits(console, staged, flags)
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
- commit_made = process_global_commit(console, flags)
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:
@@ -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
- diff = subprocess.run(
253
- ["git", "diff", "--staged"],
254
- stdout=subprocess.PIPE,
255
- text=True,
256
- ).stdout
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
- subprocess.run(["git", "commit", "-m", commit, *raw_argv])
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
- commit_message = analyze_changes(console)
444
- selected_commit = prompt_commit_message(console, commit_message)
445
- if selected_commit:
446
- commit_changes(console, selected_commit, flags["rawArgv"])
447
- return True
448
- return False
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
- selected_commit = prompt_commit_message(console, commit_message)
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 selected_commit:
553
- # Commit only the files in this directory
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
- console.print(f"\n[bold yellow]⊘ Skipped {directory}[/bold yellow]")
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
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "devcommit"
3
- version = "0.1.4.7"
3
+ version = "0.1.4.8"
4
4
  description = "AI-powered git commit message generator"
5
5
  readme = "README.md"
6
6
  license = {file = "COPYING"}
File without changes