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 +0 -0
- nogit/_watcher_entry.py +9 -0
- nogit/automatic.py +127 -0
- nogit/backups.py +184 -0
- nogit/cli.py +97 -0
- nogit/data/__init__.py +0 -0
- nogit/data/mime_types.json +1269 -0
- nogit/diff.py +204 -0
- nogit/ignore.py +59 -0
- nogit/restore.py +116 -0
- nogit/snapshot.py +196 -0
- nogit/utils.py +440 -0
- nogit/watcher.py +127 -0
- nogit-0.1.0.dist-info/METADATA +136 -0
- nogit-0.1.0.dist-info/RECORD +19 -0
- nogit-0.1.0.dist-info/WHEEL +5 -0
- nogit-0.1.0.dist-info/entry_points.txt +2 -0
- nogit-0.1.0.dist-info/licenses/LICENSE +224 -0
- nogit-0.1.0.dist-info/top_level.txt +1 -0
nogit/__init__.py
ADDED
|
File without changes
|
nogit/_watcher_entry.py
ADDED
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
|