auto-coder 0.1.259__py3-none-any.whl → 0.1.260__py3-none-any.whl

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.

Potentially problematic release.


This version of auto-coder might be problematic. Click here for more details.

@@ -1,6 +1,9 @@
1
1
  from prompt_toolkit.completion import Completer, Completion, CompleteEvent
2
2
  from prompt_toolkit.document import Document
3
3
  import pydantic
4
+ from typing import Callable,Dict,Any
5
+ from pydantic import BaseModel,SkipValidation
6
+ from autocoder.common import AutoCoderArgs
4
7
  import os
5
8
 
6
9
  COMMANDS = {
@@ -38,6 +41,8 @@ COMMANDS = {
38
41
  "/speed-test": "",
39
42
  "/input_price": "",
40
43
  "/output_price": "",
44
+ },
45
+ "/auto": {
41
46
  }
42
47
  }
43
48
 
@@ -48,6 +53,24 @@ class Tag(pydantic.BaseModel):
48
53
  end_tag: str
49
54
 
50
55
 
56
+ class FileSystemModel(pydantic.BaseModel):
57
+ project_root: str
58
+ get_all_file_names_in_project: SkipValidation[Callable]
59
+ get_all_file_in_project: SkipValidation[Callable]
60
+ get_all_dir_names_in_project: SkipValidation[Callable]
61
+ get_all_file_in_project_with_dot: SkipValidation[Callable]
62
+ get_symbol_list: SkipValidation[Callable]
63
+
64
+ class MemoryConfig(BaseModel):
65
+ """
66
+ A model to encapsulate memory configuration and operations.
67
+ """
68
+ memory: Dict[str, Any]
69
+ save_memory_func: SkipValidation[Callable]
70
+
71
+ class Config:
72
+ arbitrary_types_allowed = True
73
+
51
74
  class CommandTextParser:
52
75
  def __init__(self, text: str, command: str):
53
76
  self.text = text
@@ -322,3 +345,546 @@ class CommandTextParser:
322
345
  self.consume_tag()
323
346
  else:
324
347
  self.consume_coding_value()
