From 9dba24efdbbab7acac251a481ac6fbd86058ccde Mon Sep 17 00:00:00 2001 From: bartool Date: Fri, 6 Mar 2026 18:57:01 +0100 Subject: [PATCH] first attempt --- main.py | 135 ++++++++++++++++++++++++++++++++++++++++++++++ systray.py | 153 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 288 insertions(+) create mode 100644 main.py create mode 100644 systray.py diff --git a/main.py b/main.py new file mode 100644 index 0000000..8bd5b3f --- /dev/null +++ b/main.py @@ -0,0 +1,135 @@ +import time +import os +from watchdog.observers import Observer +from watchdog.events import FileSystemEventHandler +from git import Repo + +# KONFIGURACJA +PATH_TO_REPO = './moje-repo' # Ścieżka do Twojego katalogu git +COMMIT_MESSAGE = "Automatyczny commit: zmiana w pliku" + +import pathspec +from watchdog.events import FileSystemEventHandler + + +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() + + # Słownik do przechowywania czasu ostatniego commitu dla każdego pliku + self.last_committed = {} + self.debounce_seconds = 0.5 # Minimalny odstęp między commitami dla jednego pliku + + 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: + # Tworzymy wzorzec dopasowania na podstawie linii w pliku + return pathspec.PathSpec.from_lines('gitwildmatch', f) + return None + + def _is_ignored(self, file_path): + # Sprawdzamy ścieżkę względem głównego folderu repozytorium + relative_path = os.path.relpath(file_path, self.repo_path) + + # Zawsze ignoruj folder .git + if ".git" in relative_path.split(os.sep): + return True + + if self.spec and self.spec.match_file(relative_path): + return True + return False + + def on_modified(self, event): + if not event.is_directory and not self._is_ignored(event.src_path): + self._process_event(event.src_path, "update-file") + + def on_created(self, event): + if not event.is_directory and not self._is_ignored(event.src_path): + self._process_event(event.src_path, "new-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: + # Sprawdzamy, czy nowa lokalizacja nie jest ignorowana + if self._is_ignored(event.dest_path): + # Jeśli przenieśliśmy plik do folderu ignorowanego, traktujemy to jak usunięcie + self._process_event(event.src_path, "remove-file") + else: + self._process_rename(event.src_path, event.dest_path) + + def _process_event(self, file_path, action_label): + if self._is_ignored(file_path): + return + + current_time = time.time() + last_time = self.last_committed.get(file_path, 0) + + # Sprawdź, czy minęło dość czasu od ostatniego commitu tego pliku + if current_time - last_time > self.debounce_seconds: + self.last_committed[file_path] = current_time + self._commit(file_path, action_label) + + def _process_rename(self, old_path, new_path): + current_time = time.time() + # Używamy nowej ścieżki jako klucza do debounce + last_time = self.last_committed.get(new_path, 0) + + if current_time - last_time > self.debounce_seconds: + self.last_committed[new_path] = current_time + + try: + old_rel = os.path.relpath(old_path, self.repo_path) + new_rel = os.path.relpath(new_path, self.repo_path) + + # Git sam wykrywa rename, jeśli dodamy oba pliki (usunięty i nowy) + # Ale możemy to zrobić jawnie dla czystości: + self.repo.index.remove([old_rel], working_tree=False) + self.repo.index.add([new_rel]) + + if self.repo.index.diff("HEAD"): + self.repo.index.commit(f"rename-file: {old_rel} -> {new_rel}") + print(f"🔄 RENAME: {old_rel} -> {new_rel}") + except Exception as e: + print(f"⚠️ Błąd podczas zmiany nazwy: {e}") + + def _commit(self, file_path, action_label): + try: + relative_path = os.path.relpath(file_path, self.repo_path) + + if action_label == "remove-file": + self.repo.index.remove([relative_path]) + else: + self.repo.index.add([relative_path]) + if self.repo.index.diff("HEAD"): + self.repo.index.commit(f"{action_label}: {relative_path}") + print(f"{action_label}: {relative_path}") + # if self.repo.is_dirty(path=relative_path) or relative_path in self.repo.untracked_files: + # self.repo.index.add([relative_path]) + # self.repo.index.commit(f"Auto-commit: {relative_path}") + # print(f"✅ Zapisano: {relative_path}") + except Exception as e: + print(f"❌ Błąd: {e}") + + +if __name__ == "__main__": + if not os.path.exists(os.path.join(PATH_TO_REPO, ".git")): + print("Błąd: Wskazany katalog nie jest repozytorium Gita!") + else: + event_handler = GitAutoCommitHandler(PATH_TO_REPO) + observer = Observer() + observer.schedule(event_handler, PATH_TO_REPO, recursive=True) + + print(f"🚀 Śledzenie katalogu: {PATH_TO_REPO}...") + observer.start() + try: + while True: + time.sleep(1) + except KeyboardInterrupt: + observer.stop() + observer.join() \ No newline at end of file diff --git a/systray.py b/systray.py new file mode 100644 index 0000000..0b29137 --- /dev/null +++ b/systray.py @@ -0,0 +1,153 @@ +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() \ No newline at end of file