import os import time import threading from watchdog.observers import Observer from watchdog.events import FileSystemEventHandler from git import Repo import pathspec from plyer import notification from pystray import Icon, Menu, MenuItem from PIL import Image, ImageDraw import logging # Logi będą zapisywane w tym samym folderze co skrypt log_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "debug_log.txt") logging.basicConfig( filename=log_path, level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s' ) # --- TWOJA KLASA GIT (Z POPRZEDNICH KROKÓW) --- class GitAutoCommitHandler(FileSystemEventHandler): def __init__(self, repo_path): self.repo_path = os.path.abspath(repo_path) self.repo = Repo(self.repo_path) self.spec = self._load_gitignore() self.last_committed = {} self.debounce_seconds = 3.0 def _load_gitignore(self): gitignore_path = os.path.join(self.repo_path, '.gitignore') if os.path.exists(gitignore_path): with open(gitignore_path, 'r') as f: return pathspec.PathSpec.from_lines('gitwildmatch', f) return None def _is_ignored(self, file_path): abs_file_path = os.path.abspath(file_path) relative_path = os.path.relpath(abs_file_path, self.repo_path) if ".git" in relative_path.split(os.sep): logging.debug(f"Ignorowanie (folder .git): {relative_path}") return True return self.spec.match_file(relative_path) if self.spec else False def on_created(self, event): if not event.is_directory: self._process_event(event.src_path, "new-file") def on_modified(self, event): if not event.is_directory: self._process_event(event.src_path, "update-file") def on_deleted(self, event): if not event.is_directory: self._process_event(event.src_path, "remove-file") def on_moved(self, event): if not event.is_directory: self._process_rename(event.src_path, event.dest_path) def _process_event(self, file_path, action): abs_path = os.path.abspath(file_path) if self._is_ignored(abs_path): return logging.debug(f"Zdarzenie: {action} - {file_path}") now = time.time() if now - self.last_committed.get(abs_path, 0) > self.debounce_seconds: self.last_committed[abs_path] = now self._commit(abs_path, action) def _process_rename(self, old, new): if self._is_ignored(new): return rel_old, rel_new = os.path.relpath(old, self.repo_path), os.path.relpath(new, self.repo_path) self.repo.index.remove([rel_old], working_tree=False) self.repo.index.add([rel_new]) self._finalize_commit(f"rename-file: {rel_old} -> {rel_new}", rel_new) def _commit(self, file_path, action): logging.debug(f"Commitowanie: {action} - {file_path}") rel_path = os.path.relpath(file_path, self.repo_path) if action == "remove-file": self.repo.index.remove([rel_path]) else: self.repo.index.add([rel_path]) self._finalize_commit(f"{action}: {rel_path}", rel_path) # def _finalize_commit(self, msg, display_name): # if self.repo.index.diff("HEAD"): # self.repo.index.commit(msg) # # POWIADOMIENIE WINDOWS # notification.notify( # title="Git Auto-Commit", # message=f"Zapisano zmianę: {display_name}", # app_name="Git Watcher", # timeout=3 # ) def _finalize_commit(self, msg, display_name): try: if self.repo.is_dirty(untracked_files=True): # Dokładniejsze sprawdzenie self.repo.index.commit(msg) logging.info(f"✅ COMMIT SUCCESS: {msg}") # Próba powiadomienia z łapaniem błędów try: notification.notify( title="Git Auto-Commit", message=f"Zapisano: {display_name}", timeout=5 ) except Exception as e: logging.error(f"Błąd powiadomienia: {e}") except Exception as e: logging.error(f"Błąd finalizacji commitu: {e}") # --- LOGIKA TRAY I URUCHAMIANIA --- def create_image(): # Generuje prostą ikonę (niebieskie koło), jeśli nie masz pliku .ico width, height = 64, 64 image = Image.new('RGB', (width, height), (255, 255, 255)) dc = ImageDraw.Draw(image) dc.ellipse([10, 10, 54, 54], fill=(0, 122, 204)) return image def run_watcher(path, stop_event): event_handler = GitAutoCommitHandler(path) observer = Observer() observer.schedule(event_handler, path, recursive=True) observer.start() while not stop_event.is_set(): time.sleep(1) observer.stop() observer.join() def main(): logging.info("Aplikacja wystartowała") repo_path = "./moje-repo" # Zmień na swoją ścieżkę logging.info("Repozytorium: %s", repo_path) stop_event = threading.Event() # Uruchomienie obserwatora w osobnym wątku watcher_thread = threading.Thread(target=run_watcher, args=(repo_path, stop_event)) watcher_thread.daemon = True watcher_thread.start() # Menu zasobnika def on_quit(icon, item): stop_event.set() icon.stop() icon = Icon("GitWatcher", create_image(), menu=Menu( MenuItem(f"Śledzę: {os.path.abspath(repo_path)}", None, enabled=False), MenuItem("Zakończ", on_quit) )) icon.run() if __name__ == "__main__": main()