git-llm-tool 0.1.12__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.
@@ -0,0 +1,5 @@
1
+ """Git-LLM-Tool: AI-powered git commit message and changelog generator."""
2
+
3
+ __version__ = "0.1.5"
4
+ __author__ = "skyler-gogolook"
5
+ __email__ = "skyler.lo@gogolook.com"
@@ -0,0 +1,6 @@
1
+ """Entry point for running git-llm-tool as a module."""
2
+
3
+ from git_llm_tool.cli import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
git_llm_tool/cli.py ADDED
@@ -0,0 +1,167 @@
1
+ """Main CLI interface for git-llm-tool."""
2
+
3
+ import click
4
+ from pathlib import Path
5
+
6
+ from git_llm_tool import __version__
7
+ from git_llm_tool.core.config import ConfigLoader, get_config
8
+ from git_llm_tool.core.exceptions import ConfigError
9
+ from git_llm_tool.commands.commit_cmd import execute_commit
10
+ from git_llm_tool.commands.changelog_cmd import execute_changelog
11
+
12
+
13
+ @click.group()
14
+ @click.version_option(version=__version__)
15
+ @click.pass_context
16
+ def main(ctx):
17
+ """AI-powered git commit message and changelog generator."""
18
+ ctx.ensure_object(dict)
19
+
20
+
21
+ @main.command()
22
+ @click.option(
23
+ "--apply", "-a", is_flag=True,
24
+ help="Apply the commit message directly without opening editor"
25
+ )
26
+ @click.option(
27
+ "--model", "-m",
28
+ help="LLM model to use (overrides config)"
29
+ )
30
+ @click.option(
31
+ "--language", "-l",
32
+ help="Output language (overrides config)"
33
+ )
34
+ @click.option(
35
+ "--verbose", "-v", is_flag=True,
36
+ help="Enable verbose output"
37
+ )
38
+ @click.option(
39
+ "--no-jira", is_flag=True,
40
+ help="Skip Jira ticket input"
41
+ )
42
+ @click.pass_context
43
+ def commit(ctx, apply, model, language, verbose, no_jira):
44
+ """Generate AI-powered commit message from staged changes."""
45
+ execute_commit(apply=apply, model=model, language=language, verbose=verbose, no_jira=no_jira)
46
+
47
+
48
+ @main.command()
49
+ @click.option(
50
+ "--from", "from_ref",
51
+ help="Starting reference (default: last tag)"
52
+ )
53
+ @click.option(
54
+ "--to", "to_ref", default="HEAD",
55
+ help="Ending reference (default: HEAD)"
56
+ )
57
+ @click.option(
58
+ "--output", "-o",
59
+ help="Output file (default: stdout)"
60
+ )
61
+ @click.option(
62
+ "--force", "-f", is_flag=True,
63
+ help="Force overwrite existing output file"
64
+ )
65
+ @click.option(
66
+ "--verbose", "-v", is_flag=True,
67
+ help="Enable verbose output"
68
+ )
69
+ @click.pass_context
70
+ def changelog(ctx, from_ref, to_ref, output, force, verbose):
71
+ """Generate changelog from git history."""
72
+ execute_changelog(from_ref=from_ref, to_ref=to_ref, output=output, force=force, verbose=verbose)
73
+
74
+
75
+ @main.group()
76
+ def config():
77
+ """Configuration management commands."""
78
+ pass
79
+
80
+
81
+ @config.command()
82
+ @click.argument("key")
83
+ @click.argument("value")
84
+ def set(key, value):
85
+ """Set configuration value."""
86
+ try:
87
+ config_loader = ConfigLoader()
88
+ config_loader.set_value(key, value)
89
+
90
+ # Save to global config
91
+ config_loader.save_config()
92
+
93
+ click.echo(f"✅ Set {key} = {value}")
94
+ except ConfigError as e:
95
+ click.echo(f"❌ Configuration error: {e}", err=True)
96
+ except Exception as e:
97
+ click.echo(f"❌ Unexpected error: {e}", err=True)
98
+
99
+
100
+ @config.command()
101
+ @click.argument("key", required=False)
102
+ def get(key):
103
+ """Get configuration value(s)."""
104
+ try:
105
+ config = get_config()
106
+
107
+ if key:
108
+ # Get specific key
109
+ config_loader = ConfigLoader()
110
+ value = config_loader.get_value(key)
111
+ click.echo(f"{key} = {value}")
112
+ else:
113
+ # Show all configuration
114
+ click.echo("📋 Current Configuration:")
115
+ click.echo(f" llm.default_model = {config.llm.default_model}")
116
+ click.echo(f" llm.language = {config.llm.language}")
117
+
118
+ if config.llm.api_keys:
119
+ click.echo(" llm.api_keys:")
120
+ for provider, key_value in config.llm.api_keys.items():
121
+ # Hide API key for security
122
+ masked_key = key_value[:8] + "..." if len(key_value) > 8 else "***"
123
+ click.echo(f" {provider} = {masked_key}")
124
+
125
+ if config.llm.azure_openai:
126
+ click.echo(" llm.azure_openai:")
127
+ for key, value in config.llm.azure_openai.items():
128
+ click.echo(f" {key} = {value}")
129
+
130
+ click.echo(f" jira.enabled = {config.jira.enabled}")
131
+ if config.jira.ticket_pattern:
132
+ click.echo(f" jira.ticket_pattern = {config.jira.ticket_pattern}")
133
+
134
+ if config.editor.preferred_editor:
135
+ click.echo(f" editor.preferred_editor = {config.editor.preferred_editor}")
136
+
137
+ except ConfigError as e:
138
+ click.echo(f"❌ Configuration error: {e}", err=True)
139
+ except Exception as e:
140
+ click.echo(f"❌ Unexpected error: {e}", err=True)
141
+
142
+
143
+ @config.command()
144
+ def init():
145
+ """Initialize configuration file."""
146
+ try:
147
+ config_path = Path.home() / ".git-llm-tool" / "config.yaml"
148
+
149
+ if config_path.exists():
150
+ if not click.confirm(f"Configuration file already exists at {config_path}. Overwrite?"):
151
+ click.echo("❌ Initialization cancelled.")
152
+ return
153
+
154
+ # Create default configuration
155
+ config_loader = ConfigLoader()
156
+ config_loader.save_config(config_path)
157
+
158
+ click.echo(f"✅ Configuration initialized at {config_path}")
159
+ click.echo("💡 You can now set API keys with:")
160
+ click.echo(" git-llm config set llm.api_keys.openai sk-your-key-here")
161
+
162
+ except Exception as e:
163
+ click.echo(f"❌ Failed to initialize configuration: {e}", err=True)
164
+
165
+
166
+ if __name__ == "__main__":
167
+ main()
@@ -0,0 +1 @@
1
+ """Commands module."""
@@ -0,0 +1,189 @@
1
+ """Changelog command implementation."""
2
+
3
+ import click
4
+ import os
5
+ from datetime import datetime
6
+ from typing import Optional
7
+
8
+ from git_llm_tool.core.config import get_config
9
+ from git_llm_tool.core.git_helper import GitHelper
10
+ from git_llm_tool.core.exceptions import GitError, ApiError, ConfigError
11
+ from git_llm_tool.providers import get_provider
12
+
13
+
14
+ def _manage_changelog_file(new_content: str, verbose: bool = False) -> str:
15
+ """Manage the changelog.md file in the repository root.
16
+
17
+ Args:
18
+ new_content: New changelog content to add
19
+ verbose: Enable verbose output
20
+
21
+ Returns:
22
+ Path to the changelog file
23
+ """
24
+ # Get repository root
25
+ git_helper = GitHelper()
26
+ repo_info = git_helper.get_repository_info()
27
+ repo_root = repo_info.get('repository_root', os.getcwd())
28
+
29
+ changelog_path = os.path.join(repo_root, 'changelog.md')
30
+
31
+ # Get current date
32
+ current_date = datetime.now().strftime('%Y-%m-%d')
33
+
34
+ # Create header with date
35
+ header = f"\n## {current_date}\n\n"
36
+
37
+ # Clean the new content (remove any duplicate titles)
38
+ cleaned_content = new_content.strip()
39
+ if cleaned_content.startswith('# Changelog'):
40
+ # Remove the duplicate title line
41
+ lines = cleaned_content.split('\n')
42
+ lines = [line for line in lines[1:] if line.strip()] # Skip title and empty lines
43
+ cleaned_content = '\n'.join(lines)
44
+
45
+ # Prepare content to add
46
+ content_to_add = header + cleaned_content + "\n"
47
+
48
+ if os.path.exists(changelog_path):
49
+ if verbose:
50
+ click.echo(f"📝 Found existing changelog at {changelog_path}")
51
+
52
+ # Read existing content
53
+ try:
54
+ with open(changelog_path, 'r', encoding='utf-8') as f:
55
+ existing_content = f.read()
56
+ except IOError as e:
57
+ raise Exception(f"Failed to read existing changelog: {e}")
58
+
59
+ # Check if we're at the beginning of the file or need to add after title
60
+ if existing_content.strip().startswith('# '):
61
+ # Find the end of the title line
62
+ lines = existing_content.split('\n')
63
+ title_line = lines[0]
64
+ rest_content = '\n'.join(lines[1:])
65
+
66
+ # Insert new content after title
67
+ final_content = title_line + '\n' + content_to_add + rest_content
68
+ else:
69
+ # Prepend to existing content
70
+ final_content = content_to_add + existing_content
71
+
72
+ else:
73
+ if verbose:
74
+ click.echo(f"📄 Creating new changelog at {changelog_path}")
75
+
76
+ # Create new changelog with header
77
+ final_content = f"# Changelog\n{content_to_add}"
78
+
79
+ # Write the file
80
+ try:
81
+ with open(changelog_path, 'w', encoding='utf-8') as f:
82
+ f.write(final_content)
83
+ if verbose:
84
+ click.echo(f"✅ Updated changelog at {changelog_path}")
85
+ except IOError as e:
86
+ raise Exception(f"Failed to write changelog: {e}")
87
+
88
+ return changelog_path
89
+
90
+
91
+ def execute_changelog(
92
+ from_ref: Optional[str] = None,
93
+ to_ref: str = "HEAD",
94
+ output: Optional[str] = None,
95
+ force: bool = False,
96
+ verbose: bool = False
97
+ ) -> None:
98
+ """Execute the changelog command logic.
99
+
100
+ Args:
101
+ from_ref: Starting reference (default: last tag)
102
+ to_ref: Ending reference (default: HEAD)
103
+ output: Output file path
104
+ force: Force overwrite existing file
105
+ verbose: Enable verbose output
106
+ """
107
+ try:
108
+ # Load configuration
109
+ config = get_config()
110
+
111
+ if verbose:
112
+ click.echo(f"📄 Using model: {config.llm.default_model}")
113
+ click.echo(f"🌐 Using language: {config.llm.language}")
114
+
115
+ # Initialize Git helper
116
+ git_helper = GitHelper()
117
+
118
+ # Get commit messages in range
119
+ if verbose:
120
+ click.echo("📊 Getting commit messages...")
121
+
122
+ try:
123
+ commit_messages = git_helper.get_commit_messages(from_ref, to_ref)
124
+ except GitError as e:
125
+ click.echo(f"❌ {e}", err=True)
126
+ return
127
+
128
+ if verbose:
129
+ click.echo(f"📝 Found {len(commit_messages)} commits")
130
+
131
+ # Get LLM provider
132
+ try:
133
+ provider = get_provider(config)
134
+ if verbose:
135
+ click.echo(f"🤖 Using provider: {provider.__class__.__name__}")
136
+ except ApiError as e:
137
+ click.echo(f"❌ {e}", err=True)
138
+ return
139
+
140
+ # Generate changelog
141
+ click.echo("🤖 Generating changelog...")
142
+
143
+ try:
144
+ changelog = provider.generate_changelog(commit_messages)
145
+ except ApiError as e:
146
+ click.echo(f"❌ API Error: {e}", err=True)
147
+ return
148
+
149
+ if verbose:
150
+ click.echo(f"✨ Generated changelog ({len(changelog)} characters)")
151
+
152
+ # Output changelog
153
+ if output:
154
+ # Custom output file specified
155
+ if os.path.exists(output) and not force:
156
+ if not click.confirm(f"File {output} exists. Overwrite?"):
157
+ click.echo("❌ Changelog generation cancelled.")
158
+ return
159
+
160
+ # Write to custom file
161
+ try:
162
+ with open(output, 'w', encoding='utf-8') as f:
163
+ f.write(changelog)
164
+ click.echo(f"✅ Changelog saved to {output}")
165
+ except IOError as e:
166
+ click.echo(f"❌ Failed to write to {output}: {e}", err=True)
167
+ else:
168
+ # Auto-manage changelog.md in repository root
169
+ try:
170
+ changelog_path = _manage_changelog_file(changelog, verbose)
171
+ click.echo(f"✅ Changelog updated in {changelog_path}")
172
+
173
+ # Also show the generated content
174
+ if verbose:
175
+ click.echo("\n" + "="*60)
176
+ click.echo("📋 Generated Content:")
177
+ click.echo("="*60)
178
+ click.echo(changelog)
179
+ click.echo("="*60)
180
+ except Exception as e:
181
+ click.echo(f"❌ Failed to update changelog.md: {e}", err=True)
182
+
183
+ except ConfigError as e:
184
+ click.echo(f"❌ Configuration error: {e}", err=True)
185
+ except Exception as e:
186
+ click.echo(f"❌ Unexpected error: {e}", err=True)
187
+ if verbose:
188
+ import traceback
189
+ click.echo(traceback.format_exc(), err=True)
@@ -0,0 +1,134 @@
1
+ """Commit command implementation."""
2
+
3
+ import click
4
+ from typing import Optional
5
+
6
+ from git_llm_tool.core.config import get_config
7
+ from git_llm_tool.core.git_helper import GitHelper
8
+ from git_llm_tool.core.jira_helper import JiraHelper
9
+ from git_llm_tool.core.exceptions import GitError, ApiError, ConfigError, JiraError
10
+ from git_llm_tool.providers import get_provider
11
+
12
+
13
+ def execute_commit(
14
+ apply: bool = False,
15
+ model: Optional[str] = None,
16
+ language: Optional[str] = None,
17
+ verbose: bool = False,
18
+ no_jira: bool = False
19
+ ) -> None:
20
+ """Execute the commit command logic.
21
+
22
+ Args:
23
+ apply: Whether to apply commit directly without editor
24
+ model: Override model from config
25
+ language: Override language from config
26
+ verbose: Enable verbose output
27
+ no_jira: Skip Jira ticket input
28
+ """
29
+ try:
30
+ # Load configuration
31
+ config = get_config()
32
+
33
+ # Override config with CLI parameters
34
+ if model:
35
+ config.llm.default_model = model
36
+ if language:
37
+ config.llm.language = language
38
+
39
+ if verbose:
40
+ click.echo(f"📄 Using model: {config.llm.default_model}")
41
+ click.echo(f"🌐 Using language: {config.llm.language}")
42
+
43
+ # Initialize Git helper
44
+ git_helper = GitHelper()
45
+
46
+ # Get staged diff
47
+ if verbose:
48
+ click.echo("📊 Getting staged changes...")
49
+
50
+ try:
51
+ diff = git_helper.get_staged_diff()
52
+ except GitError as e:
53
+ click.echo(f"❌ {e}", err=True)
54
+ click.echo("💡 Tip: Use 'git add' to stage files before committing", err=True)
55
+ return
56
+
57
+ if verbose:
58
+ click.echo(f"📝 Found {len(diff.splitlines())} lines of changes")
59
+
60
+ # Get LLM provider
61
+ try:
62
+ provider = get_provider(config)
63
+ if verbose:
64
+ click.echo(f"🤖 Using provider: {provider.__class__.__name__}")
65
+ except ApiError as e:
66
+ click.echo(f"❌ {e}", err=True)
67
+ return
68
+
69
+ # Initialize Jira helper and get Jira context
70
+ jira_ticket, work_hours = None, None
71
+
72
+ if not no_jira:
73
+ jira_helper = JiraHelper(config, git_helper)
74
+
75
+ try:
76
+ jira_helper.validate_config()
77
+ jira_ticket, work_hours = jira_helper.get_jira_context(verbose=verbose)
78
+
79
+ if jira_ticket or work_hours:
80
+ jira_info = jira_helper.format_jira_info(jira_ticket, work_hours)
81
+ click.echo(f"📋 Jira Info: {jira_info}")
82
+ except JiraError as e:
83
+ if verbose:
84
+ click.echo(f"⚠️ Jira processing skipped: {e}")
85
+ # Continue without Jira integration
86
+ pass
87
+ else:
88
+ if verbose:
89
+ click.echo("🔒 Jira integration skipped (--no-jira flag)")
90
+
91
+ # Generate commit message
92
+ click.echo("🤖 Generating commit message...")
93
+
94
+ try:
95
+ commit_message = provider.generate_commit_message(
96
+ diff=diff,
97
+ jira_ticket=jira_ticket,
98
+ work_hours=work_hours
99
+ )
100
+ except ApiError as e:
101
+ click.echo(f"❌ API Error: {e}", err=True)
102
+ return
103
+
104
+ if verbose:
105
+ click.echo(f"✨ Generated message: {commit_message}")
106
+
107
+ # Apply commit or open editor
108
+ if apply:
109
+ # Direct commit
110
+ try:
111
+ git_helper.commit_with_message(commit_message)
112
+ click.echo("✅ Commit applied successfully!")
113
+ click.echo(f"📝 Message: {commit_message}")
114
+ except GitError as e:
115
+ click.echo(f"❌ Commit failed: {e}", err=True)
116
+ else:
117
+ # Open editor for review
118
+ click.echo("📝 Opening editor for review...")
119
+ try:
120
+ committed = git_helper.open_commit_editor(commit_message, config)
121
+ if committed:
122
+ click.echo("✅ Commit created successfully!")
123
+ else:
124
+ click.echo("❌ Commit cancelled by user")
125
+ except GitError as e:
126
+ click.echo(f"❌ Editor error: {e}", err=True)
127
+
128
+ except ConfigError as e:
129
+ click.echo(f"❌ Configuration error: {e}", err=True)
130
+ except Exception as e:
131
+ click.echo(f"❌ Unexpected error: {e}", err=True)
132
+ if verbose:
133
+ import traceback
134
+ click.echo(traceback.format_exc(), err=True)
@@ -0,0 +1 @@
1
+ """Core module."""