Compare commits

...

7 Commits

5 changed files with 108 additions and 47 deletions

View File

@@ -10,8 +10,6 @@ The application runs **silently in the background** with a **system tray icon**
The final application must be packaged using **PyInstaller as a single executable file** with **no console window**. The final application must be packaged using **PyInstaller as a single executable file** with **no console window**.
---
# Core Features # Core Features
## Background Operation ## Background Operation
@@ -25,7 +23,6 @@ The tray icon menu must contain:
* **Select Repository Folder** * **Select Repository Folder**
* **Exit Application** * **Exit Application**
---
# Git Monitoring Behavior # Git Monitoring Behavior
@@ -33,7 +30,11 @@ The application monitors a selected folder that contains a **Git repository**.
Every filesystem change triggers an automatic commit. Every filesystem change triggers an automatic commit.
### .gitignore Support
The application respects `.gitignore` rules of the repository. Files matched by ignore rules will not trigger any actions or commits.
The following actions must be detected: The following actions must be detected:
...
* File created * File created
* File modified * File modified
@@ -55,15 +56,13 @@ delete: old_part.nc
rename: part_v1.nc -> part_v2.nc rename: part_v1.nc -> part_v2.nc
``` ```
### Git Operations
Commits must be executed automatically using Git. Commits must be executed automatically using Git.
If the directory is not a Git repository, the application should: If the directory is not a Git repository, the application should:
* notify the user * notify the user
* not start monitoring * not start monitoring
---
# Windows Notifications # Windows Notifications
The application should generate **Windows toast notifications** for: The application should generate **Windows toast notifications** for:
@@ -78,6 +77,7 @@ Example notification:
``` ```
Git Monitor Git Monitor
modify: program.nc committed modify: program.nc committed
```
# Logging # Logging
@@ -101,8 +101,6 @@ Logged events:
* git commit executed * git commit executed
* errors and exceptions * errors and exceptions
---
# System Tray UI # System Tray UI
The application must provide a **tray icon**. The application must provide a **tray icon**.
@@ -131,8 +129,6 @@ Once selected:
Stops monitoring and terminates the application. Stops monitoring and terminates the application.
---
# Application Architecture # Application Architecture
The application must be **modular** and structured using classes. The application must be **modular** and structured using classes.
@@ -151,8 +147,6 @@ config.py
logger.py logger.py
``` ```
---
# Modules # Modules
## main.py ## main.py
@@ -171,8 +165,6 @@ Main class:
Application Application
``` ```
---
# tray_app.py # tray_app.py
Handles the **system tray icon and menu**. Handles the **system tray icon and menu**.
@@ -204,8 +196,6 @@ stop_monitoring()
exit_app() exit_app()
``` ```
---
# file_watcher.py # file_watcher.py
Responsible for filesystem monitoring. Responsible for filesystem monitoring.
@@ -237,8 +227,6 @@ on_deleted
on_moved on_moved
``` ```
---
# git_manager.py # git_manager.py
Handles all Git operations. Handles all Git operations.
@@ -274,7 +262,6 @@ Commit format:
f"{action}: {file}" f"{action}: {file}"
``` ```
---
# notifier.py # notifier.py
@@ -347,8 +334,6 @@ Example:
The application is fully implemented and tested for Windows 11 background operation. The application is fully implemented and tested for Windows 11 background operation.
---
# Current Architecture (Finalized) # Current Architecture (Finalized)
The project is structured as a proper Python package to ensure compatibility with PyInstaller and robust path handling. The project is structured as a proper Python package to ensure compatibility with PyInstaller and robust path handling.

View File

@@ -1,9 +1,11 @@
import time import time
import os import os
import threading
from watchdog.observers import Observer from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler from watchdog.events import FileSystemEventHandler
from git_monitor.logger import logger from git_monitor.logger import logger
from git_monitor.git_manager import git_manager from git_monitor.git_manager import git_manager
from git_monitor.notifier import notifier
class RepositoryWatcher: class RepositoryWatcher:
def __init__(self, path): def __init__(self, path):
@@ -43,22 +45,35 @@ class GitEventHandler(FileSystemEventHandler):
self.handle_event(event, action, custom_file_name=file_name) self.handle_event(event, action, custom_file_name=file_name)
def handle_event(self, event, action, custom_file_name=None): def handle_event(self, event, action, custom_file_name=None):
# Ignore .git directory and the log file
file_path = event.src_path if action != "rename" else event.dest_path file_path = event.src_path if action != "rename" else event.dest_path
file_name = os.path.basename(file_path) file_name = os.path.basename(file_path)
# Check if any part of the path is .git
if ".git" in file_path.split(os.sep) or file_name == "git_monitor.log": if ".git" in file_path.split(os.sep) or file_name == "git_monitor.log":
return return
if git_manager.is_ignored(file_path):
logger.info(f"Ignored: {file_path} (matches .gitignore)")
return
display_name = custom_file_name if custom_file_name else file_name display_name = custom_file_name if custom_file_name else file_name
logger.info(f"File event: {action} on {display_name}") logger.info(f"Change detected: {action} on {display_name}")
repo_root = git_manager.repo_path repo_root = git_manager.repo_path
if repo_root: if repo_root:
if custom_file_name: rel_path = os.path.relpath(file_path, repo_root) if not custom_file_name else custom_file_name
# For renames, we use the custom format provided
git_manager.commit_change(action, custom_file_name) # Interactive notification instead of immediate commit
else: def save_now():
rel_path = os.path.relpath(file_path, repo_root)
git_manager.commit_change(action, rel_path) git_manager.commit_change(action, rel_path)
def ask_later():
logger.info(f"User deferred change: {rel_path} for 5 minutes.")
# Reschedule the same event after 5 minutes (300 seconds)
threading.Timer(300, self.handle_event, [event, action, custom_file_name]).start()
notifier.notify_interactive(
"Zmiana wykryta!",
f"Wykryto zmianę: {action}: {display_name}. Co chcesz zrobić?",
on_save=save_now,
on_later=ask_later
)

View File

@@ -34,6 +34,18 @@ class GitManager:
except (exc.InvalidGitRepositoryError, exc.NoSuchPathError): except (exc.InvalidGitRepositoryError, exc.NoSuchPathError):
return False return False
def is_ignored(self, file_path):
"""Check if a file is ignored by .gitignore rules."""
if not self.repo:
return False
try:
# git check-ignore returns the path if it is ignored
ignored_files = self.repo.ignored(file_path)
return len(ignored_files) > 0
except Exception as e:
logger.error(f"Error checking ignore status for {file_path}: {e}")
return False
def commit_change(self, action, file_name): def commit_change(self, action, file_name):
if not self.repo: if not self.repo:
logger.error("No repository loaded for commit.") logger.error("No repository loaded for commit.")

View File

@@ -1,45 +1,93 @@
import threading
from git_monitor.logger import logger from git_monitor.logger import logger
# Modern Windows 10/11 toasts with buttons
try:
from windows_toasts import WindowsToaster, ToastText2, ToastActivatedEventArgs, ToastButton
WINDOWS_TOASTS_AVAILABLE = True
toaster = WindowsToaster('Git Monitor')
except ImportError:
logger.warning("windows-toasts not found. Interactive buttons will not be available.")
WINDOWS_TOASTS_AVAILABLE = False
# Fallbacks
try: try:
from plyer import notification from plyer import notification
PLYER_AVAILABLE = True PLYER_AVAILABLE = True
except ImportError: except ImportError:
logger.warning("plyer not found. Will try win10toast.")
PLYER_AVAILABLE = False PLYER_AVAILABLE = False
try: try:
from win10toast import ToastNotifier from win10toast import ToastNotifier
WIN10TOAST_AVAILABLE = True WIN10TOAST_AVAILABLE = True
toaster = ToastNotifier() legacy_toaster = ToastNotifier()
except ImportError: except ImportError:
WIN10TOAST_AVAILABLE = False WIN10TOAST_AVAILABLE = False
class Notifier: class Notifier:
def notify(self, title, message): def notify(self, title, message):
"""Simple notification (no buttons)."""
logger.info(f"Notification: {title} - {message}") logger.info(f"Notification: {title} - {message}")
# Try plyer first if WINDOWS_TOASTS_AVAILABLE:
try:
toast = ToastText2()
toast.headline = title
toast.body = message
toaster.show_toast(toast)
return
except Exception as e:
logger.error(f"windows-toasts simple notification failed: {e}")
# Fallback to older libraries
if PLYER_AVAILABLE: if PLYER_AVAILABLE:
try: try:
notification.notify( notification.notify(title=title, message=message, app_name="Git Monitor")
title=title,
message=message,
app_name="Git Monitor"
)
return return
except Exception as e: except: pass
logger.error(f"Plyer notification failed: {e}. Trying fallback...")
# Fallback to win10toast
if WIN10TOAST_AVAILABLE: if WIN10TOAST_AVAILABLE:
try: try:
# threaded=True prevents blocking the app legacy_toaster.show_toast(title, message, duration=5, threaded=True)
toaster.show_toast(title, message, duration=5, threaded=True)
return return
except Exception as e: except: pass
logger.error(f"win10toast notification failed: {e}")
# Final fallback to stdout def notify_interactive(self, title, message, on_save, on_later):
print(f"[{title}] {message}") """Interactive notification with 'Save now' and 'Ask in 5 min' buttons."""
logger.info(f"Interactive Notification: {title} - {message}")
if not WINDOWS_TOASTS_AVAILABLE:
logger.warning("Interactive notifications not available, performing default action (Save).")
on_save()
return
try:
toast = ToastText2()
toast.headline = title
toast.body = message
# Action buttons
toast.AddAction(ToastButton("Zapisz teraz", "save"))
toast.AddAction(ToastButton("Zapytaj mnie za 5 min", "later"))
# Handler for button clicks
def on_activated(args: ToastActivatedEventArgs):
if args.arguments == "save":
logger.info("User clicked 'Zapisz teraz'")
on_save()
elif args.arguments == "later":
logger.info("User clicked 'Zapytaj mnie za 5 min'")
on_later()
else:
# Default click on toast body
on_save()
toast.on_activated = on_activated
toaster.show_toast(toast)
except Exception as e:
logger.error(f"Interactive notification error: {e}")
# Fallback to immediate save if notification fails
on_save()
notifier = Notifier() notifier = Notifier()

View File

@@ -4,3 +4,4 @@ pystray
Pillow Pillow
plyer plyer
win10toast win10toast
windows-toasts