diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..daa79fc --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,68 @@ +name: Build and Release + +on: + push: + branches: + - main + +jobs: + create_release: + name: Create Release + runs-on: ubuntu-latest + outputs: + upload_url: ${{ steps.create_release.outputs.upload_url }} + steps: + - name: Create Release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: v${{ github.run_number }} + release_name: Release v${{ github.run_number }} + draft: false + prerelease: false + + build_and_upload: + name: Build and Upload on ${{ matrix.os }} + needs: create_release + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + include: + - os: ubuntu-latest + asset_name: ai-commit-linux + asset_path_suffix: AI-Commit + - os: windows-latest + asset_name: ai-commit-windows.exe + asset_path_suffix: AI-Commit.exe + - os: macos-latest + asset_name: ai-commit-macos + asset_path_suffix: AI-Commit + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: 3.10 + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Build with PyInstaller + run: pyinstaller --name "AI-Commit" --windowed --onefile --icon="assets/icon.ico" main.py + + - name: Upload Release Asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ needs.create_release.outputs.upload_url }} + asset_path: ./dist/${{ matrix.asset_path_suffix }} + asset_name: ${{ matrix.asset_name }} + asset_content_type: application/octet-stream diff --git a/.github/workflows/pull-request-check.yml b/.github/workflows/pull-request-check.yml new file mode 100644 index 0000000..6b58f70 --- /dev/null +++ b/.github/workflows/pull-request-check.yml @@ -0,0 +1,43 @@ +name: Test and Build on PR + +on: + pull_request: + branches: [ "main" ] + +jobs: + build: + name: Build on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + python-version: ["3.10"] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + # Placeholder for tests. Uncomment and adapt when have tests. + # - name: Run Tests + # run: | + # pip install pytest + # pytest + + - name: Build with PyInstaller + run: pyinstaller --name "AI-Commit" --windowed --onefile --icon="assets/icon.ico" main.py + + - name: Upload Artifact + uses: actions/upload-artifact@v4 + with: + name: AI-Commit-${{ matrix.os }} + path: dist/ diff --git a/.gitignore b/.gitignore index a413f46..01785d2 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,7 @@ src/__pycache__ src/core/__pycache__ src/gui/__pycache__ src/utils/__pycache__ +venv build -AI-Commit.spec \ No newline at end of file +dist +*.spec \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..a0e1305 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,21 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files + +- repo: https://github.com/psf/black + rev: 24.4.2 + hooks: + - id: black + +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.4.4 + hooks: + - id: ruff + args: [--fix, --exit-non-zero-on-fix] diff --git a/dist/AI-Commit.exe b/dist/AI-Commit.exe deleted file mode 100644 index 08a3402..0000000 Binary files a/dist/AI-Commit.exe and /dev/null differ diff --git a/requirements.txt b/requirements.txt index e0ccd2b..10cae9d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,8 @@ google-generativeai>=0.3.0 openai>=1.0.0 -pathlib2>=2.3.0; python_version < '3.4' \ No newline at end of file +pathlib2>=2.3.0; python_version < '3.4' +pyinstaller>=5.0.0 +pre-commit>=3.0.0 +ruff>=0.1.0 +black>=23.0.0 +Pillow>=10.0.0 \ No newline at end of file diff --git a/scripts/ai-commit.desktop b/scripts/ai-commit.desktop new file mode 100755 index 0000000..f0d03f9 --- /dev/null +++ b/scripts/ai-commit.desktop @@ -0,0 +1,8 @@ +[Desktop Entry] +Name=AI-Commit +Comment=Generate commit messages with AI +Exec=/usr/local/bin/ai-commit +Icon=ai-commit +Terminal=false +Type=Application +Categories=Development; diff --git a/scripts/build.bat b/scripts/build.bat new file mode 100755 index 0000000..501d9b3 --- /dev/null +++ b/scripts/build.bat @@ -0,0 +1,26 @@ +@echo off +setlocal + +echo --- Starting Windows build process --- + +:: 1. Create & activate virtual environment +echo --- Setting up Python virtual environment --- +if not exist venv ( py -m venv venv ) +call venv\Scripts\activate + +:: 2. Install dependencies +echo --- Installing dependencies from requirements.txt --- +pip install -r requirements.txt + +:: 3. Run PyInstaller +echo --- Building executable with PyInstaller --- +pyinstaller --name "AI-Commit" --windowed --onefile --icon="assets/icon.ico" main.py + +echo --- Executable build complete! --- +echo Executable created in the 'dist' folder. +echo. +echo To create the Windows installer (.exe), install Inno Setup from https://jrsoftware.org/isinfo.php +echo then right-click and 'Compile' the 'scripts/installer.iss' file. + +endlocal +pause \ No newline at end of file diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100755 index 0000000..0a032ef --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,52 @@ +#!/bin/bash +# Script to create the AICommit installer for Linux +# Exit on error +set -e + +echo "--- Starting Linux build process ---" + +# 1. Create & activate virtual environment +echo "--- Setting up Python virtual environment ---" +python3 -m venv venv +source venv/bin/activate + +# 2. Install dependencies +echo "--- Installing dependencies from requirements.txt ---" +pip install -r requirements.txt + +# 3. Run PyInstaller +echo "--- Building executable with PyInstaller ---" +pyinstaller --name "AICommit" --windowed --onefile --icon="assets/icon.ico" main.py + +# 4. Prepare installer package +echo "--- Creating Linux installer package ---" +INSTALLER_DIR="AICommit-linux-installer" +rm -rf $INSTALLER_DIR +mkdir -p $INSTALLER_DIR/usr/local/bin +mkdir -p $INSTALLER_DIR/usr/share/applications +mkdir -p $INSTALLER_DIR/usr/share/icons/hicolor/256x256/apps + +# Copy necessary files +cp dist/AICommit $INSTALLER_DIR/usr/local/bin/AICommit +cp assets/icon.png $INSTALLER_DIR/usr/share/icons/hicolor/256x256/apps/AICommit.png +cp scripts/AICommit.desktop $INSTALLER_DIR/usr/share/applications/ + +# Create the installation script +cat > $INSTALLER_DIR/install.sh <>', self.on_repo_selected) # Files Frame - files_frame = ttk.LabelFrame(main_frame, text="📝 Changed Files", padding="10") + files_frame = ttk.LabelFrame(main_frame, text=UI_STRINGS.FILES_FRAME_TITLE, padding="10") files_frame.grid(row=3, column=0, columnspan=3, sticky=(tk.W, tk.E, tk.N, tk.S), pady=5) # File buttons file_buttons = ttk.Frame(files_frame) file_buttons.pack(fill=tk.X, pady=(0, 5)) - ttk.Button(file_buttons, text="✅ Select All", command=self.select_all_files, width=15).pack(side=tk.LEFT, padx=2) - ttk.Button(file_buttons, text="❌ Clear Selection", command=self.clear_file_selection, width=15).pack(side=tk.LEFT, padx=2) - ttk.Button(file_buttons, text="➕ Add to Stage", command=self.add_selected_files, width=15).pack(side=tk.LEFT, padx=2) + ttk.Button(file_buttons, text=UI_STRINGS.SELECT_ALL_BUTTON, command=self.select_all_files, width=15).pack(side=tk.LEFT, padx=2) + ttk.Button(file_buttons, text=UI_STRINGS.CLEAR_SELECTION_BUTTON, command=self.clear_file_selection, width=15).pack(side=tk.LEFT, padx=2) + ttk.Button(file_buttons, text=UI_STRINGS.ADD_TO_STAGE_BUTTON, command=self.add_selected_files, width=15).pack(side=tk.LEFT, padx=2) # Info label self.info_label = ttk.Label( files_frame, - text="â„šī¸ Select files and click 'Add to Stage' before generating commit message", + text=UI_STRINGS.INFO_LABEL, foreground=self.theme_manager.get_accent_color(), font=('Helvetica', 8, 'italic') ) @@ -171,7 +174,7 @@ def setup_ui(self): scrollbar.config(command=self.files_listbox.yview) # Commit Message Frame - message_frame = ttk.LabelFrame(main_frame, text="đŸ’Ŧ Commit Message", padding="10") + message_frame = ttk.LabelFrame(main_frame, text=UI_STRINGS.COMMIT_FRAME_TITLE, padding="10") message_frame.grid(row=4, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=5) # Message text area @@ -189,19 +192,19 @@ def setup_ui(self): # Message buttons msg_buttons = ttk.Frame(message_frame) msg_buttons.pack(fill=tk.X) - ttk.Button(msg_buttons, text="🤖 Generate with AI", command=self.auto_add_and_generate, width=20).pack(side=tk.LEFT, padx=2) - ttk.Button(msg_buttons, text="đŸ—‘ī¸ Clear", command=self.clear_message, width=12).pack(side=tk.LEFT, padx=2) + ttk.Button(msg_buttons, text=UI_STRINGS.GENERATE_BUTTON, command=self.auto_add_and_generate, width=20).pack(side=tk.LEFT, padx=2) + ttk.Button(msg_buttons, text=UI_STRINGS.CLEAR_BUTTON, command=self.clear_message, width=12).pack(side=tk.LEFT, padx=2) # Action Buttons action_frame = ttk.Frame(main_frame) action_frame.grid(row=5, column=0, columnspan=3, pady=10) - ttk.Button(action_frame, text="✅ Commit & Push", command=self.commit_and_push, width=18).pack(side=tk.LEFT, padx=5) - ttk.Button(action_frame, text="💾 Commit Only", command=self.commit_only, width=15).pack(side=tk.LEFT, padx=5) - ttk.Button(action_frame, text="❌ Cancel", command=self.root.quit, width=12).pack(side=tk.LEFT, padx=5) + ttk.Button(action_frame, text=UI_STRINGS.COMMIT_PUSH_BUTTON, command=self.commit_and_push, width=18).pack(side=tk.LEFT, padx=5) + ttk.Button(action_frame, text=UI_STRINGS.COMMIT_ONLY_BUTTON, command=self.commit_only, width=15).pack(side=tk.LEFT, padx=5) + ttk.Button(action_frame, text=UI_STRINGS.CANCEL_BUTTON, command=self.root.quit, width=12).pack(side=tk.LEFT, padx=5) # Log Frame - log_frame = ttk.LabelFrame(main_frame, text="📋 Activity Log", padding="5") + log_frame = ttk.LabelFrame(main_frame, text=UI_STRINGS.LOG_FRAME_TITLE, padding="5") log_frame.grid(row=6, column=0, columnspan=3, sticky=(tk.W, tk.E, tk.N, tk.S), pady=5) self.log_text = scrolledtext.ScrolledText( @@ -216,63 +219,34 @@ def setup_ui(self): self.log_text.pack(fill=tk.BOTH, expand=True) # Status bar - self.status_label = ttk.Label(main_frame, text="Ready", relief=tk.SUNKEN, anchor=tk.W) + self.status_label = ttk.Label(main_frame, text=UI_STRINGS.STATUS_READY, relief=tk.SUNKEN, anchor=tk.W) self.status_label.grid(row=7, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(5, 0)) def set_app_icon(self): - """Set application icon with better path handling for EXE""" + """Set application icon.""" try: - # Debug: Print current paths - print("Setting application icon...") - - # Determine if running as EXE or script if getattr(sys, 'frozen', False): - # Running as EXE - use temporary extraction directory + # Running as EXE base_path = sys._MEIPASS - print(f"Running as EXE, MEIPASS: {base_path}") else: - # Running as script - use current directory - base_path = os.path.dirname(os.path.abspath(__file__)) - print(f"Running as script, base path: {base_path}") - - icon_paths = [ - os.path.join(base_path, "assets", "icon.ico"), - os.path.join(base_path, "assets", "icon.png"), - os.path.join(base_path, "icon.ico"), - os.path.join(base_path, "icon.png"), - "assets/icon.ico", - "assets/icon.png", - "icon.ico", - "icon.png" - ] - - icon_found = False - for path in icon_paths: - exists = os.path.exists(path) - print(f"Checking: {path} - {'EXISTS' if exists else 'MISSING'}") - - if exists: - try: - if path.endswith('.ico'): - self.root.iconbitmap(path) - print(f"Successfully set icon from: {path}") - icon_found = True - break - elif path.endswith('.png'): - icon_img = tk.PhotoImage(file=path) - self.root.iconphoto(True, icon_img) - print(f"Successfully set icon from: {path}") - icon_found = True - break - except Exception as icon_error: - print(f"Failed to set icon from {path}: {icon_error}") - continue - - if not icon_found: - print("Warning: No valid icon file found in any location") - + # Running as script + base_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) + + icon_ico_path = os.path.join(base_path, "assets", "icon.ico") + icon_png_path = os.path.join(base_path, "assets", "icon.png") + + if sys.platform == "win32" and os.path.exists(icon_ico_path): + self.root.iconbitmap(icon_ico_path) + self.log("đŸ–ŧī¸ Application icon set from .ico file.") + elif os.path.exists(icon_png_path): + icon_img = tk.PhotoImage(file=icon_png_path) + self.root.iconphoto(True, icon_img) + self.log("đŸ–ŧī¸ Application icon set from .png file.") + else: + self.log("âš ī¸ Could not find application icon.", "warning") + except Exception as e: - print(f"Icon setting error: {e}") + self.log(f"❌ Failed to set application icon: {e}", "error") def open_settings(self): """Open settings dialog""" @@ -286,7 +260,7 @@ def browse_repository(self): """Browse for specific repository folder""" folder = filedialog.askdirectory( initialdir=self.settings_manager.get("parent_folder", str(Path.home())), - title="Select Git Repository" + title=UI_STRINGS.REPO_FRAME_TITLE ) if folder and self.git_manager.is_git_repo(folder): # Add to recent repos @@ -388,7 +362,7 @@ def scan_repositories(self): messagebox.showwarning("No Repositories", f"No git repositories found in:\n{parent_folder}\n\nTry changing the parent folder in Settings.") - self.set_status("Ready") + self.set_status(UI_STRINGS.STATUS_READY) def on_repo_selected(self, event): """Handle repository selection""" @@ -504,7 +478,7 @@ def add_selected_files(self): else: messagebox.showerror("Error", "Failed to stage files. Check Activity Log for details.") - self.set_status("Ready") + self.set_status(UI_STRINGS.STATUS_READY) self.root.after(500, self.load_changed_files) def auto_add_and_generate(self): @@ -542,7 +516,7 @@ def auto_add_and_generate(self): self.root.after(200, self.generate_commit_message) else: messagebox.showwarning("No Files", "No files were staged") - self.set_status("Ready") + self.set_status(UI_STRINGS.STATUS_READY) def generate_commit_message(self): """Generate commit message using AI""" @@ -585,7 +559,7 @@ def _generate_message_thread(self, diff: str, provider: str): self.root.after(0, self._show_error, str(e)) finally: self.root.after(0, lambda: setattr(self, '_is_generating', False)) - self.root.after(0, lambda: self.set_status("Ready")) + self.root.after(0, lambda: self.set_status(UI_STRINGS.STATUS_READY)) def _update_message(self, message: str): """Update commit message (called from main thread)""" @@ -593,14 +567,14 @@ def _update_message(self, message: str): self.message_text.insert(1.0, message) self.log("✅ Commit message generated successfully", "success") self._is_generating = False - self.set_status("Ready") + self.set_status(UI_STRINGS.STATUS_READY) def _show_error(self, error: str): """Show error message (called from main thread)""" self.log(f"❌ Error: {error}", "error") messagebox.showerror("Error", f"Failed to generate message:\n{error}") self._is_generating = False - self.set_status("Ready") + self.set_status(UI_STRINGS.STATUS_READY) def clear_message(self): """Clear commit message""" @@ -644,7 +618,7 @@ def _commit(self, push: bool = True): if not success: self.log(f"❌ Commit failed: {output}", "error") messagebox.showerror("Commit Failed", f"Failed to commit:\n{output}") - self.set_status("Ready") + self.set_status(UI_STRINGS.STATUS_READY) return self.log("✅ Commit successful!", "success") @@ -670,6 +644,6 @@ def _commit(self, push: bool = True): else: messagebox.showinfo("Success", "Commit completed successfully!") - self.set_status("Ready") + self.set_status(UI_STRINGS.STATUS_READY) self.clear_message() self.load_changed_files() \ No newline at end of file