Compare commits
6 Commits
5f7649867b
...
3206363a88
| Author | SHA1 | Date | |
|---|---|---|---|
| 3206363a88 | |||
| 5d655e4207 | |||
| 692ea1b1ed | |||
| e621ac04f7 | |||
| 7241974d8b | |||
| 10385bb1c2 |
@@ -30,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
|
||||||
@@ -52,14 +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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
)
|
||||||
|
|||||||
@@ -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.")
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -4,3 +4,4 @@ pystray
|
|||||||
Pillow
|
Pillow
|
||||||
plyer
|
plyer
|
||||||
win10toast
|
win10toast
|
||||||
|
windows-toasts
|
||||||
|
|||||||
Reference in New Issue
Block a user