348
+
349
+
350
+ class CommandCompleter(Completer):
351
+ def __init__(self, commands, file_system_model: FileSystemModel, memory_model: MemoryConfig):
352
+ self.commands = commands
353
+ self.file_system_model = file_system_model
354
+ self.memory_model = memory_model
355
+ self.all_file_names = file_system_model.get_all_file_names_in_project()
356
+ self.all_files = file_system_model.get_all_file_in_project()
357
+ self.all_dir_names = file_system_model.get_all_dir_names_in_project()
358
+ self.all_files_with_dot = file_system_model.get_all_file_in_project_with_dot()
359
+ self.symbol_list = file_system_model.get_symbol_list()
360
+ self.current_file_names = []
361
+
362
+ def get_completions(self, document, complete_event):
363
+ text = document.text_before_cursor
364
+ words = text.split()
365
+
366
+ if len(words) > 0:
367
+ if words[0] == "/mode":
368
+ left_word = text[len("/mode"):]
369
+ for mode in ["normal", "auto_detect", "voice_input"]:
370
+ if mode.startswith(left_word.strip()):
371
+ yield Completion(mode, start_position=-len(left_word.strip()))
372
+
373
+ if words[0] == "/add_files":
374
+ new_text = text[len("/add_files"):]
375
+ parser = CommandTextParser(new_text, words[0])
376
+ parser.add_files()
377
+ current_word = parser.current_word()
378
+
379
+ if parser.last_sub_command() == "/refresh":
380
+ return
381
+
382
+ for command in parser.get_sub_commands():
383
+ if command.startswith(current_word):
384
+ yield Completion(command, start_position=-len(current_word))
385
+
386
+ if parser.first_sub_command() == "/group" and (
387
+ parser.last_sub_command() == "/group"
388
+ or parser.last_sub_command() == "/drop"
389
+ ):
390
+ group_names = self.memory_model.memory["current_files"]["groups"].keys()
391
+ if "," in current_word:
392
+ current_word = current_word.split(",")[-1]
393
+
394
+ for group_name in group_names:
395
+ if group_name.startswith(current_word):
396
+ yield Completion(
397
+ group_name, start_position=-len(current_word)
398
+ )
399
+
400
+ if parser.first_sub_command() != "/group":
401
+ if current_word and current_word.startswith("."):
402
+ for file_name in self.all_files_with_dot:
403
+ if file_name.startswith(current_word):
404
+ yield Completion(
405
+ file_name, start_position=-
406
+ len(current_word)
407
+ )
408
+ else:
409
+ for file_name in self.all_file_names:
410
+ if file_name.startswith(current_word):
411
+ yield Completion(
412
+ file_name, start_position=-
413
+ len(current_word)
414
+ )
415
+ for file_name in self.all_files:
416
+ if current_word and current_word in file_name:
417
+ yield Completion(
418
+ file_name, start_position=-
419
+ len(current_word)
420
+ )
421
+ elif words[0] == "/remove_files":
422
+ new_words = text[len("/remove_files"):].strip().split(",")
423
+
424
+ is_at_space = text[-1] == " "
425
+ last_word = new_words[-2] if len(new_words) > 1 else ""
426
+ current_word = new_words[-1] if new_words else ""
427
+
428
+ if is_at_space:
429
+ last_word = current_word
430
+ current_word = ""
431
+
432
+ # /remove_files /all [cursor] or /remove_files /all p[cursor]
433
+ if not last_word and not current_word:
434
+ if "/all".startswith(current_word):
435
+ yield Completion("/all", start_position=-len(current_word))
436
+ for file_name in self.current_file_names:
437
+ yield Completion(file_name, start_position=-len(current_word))
438
+
439
+ # /remove_files /a[cursor] or /remove_files p[cursor]
440
+ if current_word:
441
+ if "/all".startswith(current_word):
442
+ yield Completion("/all", start_position=-len(current_word))
443
+ for file_name in self.current_file_names:
444
+ if current_word and current_word in file_name:
445
+ yield Completion(
446
+ file_name, start_position=-len(current_word)
447
+ )
448
+ elif words[0] == "/exclude_dirs":
449
+ new_words = text[len("/exclude_dirs"):].strip().split(",")
450
+ current_word = new_words[-1]
451
+
452
+ for file_name in self.all_dir_names:
453
+ if current_word and current_word in file_name:
454
+ yield Completion(file_name, start_position=-len(current_word))
455
+
456
+ elif words[0] == "/lib":
457
+ new_text = text[len("/lib"):]
458
+ parser = CommandTextParser(new_text, words[0])
459
+ parser.lib()
460
+ current_word = parser.current_word()
461
+
462
+ for command in parser.get_sub_commands():
463
+ if command.startswith(current_word):
464
+ yield Completion(command, start_position=-len(current_word))
465
+
466
+ if parser.last_sub_command() in ["/add", "/remove", "/get"]:
467
+ for lib_name in self.memory_model.memory.get("libs", {}).keys():
468
+ if lib_name.startswith(current_word):
469
+ yield Completion(
470
+ lib_name, start_position=-len(current_word)
471
+ )
472
+ elif words[0] == "/mcp":
473
+ new_text = text[len("/mcp"):]
474
+ parser = CommandTextParser(new_text, words[0])
475
+ parser.lib()
476
+ current_word = parser.current_word()
477
+ for command in parser.get_sub_commands():
478
+ if command.startswith(current_word):
479
+ yield Completion(command, start_position=-len(current_word))
480
+ elif words[0] == "/models":
481
+ new_text = text[len("/models"):]
482
+ parser = CommandTextParser(new_text, words[0])
483
+ parser.lib()
484
+ current_word = parser.current_word()
485
+ for command in parser.get_sub_commands():
486
+ if command.startswith(current_word):
487
+ yield Completion(command, start_position=-len(current_word))
488
+
489
+ elif words[0] == "/coding":
490
+ new_text = text[len("/coding"):]
491
+ parser = CommandTextParser(new_text, words[0])
492
+ parser.lib()
493
+ current_word = parser.current_word()
494
+ for command in parser.get_sub_commands():
495
+ if command.startswith(current_word):
496
+ yield Completion(command, start_position=-len(current_word))
497
+
498
+ elif words[0] == "/conf":
499
+ new_words = text[len("/conf"):].strip().split()
500
+ is_at_space = text[-1] == " "
501
+ last_word = new_words[-2] if len(new_words) > 1 else ""
502
+ current_word = new_words[-1] if new_words else ""
503
+ completions = []
504
+
505
+ if is_at_space:
506
+ last_word = current_word
507
+ current_word = ""
508
+
509
+ # /conf /drop [curor] or /conf /drop p[cursor]
510
+ if last_word == "/drop":
511
+ completions = [
512
+ field_name
513
+ for field_name in self.memory_model.memory["conf"].keys()
514
+ if field_name.startswith(current_word)
515
+ ]
516
+ # /conf [curosr]
517
+ elif not last_word and not current_word:
518
+ completions = [
519
+ "/drop"] if "/drop".startswith(current_word) else []
520
+ completions += [
521
+ field_name + ":"
522
+ for field_name in AutoCoderArgs.model_fields.keys()
523
+ if field_name.startswith(current_word)
524
+ ]
525
+ # /conf p[cursor]
526
+ elif not last_word and current_word:
527
+ completions = [
528
+ "/drop"] if "/drop".startswith(current_word) else []
529
+ completions += [
530
+ field_name + ":"
531
+ for field_name in AutoCoderArgs.model_fields.keys()
532
+ if field_name.startswith(current_word)
533
+ ]
534
+
535
+ for completion in completions:
536
+ yield Completion(completion, start_position=-len(current_word))
537
+ elif words[0] in ["/chat", "/coding","/auto"]:
538
+ image_extensions = (
539
+ ".png",
540
+ ".jpg",
541
+ ".jpeg",
542
+ ".gif",
543
+ ".bmp",
544
+ ".tiff",
545
+ ".tif",
546
+ ".webp",
547
+ ".svg",
548
+ ".ico",
549
+ ".heic",
550
+ ".heif",
551
+ ".raw",
552
+ ".cr2",
553
+ ".nef",
554
+ ".arw",
555
+ ".dng",
556
+ ".orf",
557
+ ".rw2",
558
+ ".pef",
559
+ ".srw",
560
+ ".eps",
561
+ ".ai",
562
+ ".psd",
563
+ ".xcf",
564
+ )
565
+ new_text = text[len(words[0]):]
566
+ parser = CommandTextParser(new_text, words[0])
567
+
568
+ parser.coding()
569
+ current_word = parser.current_word()
570
+
571
+ if len(new_text.strip()) == 0 or new_text.strip() == "/":
572
+ for command in parser.get_sub_commands():
573
+ if command.startswith(current_word):
574
+ yield Completion(command, start_position=-len(current_word))
575
+
576
+ all_tags = parser.tags
577
+
578
+ if current_word.startswith("@"):
579
+ name = current_word[1:]
580
+ target_set = set()
581
+
582
+ for file_name in self.current_file_names:
583
+ base_file_name = os.path.basename(file_name)
584
+ if name in base_file_name:
585
+ target_set.add(base_file_name)
586
+ path_parts = file_name.split(os.sep)
587
+ display_name = (
588
+ os.sep.join(path_parts[-3:])
589
+ if len(path_parts) > 3
590
+ else file_name
591
+ )
592
+ relative_path = os.path.relpath(
593
+ file_name, self.file_system_model.project_root)
594
+ yield Completion(
595
+ relative_path,
596
+ start_position=-len(name),
597
+ display=f"{display_name} (in active files)",
598
+ )
599
+
600
+ for file_name in self.all_file_names:
601
+ if file_name.startswith(name) and file_name not in target_set:
602
+ target_set.add(file_name)
603
+
604
+ path_parts = file_name.split(os.sep)
605
+ display_name = (
606
+ os.sep.join(path_parts[-3:])
607
+ if len(path_parts) > 3
608
+ else file_name
609
+ )
610
+ relative_path = os.path.relpath(
611
+ file_name, self.file_system_model.project_root)
612
+
613
+ yield Completion(
614
+ relative_path,
615
+ start_position=-len(name),
616
+ display=f"{display_name}",
617
+ )
618
+
619
+ for file_name in self.all_files:
620
+ if name in file_name and file_name not in target_set:
621
+ path_parts = file_name.split(os.sep)
622
+ display_name = (
623
+ os.sep.join(path_parts[-3:])
624
+ if len(path_parts) > 3
625
+ else file_name
626
+ )
627
+ relative_path = os.path.relpath(
628
+ file_name, self.file_system_model.project_root)
629
+ yield Completion(
630
+ relative_path,
631
+ start_position=-len(name),
632
+ display=f"{display_name}",
633
+ )
634
+
635
+ if current_word.startswith("@@"):
636
+ name = current_word[2:]
637
+ for symbol in self.symbol_list:
638
+ if name in symbol.symbol_name:
639
+ file_name = symbol.file_name
640
+ path_parts = file_name.split(os.sep)
641
+ display_name = (
642
+ os.sep.join(path_parts[-3:])
643
+ if len(path_parts) > 3
644
+ else symbol.symbol_name
645
+ )
646
+ relative_path = os.path.relpath(
647
+ file_name, self.file_system_model.project_root)
648
+ yield Completion(
649
+ f"{symbol.symbol_name}(location: {relative_path})",
650
+ start_position=-len(name),
651
+ display=f"{symbol.symbol_name} ({display_name}/{symbol.symbol_type})",
652
+ )
653
+
654
+ tags = [tag for tag in parser.tags]
655
+
656
+ if current_word.startswith("<"):
657
+ name = current_word[1:]
658
+ for tag in ["<img>", "</img>"]:
659
+ if all_tags and all_tags[-1].start_tag == "<img>":
660
+ if tag.startswith(name):
661
+ yield Completion(
662
+ "</img>", start_position=-len(current_word)
663
+ )
664
+ elif tag.startswith(name):
665
+ yield Completion(tag, start_position=-len(current_word))
666
+
667
+ if tags and tags[-1].start_tag == "<img>" and tags[-1].end_tag == "":
668
+ raw_file_name = tags[0].content
669
+ file_name = raw_file_name.strip()
670
+ parent_dir = os.path.dirname(file_name)
671
+ file_basename = os.path.basename(file_name)
672
+ search_dir = parent_dir if parent_dir else "."
673
+ for root, dirs, files in os.walk(search_dir):
674
+ # 只处理直接子目录
675
+ if root != search_dir:
676
+ continue
677
+
678
+ # 补全子目录
679
+ for dir in dirs:
680
+ full_path = os.path.join(root, dir)
681
+ if full_path.startswith(file_name):
682
+ relative_path = os.path.relpath(
683
+ full_path, search_dir)
684
+ yield Completion(
685
+ relative_path,
686
+ start_position=-len(file_basename),
687
+ )
688
+
689
+ # 补全文件
690
+ for file in files:
691
+ if file.lower().endswith(
692
+ image_extensions
693
+ ) and file.startswith(file_basename):
694
+ full_path = os.path.join(root, file)
695
+ relative_path = os.path.relpath(
696
+ full_path, search_dir)
697
+ yield Completion(
698
+ relative_path,
699
+ start_position=-len(file_basename),
700
+ )
701
+
702
+ # 只处理一层子目录,然后退出循环
703
+ break
704
+
705
+ elif not words[0].startswith("/"):
706
+ image_extensions = (
707
+ ".png",
708
+ ".jpg",
709
+ ".jpeg",
710
+ ".gif",
711
+ ".bmp",
712
+ ".tiff",
713
+ ".tif",
714
+ ".webp",
715
+ ".svg",
716
+ ".ico",
717
+ ".heic",
718
+ ".heif",
719
+ ".raw",
720
+ ".cr2",
721
+ ".nef",
722
+ ".arw",
723
+ ".dng",
724
+ ".orf",
725
+ ".rw2",
726
+ ".pef",
727
+ ".srw",
728
+ ".eps",
729
+ ".ai",
730
+ ".psd",
731
+ ".xcf",
732
+ )
733
+ new_text = text
734
+ parser = CommandTextParser(new_text, "/auto")
735
+
736
+ parser.coding()
737
+ current_word = parser.current_word()
738
+
739
+ if len(new_text.strip()) == 0 or new_text.strip() == "/":
740
+ for command in parser.get_sub_commands():
741
+ if command.startswith(current_word):
742
+ yield Completion(command, start_position=-len(current_word))
743
+
744
+ all_tags = parser.tags
745
+
746
+ if current_word.startswith("@"):
747
+ name = current_word[1:]
748
+ target_set = set()
749
+
750
+ for file_name in self.current_file_names:
751
+ base_file_name = os.path.basename(file_name)
752
+ if name in base_file_name:
753
+ target_set.add(base_file_name)
754
+ path_parts = file_name.split(os.sep)
755
+ display_name = (
756
+ os.sep.join(path_parts[-3:])
757
+ if len(path_parts) > 3
758
+ else file_name
759
+ )
760
+ relative_path = os.path.relpath(
761
+ file_name, self.file_system_model.project_root)
762
+ yield Completion(
763
+ relative_path,
764
+ start_position=-len(name),
765
+ display=f"{display_name} (in active files)",
766
+ )
767
+
768
+ for file_name in self.all_file_names:
769
+ if file_name.startswith(name) and file_name not in target_set:
770
+ target_set.add(file_name)
771
+
772
+ path_parts = file_name.split(os.sep)
773
+ display_name = (
774
+ os.sep.join(path_parts[-3:])
775
+ if len(path_parts) > 3
776
+ else file_name
777
+ )
778
+ relative_path = os.path.relpath(
779
+ file_name, self.file_system_model.project_root)
780
+
781
+ yield Completion(
782
+ relative_path,
783
+ start_position=-len(name),
784
+ display=f"{display_name}",
785
+ )
786
+
787
+ for file_name in self.all_files:
788
+ if name in file_name and file_name not in target_set:
789
+ path_parts = file_name.split(os.sep)
790
+ display_name = (
791
+ os.sep.join(path_parts[-3:])
792
+ if len(path_parts) > 3
793
+ else file_name
794
+ )
795
+ relative_path = os.path.relpath(
796
+ file_name, self.file_system_model.project_root)
797
+ yield Completion(
798
+ relative_path,
799
+ start_position=-len(name),
800
+ display=f"{display_name}",
801
+ )
802
+
803
+ if current_word.startswith("@@"):
804
+ name = current_word[2:]
805
+ for symbol in self.symbol_list:
806
+ if name in symbol.symbol_name:
807
+ file_name = symbol.file_name
808
+ path_parts = file_name.split(os.sep)
809
+ display_name = (
810
+ os.sep.join(path_parts[-3:])
811
+ if len(path_parts) > 3
812
+ else symbol.symbol_name
813
+ )
814
+ relative_path = os.path.relpath(
815
+ file_name, self.file_system_model.project_root)
816
+ yield Completion(
817
+ f"{symbol.symbol_name}(location: {relative_path})",
818
+ start_position=-len(name),
819
+ display=f"{symbol.symbol_name} ({display_name}/{symbol.symbol_type})",
820
+ )
821
+
822
+ tags = [tag for tag in parser.tags]
823
+
824
+ if current_word.startswith("<"):
825
+ name = current_word[1:]
826
+ for tag in ["<img>", "</img>"]:
827
+ if all_tags and all_tags[-1].start_tag == "<img>":
828
+ if tag.startswith(name):
829
+ yield Completion(
830
+ "</img>", start_position=-len(current_word)
831
+ )
832
+ elif tag.startswith(name):
833
+ yield Completion(tag, start_position=-len(current_word))
834
+
835
+ if tags and tags[-1].start_tag == "<img>" and tags[-1].end_tag == "":
836
+ raw_file_name = tags[0].content
837
+ file_name = raw_file_name.strip()
838
+ parent_dir = os.path.dirname(file_name)
839
+ file_basename = os.path.basename(file_name)
840
+ search_dir = parent_dir if parent_dir else "."
841
+ for root, dirs, files in os.walk(search_dir):
842
+ # 只处理直接子目录
843
+ if root != search_dir:
844
+ continue
845
+
846
+ # 补全子目录
847
+ for dir in dirs:
848
+ full_path = os.path.join(root, dir)
849
+ if full_path.startswith(file_name):
850
+ relative_path = os.path.relpath(
851
+ full_path, search_dir)
852
+ yield Completion(
853
+ relative_path,
854
+ start_position=-len(file_basename),
855
+ )
856
+
857
+ # 补全文件
858
+ for file in files:
859
+ if file.lower().endswith(
860
+ image_extensions
861
+ ) and file.startswith(file_basename):
862
+ full_path = os.path.join(root, file)
863
+ relative_path = os.path.relpath(
864
+ full_path, search_dir)
865
+ yield Completion(
866
+ relative_path,
867
+ start_position=-len(file_basename),
868
+ )
869
+
870
+ # 只处理一层子目录,然后退出循环
871
+ break
872
+ else:
873
+ for command in self.commands:
874
+ if command.startswith(text):
875
+ yield Completion(command, start_position=-len(text))
876
+
877
+ else:
878
+ for command in self.commands:
879
+ if command.startswith(text):
880
+ yield Completion(command, start_position=-len(text))
881
+
882
+ def update_current_files(self, files):
883
+ self.current_file_names = [f for f in files]
884
+
885
+ def refresh_files(self):
886
+ self.all_file_names = self.file_system_model.get_all_file_names_in_project()
887
+ self.all_files = self.file_system_model.get_all_file_in_project()
888
+ self.all_dir_names = self.file_system_model.get_all_dir_names_in_project()
889
+ self.all_files_with_dot = self.file_system_model.get_all_file_in_project_with_dot()
890
+ self.symbol_list = self.file_system_model.get_symbol_list()
@@ -1,7 +1,7 @@
1
1
  import os
