nogit 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
nogit/__init__.py ADDED
File without changes
@@ -0,0 +1,9 @@
1
+ """Point d'entrée du watcher lancé en process détaché."""
2
+
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ if __name__ == "__main__":
7
+ project_path = Path(sys.argv[1])
8
+ from nogit.watcher import run
9
+ run(project_path)
nogit/automatic.py ADDED
@@ -0,0 +1,127 @@
1
+ """nogit automatic / manual — gère le watcher de snapshots automatiques."""
2
+
3
+ import os
4
+ import sys
5
+ import subprocess
6
+ from pathlib import Path
7
+
8
+ from nogit.utils import (
9
+ register_project,
10
+ get_project_mode,
11
+ set_watcher_pid,
12
+ clear_watcher_pid,
13
+ )
14
+ from nogit.ignore import ensure_nogitignore, load_patterns, is_ignored
15
+
16
+
17
+ def _show_tree(project_path: Path, max_depth: int = 3) -> int:
18
+ """Affiche l'arborescence surveillée. Retourne la profondeur max trouvée."""
19
+ ensure_nogitignore(project_path)
20
+ dir_patterns, file_patterns = load_patterns(project_path)
21
+ max_found = [0]
22
+
23
+ def _walk(path: Path, depth: int, prefix: str):
24
+ if depth > max_depth:
25
+ max_found[0] = max(max_found[0], depth)
26
+ return
27
+ try:
28
+ entries = sorted(path.iterdir(), key=lambda p: (p.is_file(), p.name))
29
+ except PermissionError:
30
+ return
31
+ entries = [
32
+ e for e in entries
33
+ if not is_ignored(e, project_path, dir_patterns, file_patterns)
34
+ ]
35
+ for i, entry in enumerate(entries):
36
+ max_found[0] = max(max_found[0], depth)
37
+ connector = "└── " if i == len(entries) - 1 else "├── "
38
+ print(f" {prefix}{connector}{entry.name}")
39
+ if entry.is_dir():
40
+ extension = " " if i == len(entries) - 1 else "│ "
41
+ _walk(entry, depth + 1, prefix + extension)
42
+
43
+ print(f"\n {project_path}")
44
+ _walk(project_path, 1, "")
45
+ return max_found[0]
46
+
47
+
48
+ def run_automatic(project_path: Path):
49
+ mode, pid = get_project_mode(project_path)
50
+ if mode == "auto":
51
+ print(f"[nogit] Watcher déjà actif (PID {pid}).")
52
+ return
53
+
54
+ if project_path == project_path.parent:
55
+ answer = input(f" Attention : vous lancez nogit depuis la racine de votre disque ({project_path}). Continuer ? [o/N] : ").strip().lower()
56
+ if answer not in ("o", "oui", "y", "yes"):
57
+ print("[nogit] Annulé.")
58
+ return
59
+ answer2 = input(f" Vraiment ? Cela va surveiller tout votre disque. Confirmer ? [o/N] : ").strip().lower()
60
+ if answer2 not in ("o", "oui", "y", "yes"):
61
+ print("[nogit] Annulé.")
62
+ return
63
+
64
+ print(f"\n[nogit] Arborescence qui sera surveillée :\n")
65
+ depth = _show_tree(project_path)
66
+ print()
67
+ if depth > 3:
68
+ answer = input(f" Attention : ce dossier a une profondeur de {depth}. Êtes-vous sûr d'être dans le bon dossier ? [o/N] : ").strip().lower()
69
+ else:
70
+ answer = input(" Activer le watcher automatique ? [o/N] : ").strip().lower()
71
+ if answer not in ("o", "oui", "y", "yes"):
72
+ print("[nogit] Annulé.")
73
+ return
74
+
75
+ register_project(project_path)
76
+
77
+ # Snapshot initial full
78
+ from nogit.snapshot import run_full
79
+ run_full(project_path)
80
+
81
+ # Lancer le watcher dans un terminal visible
82
+ cmd = [sys.executable, "-m", "nogit._watcher_entry", str(project_path)]
83
+ if sys.platform == "win32":
84
+ proc = subprocess.Popen(
85
+ ["cmd", "/c", "start", "nogit watcher", sys.executable, "-m", "nogit._watcher_entry", str(project_path)],
86
+ creationflags=subprocess.CREATE_NEW_PROCESS_GROUP,
87
+ )
88
+ else:
89
+ terminals = ["gnome-terminal", "xterm", "konsole"]
90
+ launched = False
91
+ for term in terminals:
92
+ try:
93
+ proc = subprocess.Popen(
94
+ [term, "--", sys.executable, "-m", "nogit._watcher_entry", str(project_path)],
95
+ start_new_session=True,
96
+ )
97
+ launched = True
98
+ break
99
+ except FileNotFoundError:
100
+ continue
101
+ if not launched:
102
+ proc = subprocess.Popen(
103
+ cmd,
104
+ start_new_session=True,
105
+ close_fds=True,
106
+ )
107
+
108
+ print(f"[nogit] Watcher démarré dans un nouveau terminal.")
109
+
110
+
111
+ def run_manual(project_path: Path):
112
+ mode, pid = get_project_mode(project_path)
113
+ if mode == "manual":
114
+ print("[nogit] Ce projet est déjà en mode manuel.")
115
+ return
116
+
117
+ # Tuer le watcher
118
+ try:
119
+ if sys.platform == "win32":
120
+ subprocess.run(["taskkill", "/PID", str(pid), "/F"], capture_output=True)
121
+ else:
122
+ os.kill(pid, 15) # SIGTERM
123
+ except OSError:
124
+ pass
125
+
126
+ clear_watcher_pid(project_path)
127
+ print(f"[nogit] Watcher arrêté. Projet repassé en mode manuel.")
nogit/backups.py ADDED
@@ -0,0 +1,184 @@
1
+ """nogit backups — liste les snapshots d'un projet ou de tous les projets."""
2
+
3
+ from pathlib import Path
4
+ from datetime import datetime
5
+ from nogit.utils import load_registry, save_registry, get_project_mode, get_external_backup_dir, set_external_backup_dir
6
+
7
+
8
+ def _format_date(stem: str) -> str:
9
+ try:
10
+ dt = datetime.strptime(stem, "%Y-%m-%dT%H-%M-%S")
11
+ return dt.strftime("%d/%m/%Y %H:%M:%S")
12
+ except ValueError:
13
+ return stem
14
+
15
+
16
+ def _format_size(size_bytes: int) -> str:
17
+ if size_bytes < 1024 * 1024:
18
+ return f"{size_bytes / 1024:.1f} KB"
19
+ return f"{size_bytes / (1024 * 1024):.1f} MB"
20
+
21
+
22
+ def _list_snapshots(project_path: Path) -> list[Path]:
23
+ """Retourne les snapshots d'un projet triés du plus récent au plus ancien."""
24
+ snap_dir = project_path / ".nogit" / "snapshots"
25
+ if not snap_dir.exists():
26
+ return []
27
+ return sorted(snap_dir.glob("*.zip"), reverse=True)
28
+
29
+
30
+ def _show_snapshots_and_diff(project_path: Path):
31
+ from nogit import diff as diff_module
32
+ from nogit import restore as restore_module
33
+
34
+ snapshots = _list_snapshots(project_path)
35
+ if not snapshots:
36
+ print(" Aucun snapshot trouvé.")
37
+ return
38
+
39
+ print(f"\n Snapshots de {project_path}")
40
+ print()
41
+ for i, snap in enumerate(snapshots, 1):
42
+ size = _format_size(snap.stat().st_size)
43
+ date_str = _format_date(snap.stem)
44
+ print(f" {i:>3}. {date_str} ({size})")
45
+
46
+ print()
47
+ action = input(" > [d]iff / [r]estore / Entrée pour annuler : ").strip().lower()
48
+ if not action:
49
+ return
50
+
51
+ if action in ("d", "diff"):
52
+ choice = input(" > diff entre (ex: 1 2) : ").strip()
53
+ parts = choice.split()
54
+ if len(parts) != 2:
55
+ print(" Format invalide. Exemple : 1 2")
56
+ return
57
+ try:
58
+ a, b = int(parts[0]) - 1, int(parts[1]) - 1
59
+ if not (0 <= a < len(snapshots) and 0 <= b < len(snapshots)):
60
+ print(" Numéros invalides.")
61
+ return
62
+ # snap A = le plus ancien (index le plus grand car liste inversée)
63
+ older, newer = sorted([a, b], reverse=True)
64
+ diff_module.run(str(snapshots[older]), str(snapshots[newer]))
65
+ except ValueError:
66
+ print(" Numéros invalides.")
67
+
68
+ elif action in ("r", "restore"):
69
+ choice = input(" > numéro du snapshot à restaurer : ").strip()
70
+ try:
71
+ idx = int(choice)
72
+ restore_module.run(idx)
73
+ except ValueError:
74
+ print(" Numéro invalide.")
75
+
76
+
77
+ def run(global_: bool = False, external: bool = False):
78
+ if external:
79
+ _run_external()
80
+ elif global_:
81
+ _run_global()
82
+ else:
83
+ _run_local()
84
+
85
+
86
+ def _run_external():
87
+ ext_dir = get_external_backup_dir()
88
+ if ext_dir:
89
+ print(f"\n[nogit] Backup externe configuré : {ext_dir}")
90
+ if ext_dir.exists():
91
+ print(f" Dossier accessible.")
92
+ else:
93
+ print(f" Avertissement : dossier introuvable (disque déconnecté ?)")
94
+ print()
95
+ answer = input(" Changer le chemin ? [o/N] : ").strip().lower()
96
+ if answer not in ("o", "oui", "y", "yes"):
97
+ return
98
+ else:
99
+ print("\n[nogit] Aucun backup externe configuré.")
100
+ print()
101
+
102
+ new_path = input(" Chemin du dossier de backup externe : ").strip().strip('"')
103
+ if not new_path:
104
+ print("[nogit] Annulé.")
105
+ return
106
+
107
+ dest = Path(new_path)
108
+ if not dest.exists():
109
+ answer = input(f" Ce dossier n'existe pas. Le créer ? [o/N] : ").strip().lower()
110
+ if answer not in ("o", "oui", "y", "yes"):
111
+ print("[nogit] Annulé.")
112
+ return
113
+ dest.mkdir(parents=True, exist_ok=True)
114
+
115
+ set_external_backup_dir(dest)
116
+ print(f"[nogit] ✓ Backup externe configuré → {dest}")
117
+
118
+
119
+ def _run_local():
120
+ project_path = Path.cwd()
121
+ mode, _ = get_project_mode(project_path)
122
+ label = "[auto]" if mode == "auto" else "[manuel]"
123
+ print(f"\n {project_path} {label}")
124
+ _show_snapshots_and_diff(project_path)
125
+
126
+
127
+ def _run_global():
128
+ registry = load_registry()
129
+
130
+ if not registry:
131
+ print("[nogit] Aucun projet enregistré.")
132
+ return
133
+
134
+ projects = [k for k in registry.keys() if not k.startswith("_")]
135
+ orphans = []
136
+ valid_projects = []
137
+ print()
138
+ for i, project in enumerate(projects, 1):
139
+ path = Path(project)
140
+ nogit_dir = path / ".nogit"
141
+
142
+ if not nogit_dir.exists():
143
+ print(f" {i:>3}. {project}")
144
+ print(f" [supprimé] le dossier .nogit n'existe plus — cette entrée sera retirée au prochain lancement")
145
+ orphans.append(project)
146
+ continue
147
+
148
+ valid_projects.append(project)
149
+ entry = registry[project]
150
+ if isinstance(entry, dict):
151
+ snapshots_paths = [Path(p) for p in entry.get("snapshots", [])]
152
+ count = len(snapshots_paths)
153
+ else:
154
+ snapshots_paths = _list_snapshots(path)
155
+ count = len(snapshots_paths)
156
+ mode, _ = get_project_mode(path)
157
+ mode_label = "[auto]" if mode == "auto" else "[manuel]"
158
+ label = f"{count} snapshot{'s' if count != 1 else ''}"
159
+ print(f" {i:>3}. {project} ({label}) {mode_label}")
160
+ for snap in snapshots_paths[:3]:
161
+ date_str = _format_date(Path(snap).stem)
162
+ print(f" {date_str}")
163
+ if len(snapshots_paths) > 3:
164
+ print(f" ... {len(snapshots_paths) - 3} snapshot(s) supplémentaire(s)")
165
+
166
+ if orphans:
167
+ for project in orphans:
168
+ del registry[project]
169
+ save_registry(registry)
170
+ projects = valid_projects
171
+
172
+ print()
173
+ choice = input(" > numéro du projet (ou Entrée pour annuler) : ").strip()
174
+ if not choice:
175
+ return
176
+
177
+ try:
178
+ idx = int(choice) - 1
179
+ if not (0 <= idx < len(projects)):
180
+ print(" Numéro invalide.")
181
+ return
182
+ _show_snapshots_and_diff(Path(projects[idx]))
183
+ except ValueError:
184
+ print(" Numéro invalide.")
nogit/cli.py ADDED
@@ -0,0 +1,97 @@
1
+ """NoGit CLI — git sans le bloat."""
2
+
3
+ import sys
4
+ import argparse
5
+ from pathlib import Path
6
+ from nogit import snapshot, backups, diff, automatic, restore
7
+
8
+
9
+ _GPL_NOTICE = (
10
+ "NoGit Copyright (C) 2026 Mehdi. A\n"
11
+ "This program comes with ABSOLUTELY NO WARRANTY; for details type `nogit show w'.\n"
12
+ "This is free software, and you are welcome to redistribute it\n"
13
+ "under certain conditions; type `nogit show c' for details.\n"
14
+ )
15
+
16
+
17
+ class _Parser(argparse.ArgumentParser):
18
+ def print_help(self, file=None):
19
+ print(_GPL_NOTICE)
20
+ super().print_help(file)
21
+
22
+
23
+ def main():
24
+ parser = _Parser(
25
+ prog="nogit",
26
+ description="Git sans le bloat — snapshots ZIP + diff intelligent.",
27
+ formatter_class=lambda prog: argparse.HelpFormatter(prog, max_help_position=60),
28
+ )
29
+ subparsers = parser.add_subparsers(dest="command", metavar="commande")
30
+
31
+ # nogit snapshot
32
+ subparsers.add_parser(
33
+ "snapshot",
34
+ help="Crée un snapshot ZIP complet du dossier courant (mode manuel uniquement)."
35
+ )
36
+
37
+ # nogit backups
38
+ backups_parser = subparsers.add_parser(
39
+ "backups",
40
+ help="Liste les snapshots du dossier courant. Permet de lancer un diff ou un restore. --global : tous les projets de la machine. --external : configurer un backup secondaire."
41
+ )
42
+ backups_parser.add_argument(
43
+ "--global", dest="global_", action="store_true",
44
+ help="Liste tous les projets NoGit enregistrés sur la machine. Nettoie automatiquement les projets supprimés."
45
+ )
46
+ backups_parser.add_argument(
47
+ "--external", dest="external", action="store_true",
48
+ help="Configure le dossier de backup externe (disque secondaire, NAS...). Les snapshots y sont copiés automatiquement."
49
+ )
50
+
51
+ # nogit diff
52
+ diff_parser = subparsers.add_parser(
53
+ "diff",
54
+ help="Compare deux snapshots ZIP. Génère un dossier dans .nogit/diff/ avec diff.txt, summary.json et un .diff.json par fichier modifié."
55
+ )
56
+ diff_parser.add_argument("snap1", help="Premier snapshot ZIP.")
57
+ diff_parser.add_argument("snap2", help="Deuxième snapshot ZIP.")
58
+
59
+ # nogit automatic
60
+ subparsers.add_parser(
61
+ "automatic",
62
+ help="Active le watcher : surveille le dossier et crée un snapshot incrémental à chaque modification détectée."
63
+ )
64
+
65
+ # nogit manual
66
+ subparsers.add_parser(
67
+ "manual",
68
+ help="Désactive le watcher et repasse en mode snapshot manuel."
69
+ )
70
+
71
+ # nogit restore
72
+ restore_parser = subparsers.add_parser(
73
+ "restore",
74
+ help="Restaure le projet depuis un snapshot. Remonte automatiquement la chaîne si le snapshot est incrémental."
75
+ )
76
+ restore_parser.add_argument("index", type=int, help="Numéro du snapshot affiché dans 'nogit backups'.")
77
+
78
+ args = parser.parse_args()
79
+
80
+ if args.command == "snapshot":
81
+ snapshot.run()
82
+ elif args.command == "backups":
83
+ backups.run(global_=args.global_, external=args.external)
84
+ elif args.command == "diff":
85
+ diff.run(args.snap1, args.snap2)
86
+ elif args.command == "automatic":
87
+ automatic.run_automatic(Path.cwd())
88
+ elif args.command == "manual":
89
+ automatic.run_manual(Path.cwd())
90
+ elif args.command == "restore":
91
+ restore.run(args.index)
92
+ else:
93
+ backups.run()
94
+
95
+
96
+ if __name__ == "__main__":
97
+ main()
nogit/data/__init__.py ADDED
File without changes