diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d42806..362f135 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,22 @@ + + +## Version 0.10.0 - 2025-12-13 + +Updates to `rgithook.py` improve the reliability of Git hook execution. This ensures that pre-commit checks are consistently applied during development workflows. + +**Changes:** 1 files, 258 lines +**Languages:** Python (258 lines) + +## Version 0.9.0 - 2025-11-16 + +You can now export data as JSON or RSS. A command-line tool is available to extract mentions, and documentation has been added to help you use it. + +**Changes:** 2 files, 209 lines +**Languages:** Markdown (8 lines), Python (201 lines) + ## Version 0.8.0 - 2025-11-05 Users can now connect external tools to automate more complex tasks. Developers can integrate new tools using the updated elon.py file. diff --git a/rgithook.py b/rgithook.py index d1c3a32..f4618c3 100755 --- a/rgithook.py +++ b/rgithook.py @@ -4,7 +4,9 @@ API_URL = "https://static.molodetz.nl/rp.cgi/api/v1/chat/completions" MODEL = "google/gemma-3-12b-it:free" TEMPERATURE = 1.0 MAX_TOKENS = None -HOOK_PATH = ".git/hooks/prepare-commit-msg" +HOOK_PATH_PRECOMMIT = ".git/hooks/pre-commit" +HOOK_PATH_PREPAREMSG = ".git/hooks/prepare-commit-msg" +TEMP_MSG_FILE = ".git/COMMIT_MSG_GENERATED" import sys import subprocess @@ -32,23 +34,29 @@ def install_hook(): try: if not os.path.exists('.git'): return False - + script_path = os.path.abspath(__file__) - hook_dir = os.path.dirname(HOOK_PATH) - + hook_dir = os.path.dirname(HOOK_PATH_PRECOMMIT) + if not os.path.exists(hook_dir): os.makedirs(hook_dir) - - if os.path.exists(HOOK_PATH): - with open(HOOK_PATH, 'r') as f: - if script_path in f.read(): - return False - - with open(HOOK_PATH, 'w') as f: - f.write(f'#!/bin/bash\n{script_path} "$@"\n') - - os.chmod(HOOK_PATH, 0o755) - return True + + installed = False + + for hook_path, mode in [(HOOK_PATH_PRECOMMIT, 'pre-commit'), (HOOK_PATH_PREPAREMSG, 'prepare-commit-msg')]: + needs_install = True + if os.path.exists(hook_path): + with open(hook_path, 'r') as f: + if script_path in f.read(): + needs_install = False + + if needs_install: + with open(hook_path, 'w') as f: + f.write(f'#!/bin/bash\n{script_path} {mode} "$@"\n') + os.chmod(hook_path, 0o755) + installed = True + + return installed except Exception: return False @@ -217,52 +225,52 @@ def call_ai(prompt): def generate_commit_message(diff, files): try: files_list = "\n".join([f"- {f}" for f in files[:20]]) - - prompt = f"""You write commit messages for code changes. -Changed files: + prompt = f"""You are a senior software engineer writing precise git commit messages following the Conventional Commits specification. + +CONTEXT: +Files modified: {len(files)} {files_list} -Code changes: +DIFF: {diff[:12000]} -Write a commit message with this format: -: +TASK: +Analyze the changes and write a commit message that accurately describes the modifications. -Format rules: -- All lowercase for the prefix -- Colon and space after prefix -- Start description with lowercase letter -- No period at the end -- Max 72 characters total -- Use imperative mood (Add not Added) +OUTPUT FORMAT: +: -Choose one prefix: -- fix: for bug fixes - Example: fix: resolve null pointer error in user login - Example: fix: correct date format in export function +RULES: +1. Use exactly one line per logical change +2. Type must be lowercase +3. Description starts with lowercase verb in imperative mood +4. No trailing punctuation +5. Maximum 72 characters per line +6. Group related changes under a single type when appropriate +7. Order by importance (most significant change first) -- feat: for new features - Example: feat: add dark mode toggle to settings - Example: feat: implement search filter for products +TYPES (select the most appropriate): +- feat: new functionality or capability added to the codebase +- fix: correction of a bug or erroneous behavior +- refactor: code restructuring without changing external behavior +- perf: performance optimization or improvement +- docs: documentation additions or modifications +- test: test additions or modifications +- build: build system or dependency changes +- ci: continuous integration configuration changes +- style: formatting changes (whitespace, semicolons, etc.) +- chore: routine maintenance tasks -- docs: for documentation changes - Example: docs: update api endpoint descriptions - Example: docs: add setup guide for development +ANALYSIS GUIDELINES: +- Identify the primary purpose of the change +- Distinguish between new features and modifications to existing ones +- Recognize bug fixes by error handling, null checks, or condition corrections +- Note refactoring by structural changes without behavior modification +- Detect performance work by optimization patterns or caching additions -- perf: for performance improvements - Example: perf: reduce database query time by 40% - Example: perf: optimize image loading with lazy load - -- refactor: for code restructuring - Example: refactor: simplify user validation logic - Example: refactor: extract common functions to utils - -- maintenance: for routine updates and maintenance - Example: maintenance: update dependencies to latest versions - Example: maintenance: clean up unused imports and files - -Reply with ONLY the commit message, nothing else.""" +OUTPUT: +Provide only the commit message lines, no explanations or additional text.""" message = call_ai(prompt) if not message: @@ -270,30 +278,40 @@ Reply with ONLY the commit message, nothing else.""" message = message.strip().strip('"').strip("'") - prefixes = ['fix:', 'feat:', 'docs:', 'perf:', 'refactor:', 'maintenance:'] - if not any(message.startswith(p) for p in prefixes): - message = f"feat: {message}" + lines = message.split('\n') + processed_lines = [] + prefixes = ['feat:', 'fix:', 'refactor:', 'perf:', 'docs:', 'test:', 'build:', 'ci:', 'style:', 'chore:'] + for line in lines: + line = line.strip() + if not line: + continue + if not any(line.startswith(p) for p in prefixes): + line = f"chore: {line}" + processed_lines.append(line) - return message + if not processed_lines: + return generate_fallback_message(files) + + return '\n'.join(processed_lines) except Exception: return generate_fallback_message(files) def generate_fallback_message(files): try: if not files: - return "feat: update project" - + return "chore: update project" + exts = set() for f in files: ext = os.path.splitext(f)[1] if ext: exts.add(ext[1:]) - + if exts: - return f"feat: update {', '.join(sorted(exts)[:3])} files" - return "feat: update project files" + return f"chore: update {', '.join(sorted(exts)[:3])} files" + return "chore: update project files" except Exception: - return "feat: update project" + return "chore: update project" def create_git_tag(version): try: @@ -334,20 +352,32 @@ def update_changelog(version, commit_message, stats, files): lang_summary = ", ".join(lang_stats) if lang_stats else "No code changes" - functional_desc_prompt = f"""You write changelog entries for software releases. + functional_desc_prompt = f"""You are a technical writer creating changelog entries for a software release. -Commit message: {commit_message} -Changed files: {", ".join(files[:10])} +COMMIT SUMMARY: +{commit_message} -Write a short functional description of what changed for users or developers. -Use simple clear words, be direct, max 2 sentences. -Focus on what it does, not how. +AFFECTED FILES: +{", ".join(files[:10])} -Reply with ONLY the description text.""" +TASK: +Write a concise changelog entry describing the functional impact of these changes. + +REQUIREMENTS: +1. Maximum two sentences +2. Focus on user-visible or developer-relevant changes +3. Use present tense, active voice +4. Be specific about what functionality is added, modified, or fixed +5. Avoid implementation details unless architecturally significant +6. No marketing language or superlatives + +OUTPUT: +Provide only the changelog description text, no formatting or prefixes.""" functional_desc = call_ai(functional_desc_prompt) if not functional_desc: - functional_desc = commit_message.split(':', 1)[1].strip() if ':' in commit_message else commit_message + first_line = commit_message.split('\n')[0] + functional_desc = first_line.split(':', 1)[1].strip() if ':' in first_line else first_line entry_lines = [ f"## Version {version} - {today}", @@ -382,6 +412,48 @@ Reply with ONLY the description text.""" except Exception: pass +def run_pre_commit(): + diff = get_git_diff() + files = get_changed_files() + + if not diff and not files: + sys.exit(0) + + stats = analyze_diff_stats(diff) + commit_message = generate_commit_message(diff, files) + + current_version, source = get_version() + new_version = update_version(current_version, source) + + update_changelog(new_version, commit_message, stats, files) + safe_run(['git', 'add', 'CHANGELOG.md']) + + with open(TEMP_MSG_FILE, 'w') as f: + f.write(f"{new_version}\n{commit_message}") + + sys.exit(0) + + +def run_prepare_commit_msg(commit_msg_file): + if not os.path.exists(TEMP_MSG_FILE): + sys.exit(0) + + with open(TEMP_MSG_FILE, 'r') as f: + content = f.read() + + lines = content.split('\n', 1) + new_version = lines[0] if lines else "0.1.0" + commit_message = lines[1] if len(lines) > 1 else "chore: update project" + + with open(commit_msg_file, 'w') as f: + f.write(commit_message + '\n') + + create_git_tag(new_version) + + os.remove(TEMP_MSG_FILE) + sys.exit(0) + + def main(): try: if len(sys.argv) < 2: @@ -390,36 +462,24 @@ def main(): else: print("Git hook already installed") sys.exit(0) - - commit_msg_file = sys.argv[1] - - current_version, source = get_version() - new_version = update_version(current_version, source) - - diff = get_git_diff() - files = get_changed_files() - stats = analyze_diff_stats(diff) - - commit_message = generate_commit_message(diff, files) - - update_changelog(new_version, commit_message, stats, files) - safe_run(['git', 'add', 'CHANGELOG.md']) - - create_git_tag(new_version) - - with open(commit_msg_file, 'w') as f: - f.write(commit_message + '\n') - - sys.exit(0) + + mode = sys.argv[1] + + if mode == 'pre-commit': + run_pre_commit() + elif mode == 'prepare-commit-msg': + if len(sys.argv) >= 3: + run_prepare_commit_msg(sys.argv[2]) + sys.exit(0) + else: + sys.exit(0) except Exception: try: - if len(sys.argv) >= 2: - with open(sys.argv[1], 'w') as f: - f.write("feat: update project\n") + if os.path.exists(TEMP_MSG_FILE): + os.remove(TEMP_MSG_FILE) except Exception: pass sys.exit(0) if __name__ == '__main__': main() -