From f64473ffaef1620a0849cdcf6c815b577d2a8e13 Mon Sep 17 00:00:00 2001 From: sumansaurabh Date: Thu, 6 Mar 2025 12:10:13 +0530 Subject: [PATCH 01/22] feat: Consolidate configuration subcommands for LLM and JIRA settings --- penify_hook/commands/config_commands.py | 6 +- penify_hook/main.py | 85 +++++++++++++------------ 2 files changed, 48 insertions(+), 43 deletions(-) diff --git a/penify_hook/commands/config_commands.py b/penify_hook/commands/config_commands.py index 7376ada..19b1c35 100644 --- a/penify_hook/commands/config_commands.py +++ b/penify_hook/commands/config_commands.py @@ -6,6 +6,7 @@ import pkg_resources from pathlib import Path from threading import Thread +import logging def save_llm_config(model, api_base, api_key): """ @@ -267,13 +268,10 @@ def log_message(self, format, *args): print("Configuration completed.") -def get_token(passed_token): +def get_token(): """ Get the token based on priority. """ - if passed_token: - return passed_token - import os env_token = os.getenv('PENIFY_API_TOKEN') if env_token: diff --git a/penify_hook/main.py b/penify_hook/main.py index 1ce977d..7bd5475 100644 --- a/penify_hook/main.py +++ b/penify_hook/main.py @@ -33,7 +33,6 @@ def main(): parser = argparse.ArgumentParser(description="Penify CLI tool for managing Git hooks and generating documentation.") - parser.add_argument("-t", "--token", help="API token for authentication.") subparsers = parser.add_subparsers(title="subcommands", dest="subcommand") @@ -65,24 +64,28 @@ def main(): commit_parser.add_argument("--jira-user", help="JIRA username or email") commit_parser.add_argument("--jira-api-token", help="JIRA API token") - # Add a new subcommand: config-llm - llm_config_parser = subparsers.add_parser("config-llm", help="Configure LLM settings") + # Consolidated config subcommand + config_parser = subparsers.add_parser("config", help="Configure various settings") + config_subparsers = config_parser.add_subparsers(title="config_type", dest="config_type") + + # Config subcommand: llm + llm_config_parser = config_subparsers.add_parser("llm", help="Configure LLM settings") llm_config_parser.add_argument("--model", required=True, help="LLM model to use") llm_config_parser.add_argument("--api-base", help="API base URL for the LLM service") llm_config_parser.add_argument("--api-key", help="API key for the LLM service") - - # Add a new subcommand: config-llm-web - subparsers.add_parser("config-llm-web", help="Configure LLM settings through a web interface") - - # Add a new subcommand: config-jira - jira_config_parser = subparsers.add_parser("config-jira", help="Configure JIRA settings") + + # Config subcommand: llm-web + config_subparsers.add_parser("llm-web", help="Configure LLM settings through a web interface") + + # Config subcommand: jira + jira_config_parser = config_subparsers.add_parser("jira", help="Configure JIRA settings") jira_config_parser.add_argument("--url", required=True, help="JIRA base URL") jira_config_parser.add_argument("--username", required=True, help="JIRA username or email") jira_config_parser.add_argument("--api-token", required=True, help="JIRA API token") jira_config_parser.add_argument("--verify", action="store_true", help="Verify JIRA connection") - - # Add a new subcommand: config-jira-web - subparsers.add_parser("config-jira-web", help="Configure JIRA settings through a web interface") + + # Config subcommand: jira-web + config_subparsers.add_parser("jira-web", help="Configure JIRA settings through a web interface") # Subcommand: login subparsers.add_parser("login", help="Log in to Penify and obtain an API token.") @@ -90,7 +93,7 @@ def main(): args = parser.parse_args() # Get the token based on priority - token = get_token(args.token) + token = get_token() # Process commands if args.subcommand == "install-hook": @@ -143,34 +146,38 @@ def main(): llm_model, llm_api_base, llm_api_key, jira_url, jira_user, jira_api_token) - elif args.subcommand == "config-llm": - save_llm_config(args.model, args.api_base, args.api_key) - print(f"LLM configuration set: Model={args.model}, API Base={args.api_base or 'default'}") - - elif args.subcommand == "config-llm-web": - config_llm_web() - - elif args.subcommand == "config-jira": - save_jira_config(args.url, args.username, args.api_token) - print(f"JIRA configuration set: URL={args.url}, Username={args.username}") + elif args.subcommand == "config": + if args.config_type == "llm": + save_llm_config(args.model, args.api_base, args.api_key) + print(f"LLM configuration set: Model={args.model}, API Base={args.api_base or 'default'}") + + elif args.config_type == "llm-web": + config_llm_web() - # Verify connection if requested - if args.verify: - if JiraClient: - jira_client = JiraClient( - jira_url=args.url, - jira_user=args.username, - jira_api_token=args.api_token - ) - if jira_client.is_connected(): - print("JIRA connection verified successfully!") + elif args.config_type == "jira": + save_jira_config(args.url, args.username, args.api_token) + print(f"JIRA configuration set: URL={args.url}, Username={args.username}") + + # Verify connection if requested + if args.verify: + if JiraClient: + jira_client = JiraClient( + jira_url=args.url, + jira_user=args.username, + jira_api_token=args.api_token + ) + if jira_client.is_connected(): + print("JIRA connection verified successfully!") + else: + print("Failed to connect to JIRA. Please check your credentials.") else: - print("Failed to connect to JIRA. Please check your credentials.") - else: - print("JIRA package not installed. Cannot verify connection.") - - elif args.subcommand == "config-jira-web": - config_jira_web() + print("JIRA package not installed. Cannot verify connection.") + + elif args.config_type == "jira-web": + config_jira_web() + + else: + config_parser.print_help() elif args.subcommand == "login": login(API_URL, DASHBOARD_URL) From f22736cceff613a36cb1a8cc7705ef11e7076c24 Mon Sep 17 00:00:00 2001 From: sumansaurabh Date: Thu, 6 Mar 2025 12:23:53 +0530 Subject: [PATCH 02/22] feat(hook_commands): enhance post-commit hook to automatically generate documentation for changed files --- penify_hook/commands/hook_commands.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/penify_hook/commands/hook_commands.py b/penify_hook/commands/hook_commands.py index dcb515f..9a56f40 100644 --- a/penify_hook/commands/hook_commands.py +++ b/penify_hook/commands/hook_commands.py @@ -4,13 +4,15 @@ HOOK_FILENAME = "post-commit" HOOK_TEMPLATE = """#!/bin/sh # This is a post-commit hook generated by penifycli. +# Automatically generates documentation for changed files after each commit. -penifycli -t {token} -gf {git_folder_path} +penifycli docgen -gf {git_folder_path} -t {token} """ def install_git_hook(location, token): """ - Install a post-commit hook in the specified location. + Install a post-commit hook in the specified location that generates documentation + for changed files after each commit. """ hooks_dir = Path(location) / ".git/hooks" hook_path = hooks_dir / HOOK_FILENAME @@ -24,6 +26,7 @@ def install_git_hook(location, token): hook_path.chmod(0o755) # Make the hook script executable print(f"Post-commit hook installed in {hook_path}") + print(f"Documentation will now be automatically generated after each commit.") def uninstall_git_hook(location): """ From 52ad0b60e050eaed758f459435571ad3e0cf7322 Mon Sep 17 00:00:00 2001 From: sumansaurabh Date: Thu, 6 Mar 2025 12:26:13 +0530 Subject: [PATCH 03/22] feat: Rename and restructure documentation generation commands for clarity and consistency --- README.md | 2 +- penify_hook/main.py | 62 ++++++++++++++++++++++++++++++++------------- 2 files changed, 45 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index ec60b4d..5d34fbf 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ penifycli uninstall-hook -l /path/to/git/repo To generate documentation for files or folders: ```bash -penifycli doc-gen [options] +penifycli docgen [options] ``` Options: diff --git a/penify_hook/main.py b/penify_hook/main.py index 7bd5475..f3439ef 100644 --- a/penify_hook/main.py +++ b/penify_hook/main.py @@ -33,23 +33,36 @@ def main(): parser = argparse.ArgumentParser(description="Penify CLI tool for managing Git hooks and generating documentation.") - subparsers = parser.add_subparsers(title="subcommands", dest="subcommand") - # Subcommand: install-hook - install_parser = subparsers.add_parser("install-hook", help="Install the Git post-commit hook.") + # Create docgen parser as main command with subcommands + docgen_parser = subparsers.add_parser("docgen", help="Generate documentation and manage Git hooks.") + docgen_subparsers = docgen_parser.add_subparsers(title="docgen_subcommand", dest="docgen_subcommand") + + # Docgen main options (for direct documentation generation) + docgen_parser.add_argument("-fl", "--file_path", help="Path of the file to generate documentation.") + docgen_parser.add_argument("-cf", "--complete_folder_path", help="Generate documentation for the entire folder.") + docgen_parser.add_argument("-gf", "--git_folder_path", help="Path to the folder with git to scan for modified files.", default=os.getcwd()) + + # Subcommand: install-hook (as part of docgen) + install_hook_parser = docgen_subparsers.add_parser("install-hook", help="Install the Git post-commit hook.") + install_hook_parser.add_argument("-l", "--location", required=False, + help="Location in which to install the Git hook. Defaults to current directory.", + default=os.getcwd()) + + # Subcommand: uninstall-hook (as part of docgen) + uninstall_hook_parser = docgen_subparsers.add_parser("uninstall-hook", help="Uninstall the Git post-commit hook.") + uninstall_hook_parser.add_argument("-l", "--location", required=False, + help="Location from which to uninstall the Git hook. Defaults to current directory.", + default=os.getcwd()) + + # Legacy commands for backward compatibility (deprecated but still functional) + install_parser = subparsers.add_parser("install-hook", help="[DEPRECATED] Install the Git post-commit hook.") install_parser.add_argument("-l", "--location", required=True, help="Location in which to install the Git hook.") - # Subcommand: uninstall-hook - uninstall_parser = subparsers.add_parser("uninstall-hook", help="Uninstall the Git post-commit hook.") + uninstall_parser = subparsers.add_parser("uninstall-hook", help="[DEPRECATED] Uninstall the Git post-commit hook.") uninstall_parser.add_argument("-l", "--location", required=True, help="Location from which to uninstall the Git hook.") - # Subcommand: doc-gen - doc_gen_parser = subparsers.add_parser("doc-gen", help="Generate documentation for specified files or folders.") - doc_gen_parser.add_argument("-fl", "--file_path", help="Path of the file to generate documentation.") - doc_gen_parser.add_argument("-cf", "--complete_folder_path", help="Generate documentation for the entire folder.") - doc_gen_parser.add_argument("-gf", "--git_folder_path", help="Path to the folder with git to scan for modified files.", default=os.getcwd()) - # Subcommand: commit commit_parser = subparsers.add_parser("commit", help="Commit with a message.") commit_parser.add_argument("-gf", "--git_folder_path", help="Path to the folder with git.", default=os.getcwd()) @@ -96,21 +109,34 @@ def main(): token = get_token() # Process commands - if args.subcommand == "install-hook": + if args.subcommand == "docgen": + if args.docgen_subcommand == "install-hook": + if not token: + print("Error: API token is required.") + sys.exit(1) + install_git_hook(args.location, token) + + elif args.docgen_subcommand == "uninstall-hook": + uninstall_git_hook(args.location) + + else: # Direct documentation generation + if not token: + print("Error: API token is required.") + sys.exit(1) + generate_doc(API_URL, token, args.file_path, args.complete_folder_path, args.git_folder_path) + + # Legacy commands (deprecated) + elif args.subcommand == "install-hook": + print("Warning: 'install-hook' is deprecated. Please use 'docgen install-hook' instead.") if not token: print("Error: API token is required.") sys.exit(1) install_git_hook(args.location, token) elif args.subcommand == "uninstall-hook": + print("Warning: 'uninstall-hook' is deprecated. Please use 'docgen uninstall-hook' instead.") uninstall_git_hook(args.location) - elif args.subcommand == "doc-gen": - if not token: - print("Error: API token is required.") - sys.exit(1) - generate_doc(API_URL, token, args.file_path, args.complete_folder_path, args.git_folder_path) - elif args.subcommand == "commit": if not token: print("Error: API token is required.") From 4017f74e786844af9fda1217d2c3f577fccd5484 Mon Sep 17 00:00:00 2001 From: sumansaurabh Date: Thu, 6 Mar 2025 12:52:38 +0530 Subject: [PATCH 04/22] feat: Enhance CLI tool description and help messages for improved user guidance --- penify_hook/main.py | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/penify_hook/main.py b/penify_hook/main.py index f3439ef..1f0e388 100644 --- a/penify_hook/main.py +++ b/penify_hook/main.py @@ -31,7 +31,16 @@ def main(): logging.basicConfig(level=logging.WARNING, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') - parser = argparse.ArgumentParser(description="Penify CLI tool for managing Git hooks and generating documentation.") + # Multi-line description using triple quotes + description = """Penify CLI tool for: +1. AI commit message generation +2. Using JIRA descriptions to enhance commit messages +3. Generating code documentation for code files +4. Installing Git hooks for automatic documentation generation +5. For more information, visit https://docs.penify.dev/ +""" + + parser = argparse.ArgumentParser(description=description, formatter_class=argparse.RawDescriptionHelpFormatter) subparsers = parser.add_subparsers(title="subcommands", dest="subcommand") @@ -56,17 +65,10 @@ def main(): help="Location from which to uninstall the Git hook. Defaults to current directory.", default=os.getcwd()) - # Legacy commands for backward compatibility (deprecated but still functional) - install_parser = subparsers.add_parser("install-hook", help="[DEPRECATED] Install the Git post-commit hook.") - install_parser.add_argument("-l", "--location", required=True, help="Location in which to install the Git hook.") - - uninstall_parser = subparsers.add_parser("uninstall-hook", help="[DEPRECATED] Uninstall the Git post-commit hook.") - uninstall_parser.add_argument("-l", "--location", required=True, help="Location from which to uninstall the Git hook.") - # Subcommand: commit commit_parser = subparsers.add_parser("commit", help="Commit with a message.") commit_parser.add_argument("-gf", "--git_folder_path", help="Path to the folder with git.", default=os.getcwd()) - commit_parser.add_argument("-m", "--message", required=False, help="Commit message.", default="N/A") + commit_parser.add_argument("-m", "--message", required=False, help="Commit with contextual commit message. Required before using COMMIT feature", default="N/A") commit_parser.add_argument("-e", "--terminal", required=False, help="Open edit terminal", default="False") # Add LLM options commit_parser.add_argument("--llm", "--llm-model", dest="llm_model", help="LLM model to use") @@ -78,11 +80,11 @@ def main(): commit_parser.add_argument("--jira-api-token", help="JIRA API token") # Consolidated config subcommand - config_parser = subparsers.add_parser("config", help="Configure various settings") + config_parser = subparsers.add_parser("config", help="Configure Local LLM and JIRA settings. It's required to set up local LLM and JIRA settings before using the commit command.") config_subparsers = config_parser.add_subparsers(title="config_type", dest="config_type") # Config subcommand: llm - llm_config_parser = config_subparsers.add_parser("llm", help="Configure LLM settings") + llm_config_parser = config_subparsers.add_parser("llm", help="Configure LLM settings like Local LLM model or use your own LLM service.") llm_config_parser.add_argument("--model", required=True, help="LLM model to use") llm_config_parser.add_argument("--api-base", help="API base URL for the LLM service") llm_config_parser.add_argument("--api-key", help="API key for the LLM service") @@ -91,7 +93,7 @@ def main(): config_subparsers.add_parser("llm-web", help="Configure LLM settings through a web interface") # Config subcommand: jira - jira_config_parser = config_subparsers.add_parser("jira", help="Configure JIRA settings") + jira_config_parser = config_subparsers.add_parser("jira", help="Configure JIRA settings so that commit messages can be linked to JIRA issues.") jira_config_parser.add_argument("--url", required=True, help="JIRA base URL") jira_config_parser.add_argument("--username", required=True, help="JIRA username or email") jira_config_parser.add_argument("--api-token", required=True, help="JIRA API token") @@ -101,7 +103,7 @@ def main(): config_subparsers.add_parser("jira-web", help="Configure JIRA settings through a web interface") # Subcommand: login - subparsers.add_parser("login", help="Log in to Penify and obtain an API token.") + subparsers.add_parser("login", help="Log in to Penify to use advanced features like code documentation generation or code analysis. Basic features like commit documentation are available without logging in.") args = parser.parse_args() From e0a9cb1f13b63dff4df7d8f5dafcd90e8c734e1f Mon Sep 17 00:00:00 2001 From: sumansaurabh Date: Thu, 6 Mar 2025 12:53:08 +0530 Subject: [PATCH 05/22] feat: Remove obsolete snorkell-auto-documentation workflow --- .../workflows/snorkell-auto-documentation.yml | 19 ------------------- 1 file changed, 19 deletions(-) delete mode 100644 .github/workflows/snorkell-auto-documentation.yml diff --git a/.github/workflows/snorkell-auto-documentation.yml b/.github/workflows/snorkell-auto-documentation.yml deleted file mode 100644 index 926c7e5..0000000 --- a/.github/workflows/snorkell-auto-documentation.yml +++ /dev/null @@ -1,19 +0,0 @@ -# This workflow will improvise current file with AI genereated documentation and Create new PR - -name: Penify - Revolutionizing Documentation on GitHub - -on: - push: - branches: ["main"] - workflow_dispatch: - -jobs: - Documentation: - runs-on: ubuntu-latest - steps: - - name: Penify DocGen Client - uses: SingularityX-ai/snorkell-documentation-client@v1.0.0 - with: - client_id: ${{ secrets.SNORKELL_CLIENT_ID }} - api_key: ${{ secrets.SNORKELL_API_KEY }} - branch_name: "main" \ No newline at end of file From 00c191c43ac1b71ed1e0966b00a1a366fbf7b3d8 Mon Sep 17 00:00:00 2001 From: sumansaurabh Date: Thu, 6 Mar 2025 13:05:31 +0530 Subject: [PATCH 06/22] feat: Update README and enhance CLI command descriptions for clarity and usability --- README.md | 2 +- penify_hook/main.py | 98 ++++++++++++++++++++++----------------------- 2 files changed, 49 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index 5d34fbf..58d1d04 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Penify CLI -Penify CLI is a command-line tool for managing Git hooks, generating documentation, and streamlining the development workflow. It provides functionality to install and uninstall Git post-commit hooks, generate documentation for files or folders, perform Git commits with automated message generation, and manage authentication. +Penify CLI is a command-line tool for managing Git commits, generating documentation, and streamlining the development workflow. It provides AI-powered commit message generation, JIRA integration, and documentation tools. ## Installation diff --git a/penify_hook/main.py b/penify_hook/main.py index 1f0e388..e32b13f 100644 --- a/penify_hook/main.py +++ b/penify_hook/main.py @@ -42,33 +42,23 @@ def main(): parser = argparse.ArgumentParser(description=description, formatter_class=argparse.RawDescriptionHelpFormatter) + # Create subparsers for the main commands subparsers = parser.add_subparsers(title="subcommands", dest="subcommand") - - # Create docgen parser as main command with subcommands - docgen_parser = subparsers.add_parser("docgen", help="Generate documentation and manage Git hooks.") - docgen_subparsers = docgen_parser.add_subparsers(title="docgen_subcommand", dest="docgen_subcommand") - # Docgen main options (for direct documentation generation) - docgen_parser.add_argument("-fl", "--file_path", help="Path of the file to generate documentation.") - docgen_parser.add_argument("-cf", "--complete_folder_path", help="Generate documentation for the entire folder.") - docgen_parser.add_argument("-gf", "--git_folder_path", help="Path to the folder with git to scan for modified files.", default=os.getcwd()) + # Group commands logically + basic_title = "Basic Commands (No login required)" + advanced_title = "Advanced Commands (Login required)" - # Subcommand: install-hook (as part of docgen) - install_hook_parser = docgen_subparsers.add_parser("install-hook", help="Install the Git post-commit hook.") - install_hook_parser.add_argument("-l", "--location", required=False, - help="Location in which to install the Git hook. Defaults to current directory.", - default=os.getcwd()) + # Create grouped subparsers (visually separated in help output) + parser.add_argument_group(basic_title) + parser.add_argument_group(advanced_title) + + # ===== BASIC COMMANDS (No login required) ===== - # Subcommand: uninstall-hook (as part of docgen) - uninstall_hook_parser = docgen_subparsers.add_parser("uninstall-hook", help="Uninstall the Git post-commit hook.") - uninstall_hook_parser.add_argument("-l", "--location", required=False, - help="Location from which to uninstall the Git hook. Defaults to current directory.", - default=os.getcwd()) - # Subcommand: commit - commit_parser = subparsers.add_parser("commit", help="Commit with a message.") + commit_parser = subparsers.add_parser("commit", help="Generate smart commit messages (no login required).") commit_parser.add_argument("-gf", "--git_folder_path", help="Path to the folder with git.", default=os.getcwd()) - commit_parser.add_argument("-m", "--message", required=False, help="Commit with contextual commit message. Required before using COMMIT feature", default="N/A") + commit_parser.add_argument("-m", "--message", required=False, help="Commit with contextual commit message.", default="N/A") commit_parser.add_argument("-e", "--terminal", required=False, help="Open edit terminal", default="False") # Add LLM options commit_parser.add_argument("--llm", "--llm-model", dest="llm_model", help="LLM model to use") @@ -79,12 +69,12 @@ def main(): commit_parser.add_argument("--jira-user", help="JIRA username or email") commit_parser.add_argument("--jira-api-token", help="JIRA API token") - # Consolidated config subcommand - config_parser = subparsers.add_parser("config", help="Configure Local LLM and JIRA settings. It's required to set up local LLM and JIRA settings before using the commit command.") + # Subcommand: config + config_parser = subparsers.add_parser("config", help="Configure local settings (no login required).") config_subparsers = config_parser.add_subparsers(title="config_type", dest="config_type") # Config subcommand: llm - llm_config_parser = config_subparsers.add_parser("llm", help="Configure LLM settings like Local LLM model or use your own LLM service.") + llm_config_parser = config_subparsers.add_parser("llm", help="Configure LLM settings.") llm_config_parser.add_argument("--model", required=True, help="LLM model to use") llm_config_parser.add_argument("--api-base", help="API base URL for the LLM service") llm_config_parser.add_argument("--api-key", help="API key for the LLM service") @@ -93,7 +83,7 @@ def main(): config_subparsers.add_parser("llm-web", help="Configure LLM settings through a web interface") # Config subcommand: jira - jira_config_parser = config_subparsers.add_parser("jira", help="Configure JIRA settings so that commit messages can be linked to JIRA issues.") + jira_config_parser = config_subparsers.add_parser("jira", help="Configure JIRA settings.") jira_config_parser.add_argument("--url", required=True, help="JIRA base URL") jira_config_parser.add_argument("--username", required=True, help="JIRA username or email") jira_config_parser.add_argument("--api-token", required=True, help="JIRA API token") @@ -101,9 +91,32 @@ def main(): # Config subcommand: jira-web config_subparsers.add_parser("jira-web", help="Configure JIRA settings through a web interface") - - # Subcommand: login - subparsers.add_parser("login", help="Log in to Penify to use advanced features like code documentation generation or code analysis. Basic features like commit documentation are available without logging in.") + + # ===== ADVANCED COMMANDS (Login required) ===== + + # Subcommand: login (bridge between basic and advanced) + login_parser = subparsers.add_parser("login", help="Log in to Penify to use advanced features like documentation generation.") + + # Advanced Subcommand: docgen + docgen_parser = subparsers.add_parser("docgen", help="[REQUIRES LOGIN] Generate documentation and manage Git hooks.") + docgen_subparsers = docgen_parser.add_subparsers(title="docgen_subcommand", dest="docgen_subcommand") + + # Docgen main options (for direct documentation generation) + docgen_parser.add_argument("-fl", "--file_path", help="Path of the file to generate documentation.") + docgen_parser.add_argument("-cf", "--complete_folder_path", help="Generate documentation for the entire folder.") + docgen_parser.add_argument("-gf", "--git_folder_path", help="Path to the folder with git to scan for modified files.", default=os.getcwd()) + + # Subcommand: install-hook (as part of docgen) + install_hook_parser = docgen_subparsers.add_parser("install-hook", help="Install the Git post-commit hook.") + install_hook_parser.add_argument("-l", "--location", required=False, + help="Location in which to install the Git hook. Defaults to current directory.", + default=os.getcwd()) + + # Subcommand: uninstall-hook (as part of docgen) + uninstall_hook_parser = docgen_subparsers.add_parser("uninstall-hook", help="Uninstall the Git post-commit hook.") + uninstall_hook_parser.add_argument("-l", "--location", required=False, + help="Location from which to uninstall the Git hook. Defaults to current directory.", + default=os.getcwd()) args = parser.parse_args() @@ -112,38 +125,22 @@ def main(): # Process commands if args.subcommand == "docgen": + # Check for login for all advanced commands + if not token: + print("Error: This command requires login. Please run 'penifycli login' first.") + sys.exit(1) + if args.docgen_subcommand == "install-hook": - if not token: - print("Error: API token is required.") - sys.exit(1) install_git_hook(args.location, token) elif args.docgen_subcommand == "uninstall-hook": uninstall_git_hook(args.location) else: # Direct documentation generation - if not token: - print("Error: API token is required.") - sys.exit(1) generate_doc(API_URL, token, args.file_path, args.complete_folder_path, args.git_folder_path) - # Legacy commands (deprecated) - elif args.subcommand == "install-hook": - print("Warning: 'install-hook' is deprecated. Please use 'docgen install-hook' instead.") - if not token: - print("Error: API token is required.") - sys.exit(1) - install_git_hook(args.location, token) - - elif args.subcommand == "uninstall-hook": - print("Warning: 'uninstall-hook' is deprecated. Please use 'docgen uninstall-hook' instead.") - uninstall_git_hook(args.location) - elif args.subcommand == "commit": - if not token: - print("Error: API token is required.") - sys.exit(1) - + # For commit, token is now optional - some functionality may be limited without it open_terminal = args.terminal.lower() == "true" # Get LLM configuration @@ -175,6 +172,7 @@ def main(): jira_url, jira_user, jira_api_token) elif args.subcommand == "config": + # Config doesn't require token if args.config_type == "llm": save_llm_config(args.model, args.api_base, args.api_key) print(f"LLM configuration set: Model={args.model}, API Base={args.api_base or 'default'}") From 4e9225555890e415c24d4424d7781344d76b681f Mon Sep 17 00:00:00 2001 From: sumansaurabh Date: Thu, 6 Mar 2025 13:08:15 +0530 Subject: [PATCH 07/22] feat: Simplify argument names in documentation generation CLI for clarity --- penify_hook/main.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/penify_hook/main.py b/penify_hook/main.py index e32b13f..c0f69d3 100644 --- a/penify_hook/main.py +++ b/penify_hook/main.py @@ -103,8 +103,7 @@ def main(): # Docgen main options (for direct documentation generation) docgen_parser.add_argument("-fl", "--file_path", help="Path of the file to generate documentation.") - docgen_parser.add_argument("-cf", "--complete_folder_path", help="Generate documentation for the entire folder.") - docgen_parser.add_argument("-gf", "--git_folder_path", help="Path to the folder with git to scan for modified files.", default=os.getcwd()) + docgen_parser.add_argument("-l", "--git_folder_path", help="Path to the folder with git to scan for modified files.", default=os.getcwd()) # Subcommand: install-hook (as part of docgen) install_hook_parser = docgen_subparsers.add_parser("install-hook", help="Install the Git post-commit hook.") From f20938cd15520b4d763f149cfe810f6bf51533e5 Mon Sep 17 00:00:00 2001 From: sumansaurabh Date: Thu, 6 Mar 2025 13:16:09 +0530 Subject: [PATCH 08/22] feat: Simplify argument names and improve documentation generation command for clarity --- penify_hook/main.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/penify_hook/main.py b/penify_hook/main.py index c0f69d3..961e8b9 100644 --- a/penify_hook/main.py +++ b/penify_hook/main.py @@ -102,8 +102,7 @@ def main(): docgen_subparsers = docgen_parser.add_subparsers(title="docgen_subcommand", dest="docgen_subcommand") # Docgen main options (for direct documentation generation) - docgen_parser.add_argument("-fl", "--file_path", help="Path of the file to generate documentation.") - docgen_parser.add_argument("-l", "--git_folder_path", help="Path to the folder with git to scan for modified files.", default=os.getcwd()) + docgen_parser.add_argument("-l", "--location", help="Path to the folder or file to Generate Documentation.", default=os.getcwd()) # Subcommand: install-hook (as part of docgen) install_hook_parser = docgen_subparsers.add_parser("install-hook", help="Install the Git post-commit hook.") @@ -136,7 +135,7 @@ def main(): uninstall_git_hook(args.location) else: # Direct documentation generation - generate_doc(API_URL, token, args.file_path, args.complete_folder_path, args.git_folder_path) + generate_doc(API_URL, token, args.file_path) elif args.subcommand == "commit": # For commit, token is now optional - some functionality may be limited without it From 8eb55c2ead371007ea92a7e4ab1a23fdad76ed9e Mon Sep 17 00:00:00 2001 From: sumansaurabh Date: Fri, 7 Mar 2025 02:12:42 +0530 Subject: [PATCH 09/22] feat: Enhance 'docgen' command description and improve help messages for clarity --- penify_hook/main.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/penify_hook/main.py b/penify_hook/main.py index 961e8b9..ad1438b 100644 --- a/penify_hook/main.py +++ b/penify_hook/main.py @@ -96,13 +96,18 @@ def main(): # Subcommand: login (bridge between basic and advanced) login_parser = subparsers.add_parser("login", help="Log in to Penify to use advanced features like documentation generation.") + + docgen_description="""By default, 'docgen' generates documentation for the latest Git commit diff. Use the -l flag to document a specific file or folder. + +The 'install-hook' command sets up a Git post-commit hook to auto-generate documentation after each commit. +""" # Advanced Subcommand: docgen - docgen_parser = subparsers.add_parser("docgen", help="[REQUIRES LOGIN] Generate documentation and manage Git hooks.") - docgen_subparsers = docgen_parser.add_subparsers(title="docgen_subcommand", dest="docgen_subcommand") + docgen_parser = subparsers.add_parser("docgen", help="[REQUIRES LOGIN] Generate code documentation for the file or folder", description=docgen_description, formatter_class=argparse.RawDescriptionHelpFormatter) + docgen_subparsers = docgen_parser.add_subparsers(title="docgen_subcommand") # Docgen main options (for direct documentation generation) - docgen_parser.add_argument("-l", "--location", help="Path to the folder or file to Generate Documentation.", default=os.getcwd()) + docgen_parser.add_argument("-l", "--location", help="[Optional] Path to the folder or file to Generate Documentation. By default it will pick the root directory.", default=os.getcwd()) # Subcommand: install-hook (as part of docgen) install_hook_parser = docgen_subparsers.add_parser("install-hook", help="Install the Git post-commit hook.") From f7a5281c2c1769be1c43f41a0564b21ffc5e4886 Mon Sep 17 00:00:00 2001 From: sumansaurabh Date: Fri, 7 Mar 2025 02:20:20 +0530 Subject: [PATCH 10/22] feat: Refactor documentation generation logic to support file and folder inputs, enhancing error handling and improving git folder detection --- penify_hook/commands/doc_commands.py | 18 +++++++++++++++--- penify_hook/git_analyzer.py | 23 ++++++++++++++++++++++- penify_hook/main.py | 2 +- 3 files changed, 38 insertions(+), 5 deletions(-) diff --git a/penify_hook/commands/doc_commands.py b/penify_hook/commands/doc_commands.py index 500d81c..9e01dd4 100644 --- a/penify_hook/commands/doc_commands.py +++ b/penify_hook/commands/doc_commands.py @@ -1,22 +1,34 @@ +import os import sys from ..folder_analyzer import FolderAnalyzerGenHook from ..file_analyzer import FileAnalyzerGenHook from ..git_analyzer import GitDocGenHook from ..api_client import APIClient -def generate_doc(api_url, token, file_path=None, complete_folder_path=None, git_folder_path=None): +def generate_doc(api_url, token, location=None): """ Generates documentation based on the given parameters. """ api_client = APIClient(api_url, token) - if file_path: + if location is None: + current_folder_path = os.getcwd() try: - analyzer = FileAnalyzerGenHook(file_path, api_client) + analyzer = GitDocGenHook(current_folder_path, api_client) analyzer.run() except Exception as e: print(f"Error: {e}") sys.exit(1) + + # if location is a file + if len(location.split('.')) > 1: + try: + analyzer = FileAnalyzerGenHook(location, api_client) + analyzer.run() + except Exception as e: + print(f"Error: {e}") + sys.exit(1) + elif complete_folder_path: try: analyzer = FolderAnalyzerGenHook(complete_folder_path, api_client) diff --git a/penify_hook/git_analyzer.py b/penify_hook/git_analyzer.py index d3d1333..63e1d29 100644 --- a/penify_hook/git_analyzer.py +++ b/penify_hook/git_analyzer.py @@ -4,14 +4,35 @@ from tqdm import tqdm from .api_client import APIClient + class GitDocGenHook: def __init__(self, repo_path: str, api_client: APIClient): - self.repo_path = repo_path + self.repo_path = self._recursive_search_git_folder(repo_path) self.api_client = api_client self.repo = Repo(repo_path) self.supported_file_types = set(self.api_client.get_supported_file_types()) self.repo_details = self.get_repo_details() + def _recursive_search_git_folder(self, folder_path): + """Recursively search for the .git folder in the specified directory. + + This function searches for the .git folder in the specified directory and + its parent directories. It returns the path to the .git folder if found, + or None if the folder is not found. + + Args: + folder_path (str): The path to the directory to search. + + Returns: + str: The path to the .git folder if found, None otherwise. + """ + if not folder_path or folder_path == '/': + return None + git_folder = os.path.join(folder_path, '.git') + if os.path.exists(git_folder): + return git_folder + return self._recursive_search_git_folder(os.path.dirname(folder_path)) + def get_repo_details(self): """Get the details of the repository, including the hosting service, organization name, and repository name. diff --git a/penify_hook/main.py b/penify_hook/main.py index ad1438b..636aff6 100644 --- a/penify_hook/main.py +++ b/penify_hook/main.py @@ -130,7 +130,7 @@ def main(): if args.subcommand == "docgen": # Check for login for all advanced commands if not token: - print("Error: This command requires login. Please run 'penifycli login' first.") + logging.error("Error: Unable to authenticate. Please run 'penifycli login'.") sys.exit(1) if args.docgen_subcommand == "install-hook": From dbe50a91c35918c11c4930088117265588c6c89e Mon Sep 17 00:00:00 2001 From: sumansaurabh Date: Fri, 7 Mar 2025 02:24:38 +0530 Subject: [PATCH 11/22] feat: Implement logging in GitDocGenHook for better error tracking and process visibility --- penify_hook/git_analyzer.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/penify_hook/git_analyzer.py b/penify_hook/git_analyzer.py index 63e1d29..200b487 100644 --- a/penify_hook/git_analyzer.py +++ b/penify_hook/git_analyzer.py @@ -3,7 +3,10 @@ from git import Repo from tqdm import tqdm from .api_client import APIClient +import logging +# Set up logger +logger = logging.getLogger(__name__) class GitDocGenHook: def __init__(self, repo_path: str, api_client: APIClient): @@ -86,7 +89,7 @@ def get_repo_details(self): repo_name = match.group(3) except Exception as e: - print(f"Error determining repo details: {e}") + logger.error(f"Error determining GIT provider: {e}") return { "organization_name": org_name, @@ -185,13 +188,13 @@ def process_file(self, file_path): file_extension = os.path.splitext(file_path)[1].lower() if not file_extension: - print(f"File {file_path} has no extension. Skipping.") + logger.info(f"File {file_path} has no extension. Skipping.") return False file_extension = file_extension[1:] # Remove the leading dot if file_extension not in self.supported_file_types: - print(f"File type {file_extension} is not supported. Skipping {file_path}.") + logger.info(f"File type {file_extension} is not supported. Skipping {file_path}.") return False with open(file_abs_path, 'r') as file: @@ -205,7 +208,7 @@ def process_file(self, file_path): diff_text = self.repo.git.diff(prev_commit.hexsha, last_commit.hexsha, '--', file_path) if not diff_text: - print(f"No changes detected for {file_path}") + logger.info(f"No changes detected for {file_path}") return False modified_lines = self.get_modified_lines(diff_text) @@ -215,11 +218,12 @@ def process_file(self, file_path): return False if response == content: - print(f"No changes detected for {file_path}") + logger.info(f"No changes detected for {file_path}") return False # If the response is successful, replace the file content with open(file_abs_path, 'w') as file: file.write(response) + logger.info(f"Updated file {file_path} with generated documentation") return True def run(self): @@ -234,6 +238,7 @@ def run(self): process. If any modifications are made to the files, an auto-commit is created to save those changes. """ + logger.info("Starting doc_gen_hook processing") modified_files = self.get_modified_files_in_last_commit() changes_made = False total_files = len(modified_files) @@ -246,12 +251,12 @@ def run(self): self.repo.git.add(file) changes_made = True except Exception as file_error: - print(f"Error processing file [{file}]: {file_error}") + logger.error(f"Error processing file [{file}]: {file_error}") pbar.update(1) # Update the progress bar # If any file was modified, create a new commit if changes_made: # self.repo.git.commit('-m', 'Auto-commit: Updated files after doc_gen_hook processing.') - print("Auto-commit created with changes.") + logger.info("Auto-commit created with changes.") else: - print("doc_gen_hook complete. No changes made.") \ No newline at end of file + logger.info("doc_gen_hook complete. No changes made.") \ No newline at end of file From 6693bf9555f58428cf44f497f5df1748143e8fb9 Mon Sep 17 00:00:00 2001 From: sumansaurabh Date: Fri, 7 Mar 2025 02:39:45 +0530 Subject: [PATCH 12/22] feat: Improve documentation generation process with enhanced logging and error handling --- penify_hook/commands/doc_commands.py | 18 ++++++++---------- penify_hook/git_analyzer.py | 26 +++++++++----------------- penify_hook/main.py | 6 +++--- 3 files changed, 20 insertions(+), 30 deletions(-) diff --git a/penify_hook/commands/doc_commands.py b/penify_hook/commands/doc_commands.py index 9e01dd4..66b8f7c 100644 --- a/penify_hook/commands/doc_commands.py +++ b/penify_hook/commands/doc_commands.py @@ -11,8 +11,13 @@ def generate_doc(api_url, token, location=None): """ api_client = APIClient(api_url, token) + print("Generating documentation...") + print(f"API URL: {api_url}") + print(f"API Token: {token}") + print(f"Location: {location}") if location is None: current_folder_path = os.getcwd() + print(f"Current Folder Path: {current_folder_path}") try: analyzer = GitDocGenHook(current_folder_path, api_client) analyzer.run() @@ -21,7 +26,7 @@ def generate_doc(api_url, token, location=None): sys.exit(1) # if location is a file - if len(location.split('.')) > 1: + elif len(location.split('.')) > 1: try: analyzer = FileAnalyzerGenHook(location, api_client) analyzer.run() @@ -29,17 +34,10 @@ def generate_doc(api_url, token, location=None): print(f"Error: {e}") sys.exit(1) - elif complete_folder_path: - try: - analyzer = FolderAnalyzerGenHook(complete_folder_path, api_client) - analyzer.run() - except Exception as e: - print(f"Error: {e}") - sys.exit(1) else: try: - analyzer = GitDocGenHook(git_folder_path, api_client) + analyzer = FolderAnalyzerGenHook(location, api_client) analyzer.run() except Exception as e: print(f"Error: {e}") - sys.exit(1) + sys.exit(1) \ No newline at end of file diff --git a/penify_hook/git_analyzer.py b/penify_hook/git_analyzer.py index 200b487..3d1f391 100644 --- a/penify_hook/git_analyzer.py +++ b/penify_hook/git_analyzer.py @@ -11,30 +11,21 @@ class GitDocGenHook: def __init__(self, repo_path: str, api_client: APIClient): self.repo_path = self._recursive_search_git_folder(repo_path) + print(f"Repo path: {self.repo_path}") self.api_client = api_client self.repo = Repo(repo_path) self.supported_file_types = set(self.api_client.get_supported_file_types()) self.repo_details = self.get_repo_details() def _recursive_search_git_folder(self, folder_path): - """Recursively search for the .git folder in the specified directory. + """Recursively search for the .git folder in the specified directory and than return parent. - This function searches for the .git folder in the specified directory and - its parent directories. It returns the path to the .git folder if found, - or None if the folder is not found. - - Args: - folder_path (str): The path to the directory to search. - - Returns: - str: The path to the .git folder if found, None otherwise. """ - if not folder_path or folder_path == '/': - return None - git_folder = os.path.join(folder_path, '.git') - if os.path.exists(git_folder): - return git_folder - return self._recursive_search_git_folder(os.path.dirname(folder_path)) + if os.path.isdir(folder_path): + if '.git' in os.listdir(folder_path): + return folder_path + else: + return self._recursive_search_git_folder(os.path.dirname(folder_path)) def get_repo_details(self): """Get the details of the repository, including the hosting service, @@ -233,7 +224,7 @@ def run(self): and processes each file. It stages any files that have been modified during processing and creates an auto-commit if changes were made. A progress bar is displayed to indicate the processing status of each - file. The method handles any exceptions that occur during file + file. The method handles any exceptions that occur during file processing, printing an error message for each file that fails to process. If any modifications are made to the files, an auto-commit is created to save those changes. @@ -245,6 +236,7 @@ def run(self): with tqdm(total=total_files, desc="Processing files", unit="file", ncols=80, ascii=True) as pbar: for file in modified_files: + logging.info(f"Processing file: {file}") try: if self.process_file(file): # Stage the modified file diff --git a/penify_hook/main.py b/penify_hook/main.py index 636aff6..9421e01 100644 --- a/penify_hook/main.py +++ b/penify_hook/main.py @@ -104,10 +104,10 @@ def main(): # Advanced Subcommand: docgen docgen_parser = subparsers.add_parser("docgen", help="[REQUIRES LOGIN] Generate code documentation for the file or folder", description=docgen_description, formatter_class=argparse.RawDescriptionHelpFormatter) - docgen_subparsers = docgen_parser.add_subparsers(title="docgen_subcommand") + docgen_subparsers = docgen_parser.add_subparsers(title="docgen_subcommand", dest="docgen_subcommand") # Docgen main options (for direct documentation generation) - docgen_parser.add_argument("-l", "--location", help="[Optional] Path to the folder or file to Generate Documentation. By default it will pick the root directory.", default=os.getcwd()) + docgen_parser.add_argument("-l", "--location", help="[Optional] Path to the folder or file to Generate Documentation. By default it will pick the root directory.", default=None) # Subcommand: install-hook (as part of docgen) install_hook_parser = docgen_subparsers.add_parser("install-hook", help="Install the Git post-commit hook.") @@ -140,7 +140,7 @@ def main(): uninstall_git_hook(args.location) else: # Direct documentation generation - generate_doc(API_URL, token, args.file_path) + generate_doc(API_URL, token, args.location) elif args.subcommand == "commit": # For commit, token is now optional - some functionality may be limited without it From 308960260d4831c610c5cb7fc87a7648bc31ff1c Mon Sep 17 00:00:00 2001 From: sumansaurabh Date: Fri, 7 Mar 2025 02:42:05 +0530 Subject: [PATCH 13/22] feat: Enhance documentation with detailed function descriptions and argument explanations --- penify_hook/commands/doc_commands.py | 16 ++++++++++++++-- penify_hook/git_analyzer.py | 16 ++++++++++++++-- penify_hook/main.py | 14 +++++++++++++- 3 files changed, 41 insertions(+), 5 deletions(-) diff --git a/penify_hook/commands/doc_commands.py b/penify_hook/commands/doc_commands.py index 66b8f7c..d23c182 100644 --- a/penify_hook/commands/doc_commands.py +++ b/penify_hook/commands/doc_commands.py @@ -6,8 +6,20 @@ from ..api_client import APIClient def generate_doc(api_url, token, location=None): - """ - Generates documentation based on the given parameters. + """Generates documentation based on the given parameters. + + This function initializes an API client using the provided API URL and + token. It then generates documentation by analyzing the specified + location, which can be a folder, a file, or the current working + directory if no location is provided. The function handles different + types of analysis based on the input location and reports any errors + encountered during the process. + + Args: + api_url (str): The URL of the API to connect to for documentation generation. + token (str): The authentication token for accessing the API. + location (str?): The path to a specific file or folder to analyze. + If not provided, the current working directory is used. """ api_client = APIClient(api_url, token) diff --git a/penify_hook/git_analyzer.py b/penify_hook/git_analyzer.py index 3d1f391..df606b4 100644 --- a/penify_hook/git_analyzer.py +++ b/penify_hook/git_analyzer.py @@ -13,13 +13,25 @@ def __init__(self, repo_path: str, api_client: APIClient): self.repo_path = self._recursive_search_git_folder(repo_path) print(f"Repo path: {self.repo_path}") self.api_client = api_client - self.repo = Repo(repo_path) + self.repo = Repo(self.repo_path) self.supported_file_types = set(self.api_client.get_supported_file_types()) self.repo_details = self.get_repo_details() def _recursive_search_git_folder(self, folder_path): - """Recursively search for the .git folder in the specified directory and than return parent. + """Recursively search for the .git folder in the specified directory and + return its parent directory. + This function takes a folder path as input and checks if the specified + directory contains a '.git' folder. If it does, the function returns the + path of that directory. If not, it recursively searches the parent + directory until it finds a '.git' folder or reaches the root of the + filesystem. + + Args: + folder_path (str): The path of the directory to search for the .git folder. + + Returns: + str: The path of the directory containing the .git folder. """ if os.path.isdir(folder_path): if '.git' in os.listdir(folder_path): diff --git a/penify_hook/main.py b/penify_hook/main.py index 9421e01..8dcd62e 100644 --- a/penify_hook/main.py +++ b/penify_hook/main.py @@ -26,7 +26,19 @@ # API_URL = 'http://localhost:8000/api' def main(): - """Main entry point for the Penify CLI tool.""" + """Main entry point for the Penify CLI tool. + + This function serves as the main interface for the Penify command-line + tool, which provides various functionalities including AI commit message + generation, JIRA integration for enhancing commit messages, code + documentation generation, and Git hook installation for automatic + documentation generation. It sets up the command-line argument parser + with subcommands for basic and advanced operations. Basic commands do + not require user login, while advanced commands do. The function also + handles the parsing of arguments and the execution of the corresponding + commands based on user input. For more information about the tool and + its capabilities, please visit https://docs.penify.dev/. + """ # Configure logging logging.basicConfig(level=logging.WARNING, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') From df189c933b53c1ecfff0930ffa4aad5beb4be39a Mon Sep 17 00:00:00 2001 From: sumansaurabh Date: Fri, 7 Mar 2025 02:47:31 +0530 Subject: [PATCH 14/22] feat: Add colored terminal output for improved visibility in doc_gen_hook processing --- penify_hook/git_analyzer.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/penify_hook/git_analyzer.py b/penify_hook/git_analyzer.py index df606b4..4923ced 100644 --- a/penify_hook/git_analyzer.py +++ b/penify_hook/git_analyzer.py @@ -4,6 +4,10 @@ from tqdm import tqdm from .api_client import APIClient import logging +from colorama import Fore, Style, init + +# Initialize colorama for cross-platform colored terminal output +init(autoreset=True) # Set up logger logger = logging.getLogger(__name__) @@ -241,26 +245,35 @@ def run(self): process. If any modifications are made to the files, an auto-commit is created to save those changes. """ - logger.info("Starting doc_gen_hook processing") + logger.info(f"{Fore.CYAN}Starting doc_gen_hook processing{Style.RESET_ALL}") modified_files = self.get_modified_files_in_last_commit() changes_made = False total_files = len(modified_files) - with tqdm(total=total_files, desc="Processing files", unit="file", ncols=80, ascii=True) as pbar: + with tqdm(total=total_files, desc=f"{Fore.CYAN}Processing files{Style.RESET_ALL}", unit="file", ncols=80, ascii=True) as pbar: for file in modified_files: + file_display = f"{Fore.YELLOW}{file}{Style.RESET_ALL}" + print(f"\n{Fore.BLUE}Processing file: {file_display}") logging.info(f"Processing file: {file}") try: if self.process_file(file): # Stage the modified file self.repo.git.add(file) changes_made = True + print(f" {Fore.GREEN}✓ Documentation updated{Style.RESET_ALL}") + else: + print(f" {Fore.WHITE}○ No changes needed{Style.RESET_ALL}") except Exception as file_error: - logger.error(f"Error processing file [{file}]: {file_error}") + error_msg = f"Error processing file [{file}]: {file_error}" + logger.error(error_msg) + print(f" {Fore.RED}✗ {error_msg}{Style.RESET_ALL}") pbar.update(1) # Update the progress bar # If any file was modified, create a new commit if changes_made: # self.repo.git.commit('-m', 'Auto-commit: Updated files after doc_gen_hook processing.') - logger.info("Auto-commit created with changes.") + logger.info(f"{Fore.GREEN}Auto-commit created with changes.{Style.RESET_ALL}") + print(f"\n{Fore.GREEN}✓ Auto-commit created with changes{Style.RESET_ALL}") else: - logger.info("doc_gen_hook complete. No changes made.") \ No newline at end of file + logger.info("doc_gen_hook complete. No changes made.") + print(f"\n{Fore.CYAN}✓ doc_gen_hook complete. No changes made.{Style.RESET_ALL}") \ No newline at end of file From 8511aaa56800d0a971734d84fa5a4ff7026d342b Mon Sep 17 00:00:00 2001 From: sumansaurabh Date: Fri, 7 Mar 2025 02:50:01 +0530 Subject: [PATCH 15/22] feat: Remove unnecessary print statements in documentation generation and add colorama as a dependency --- penify_hook/commands/doc_commands.py | 6 ------ setup.py | 1 + 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/penify_hook/commands/doc_commands.py b/penify_hook/commands/doc_commands.py index d23c182..a3fafcd 100644 --- a/penify_hook/commands/doc_commands.py +++ b/penify_hook/commands/doc_commands.py @@ -22,14 +22,8 @@ def generate_doc(api_url, token, location=None): If not provided, the current working directory is used. """ api_client = APIClient(api_url, token) - - print("Generating documentation...") - print(f"API URL: {api_url}") - print(f"API Token: {token}") - print(f"Location: {location}") if location is None: current_folder_path = os.getcwd() - print(f"Current Folder Path: {current_folder_path}") try: analyzer = GitDocGenHook(current_folder_path, api_client) analyzer.run() diff --git a/setup.py b/setup.py index 0d9fcd9..219cd41 100644 --- a/setup.py +++ b/setup.py @@ -8,6 +8,7 @@ "requests", "tqdm", "GitPython", + "colorama", "litellm", "jira" # Add JIRA as a dependency ], From 411e599594d09923e850676fcf5c8d40ebbfa1e4 Mon Sep 17 00:00:00 2001 From: sumansaurabh Date: Fri, 7 Mar 2025 02:53:00 +0530 Subject: [PATCH 16/22] feat: Enhance file processing with logging, progress bar, and colored output --- penify_hook/file_analyzer.py | 78 ++++++++++++++++++++++++++++++------ 1 file changed, 65 insertions(+), 13 deletions(-) diff --git a/penify_hook/file_analyzer.py b/penify_hook/file_analyzer.py index d383c73..6dea5c3 100644 --- a/penify_hook/file_analyzer.py +++ b/penify_hook/file_analyzer.py @@ -1,6 +1,15 @@ import os from git import Repo from .api_client import APIClient +from tqdm import tqdm +from colorama import Fore, Style, init +import logging + +# Initialize colorama for cross-platform colored terminal output +init(autoreset=True) + +# Set up logger +logger = logging.getLogger(__name__) class FileAnalyzerGenHook: def __init__(self, file_path: str, api_client: APIClient): @@ -27,28 +36,35 @@ def process_file(self, file_path): file_extension = os.path.splitext(file_path)[1].lower() if not file_extension: - print(f"File {file_path} has no extension. Skipping.") + logger.info(f"File {file_path} has no extension. Skipping.") return False file_extension = file_extension[1:] # Remove the leading dot if file_extension not in self.supported_file_types: - print(f"File type {file_extension} is not supported. Skipping {file_path}.") + logger.info(f"File type {file_extension} is not supported. Skipping {file_path}.") return False with open(file_abs_path, 'r') as file: content = file.read() - modified_lines = [i for i in range(len(content.splitlines()))] + modified_lines = [i for i in range(len(content.splitlines()))] # Send data to API response = self.api_client.send_file_for_docstring_generation(file_path, content, modified_lines) + if response is None: + return False + + if response == content: + logger.info(f"No changes needed for {file_path}") + return False + # If the response is successful, replace the file content with open(file_abs_path, 'w') as file: file.write(response) - print(f"File [{self.file_path}] processed successfully.") - + logger.info(f"Updated file {file_path} with generated documentation") + return True def run(self): """Run the post-commit hook. @@ -56,12 +72,48 @@ def run(self): This method executes the post-commit hook by processing a specified file. It attempts to process the file located at `self.file_path`. If an error occurs during the processing, it catches the exception and prints - an error message indicating that the file was not processed. + an error message indicating that the file was not processed. The method + displays a progress bar and colored output to provide visual feedback on + the processing status. """ - try: - self.process_file(self.file_path) - # Stage the modified file - - except Exception as e: - print(f"File [{self.file_path}] was not processed.") - \ No newline at end of file + logger.info(f"{Fore.CYAN}Starting file analysis processing{Style.RESET_ALL}") + print(f"{Fore.CYAN}Starting file analysis for {Fore.YELLOW}{self.file_path}{Style.RESET_ALL}") + + # Create a progress bar with appropriate stages + stages = ["Validating file", "Reading content", "Processing", "Writing changes"] + + with tqdm(total=len(stages), desc=f"{Fore.CYAN}Processing file{Style.RESET_ALL}", + unit="step", ncols=80, ascii=True) as pbar: + try: + # Validate and read file + pbar.set_description(f"{Fore.CYAN}Validating file{Style.RESET_ALL}") + file_display = f"{Fore.YELLOW}{self.file_path}{Style.RESET_ALL}" + print(f"\n{Fore.BLUE}Processing file: {file_display}") + pbar.update(1) + + # Process file + pbar.set_description(f"{Fore.CYAN}Processing{Style.RESET_ALL}") + pbar.update(1) + + result = self.process_file(self.file_path) + + # Update progress + pbar.set_description(f"{Fore.CYAN}Writing changes{Style.RESET_ALL}") + pbar.update(2) # Skip to completion + + # Display appropriate message based on result + if result: + print(f" {Fore.GREEN}✓ Documentation updated for {file_display}{Style.RESET_ALL}") + else: + print(f" {Fore.WHITE}○ No changes needed for {file_display}{Style.RESET_ALL}") + + except Exception as e: + error_msg = f"Error processing file [{self.file_path}]: {str(e)}" + logger.error(error_msg) + print(f" {Fore.RED}✗ {error_msg}{Style.RESET_ALL}") + # Ensure progress bar completes even on error + remaining = len(stages) - pbar.n + if remaining > 0: + pbar.update(remaining) + + print(f"\n{Fore.CYAN}✓ File analysis complete{Style.RESET_ALL}") From 78baf759634acda315e75cb89fd896a9600738d5 Mon Sep 17 00:00:00 2001 From: sumansaurabh Date: Fri, 7 Mar 2025 03:12:32 +0530 Subject: [PATCH 17/22] feat: Refactor doc_gen_hook for improved UI output and progress tracking --- penify_hook/file_analyzer.py | 89 +++++++++++------------ penify_hook/git_analyzer.py | 30 ++++---- penify_hook/ui_utils.py | 134 +++++++++++++++++++++++++++++++++++ 3 files changed, 195 insertions(+), 58 deletions(-) create mode 100644 penify_hook/ui_utils.py diff --git a/penify_hook/file_analyzer.py b/penify_hook/file_analyzer.py index 6dea5c3..12c52ed 100644 --- a/penify_hook/file_analyzer.py +++ b/penify_hook/file_analyzer.py @@ -1,12 +1,12 @@ import os from git import Repo from .api_client import APIClient -from tqdm import tqdm -from colorama import Fore, Style, init import logging - -# Initialize colorama for cross-platform colored terminal output -init(autoreset=True) +from .ui_utils import ( + print_info, print_success, print_warning, print_error, + print_processing, print_status, create_stage_progress_bar, + update_stage, format_file_path +) # Set up logger logger = logging.getLogger(__name__) @@ -17,7 +17,7 @@ def __init__(self, file_path: str, api_client: APIClient): self.api_client = api_client self.supported_file_types = set(self.api_client.get_supported_file_types()) - def process_file(self, file_path): + def process_file(self, file_path, pbar): """Process a file by reading its content and sending it to an API for processing. @@ -28,6 +28,7 @@ def process_file(self, file_path): Args: file_path (str): The relative path to the file that needs to be processed. + pbar (tqdm): Progress bar to update during processing. Returns: bool: True if the file was processed successfully, False otherwise. @@ -35,6 +36,9 @@ def process_file(self, file_path): file_abs_path = os.path.join(os.getcwd(), file_path) file_extension = os.path.splitext(file_path)[1].lower() + # Validate file + update_stage(pbar, "Validating file") + if not file_extension: logger.info(f"File {file_path} has no extension. Skipping.") return False @@ -45,12 +49,15 @@ def process_file(self, file_path): logger.info(f"File type {file_extension} is not supported. Skipping {file_path}.") return False + # Read content + update_stage(pbar, "Reading content") with open(file_abs_path, 'r') as file: content = file.read() modified_lines = [i for i in range(len(content.splitlines()))] - # Send data to API + # Process with API + update_stage(pbar, "Generating documentation") response = self.api_client.send_file_for_docstring_generation(file_path, content, modified_lines) if response is None: @@ -59,8 +66,9 @@ def process_file(self, file_path): if response == content: logger.info(f"No changes needed for {file_path}") return False - - # If the response is successful, replace the file content + + # Write changes + update_stage(pbar, "Writing changes") with open(file_abs_path, 'w') as file: file.write(response) logger.info(f"Updated file {file_path} with generated documentation") @@ -76,44 +84,37 @@ def run(self): displays a progress bar and colored output to provide visual feedback on the processing status. """ - logger.info(f"{Fore.CYAN}Starting file analysis processing{Style.RESET_ALL}") - print(f"{Fore.CYAN}Starting file analysis for {Fore.YELLOW}{self.file_path}{Style.RESET_ALL}") + print_info(f"Starting file documentation for {format_file_path(self.file_path)}") # Create a progress bar with appropriate stages - stages = ["Validating file", "Reading content", "Processing", "Writing changes"] + stages = ["Validating", "Reading content", "Documenting", "Writing changes"] + pbar, _ = create_stage_progress_bar(stages, "Processing file") - with tqdm(total=len(stages), desc=f"{Fore.CYAN}Processing file{Style.RESET_ALL}", - unit="step", ncols=80, ascii=True) as pbar: - try: - # Validate and read file - pbar.set_description(f"{Fore.CYAN}Validating file{Style.RESET_ALL}") - file_display = f"{Fore.YELLOW}{self.file_path}{Style.RESET_ALL}" - print(f"\n{Fore.BLUE}Processing file: {file_display}") - pbar.update(1) - - # Process file - pbar.set_description(f"{Fore.CYAN}Processing{Style.RESET_ALL}") - pbar.update(1) + try: + print_processing(self.file_path) + pbar.update(1) # Complete first stage + + result = self.process_file(self.file_path, pbar) + + # Ensure all stages are completed + remaining_steps = len(stages) - pbar.n + if remaining_steps > 0: + pbar.update(remaining_steps) - result = self.process_file(self.file_path) + # Display appropriate message based on result + if result: + print_status('success', f"Documentation updated for {self.file_path}") + else: + print_status('warning', f"No changes needed for {self.file_path}") - # Update progress - pbar.set_description(f"{Fore.CYAN}Writing changes{Style.RESET_ALL}") - pbar.update(2) # Skip to completion + except Exception as e: + error_msg = f"Error processing file [{self.file_path}]: {str(e)}" + logger.error(error_msg) + print_status('error', error_msg) + + # Ensure progress bar completes even on error + remaining = len(stages) - pbar.n + if remaining > 0: + pbar.update(remaining) - # Display appropriate message based on result - if result: - print(f" {Fore.GREEN}✓ Documentation updated for {file_display}{Style.RESET_ALL}") - else: - print(f" {Fore.WHITE}○ No changes needed for {file_display}{Style.RESET_ALL}") - - except Exception as e: - error_msg = f"Error processing file [{self.file_path}]: {str(e)}" - logger.error(error_msg) - print(f" {Fore.RED}✗ {error_msg}{Style.RESET_ALL}") - # Ensure progress bar completes even on error - remaining = len(stages) - pbar.n - if remaining > 0: - pbar.update(remaining) - - print(f"\n{Fore.CYAN}✓ File analysis complete{Style.RESET_ALL}") + print_success("\n✓ File analysis complete") diff --git a/penify_hook/git_analyzer.py b/penify_hook/git_analyzer.py index 4923ced..7d9d66d 100644 --- a/penify_hook/git_analyzer.py +++ b/penify_hook/git_analyzer.py @@ -4,10 +4,11 @@ from tqdm import tqdm from .api_client import APIClient import logging -from colorama import Fore, Style, init - -# Initialize colorama for cross-platform colored terminal output -init(autoreset=True) +from .ui_utils import ( + print_info, print_success, print_warning, print_error, + print_processing, print_status, create_progress_bar, + format_file_path +) # Set up logger logger = logging.getLogger(__name__) @@ -245,35 +246,36 @@ def run(self): process. If any modifications are made to the files, an auto-commit is created to save those changes. """ - logger.info(f"{Fore.CYAN}Starting doc_gen_hook processing{Style.RESET_ALL}") + logger.info("Starting doc_gen_hook processing") + print_info("Starting doc_gen_hook processing") + modified_files = self.get_modified_files_in_last_commit() changes_made = False total_files = len(modified_files) - with tqdm(total=total_files, desc=f"{Fore.CYAN}Processing files{Style.RESET_ALL}", unit="file", ncols=80, ascii=True) as pbar: + with create_progress_bar(total_files, "Processing files", "file") as pbar: for file in modified_files: - file_display = f"{Fore.YELLOW}{file}{Style.RESET_ALL}" - print(f"\n{Fore.BLUE}Processing file: {file_display}") + print_processing(file) logging.info(f"Processing file: {file}") try: if self.process_file(file): # Stage the modified file self.repo.git.add(file) changes_made = True - print(f" {Fore.GREEN}✓ Documentation updated{Style.RESET_ALL}") + print_status('success', "Documentation updated") else: - print(f" {Fore.WHITE}○ No changes needed{Style.RESET_ALL}") + print_status('warning', "No changes needed") except Exception as file_error: error_msg = f"Error processing file [{file}]: {file_error}" logger.error(error_msg) - print(f" {Fore.RED}✗ {error_msg}{Style.RESET_ALL}") + print_status('error', error_msg) pbar.update(1) # Update the progress bar # If any file was modified, create a new commit if changes_made: # self.repo.git.commit('-m', 'Auto-commit: Updated files after doc_gen_hook processing.') - logger.info(f"{Fore.GREEN}Auto-commit created with changes.{Style.RESET_ALL}") - print(f"\n{Fore.GREEN}✓ Auto-commit created with changes{Style.RESET_ALL}") + logger.info("Auto-commit created with changes.") + print_success("\n✓ Auto-commit created with changes") else: logger.info("doc_gen_hook complete. No changes made.") - print(f"\n{Fore.CYAN}✓ doc_gen_hook complete. No changes made.{Style.RESET_ALL}") \ No newline at end of file + print_info("\n✓ doc_gen_hook complete. No changes made.") \ No newline at end of file diff --git a/penify_hook/ui_utils.py b/penify_hook/ui_utils.py new file mode 100644 index 0000000..04011db --- /dev/null +++ b/penify_hook/ui_utils.py @@ -0,0 +1,134 @@ +""" +UI utilities for Penify CLI. + +This module provides utility functions for consistent UI formatting, +colored output, and progress indicators across the Penify CLI application. +""" +import os +from colorama import Fore, Style, init +from tqdm import tqdm + +# Initialize colorama for cross-platform colored terminal output +init(autoreset=True) + +# Color constants for different message types +INFO_COLOR = Fore.CYAN +SUCCESS_COLOR = Fore.GREEN +WARNING_COLOR = Fore.YELLOW +ERROR_COLOR = Fore.RED +HIGHLIGHT_COLOR = Fore.BLUE +NEUTRAL_COLOR = Fore.WHITE + +# Status symbols +SUCCESS_SYMBOL = "✓" +WARNING_SYMBOL = "○" +ERROR_SYMBOL = "✗" +PROCESSING_SYMBOL = "⟳" + +def format_info(message): + """Format an informational message with appropriate color.""" + return f"{INFO_COLOR}{message}{Style.RESET_ALL}" + +def format_success(message): + """Format a success message with appropriate color.""" + return f"{SUCCESS_COLOR}{message}{Style.RESET_ALL}" + +def format_warning(message): + """Format a warning message with appropriate color.""" + return f"{WARNING_COLOR}{message}{Style.RESET_ALL}" + +def format_error(message): + """Format an error message with appropriate color.""" + return f"{ERROR_COLOR}{message}{Style.RESET_ALL}" + +def format_highlight(message): + """Format a highlighted message with appropriate color.""" + return f"{HIGHLIGHT_COLOR}{message}{Style.RESET_ALL}" + +def format_file_path(file_path): + """Format a file path with appropriate color.""" + return f"{WARNING_COLOR}{file_path}{Style.RESET_ALL}" + +def print_info(message): + """Print an informational message with appropriate formatting.""" + print(format_info(message)) + +def print_success(message): + """Print a success message with appropriate formatting.""" + print(format_success(message)) + +def print_warning(message): + """Print a warning message with appropriate formatting.""" + print(format_warning(message)) + +def print_error(message): + """Print an error message with appropriate formatting.""" + print(format_error(message)) + +def print_processing(file_path): + """Print a processing message for a file.""" + formatted_path = format_file_path(file_path) + print(f"\n{format_highlight(f'Processing file: {formatted_path}')}") + +def print_status(status, message): + """Print a status message with an appropriate symbol. + + Args: + status (str): One of 'success', 'warning', or 'error' + message (str): The message to print + """ + if status == 'success': + print(f" {SUCCESS_COLOR}{SUCCESS_SYMBOL} {message}{Style.RESET_ALL}") + elif status == 'warning': + print(f" {NEUTRAL_COLOR}{WARNING_SYMBOL} {message}{Style.RESET_ALL}") + elif status == 'error': + print(f" {ERROR_COLOR}{ERROR_SYMBOL} {message}{Style.RESET_ALL}") + else: + print(f" {PROCESSING_SYMBOL} {message}") + +def create_progress_bar(total, desc="Processing", unit="item"): + """Create a tqdm progress bar with consistent styling. + + Args: + total (int): Total number of items to process + desc (str): Description for the progress bar + unit (str): Unit label for the progress items + + Returns: + tqdm: A configured tqdm progress bar instance + """ + return tqdm( + total=total, + desc=format_info(desc), + unit=unit, + ncols=80, + ascii=True + ) + +def create_stage_progress_bar(stages, desc="Processing"): + """Create a tqdm progress bar for processing stages with consistent styling. + + Args: + stages (list): List of stage names + desc (str): Description for the progress bar + + Returns: + tuple: (tqdm progress bar, list of stages) + """ + pbar = tqdm( + total=len(stages), + desc=format_info(desc), + unit="step", + ncols=80, + ascii=True + ) + return pbar, stages + +def update_stage(pbar, stage_name): + """Update the progress bar with a new stage name. + + Args: + pbar (tqdm): The progress bar to update + stage_name (str): The name of the current stage + """ + pbar.set_description(format_info(stage_name)) From 4d03e68f9d835b912bd4ebddf9ca67dd731f7d4a Mon Sep 17 00:00:00 2001 From: sumansaurabh Date: Fri, 7 Mar 2025 03:23:25 +0530 Subject: [PATCH 18/22] feat: Integrate tqdm for enhanced progress tracking in file processing --- penify_hook/file_analyzer.py | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/penify_hook/file_analyzer.py b/penify_hook/file_analyzer.py index 12c52ed..1217e5b 100644 --- a/penify_hook/file_analyzer.py +++ b/penify_hook/file_analyzer.py @@ -1,10 +1,11 @@ import os from git import Repo +from tqdm import tqdm from .api_client import APIClient import logging from .ui_utils import ( - print_info, print_success, print_warning, print_error, - print_processing, print_status, create_stage_progress_bar, + format_highlight, print_info, print_success, print_warning, print_error, + print_status, create_stage_progress_bar, update_stage, format_file_path ) @@ -17,7 +18,7 @@ def __init__(self, file_path: str, api_client: APIClient): self.api_client = api_client self.supported_file_types = set(self.api_client.get_supported_file_types()) - def process_file(self, file_path, pbar): + def process_file(self, file_path, pbar: tqdm): """Process a file by reading its content and sending it to an API for processing. @@ -35,10 +36,15 @@ def process_file(self, file_path, pbar): """ file_abs_path = os.path.join(os.getcwd(), file_path) file_extension = os.path.splitext(file_path)[1].lower() - + # self.print_processing(self.file_path) + + + pbar.update(1) # Complete first stage # Validate file update_stage(pbar, "Validating file") + pbar.update(2) + if not file_extension: logger.info(f"File {file_path} has no extension. Skipping.") return False @@ -73,6 +79,12 @@ def process_file(self, file_path, pbar): file.write(response) logger.info(f"Updated file {file_path} with generated documentation") return True + + def print_processing(self, file_path): + """Print a processing message for a file.""" + formatted_path = format_file_path(file_path) + print(f"\n{format_highlight(f'Processing file: {formatted_path}')}") + def run(self): """Run the post-commit hook. @@ -84,16 +96,12 @@ def run(self): displays a progress bar and colored output to provide visual feedback on the processing status. """ - print_info(f"Starting file documentation for {format_file_path(self.file_path)}") # Create a progress bar with appropriate stages - stages = ["Validating", "Reading content", "Documenting", "Writing changes"] - pbar, _ = create_stage_progress_bar(stages, "Processing file") + stages = ["INIT","Validating", "Reading content", "Documenting", "Writing changes"] + pbar, _ = create_stage_progress_bar(stages, f"Starting file documentation") try: - print_processing(self.file_path) - pbar.update(1) # Complete first stage - result = self.process_file(self.file_path, pbar) # Ensure all stages are completed From 30bcc99ea107e57fff2e55da777bf4da1278a4d1 Mon Sep 17 00:00:00 2001 From: sumansaurabh Date: Fri, 7 Mar 2025 03:25:50 +0530 Subject: [PATCH 19/22] feat: Update progress tracking in file analysis stages for improved clarity --- penify_hook/file_analyzer.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/penify_hook/file_analyzer.py b/penify_hook/file_analyzer.py index 1217e5b..6010c2a 100644 --- a/penify_hook/file_analyzer.py +++ b/penify_hook/file_analyzer.py @@ -38,12 +38,9 @@ def process_file(self, file_path, pbar: tqdm): file_extension = os.path.splitext(file_path)[1].lower() # self.print_processing(self.file_path) - + # First update the stage, then increment the progress + update_stage(pbar, "Validating") pbar.update(1) # Complete first stage - # Validate file - update_stage(pbar, "Validating file") - - pbar.update(2) if not file_extension: logger.info(f"File {file_path} has no extension. Skipping.") @@ -55,15 +52,19 @@ def process_file(self, file_path, pbar: tqdm): logger.info(f"File type {file_extension} is not supported. Skipping {file_path}.") return False - # Read content + # Read content - first update stage, then progress update_stage(pbar, "Reading content") + pbar.update(1) + with open(file_abs_path, 'r') as file: content = file.read() modified_lines = [i for i in range(len(content.splitlines()))] - # Process with API - update_stage(pbar, "Generating documentation") + # Process with API - first update stage, then progress + update_stage(pbar, "Documenting") + pbar.update(1) + response = self.api_client.send_file_for_docstring_generation(file_path, content, modified_lines) if response is None: @@ -73,8 +74,10 @@ def process_file(self, file_path, pbar: tqdm): logger.info(f"No changes needed for {file_path}") return False - # Write changes + # Write changes - first update stage, then progress update_stage(pbar, "Writing changes") + pbar.update(1) + with open(file_abs_path, 'w') as file: file.write(response) logger.info(f"Updated file {file_path} with generated documentation") @@ -98,7 +101,7 @@ def run(self): """ # Create a progress bar with appropriate stages - stages = ["INIT","Validating", "Reading content", "Documenting", "Writing changes"] + stages = ["INIT", "Validating", "Reading content", "Documenting", "Writing changes"] pbar, _ = create_stage_progress_bar(stages, f"Starting file documentation") try: From ac019b30ed75d5b5f810d8c73103cf4d44bc2985 Mon Sep 17 00:00:00 2001 From: sumansaurabh Date: Fri, 7 Mar 2025 03:51:14 +0530 Subject: [PATCH 20/22] feat: Improve error handling and progress tracking in file processing --- penify_hook/api_client.py | 10 ++-- penify_hook/file_analyzer.py | 93 +++++++++++++++++++++++------------- penify_hook/ui_utils.py | 5 +- 3 files changed, 68 insertions(+), 40 deletions(-) diff --git a/penify_hook/api_client.py b/penify_hook/api_client.py index abeecf9..7015e64 100644 --- a/penify_hook/api_client.py +++ b/penify_hook/api_client.py @@ -41,9 +41,7 @@ def send_file_for_docstring_generation(self, file_name, content, line_numbers, r response = response.json() return response.get('modified_content') else: - print(f"Response: {response.status_code}") - print(f"Error: {response.text}") - return content + raise Exception(f"API Error: {response.text}") def generate_commit_summary(self, git_diff, instruction: str = "", repo_details = None, jira_context: dict = None): """Generate a commit summary by sending a POST request to the API endpoint. @@ -82,9 +80,9 @@ def generate_commit_summary(self, git_diff, instruction: str = "", repo_details response = response.json() return response else: - print(f"Response: {response.status_code}") - print(f"Error: {response.text}") - return None + # print(f"Response: {response.status_code}") + # print(f"Error: {response.text}") + raise Exception(f"API Error: {response.text}") except Exception as e: print(f"Error: {e}") return None diff --git a/penify_hook/file_analyzer.py b/penify_hook/file_analyzer.py index 6010c2a..9a52139 100644 --- a/penify_hook/file_analyzer.py +++ b/penify_hook/file_analyzer.py @@ -1,6 +1,8 @@ import os +import sys from git import Repo from tqdm import tqdm +import time from .api_client import APIClient import logging from .ui_utils import ( @@ -15,10 +17,11 @@ class FileAnalyzerGenHook: def __init__(self, file_path: str, api_client: APIClient): self.file_path = file_path + self.relative_file_path = os.path.relpath(file_path) self.api_client = api_client self.supported_file_types = set(self.api_client.get_supported_file_types()) - def process_file(self, file_path, pbar: tqdm): + def process_file(self, file_path, pbar): """Process a file by reading its content and sending it to an API for processing. @@ -36,12 +39,9 @@ def process_file(self, file_path, pbar: tqdm): """ file_abs_path = os.path.join(os.getcwd(), file_path) file_extension = os.path.splitext(file_path)[1].lower() - # self.print_processing(self.file_path) - - # First update the stage, then increment the progress - update_stage(pbar, "Validating") - pbar.update(1) # Complete first stage + # --- STAGE 1: Validating --- + update_stage(pbar, "Validating") if not file_extension: logger.info(f"File {file_path} has no extension. Skipping.") return False @@ -52,19 +52,26 @@ def process_file(self, file_path, pbar: tqdm): logger.info(f"File type {file_extension} is not supported. Skipping {file_path}.") return False - # Read content - first update stage, then progress - update_stage(pbar, "Reading content") + # Update progress bar to indicate we're moving to next stage pbar.update(1) - with open(file_abs_path, 'r') as file: - content = file.read() + # --- STAGE 2: Reading content --- + update_stage(pbar, "Reading content") + try: + with open(file_abs_path, 'r') as file: + content = file.read() + except Exception as e: + logger.error(f"Error reading file {file_path}: {str(e)}") + return False modified_lines = [i for i in range(len(content.splitlines()))] - # Process with API - first update stage, then progress - update_stage(pbar, "Documenting") + # Update progress bar to indicate we're moving to next stage pbar.update(1) + # --- STAGE 3: Documenting --- + update_stage(pbar, "Documenting") + response = self.api_client.send_file_for_docstring_generation(file_path, content, modified_lines) if response is None: @@ -74,21 +81,29 @@ def process_file(self, file_path, pbar: tqdm): logger.info(f"No changes needed for {file_path}") return False - # Write changes - first update stage, then progress - update_stage(pbar, "Writing changes") + # Update progress bar to indicate we're moving to next stage pbar.update(1) - with open(file_abs_path, 'w') as file: - file.write(response) - logger.info(f"Updated file {file_path} with generated documentation") - return True + # --- STAGE 4: Writing changes --- + update_stage(pbar, "Writing changes") + + try: + with open(file_abs_path, 'w') as file: + file.write(response) + logger.info(f"Updated file {file_path} with generated documentation") + + # Mark final stage as complete + pbar.update(1) + return True + except Exception as e: + logger.error(f"Error writing file {file_path}: {str(e)}") + return False def print_processing(self, file_path): """Print a processing message for a file.""" formatted_path = format_file_path(file_path) print(f"\n{format_highlight(f'Processing file: {formatted_path}')}") - def run(self): """Run the post-commit hook. @@ -101,31 +116,43 @@ def run(self): """ # Create a progress bar with appropriate stages - stages = ["INIT", "Validating", "Reading content", "Documenting", "Writing changes"] - pbar, _ = create_stage_progress_bar(stages, f"Starting file documentation") + stages = ["Validating", "Reading content", "Documenting", "Writing changes", "Completed"] + pbar, _ = create_stage_progress_bar(stages, f"Starting documenting") try: + # Print a clear indication of which file is being processed + # self.print_processing(self.file_path) + + # Process the file result = self.process_file(self.file_path, pbar) # Ensure all stages are completed remaining_steps = len(stages) - pbar.n - if remaining_steps > 0: - pbar.update(remaining_steps) + pbar.update(remaining_steps) + # Display appropriate message based on result - if result: - print_status('success', f"Documentation updated for {self.file_path}") - else: - print_status('warning', f"No changes needed for {self.file_path}") + remaining = len(stages) - pbar.n + if remaining > 0: + pbar.update(remaining) + update_stage(pbar, "Complete") + pbar.clear() + pbar.close() except Exception as e: - error_msg = f"Error processing file [{self.file_path}]: {str(e)}" - logger.error(error_msg) - print_status('error', error_msg) - - # Ensure progress bar completes even on error remaining = len(stages) - pbar.n if remaining > 0: pbar.update(remaining) + update_stage(pbar, "Complete") + pbar.clear() + pbar.close() + error_msg = f"Error: {str(e)}" + print_status('error', error_msg) + sys.exit(1) + + # Ensure progress bar completes even on error + if result: + print_success(f"\n✓ Documentation updated for {self.relative_file_path}") + else: + print_success(f"\n✓ No changes needed for {self.relative_file_path}") - print_success("\n✓ File analysis complete") diff --git a/penify_hook/ui_utils.py b/penify_hook/ui_utils.py index 04011db..ebb2142 100644 --- a/penify_hook/ui_utils.py +++ b/penify_hook/ui_utils.py @@ -131,4 +131,7 @@ def update_stage(pbar, stage_name): pbar (tqdm): The progress bar to update stage_name (str): The name of the current stage """ - pbar.set_description(format_info(stage_name)) + # Force refresh with a custom description and ensure it's visible + pbar.set_postfix_str("") # Clear any existing postfix + pbar.set_description_str(f"{format_info(stage_name)}") + pbar.refresh() # Force refresh the display From 691ade5bdd2ca33afcb593ead86c410b75dad005 Mon Sep 17 00:00:00 2001 From: sumansaurabh Date: Fri, 7 Mar 2025 03:59:51 +0530 Subject: [PATCH 21/22] feat: Enhance error reporting in API client and improve error handling in file analyzer --- penify_hook/api_client.py | 6 +++++- penify_hook/file_analyzer.py | 3 +-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/penify_hook/api_client.py b/penify_hook/api_client.py index 7015e64..505ff7e 100644 --- a/penify_hook/api_client.py +++ b/penify_hook/api_client.py @@ -41,7 +41,11 @@ def send_file_for_docstring_generation(self, file_name, content, line_numbers, r response = response.json() return response.get('modified_content') else: - raise Exception(f"API Error: {response.text}") + error_message = response.json().get('detail') + if not error_message: + error_message = response.text + + raise Exception(f"API Error: {error_message}") def generate_commit_summary(self, git_diff, instruction: str = "", repo_details = None, jira_context: dict = None): """Generate a commit summary by sending a POST request to the API endpoint. diff --git a/penify_hook/file_analyzer.py b/penify_hook/file_analyzer.py index 9a52139..0daf4c5 100644 --- a/penify_hook/file_analyzer.py +++ b/penify_hook/file_analyzer.py @@ -146,8 +146,7 @@ def run(self): update_stage(pbar, "Complete") pbar.clear() pbar.close() - error_msg = f"Error: {str(e)}" - print_status('error', error_msg) + print_status('error', e) sys.exit(1) # Ensure progress bar completes even on error From 0ec11d891c9c56fe43e6555e5fc67bbf054ce848 Mon Sep 17 00:00:00 2001 From: sumansaurabh Date: Fri, 7 Mar 2025 04:06:38 +0530 Subject: [PATCH 22/22] feat: Add repository details retrieval and refactor git folder search utility --- penify_hook/file_analyzer.py | 7 ++- penify_hook/git_analyzer.py | 90 ++---------------------------------- penify_hook/utils.py | 90 ++++++++++++++++++++++++++++++++++++ 3 files changed, 100 insertions(+), 87 deletions(-) diff --git a/penify_hook/file_analyzer.py b/penify_hook/file_analyzer.py index 0daf4c5..d058450 100644 --- a/penify_hook/file_analyzer.py +++ b/penify_hook/file_analyzer.py @@ -3,6 +3,8 @@ from git import Repo from tqdm import tqdm import time + +from penify_hook.utils import get_repo_details, recursive_search_git_folder from .api_client import APIClient import logging from .ui_utils import ( @@ -17,6 +19,9 @@ class FileAnalyzerGenHook: def __init__(self, file_path: str, api_client: APIClient): self.file_path = file_path + self.repo_path = recursive_search_git_folder(file_path) + self.repo = Repo(self.repo_path) + self.repo_details = get_repo_details(self.repo) self.relative_file_path = os.path.relpath(file_path) self.api_client = api_client self.supported_file_types = set(self.api_client.get_supported_file_types()) @@ -72,7 +77,7 @@ def process_file(self, file_path, pbar): # --- STAGE 3: Documenting --- update_stage(pbar, "Documenting") - response = self.api_client.send_file_for_docstring_generation(file_path, content, modified_lines) + response = self.api_client.send_file_for_docstring_generation(file_path, content, modified_lines, self.repo_details) if response is None: return False diff --git a/penify_hook/git_analyzer.py b/penify_hook/git_analyzer.py index 7d9d66d..9be6e60 100644 --- a/penify_hook/git_analyzer.py +++ b/penify_hook/git_analyzer.py @@ -2,6 +2,8 @@ import re from git import Repo from tqdm import tqdm + +from penify_hook.utils import get_repo_details, recursive_search_git_folder from .api_client import APIClient import logging from .ui_utils import ( @@ -15,95 +17,11 @@ class GitDocGenHook: def __init__(self, repo_path: str, api_client: APIClient): - self.repo_path = self._recursive_search_git_folder(repo_path) - print(f"Repo path: {self.repo_path}") + self.repo_path = recursive_search_git_folder(repo_path) self.api_client = api_client self.repo = Repo(self.repo_path) self.supported_file_types = set(self.api_client.get_supported_file_types()) - self.repo_details = self.get_repo_details() - - def _recursive_search_git_folder(self, folder_path): - """Recursively search for the .git folder in the specified directory and - return its parent directory. - - This function takes a folder path as input and checks if the specified - directory contains a '.git' folder. If it does, the function returns the - path of that directory. If not, it recursively searches the parent - directory until it finds a '.git' folder or reaches the root of the - filesystem. - - Args: - folder_path (str): The path of the directory to search for the .git folder. - - Returns: - str: The path of the directory containing the .git folder. - """ - if os.path.isdir(folder_path): - if '.git' in os.listdir(folder_path): - return folder_path - else: - return self._recursive_search_git_folder(os.path.dirname(folder_path)) - - def get_repo_details(self): - """Get the details of the repository, including the hosting service, - organization name, and repository name. - - This method checks the remote URL of the repository to determine whether - it is hosted on GitHub, Azure DevOps, Bitbucket, GitLab, or another - service. It extracts the organization (or user) name and the repository - name from the URL. If the hosting service cannot be determined, it will - return "Unknown Hosting Service". - - Returns: - dict: A dictionary containing the organization name, repository name, and - hosting service. - """ - remote_url = None - hosting_service = "Unknown" - org_name = None - repo_name = None - - try: - # Get the remote URL - remote = self.repo.remotes.origin.url - remote_url = remote - - # Determine the hosting service based on the URL - if "github.com" in remote: - hosting_service = "GITHUB" - match = re.match(r".*github\.com[:/](.*?)/(.*?)(\.git)?$", remote) - elif "dev.azure.com" in remote: - hosting_service = "AZUREDEVOPS" - match = re.match(r".*dev\.azure\.com/(.*?)/(.*?)/_git/(.*?)(\.git)?$", remote) - elif "visualstudio.com" in remote: - hosting_service = "AZUREDEVOPS" - match = re.match(r".*@(.*?)\.visualstudio\.com/(.*?)/_git/(.*?)(\.git)?$", remote) - elif "bitbucket.org" in remote: - hosting_service = "BITBUCKET" - match = re.match(r".*bitbucket\.org[:/](.*?)/(.*?)(\.git)?$", remote) - elif "gitlab.com" in remote: - hosting_service = "GITLAB" - match = re.match(r".*gitlab\.com[:/](.*?)/(.*?)(\.git)?$", remote) - else: - hosting_service = "Unknown Hosting Service" - match = None - - if match: - org_name = match.group(1) - repo_name = match.group(2) - - # For Azure DevOps, adjust the group indices - if hosting_service == "AZUREDEVOPS": - repo_name = match.group(3) - - except Exception as e: - logger.error(f"Error determining GIT provider: {e}") - - return { - "organization_name": org_name, - "repo_name": repo_name, - "vendor": hosting_service - } + self.repo_details = get_repo_details(self.repo) def get_modified_files_in_last_commit(self): """Get the list of files modified in the last commit. diff --git a/penify_hook/utils.py b/penify_hook/utils.py index f91a920..a8dbb0b 100644 --- a/penify_hook/utils.py +++ b/penify_hook/utils.py @@ -1,8 +1,98 @@ +import logging import os +import re + +from git import Repo +logger = logging.getLogger(__name__) + class GitRepoNotFoundError(Exception): pass + +def get_repo_details(repo: Repo): + """Get the details of the repository, including the hosting service, + organization name, and repository name. + + This method checks the remote URL of the repository to determine whether + it is hosted on GitHub, Azure DevOps, Bitbucket, GitLab, or another + service. It extracts the organization (or user) name and the repository + name from the URL. If the hosting service cannot be determined, it will + return "Unknown Hosting Service". + + Returns: + dict: A dictionary containing the organization name, repository name, and + hosting service. + """ + remote_url = None + hosting_service = "Unknown" + org_name = None + repo_name = None + + try: + # Get the remote URL + remote = repo.remotes.origin.url + remote_url = remote + + # Determine the hosting service based on the URL + if "github.com" in remote: + hosting_service = "GITHUB" + match = re.match(r".*github\.com[:/](.*?)/(.*?)(\.git)?$", remote) + elif "dev.azure.com" in remote: + hosting_service = "AZUREDEVOPS" + match = re.match(r".*dev\.azure\.com/(.*?)/(.*?)/_git/(.*?)(\.git)?$", remote) + elif "visualstudio.com" in remote: + hosting_service = "AZUREDEVOPS" + match = re.match(r".*@(.*?)\.visualstudio\.com/(.*?)/_git/(.*?)(\.git)?$", remote) + elif "bitbucket.org" in remote: + hosting_service = "BITBUCKET" + match = re.match(r".*bitbucket\.org[:/](.*?)/(.*?)(\.git)?$", remote) + elif "gitlab.com" in remote: + hosting_service = "GITLAB" + match = re.match(r".*gitlab\.com[:/](.*?)/(.*?)(\.git)?$", remote) + else: + hosting_service = "Unknown Hosting Service" + match = None + + if match: + org_name = match.group(1) + repo_name = match.group(2) + + # For Azure DevOps, adjust the group indices + if hosting_service == "AZUREDEVOPS": + repo_name = match.group(3) + + except Exception as e: + logger.error(f"Error determining GIT provider: {e}") + + return { + "organization_name": org_name, + "repo_name": repo_name, + "vendor": hosting_service + } + +def recursive_search_git_folder(folder_path): + """Recursively search for the .git folder in the specified directory and + return its parent directory. + + This function takes a folder path as input and checks if the specified + directory contains a '.git' folder. If it does, the function returns the + path of that directory. If not, it recursively searches the parent + directory until it finds a '.git' folder or reaches the root of the + filesystem. + + Args: + folder_path (str): The path of the directory to search for the .git folder. + + Returns: + str: The path of the directory containing the .git folder. + """ + if os.path.isdir(folder_path): + if '.git' in os.listdir(folder_path): + return folder_path + else: + return recursive_search_git_folder(os.path.dirname(folder_path)) + def find_git_parent(path): """Find the parent directory of a Git repository.