gac 2.2.0__py3-none-any.whl → 2.3.0__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 gac might be problematic. Click here for more details.

gac/prompt.py CHANGED
@@ -246,30 +246,30 @@ DEFAULT_USER_TEMPLATE = """<hint>
246
246
  Additional context provided by the user: <hint_text></hint_text>
247
247
  </hint>
248
248
 
249
- <git_status>
250
- <status></status>
251
- </git_status>
249
+ <git_diff>
250
+ <diff></diff>
251
+ </git_diff>
252
252
 
253
253
  <git_diff_stat>
254
254
  <diff_stat></diff_stat>
255
255
  </git_diff_stat>
256
256
 
257
- <git_diff>
258
- <diff></diff>
259
- </git_diff>
257
+ <git_status>
258
+ <status></status>
259
+ </git_status>
260
+
261
+ <language_instructions>
262
+ IMPORTANT: You MUST write the entire commit message in <language_name></language_name>.
263
+ All text in the commit message, including the summary line and body, must be in <language_name></language_name>.
264
+ <prefix_instruction></prefix_instruction>
265
+ </language_instructions>
260
266
 
261
- <instructions>
267
+ <format_instructions>
262
268
  IMMEDIATELY AFTER ANALYZING THE CHANGES, RESPOND WITH ONLY THE COMMIT MESSAGE.
263
269
  DO NOT include any preamble, reasoning, explanations or anything other than the commit message itself.
264
270
  DO NOT use markdown formatting, headers, or code blocks.
265
271
  The entire response will be passed directly to 'git commit -m'.
266
-
267
- <language>
268
- IMPORTANT: You MUST write the entire commit message in <language_name></language_name>.
269
- All text in the commit message, including the summary line and body, must be in <language_name></language_name>.
270
- <prefix_instruction></prefix_instruction>
271
- </language>
272
- </instructions>"""
272
+ </format_instructions>"""
273
273
 
274
274
 
275
275
  # ============================================================================
@@ -518,7 +518,7 @@ The ENTIRE commit message, including the prefix, must be in {language}."""
518
518
 
519
519
  user_template = user_template.replace("<prefix_instruction></prefix_instruction>", prefix_instruction)
520
520
  else:
521
- user_template = _remove_template_section(user_template, "language")
521
+ user_template = _remove_template_section(user_template, "language_instructions")
522
522
  logger.debug("Using default language (English)")
523
523
 
524
524
  user_template = re.sub(r"\n(?:[ \t]*\n){2,}", "\n\n", user_template)