2
2
  from git import Repo, GitCommandError
3
3
  from loguru import logger
4
- from typing import List, Optional
4
+ from typing import List, Optional, Dict
5
5
  from pydantic import BaseModel
6
6
  import byzerllm
7
7
  from rich.console import Console
@@ -11,6 +11,16 @@ from rich.table import Table
11
11
  from rich.text import Text
12
12
 
13
13
 
14
+ class FileChange(BaseModel):
15
+ file_path: str
16
+ before: Optional[str] = None
17
+ after: Optional[str] = None
18
+
19
+ class CommitChangesResult(BaseModel):
20
+ success: bool
21
+ changes: Dict[str, FileChange] = {}
22
+ error_message: Optional[str] = None
23
+
14
24
  class CommitResult(BaseModel):
15
25
  success: bool
16
26
  commit_message: Optional[str] = None
@@ -605,6 +615,77 @@ def generate_commit_message(changes_report: str) -> str:
605
615
  请输出commit message, 不要输出任何其他内容.
606
616
  '''
607
617
 
618
+ def get_commit_by_message(repo_path: str, message: str):
619
+ repo = get_repo(repo_path)
620
+ try:
621
+ commit_hash = repo.git.log("--all", f"--grep={message}", "--format=%H", "-n", "1")
622
+ if not commit_hash:
623
+ return None
624
+ return repo.commit(commit_hash.strip())
625
+ except GitCommandError as e:
626
+ logger.error(f"Error finding commit: {e}")
627
+ return None
628
+
629
+ def get_changes_by_commit_message(repo_path: str, message: str) -> CommitChangesResult:
630
+ """
631
+ 根据提交信息查找对应的变更内容
632
+
633
+ Args:
634
+ repo_path: Git仓库路径
635
+ message: 提交信息
636
+
637
+ Returns:
638
+ CommitChangesResult: 包含变更前后内容的字典,键为文件路径
639
+ """
640
+ try:
641
+ if repo_path:
642
+ repo = get_repo(repo_path)
643
+ else:
644
+ repo = get_repo(os.getcwd())
645
+ commit = get_commit_by_message(repo_path, message)
646
+
647
+ if not commit:
648
+ return CommitChangesResult(success=False, error_message="Commit not found")
649
+
650
+ changes = {}
651
+
652
+ # 比较当前commit与其父commit的差异
653
+ for diff_item in commit.parents[0].diff(commit):
654
+ file_path = diff_item.a_path if diff_item.a_path else diff_item.b_path
655
+
656
+ # 获取变更前内容
657
+ before_content = None
658
+ try:
659
+ if diff_item.a_blob:
660
+ before_content = repo.git.show(f"{commit.parents[0].hexsha}:{file_path}")
661
+ except GitCommandError:
662
+ pass # 文件可能是新增的
663
+
664
+ # 获取变更后内容
665
+ after_content = None
666
+ try:
667
+ if diff_item.b_blob:
668
+ after_content = repo.git.show(f"{commit.hexsha}:{file_path}")
669
+ except GitCommandError:
670
+ pass # 文件可能被删除
671
+
672
+ changes[file_path] = FileChange(
673
+ file_path=file_path,
674
+ before=before_content,
675
+ after=after_content
676
+ )
677
+
678
+ return CommitChangesResult(success=True, changes=changes)
679
+
680
+ except GitCommandError as e:
681
+ logger.error(f"Error retrieving changes: {e}")
682
+ return CommitChangesResult(success=False, error_message=str(e))
683
+ except IndexError:
684
+ return CommitChangesResult(success=False, error_message="Initial commit has no parent")
685
+ except Exception as e:
686
+ logger.error(f"Unexpected error: {e}")
687
+ return CommitChangesResult(success=False, error_message=str(e))
688
+
608
689
  def print_commit_info(commit_result: CommitResult):
609
690
  console = Console()
610
691
  table = Table(