first attempt
This commit is contained in:
135
main.py
Normal file
135
main.py
Normal file
@@ -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()
|
||||
153
systray.py
Normal file
153
systray.py
Normal file
@@ -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()
|
||||
Reference in New Issue
Block a user