@@ -526,6 +526,92 @@ The ENTIRE commit message, including the prefix, must be in {language}."""
526
526
  return system_template.strip(), user_template.strip()
527
527
 
528
528
 
529
+ def build_group_prompt(
530
+ status: str,
531
+ processed_diff: str,
532
+ diff_stat: str,
533
+ one_liner: bool,
534
+ hint: str,
535
+ infer_scope: bool,
536
+ verbose: bool,
537
+ system_template_path: str | None,
538
+ language: str | None,
539
+ translate_prefixes: bool,
540
+ ) -> tuple[str, str]:
541
+ """Build prompt for grouped commit generation (JSON output with multiple commits)."""
542
+ system_prompt, user_prompt = build_prompt(
543
+ status=status,
544
+ processed_diff=processed_diff,
545
+ diff_stat=diff_stat,
546
+ one_liner=one_liner,
547
+ hint=hint,
548
+ infer_scope=infer_scope,
549
+ verbose=verbose,
550
+ system_template_path=system_template_path,
551
+ language=language,
552
+ translate_prefixes=translate_prefixes,
553
+ )
554
+
555
+ user_prompt = _remove_template_section(user_prompt, "format_instructions")
556
+
557
+ grouping_instructions = """
558
+ <format_instructions>
559
+ Your task is to split the changed files into separate, logical commits. Think of this like sorting files into different folders where each file belongs in exactly one folder.
560
+
561
+ CRITICAL REQUIREMENT - Every File Used Exactly Once:
562
+ You must assign EVERY file from the diff to exactly ONE commit.
563
+ - NO file should be left out
564
+ - NO file should appear in multiple commits
565
+ - EVERY file must be used once and ONLY once
566
+
567
+ Think of it like dealing cards: Once you've dealt a card to a player, that card cannot be dealt to another player.
568
+
569
+ HOW TO SPLIT THE FILES:
570
+ 1. Review all changed files in the diff
571
+ 2. Group files by logical relationship (e.g., related features, bug fixes, documentation)
572
+ 3. Assign each file to exactly one commit based on what makes the most sense
573
+ 4. If a file could fit in multiple commits, pick the best fit and move on - do NOT duplicate it
574
+ 5. Continue until every single file has been assigned to a commit
575
+
576
+ ORDERING:
577
+ Order the commits in a logical sequence considering dependencies, natural progression, and overall workflow.
578
+
579
+ YOUR RESPONSE FORMAT:
580
+ Respond with valid JSON following this structure:
581
+ ```json
582
+ {
583
+ "commits": [
584
+ {
585
+ "files": ["src/auth/login.ts", "src/auth/logout.ts"],
586
+ "message": "<commit_message_conforming_to_prescribed_structure_and_format>"
587
+ },
588
+ {
589
+ "files": ["src/db/schema.sql", "src/db/migrations/001.sql"],
590
+ "message": "<commit_message_conforming_to_prescribed_structure_and_format>"
591
+ },
592
+ {
593
+ "files": ["tests/auth.test.ts", "tests/db.test.ts", "README.md"],
594
+ "message": "<commit_message_conforming_to_prescribed_structure_and_format>"
595
+ }
596
+ ]
597
+ }
598
+ ```
599
+
600
+ ☝️ Notice how EVERY file path in the example above appears exactly ONCE across all commits. "src/auth/login.ts" appears once. "tests/auth.test.ts" appears once. No file is repeated.
601
+
602
+ VALIDATION CHECKLIST - Before responding, verify:
603
+ □ Total files across all commits = Total files in the diff
604
+ □ Each file appears in exactly 1 commit (no duplicates, no omissions)
605
+ □ Every commit has at least one file
606
+ □ If you list all files from all commits and count them, you get the same count as unique files in the diff
607
+ </format_instructions>
608
+ """
609
+
610
+ user_prompt = user_prompt + grouping_instructions
611
+
612
+ return system_prompt, user_prompt
613
+
614
+
529
615
  # ============================================================================
530
616
  # Message Cleaning Helpers
531
617
  # ============================================================================
gac/security.py CHANGED
@@ -54,7 +54,7 @@ class SecretPatterns:
54
54
  PRIVATE_KEY = re.compile(r"-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----")
55
55
 
56
56
  # Bearer Tokens (require actual token with specific characteristics)
57
- BEARER_TOKEN = re.compile(r"Bearer\s+[A-Za-z0-9]{20,}[/=]*\s", re.IGNORECASE)
57
+ BEARER_TOKEN = re.compile(r"Bearer\s+[A-Za-z0-9]{20,}[/=]*(?:\s|$)", re.IGNORECASE)
58
58
 
59
59
  # JWT Tokens
60
60
  JWT_TOKEN = re.compile(r"eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+")
gac/workflow_utils.py ADDED
@@ -0,0 +1,131 @@
1
+ import logging
2
+ import tempfile
3
+ from pathlib import Path
4
+
5
+ import click
6
+ from rich.console import Console
7
+
8
+ logger = logging.getLogger(__name__)
9
+ console = Console()
10
+
11
+
12
+ def handle_confirmation_loop(
13
+ commit_message: str,
14
+ conversation_messages: list[dict[str, str]],
15
+ quiet: bool,
16
+ model: str,
17
+ ) -> tuple[str, str, list[dict[str, str]]]:
18
+ from rich.panel import Panel
19
+
20
+ from gac.utils import edit_commit_message_inplace
21
+
22
+ while True:
23
+ response = click.prompt(
24
+ "Proceed with commit above? [y/n/r/e/<feedback>]",
25
+ type=str,
26
+ show_default=False,
27
+ ).strip()
28
+ response_lower = response.lower()
29
+
30
+ if response_lower in ["y", "yes"]:
31
+ return ("yes", commit_message, conversation_messages)
32
+ if response_lower in ["n", "no"]:
33
+ return ("no", commit_message, conversation_messages)
34
+ if response == "":
35
+ continue
36
+ if response_lower in ["e", "edit"]:
37
+ edited_message = edit_commit_message_inplace(commit_message)
38
+ if edited_message:
39
+ commit_message = edited_message
40
+ conversation_messages[-1] = {"role": "assistant", "content": commit_message}
41
+ logger.info("Commit message edited by user")
42
+ console.print("\n[bold green]Edited commit message:[/bold green]")
43
+ console.print(Panel(commit_message, title="Commit Message", border_style="cyan"))
44
+ else:
45
+ console.print("[yellow]Using previous message.[/yellow]")
46
+ console.print(Panel(commit_message, title="Commit Message", border_style="cyan"))
47
+ continue
48
+ if response_lower in ["r", "reroll"]:
49
+ msg = "Please provide an alternative commit message using the same repository context."
50
+ conversation_messages.append({"role": "user", "content": msg})
51
+ console.print("[cyan]Regenerating commit message...[/cyan]")
52
+ return ("regenerate", commit_message, conversation_messages)
53
+
54
+ msg = f"Please revise the commit message based on this feedback: {response}"
55
+ conversation_messages.append({"role": "user", "content": msg})
56
+ console.print(f"[cyan]Regenerating commit message with feedback: {response}[/cyan]")
57
+ return ("regenerate", commit_message, conversation_messages)
58
+
59
+
60
+ def execute_commit(commit_message: str, no_verify: bool) -> None:
61
+ from gac.git import run_git_command
62
+
63
+ commit_args = ["commit", "-m", commit_message]
64
+ if no_verify:
65
+ commit_args.append("--no-verify")
66
+ run_git_command(commit_args)
67
+ logger.info("Commit created successfully")
68
+ console.print("[green]Commit created successfully[/green]")
69
+
70
+
71
+ def check_token_warning(
72
+ prompt_tokens: int,
73
+ warning_limit: int,
74
+ require_confirmation: bool,
75
+ ) -> bool:
76
+ if warning_limit and prompt_tokens > warning_limit:
77
+ console.print(f"[yellow]⚠️ WARNING: Prompt has {prompt_tokens} tokens (limit: {warning_limit})[/yellow]")
78
+ if require_confirmation:
79
+ proceed = click.confirm("Do you want to continue anyway?", default=True)
80
+ if not proceed:
81
+ console.print("[yellow]Aborted due to token limit.[/yellow]")
82
+ return False
83
+ return True
84
+
85
+
86
+ def display_commit_message(commit_message: str, prompt_tokens: int, model: str, quiet: bool) -> None:
87
+ from rich.panel import Panel
88
+
89
+ from gac.ai_utils import count_tokens
90
+
91
+ console.print("[bold green]Generated commit message:[/bold green]")
92
+ console.print(Panel(commit_message, title="Commit Message", border_style="cyan"))
93
+
94
+ if not quiet:
95
+ completion_tokens = count_tokens(commit_message, model)
96
+ total_tokens = prompt_tokens + completion_tokens
97
+ console.print(
98
+ f"[dim]Token usage: {prompt_tokens} prompt + {completion_tokens} completion = {total_tokens} total[/dim]"
99
+ )
100
+
101
+
102
+ def restore_staging(staged_files: list[str], staged_diff: str | None = None) -> None:
103
+ """Restore the git staging area to a previous state.
104
+
105
+ Args:
106
+ staged_files: List of file paths that should be staged
107
+ staged_diff: Optional staged diff to reapply for partial staging
108
+ """
109
+ from gac.git import run_git_command
110
+
111
+ run_git_command(["reset", "HEAD"])
112
+
113
+ if staged_diff:
114
+ temp_path: Path | None = None
115
+ try:
116
+ with tempfile.NamedTemporaryFile("w", delete=False) as tmp:
117
+ tmp.write(staged_diff)
118
+ temp_path = Path(tmp.name)
119
+ run_git_command(["apply", "--cached", str(temp_path)])
120
+ return
121
+ except Exception as e:
122
+ logger.warning(f"Failed to reapply staged diff, falling back to file list: {e}")
123
+ finally:
124
+ if temp_path:
125
+ temp_path.unlink(missing_ok=True)
126
+
127
+ for file_path in staged_files:
128
+ try:
129
+ run_git_command(["add", file_path])
130
+ except Exception as e:
131
+ logger.warning(f"Failed to restore staging for {file_path}: {e}")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gac
3
- Version: 2.2.0
3
+ Version: 2.3.0
4
4
  Summary: LLM-powered Git commit message generator with multi-provider support
5
5
  Project-URL: Homepage, https://github.com/cellwebb/gac
6
6
  Project-URL: Documentation, https://github.com/cellwebb/gac#readme
@@ -108,6 +108,7 @@ uv tool upgrade gac
108
108
  - **Understands intent**: Analyzes code structure, logic, and patterns to understand the "why" behind your changes, not just what changed
109
109
  - **Semantic awareness**: Recognizes refactoring, bug fixes, features, and breaking changes to generate contextually appropriate messages
110
110
  - **Intelligent filtering**: Prioritizes meaningful changes while ignoring generated files, dependencies, and artifacts
111
+ - **Intelligent commit grouping** - Automatically group related changes into multiple logical commits with `--group`
111
112
 
112
113
  ### 📝 **Multiple Message Formats**
113
114
 
@@ -175,6 +176,9 @@ gac -v -s
175
176
  # Quick one-liner for small changes
176
177
  gac -o
177
178
 
179
+ # Group changes into logically related commits
180
+ gac -ag
181
+
178
182
  # Debug what the LLM sees
179
183
  gac --show-prompt
180
184
 
@@ -1,21 +1,22 @@
1
1
  gac/__init__.py,sha256=z9yGInqtycFIT3g1ca24r-A3699hKVaRqGUI79wsmMc,415
2
- gac/__version__.py,sha256=r4l_6jWyV87srDy0RVRKwXgzwJscJzmR4SM825CjGh0,66
3
- gac/ai.py,sha256=iBHeLsqe6iyFj86wbvEosyy4vkjAN1BlLQeqtb_rfmo,4303
4
- gac/ai_utils.py,sha256=094ujZVlbDnHM3HPxiBSCbGi_5MD6bOKCj2SjKVDDK0,7445
5
- gac/cli.py,sha256=SOrSfrlku99O7O8zev5hRVmADAmJ7AIkM7Z0dquuCbQ,5807
2
+ gac/__version__.py,sha256=KHcXxHr9uQK6amU8n-h2x8FjOXkwzFkaiUL62tdw07w,66
3
+ gac/ai.py,sha256=6SQK4axBE0uEbF3eKVTvQtGL9X1TbxoBOrY7NuYIfiM,5009
4
+ gac/ai_utils.py,sha256=reJINfsKlX0HAg5HPlH4ImO6FvibzgRZ0ruG9u1cxa8,8312
5
+ gac/cli.py,sha256=i4VEQeq5k2Rmy8DzqVuJCUyYxSq87x_wLTmH3VgOkyg,5986
6
6
  gac/config.py,sha256=O9n09-sFOqlkf47vieEP7fI5I7uhu1cXn9PUZ5yiYkw,1974
7
7
  gac/config_cli.py,sha256=v9nFHZO1RvK9fzHyuUS6SG-BCLHMsdOMDwWamBhVVh4,1608
8
- gac/constants.py,sha256=zgylsiKnpBunoNzVT6RpAVe9m8cgxZrZ55kRN6ZP_cM,9586
8
+ gac/constants.py,sha256=yyvYycIfRJ9SZZIMIhwn1s6yohjcaNM-fGXl_R9w1dI,9586
9
9
  gac/diff_cli.py,sha256=wnVQ9OFGnM0d2Pj9WVjWbo0jxqIuRHVAwmb8wU9Pa3E,5676
10
10
  gac/errors.py,sha256=ysDIVRCd0YQVTOW3Q6YzdolxCdtkoQCAFf3_jrqbjUY,7916
11
- gac/git.py,sha256=g6tvph50zV-wrTWrxARYXEpl0NeI8-ffFwHoqhp3fSE,8033
12
- gac/init_cli.py,sha256=KvFOvjjyMpdJ3MhJFvXSuYjdfulPA6hCP11YXwjHrqw,8849
13
- gac/language_cli.py,sha256=J4xZNNrMvHamsjK4TCsOVj0lrjYDtLMuHlnTqN0-N_w,3024
14
- gac/main.py,sha256=fK48fGHeJ4qGsttbKDoMXs4Gj3NncFHz_F_cJZI70IQ,16159
11
+ gac/git.py,sha256=m7EqMYYRNRfD68HbxZD3TUY3DZlzZSdqa38SeLSNb6A,9347
12
+ gac/init_cli.py,sha256=3n6A9FX0atPpO8XMMDypxLTIPxXv-cg_YtRl4yAKUOc,14449
13
+ gac/language_cli.py,sha256=Nl1WKR-o7APOKTg_7T5LoJmg4GaiXnGUzR5XFJayYwI,3050
14
+ gac/main.py,sha256=r5zwAYBBn-qz9kix2Zn5yJ-ac3-42u6HaIoirybOaTQ,28784
15
15
  gac/preprocess.py,sha256=hk2p2X4-xVDvuy-T1VMzMa9k5fTUbhlWDyw89DCf81Q,15379
16
- gac/prompt.py,sha256=HLvsW3YQLdTfw2N9UgjZ0jWckUc1x76V7Kcqjcl8Fsk,28633
17
- gac/security.py,sha256=15Yp6YR8QC4eECJi1BUCkMteh_veZXUbLL6W8qGcDm4,9920
16
+ gac/prompt.py,sha256=ofumb6DmxJceqZLUlUyLE9b7Mwx9BpIEHweKEV9eicw,31841
17
+ gac/security.py,sha256=QT91mBEo2Y7la-aXvKuF2zhWuoOSXb6PWKLJ93kSy2k,9926
18
18
  gac/utils.py,sha256=owkUzwJBX8mi0VrP3HKxku5vJj_JlaShzTYwjjsHn-4,8126
19
+ gac/workflow_utils.py,sha256=fc0yPRBeA-7P2WiVPFa7A_NjXTW68UlbUv6_AXVYAzA,5023
19
20
  gac/providers/__init__.py,sha256=pT1xcKoZkPm6BWaxcAQ299-Ca5zZ31kf4DeQYAim9Tw,1367
20
21
  gac/providers/anthropic.py,sha256=VK5d1s1PmBNDwh_x7illQ2CIZIHNIYU28btVfizwQPs,2036
21
22
  gac/providers/cerebras.py,sha256=Ik8lhlsliGJVkgDgqlThfpra9tqbdYQZkaC4eNxRd9w,1648
@@ -36,8 +37,8 @@ gac/providers/streamlake.py,sha256=KAA2ZnpuEI5imzvdWVWUhEBHSP0BMnprKXte6CbwBWY,2
36
37
  gac/providers/synthetic.py,sha256=sRMIJTS9LpcXd9A7qp_ZjZxdqtTKRn9fl1W4YwJZP4c,1855
37
38
  gac/providers/together.py,sha256=1bUIVHfYzcEDw4hQPE8qV6hjc2JNHPv_khVgpk2IJxI,1667
38
39
  gac/providers/zai.py,sha256=kywhhrCfPBu0rElZyb-iENxQxxpVGykvePuL4xrXlaU,2739
39
- gac-2.2.0.dist-info/METADATA,sha256=Z5Vv7oBzqWKVr7lDmk_HFjq3kF13DNnCkMnXKZDKRtA,9609
40
- gac-2.2.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
41
- gac-2.2.0.dist-info/entry_points.txt,sha256=tdjN-XMmcWfL92swuRAjT62bFLOAwk9bTMRLGP5Z4aI,36
42
- gac-2.2.0.dist-info/licenses/LICENSE,sha256=vOab37NouL1PNs5BswnPayrMCqaN2sqLfMQfqPDrpZg,1103
43
- gac-2.2.0.dist-info/RECORD,,
40
+ gac-2.3.0.dist-info/METADATA,sha256=5VIkxFRlLC4NPEGIuZyRUo253XYHdQYt_8uX3gbvjjg,9782
41
+ gac-2.3.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
42
+ gac-2.3.0.dist-info/entry_points.txt,sha256=tdjN-XMmcWfL92swuRAjT62bFLOAwk9bTMRLGP5Z4aI,36
43
+ gac-2.3.0.dist-info/licenses/LICENSE,sha256=vOab37NouL1PNs5BswnPayrMCqaN2sqLfMQfqPDrpZg,1103
44
+ gac-2.3.0.dist-info/RECORD,,
File without changes