agmem 0.1.1__py3-none-any.whl → 0.1.3__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.
Files changed (100) hide show
  1. {agmem-0.1.1.dist-info → agmem-0.1.3.dist-info}/METADATA +157 -16
  2. agmem-0.1.3.dist-info/RECORD +105 -0
  3. memvcs/__init__.py +1 -1
  4. memvcs/cli.py +45 -31
  5. memvcs/commands/__init__.py +9 -9
  6. memvcs/commands/add.py +83 -76
  7. memvcs/commands/audit.py +59 -0
  8. memvcs/commands/blame.py +46 -53
  9. memvcs/commands/branch.py +13 -33
  10. memvcs/commands/checkout.py +27 -32
  11. memvcs/commands/clean.py +18 -23
  12. memvcs/commands/clone.py +11 -1
  13. memvcs/commands/commit.py +40 -39
  14. memvcs/commands/daemon.py +109 -76
  15. memvcs/commands/decay.py +77 -0
  16. memvcs/commands/diff.py +56 -57
  17. memvcs/commands/distill.py +90 -0
  18. memvcs/commands/federated.py +53 -0
  19. memvcs/commands/fsck.py +86 -61
  20. memvcs/commands/garden.py +40 -35
  21. memvcs/commands/gc.py +51 -0
  22. memvcs/commands/graph.py +41 -48
  23. memvcs/commands/init.py +16 -24
  24. memvcs/commands/log.py +25 -40
  25. memvcs/commands/merge.py +69 -27
  26. memvcs/commands/pack.py +129 -0
  27. memvcs/commands/prove.py +66 -0
  28. memvcs/commands/pull.py +31 -1
  29. memvcs/commands/push.py +4 -2
  30. memvcs/commands/recall.py +145 -0
  31. memvcs/commands/reflog.py +13 -22
  32. memvcs/commands/remote.py +1 -0
  33. memvcs/commands/repair.py +66 -0
  34. memvcs/commands/reset.py +23 -33
  35. memvcs/commands/resolve.py +130 -0
  36. memvcs/commands/resurrect.py +82 -0
  37. memvcs/commands/search.py +3 -4
  38. memvcs/commands/serve.py +2 -1
  39. memvcs/commands/show.py +66 -36
  40. memvcs/commands/stash.py +34 -34
  41. memvcs/commands/status.py +27 -35
  42. memvcs/commands/tag.py +23 -47
  43. memvcs/commands/test.py +30 -44
  44. memvcs/commands/timeline.py +111 -0
  45. memvcs/commands/tree.py +26 -27
  46. memvcs/commands/verify.py +110 -0
  47. memvcs/commands/when.py +115 -0
  48. memvcs/core/access_index.py +167 -0
  49. memvcs/core/audit.py +124 -0
  50. memvcs/core/config_loader.py +3 -1
  51. memvcs/core/consistency.py +214 -0
  52. memvcs/core/crypto_verify.py +280 -0
  53. memvcs/core/decay.py +185 -0
  54. memvcs/core/diff.py +158 -143
  55. memvcs/core/distiller.py +277 -0
  56. memvcs/core/encryption.py +169 -0
  57. memvcs/core/federated.py +86 -0
  58. memvcs/core/gardener.py +176 -145
  59. memvcs/core/hooks.py +48 -14
  60. memvcs/core/ipfs_remote.py +39 -0
  61. memvcs/core/knowledge_graph.py +135 -138
  62. memvcs/core/llm/__init__.py +10 -0
  63. memvcs/core/llm/anthropic_provider.py +50 -0
  64. memvcs/core/llm/base.py +27 -0
  65. memvcs/core/llm/factory.py +30 -0
  66. memvcs/core/llm/openai_provider.py +36 -0
  67. memvcs/core/merge.py +260 -170
  68. memvcs/core/objects.py +110 -101
  69. memvcs/core/pack.py +92 -0
  70. memvcs/core/pii_scanner.py +147 -146
  71. memvcs/core/privacy_budget.py +63 -0
  72. memvcs/core/refs.py +132 -115
  73. memvcs/core/remote.py +38 -0
  74. memvcs/core/repository.py +254 -164
  75. memvcs/core/schema.py +155 -113
  76. memvcs/core/staging.py +60 -65
  77. memvcs/core/storage/__init__.py +20 -18
  78. memvcs/core/storage/base.py +74 -70
  79. memvcs/core/storage/gcs.py +70 -68
  80. memvcs/core/storage/local.py +42 -40
  81. memvcs/core/storage/s3.py +105 -110
  82. memvcs/core/temporal_index.py +121 -0
  83. memvcs/core/test_runner.py +101 -93
  84. memvcs/core/trust.py +103 -0
  85. memvcs/core/vector_store.py +56 -36
  86. memvcs/core/zk_proofs.py +26 -0
  87. memvcs/integrations/mcp_server.py +1 -3
  88. memvcs/integrations/web_ui/server.py +25 -26
  89. memvcs/retrieval/__init__.py +22 -0
  90. memvcs/retrieval/base.py +54 -0
  91. memvcs/retrieval/pack.py +128 -0
  92. memvcs/retrieval/recaller.py +105 -0
  93. memvcs/retrieval/strategies.py +314 -0
  94. memvcs/utils/__init__.py +3 -3
  95. memvcs/utils/helpers.py +52 -52
  96. agmem-0.1.1.dist-info/RECORD +0 -67
  97. {agmem-0.1.1.dist-info → agmem-0.1.3.dist-info}/WHEEL +0 -0
  98. {agmem-0.1.1.dist-info → agmem-0.1.3.dist-info}/entry_points.txt +0 -0
  99. {agmem-0.1.1.dist-info → agmem-0.1.3.dist-info}/licenses/LICENSE +0 -0
  100. {agmem-0.1.1.dist-info → agmem-0.1.3.dist-info}/top_level.txt +0 -0
memvcs/commands/daemon.py CHANGED
@@ -14,34 +14,34 @@ from ..commands.base import require_repo
14
14
 
15
15
  class DaemonCommand:
16
16
  """Control the auto-sync daemon."""
17
-
18
- name = 'daemon'
19
- help = 'Start/stop the auto-sync daemon for automatic commits'
20
-
17
+
18
+ name = "daemon"
19
+ help = "Start/stop the auto-sync daemon for automatic commits"
20
+
21
21
  @staticmethod
22
22
  def add_arguments(parser: argparse.ArgumentParser):
23
23
  parser.add_argument(
24
- 'action',
25
- choices=['start', 'stop', 'status', 'run'],
26
- help='Daemon action: start, stop, status, or run (foreground)'
24
+ "action",
25
+ choices=["start", "stop", "status", "run"],
26
+ help="Daemon action: start, stop, status, or run (foreground)",
27
27
  )
28
28
  parser.add_argument(
29
- '--debounce',
29
+ "--debounce",
30
30
  type=int,
31
31
  default=30,
32
- help='Seconds to wait after changes before committing (default: 30)'
32
+ help="Seconds to wait after changes before committing (default: 30)",
33
33
  )
34
+ parser.add_argument("--pidfile", help="PID file path (default: .mem/daemon.pid)")
34
35
  parser.add_argument(
35
- '--pidfile',
36
- help='PID file path (default: .mem/daemon.pid)'
36
+ "--distill", action="store_true", help="Run distillation pipeline nightly (with daemon)"
37
37
  )
38
-
38
+
39
39
  @staticmethod
40
40
  def execute(args) -> int:
41
41
  repo, code = require_repo()
42
42
  if code != 0:
43
43
  return code
44
-
44
+
45
45
  if args.pidfile:
46
46
  pid_file = Path(args.pidfile).resolve()
47
47
  try:
@@ -50,21 +50,21 @@ class DaemonCommand:
50
50
  print("Error: --pidfile must be under repository root")
51
51
  return 1
52
52
  else:
53
- pid_file = repo.root / '.mem' / 'daemon.pid'
54
-
55
- if args.action == 'start':
56
- return DaemonCommand._start(repo, pid_file, args.debounce)
57
- elif args.action == 'stop':
53
+ pid_file = repo.root / ".mem" / "daemon.pid"
54
+
55
+ if args.action == "start":
56
+ return DaemonCommand._start(repo, pid_file, args.debounce, args.distill)
57
+ elif args.action == "stop":
58
58
  return DaemonCommand._stop(pid_file)
59
- elif args.action == 'status':
59
+ elif args.action == "status":
60
60
  return DaemonCommand._status(pid_file)
61
- elif args.action == 'run':
62
- return DaemonCommand._run(repo, args.debounce)
63
-
61
+ elif args.action == "run":
62
+ return DaemonCommand._run(repo, args.debounce, pid_file=pid_file, distill=args.distill)
63
+
64
64
  return 1
65
-
65
+
66
66
  @staticmethod
67
- def _start(repo, pid_file: Path, debounce: int) -> int:
67
+ def _start(repo, pid_file: Path, debounce: int, distill: bool = False) -> int:
68
68
  """Start daemon in background."""
69
69
  # Check if already running
70
70
  if pid_file.exists():
@@ -75,13 +75,13 @@ class DaemonCommand:
75
75
  return 1
76
76
  except OSError:
77
77
  pid_file.unlink()
78
-
78
+
79
79
  # Fork to background (Unix only)
80
- if os.name != 'posix':
80
+ if os.name != "posix":
81
81
  print("Background daemon not supported on this platform.")
82
82
  print("Use 'agmem daemon run' to run in foreground.")
83
83
  return 1
84
-
84
+
85
85
  # First fork
86
86
  try:
87
87
  pid = os.fork()
@@ -92,11 +92,11 @@ class DaemonCommand:
92
92
  except OSError as e:
93
93
  print(f"Fork failed: {e}")
94
94
  return 1
95
-
95
+
96
96
  # Decouple from parent
97
97
  os.setsid()
98
98
  os.umask(0)
99
-
99
+
100
100
  # Second fork
101
101
  try:
102
102
  pid = os.fork()
@@ -104,26 +104,26 @@ class DaemonCommand:
104
104
  sys.exit(0)
105
105
  except OSError:
106
106
  sys.exit(1)
107
-
107
+
108
108
  # Write PID file
109
109
  pid_file.write_text(str(os.getpid()))
110
-
110
+
111
111
  # Redirect stdio
112
112
  sys.stdout.flush()
113
113
  sys.stderr.flush()
114
-
115
- # Run daemon
116
- return DaemonCommand._run(repo, debounce, pid_file)
117
-
114
+
115
+ # Run daemon in background
116
+ return DaemonCommand._run(repo, debounce, pid_file, distill)
117
+
118
118
  @staticmethod
119
119
  def _stop(pid_file: Path) -> int:
120
120
  """Stop running daemon."""
121
121
  if not pid_file.exists():
122
122
  print("No daemon running")
123
123
  return 0
124
-
124
+
125
125
  pid = int(pid_file.read_text().strip())
126
-
126
+
127
127
  try:
128
128
  os.kill(pid, signal.SIGTERM)
129
129
  print(f"Stopped daemon (PID: {pid})")
@@ -133,16 +133,16 @@ class DaemonCommand:
133
133
  print(f"Could not stop daemon: {e}")
134
134
  pid_file.unlink()
135
135
  return 1
136
-
136
+
137
137
  @staticmethod
138
138
  def _status(pid_file: Path) -> int:
139
139
  """Show daemon status."""
140
140
  if not pid_file.exists():
141
141
  print("Daemon is not running")
142
142
  return 0
143
-
143
+
144
144
  pid = int(pid_file.read_text().strip())
145
-
145
+
146
146
  try:
147
147
  os.kill(pid, 0)
148
148
  print(f"Daemon is running (PID: {pid})")
@@ -151,9 +151,9 @@ class DaemonCommand:
151
151
  print("Daemon is not running (stale PID file)")
152
152
  pid_file.unlink()
153
153
  return 0
154
-
154
+
155
155
  @staticmethod
156
- def _run(repo, debounce: int, pid_file: Path = None) -> int:
156
+ def _run(repo, debounce: int, pid_file: Path = None, distill: bool = False) -> int:
157
157
  """Run daemon in foreground."""
158
158
  try:
159
159
  from watchdog.observers import Observer
@@ -161,84 +161,120 @@ class DaemonCommand:
161
161
  except ImportError:
162
162
  print("Daemon requires watchdog. Install with: pip install agmem[daemon]")
163
163
  return 1
164
-
165
- current_dir = repo.root / 'current'
166
-
164
+
165
+ current_dir = repo.root / "current"
166
+
167
167
  if not current_dir.exists():
168
168
  print("No current/ directory to watch")
169
169
  return 1
170
-
170
+
171
171
  class MemoryFileHandler(FileSystemEventHandler):
172
172
  def __init__(self):
173
173
  self.last_change = 0
174
174
  self.pending = False
175
-
175
+
176
176
  def on_any_event(self, event):
177
177
  # Ignore .mem and hidden files
178
- if '.mem' in event.src_path or '/.' in event.src_path:
178
+ if ".mem" in event.src_path or "/." in event.src_path:
179
179
  return
180
-
180
+
181
181
  # Ignore directories
182
182
  if event.is_directory:
183
183
  return
184
-
184
+
185
185
  self.last_change = time.time()
186
186
  self.pending = True
187
-
187
+
188
188
  handler = MemoryFileHandler()
189
189
  observer = Observer()
190
190
  observer.schedule(handler, str(current_dir), recursive=True)
191
191
  observer.start()
192
-
192
+
193
193
  print(f"Watching {current_dir} (debounce: {debounce}s)")
194
194
  print("Press Ctrl+C to stop")
195
-
195
+
196
196
  # Handle signals
197
197
  running = True
198
-
198
+
199
199
  def signal_handler(signum, frame):
200
200
  nonlocal running
201
201
  running = False
202
-
202
+
203
203
  signal.signal(signal.SIGTERM, signal_handler)
204
204
  signal.signal(signal.SIGINT, signal_handler)
205
-
205
+
206
+ # Health monitoring: periodic integrity check (configurable interval)
207
+ last_health_check = 0
208
+ health_check_interval = 3600 # 1 hour
209
+
206
210
  try:
207
211
  while running:
208
212
  time.sleep(1)
209
-
213
+
214
+ # Periodic health check (Merkle/signature, optional)
215
+ if (
216
+ health_check_interval
217
+ and (time.time() - last_health_check) >= health_check_interval
218
+ ):
219
+ try:
220
+ from ..core.crypto_verify import verify_commit, load_public_key
221
+
222
+ head = repo.refs.get_branch_commit(
223
+ repo.refs.get_current_branch() or "main"
224
+ ) or (repo.refs.get_head() or {}).get("value")
225
+ if head:
226
+ ok, err = verify_commit(
227
+ repo.object_store,
228
+ head,
229
+ load_public_key(repo.mem_dir),
230
+ mem_dir=repo.mem_dir,
231
+ )
232
+ if not ok and err and "tampered" in (err or "").lower():
233
+ sys.stderr.write(f"Health check: {err}\n")
234
+ except Exception:
235
+ pass
236
+ last_health_check = time.time()
237
+
210
238
  if handler.pending:
211
239
  elapsed = time.time() - handler.last_change
212
240
  if elapsed >= debounce:
213
241
  # Auto-commit
214
242
  DaemonCommand._auto_commit(repo)
243
+ if distill:
244
+ try:
245
+ from ..core.distiller import Distiller, DistillerConfig
246
+
247
+ d = Distiller(repo, DistillerConfig(create_safety_branch=False))
248
+ d.run()
249
+ except Exception:
250
+ pass
215
251
  handler.pending = False
216
252
  finally:
217
253
  observer.stop()
218
254
  observer.join()
219
-
255
+
220
256
  if pid_file and pid_file.exists():
221
257
  pid_file.unlink()
222
-
258
+
223
259
  print("Daemon stopped")
224
260
  return 0
225
-
261
+
226
262
  @staticmethod
227
263
  def _auto_commit(repo):
228
264
  """Perform automatic commit."""
229
265
  from datetime import datetime
230
-
266
+
231
267
  try:
232
268
  # Check for changes
233
269
  status = repo.get_status()
234
-
235
- if not status.get('modified') and not status.get('untracked'):
270
+
271
+ if not status.get("modified") and not status.get("untracked"):
236
272
  return
237
-
273
+
238
274
  # Stage all changes in current/ (validate path stays under current/)
239
- current_dir = repo.root / 'current'
240
- for memory_file in current_dir.glob('**/*'):
241
- if memory_file.is_file() and '.mem' not in str(memory_file):
275
+ current_dir = repo.root / "current"
276
+ for memory_file in current_dir.glob("**/*"):
277
+ if memory_file.is_file() and ".mem" not in str(memory_file):
242
278
  try:
243
279
  rel_path = memory_file.relative_to(current_dir)
244
280
  except ValueError:
@@ -250,18 +286,15 @@ class DaemonCommand:
250
286
  repo.stage_file(rel_str)
251
287
  except Exception:
252
288
  pass
253
-
289
+
254
290
  # Commit
255
- timestamp = datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')
256
- repo.commit(
257
- f"auto: update memory state ({timestamp})",
258
- {'auto_commit': True}
259
- )
260
-
291
+ timestamp = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")
292
+ repo.commit(f"auto: update memory state ({timestamp})", {"auto_commit": True})
293
+
261
294
  print(f"[{timestamp}] Auto-committed changes")
262
-
295
+
263
296
  except Exception as e:
264
297
  # Write conflict lock if there's an issue
265
- conflict_file = repo.root / '.mem' / 'CONFLICT.lock'
298
+ conflict_file = repo.root / ".mem" / "CONFLICT.lock"
266
299
  conflict_file.write_text(f"Auto-commit failed: {e}")
267
300
  print(f"Warning: Auto-commit failed, wrote CONFLICT.lock: {e}")
@@ -0,0 +1,77 @@
1
+ """
2
+ agmem decay - Memory decay and forgetting.
3
+
4
+ Archives low-importance, rarely-accessed memories.
5
+ """
6
+
7
+ import argparse
8
+ from pathlib import Path
9
+
10
+ from ..commands.base import require_repo
11
+ from ..core.decay import DecayEngine, DecayConfig
12
+
13
+
14
+ class DecayCommand:
15
+ """Memory decay - archive low-importance memories."""
16
+
17
+ name = "decay"
18
+ help = "Archive low-importance, old episodic memories (decay/forgetting)"
19
+
20
+ @staticmethod
21
+ def add_arguments(parser: argparse.ArgumentParser):
22
+ parser.add_argument(
23
+ "--dry-run",
24
+ action="store_true",
25
+ help="Preview what would be archived without making changes",
26
+ )
27
+ parser.add_argument(
28
+ "--apply",
29
+ action="store_true",
30
+ help="Actually archive the memories",
31
+ )
32
+
33
+ @staticmethod
34
+ def execute(args) -> int:
35
+ repo, code = require_repo()
36
+ if code != 0:
37
+ return code
38
+
39
+ config = DecayConfig()
40
+ repo_config = repo.get_config()
41
+ decay_cfg = repo_config.get("decay", {})
42
+ if decay_cfg:
43
+ config.episodic_half_life_days = decay_cfg.get(
44
+ "episodic_half_life_days", config.episodic_half_life_days
45
+ )
46
+ config.semantic_min_importance = decay_cfg.get(
47
+ "semantic_min_importance", config.semantic_min_importance
48
+ )
49
+ config.access_count_threshold = decay_cfg.get(
50
+ "access_count_threshold", config.access_count_threshold
51
+ )
52
+
53
+ engine = DecayEngine(repo, config)
54
+ candidates = engine.get_decay_candidates()
55
+
56
+ if not candidates:
57
+ print("No memories eligible for decay.")
58
+ return 0
59
+
60
+ print(f"Found {len(candidates)} memory(ies) eligible for decay:")
61
+ for c in candidates[:20]:
62
+ print(f" - {c.path} (score: {c.decay_score:.2f}) - {c.reason}")
63
+ if len(candidates) > 20:
64
+ print(f" ... and {len(candidates) - 20} more")
65
+
66
+ if args.dry_run:
67
+ print("\nDry run - no changes made. Use --apply to archive.")
68
+ return 0
69
+
70
+ if not args.apply:
71
+ print("\nUse --apply to archive these memories.")
72
+ return 0
73
+
74
+ count = engine.apply_decay(candidates)
75
+ print(f"\nArchived {count} memory(ies) to .mem/forgetting/")
76
+ print("Use 'agmem resurrect <path>' to restore.")
77
+ return 0
memvcs/commands/diff.py CHANGED
@@ -13,33 +13,35 @@ from ..core.repository import Repository
13
13
 
14
14
  class DiffCommand:
15
15
  """Show differences between commits."""
16
-
17
- name = 'diff'
18
- help = 'Show changes between commits, commit and working tree, etc.'
19
-
16
+
17
+ name = "diff"
18
+ help = "Show changes between commits, commit and working tree, etc."
19
+
20
20
  @staticmethod
21
21
  def add_arguments(parser: argparse.ArgumentParser):
22
+ parser.add_argument("ref1", nargs="?", help="First commit/branch to compare")
23
+ parser.add_argument("ref2", nargs="?", help="Second commit/branch to compare")
22
24
  parser.add_argument(
23
- 'ref1',
24
- nargs='?',
25
- help='First commit/branch to compare'
25
+ "--cached", "--staged", action="store_true", help="Show changes staged for commit"
26
26
  )
27
27
  parser.add_argument(
28
- 'ref2',
29
- nargs='?',
30
- help='Second commit/branch to compare'
28
+ "--stat", action="store_true", help="Show diffstat instead of full diff"
31
29
  )
32
30
  parser.add_argument(
33
- '--cached', '--staged',
34
- action='store_true',
35
- help='Show changes staged for commit'
31
+ "--from",
32
+ "--from-ref",
33
+ dest="from_ref",
34
+ metavar="REF",
35
+ help="Start ref for diff (supports ISO dates)",
36
36
  )
37
37
  parser.add_argument(
38
- '--stat',
39
- action='store_true',
40
- help='Show diffstat instead of full diff'
38
+ "--to",
39
+ "--to-ref",
40
+ dest="to_ref",
41
+ metavar="REF",
42
+ help="End ref for diff (supports ISO dates)",
41
43
  )
42
-
44
+
43
45
  @staticmethod
44
46
  def execute(args) -> int:
45
47
  repo, code = require_repo()
@@ -47,7 +49,7 @@ class DiffCommand:
47
49
  return code
48
50
 
49
51
  engine = DiffEngine(repo.object_store)
50
-
52
+
51
53
  # Determine what to diff
52
54
  if args.cached:
53
55
  # Diff staged changes against HEAD
@@ -55,103 +57,100 @@ class DiffCommand:
55
57
  if not head_commit:
56
58
  print("No commits yet. Nothing to diff.")
57
59
  return 0
58
-
60
+
59
61
  staged_files = repo.staging.get_staged_files()
60
62
  if not staged_files:
61
63
  print("No staged changes.")
62
64
  return 0
63
-
65
+
64
66
  # Get staged content
65
67
  working_files = {}
66
68
  for path, sf in staged_files.items():
67
69
  from ..core.objects import Blob
70
+
68
71
  blob = Blob.load(repo.object_store, sf.blob_hash)
69
72
  if blob:
70
73
  working_files[path] = blob.content
71
-
72
- tree_diff = engine.diff_working_dir(
73
- head_commit.store(repo.object_store),
74
- working_files
75
- )
76
-
77
- print(engine.format_diff(tree_diff, 'HEAD', 'staged'))
74
+
75
+ tree_diff = engine.diff_working_dir(head_commit.store(repo.object_store), working_files)
76
+
77
+ print(engine.format_diff(tree_diff, "HEAD", "staged"))
78
78
  return 0
79
-
80
- # Diff between two refs
81
- if args.ref1 and args.ref2:
82
- commit1 = repo.resolve_ref(args.ref1)
83
- commit2 = repo.resolve_ref(args.ref2)
84
-
79
+
80
+ # Diff between two refs (--from/--to override ref1/ref2)
81
+ ref1 = getattr(args, "from_ref", None) or args.ref1
82
+ ref2 = getattr(args, "to_ref", None) or args.ref2
83
+ if ref1 and ref2:
84
+ commit1 = repo.resolve_ref(ref1)
85
+ commit2 = repo.resolve_ref(ref2)
86
+
85
87
  if not commit1:
86
- print(f"Error: Unknown revision: {args.ref1}")
88
+ print(f"Error: Unknown revision: {ref1}")
87
89
  return 1
88
90
  if not commit2:
89
- print(f"Error: Unknown revision: {args.ref2}")
91
+ print(f"Error: Unknown revision: {ref2}")
90
92
  return 1
91
-
93
+
92
94
  tree_diff = engine.diff_commits(commit1, commit2)
93
-
95
+
94
96
  if args.stat:
95
97
  print(f" {tree_diff.added_count} file(s) added")
96
98
  print(f" {tree_diff.deleted_count} file(s) deleted")
97
99
  print(f" {tree_diff.modified_count} file(s) modified")
98
100
  else:
99
- print(engine.format_diff(tree_diff, args.ref1, args.ref2))
100
-
101
+ print(engine.format_diff(tree_diff, ref1, ref2))
102
+
101
103
  return 0
102
-
104
+
103
105
  # Diff working tree against a ref
104
106
  if args.ref1:
105
107
  commit_hash = repo.resolve_ref(args.ref1)
106
108
  if not commit_hash:
107
109
  print(f"Error: Unknown revision: {args.ref1}")
108
110
  return 1
109
-
111
+
110
112
  # Get working directory files
111
113
  working_files = {}
112
114
  for root, dirs, files in os.walk(repo.current_dir):
113
- dirs[:] = [d for d in dirs if not d.startswith('.')]
115
+ dirs[:] = [d for d in dirs if not d.startswith(".")]
114
116
  for filename in files:
115
117
  full_path = Path(root) / filename
116
118
  rel_path = str(full_path.relative_to(repo.current_dir))
117
119
  working_files[rel_path] = full_path.read_bytes()
118
-
120
+
119
121
  tree_diff = engine.diff_working_dir(commit_hash, working_files)
120
-
122
+
121
123
  if args.stat:
122
124
  print(f" {tree_diff.added_count} file(s) added")
123
125
  print(f" {tree_diff.deleted_count} file(s) deleted")
124
126
  print(f" {tree_diff.modified_count} file(s) modified")
125
127
  else:
126
- print(engine.format_diff(tree_diff, args.ref1, 'working'))
127
-
128
+ print(engine.format_diff(tree_diff, args.ref1, "working"))
129
+
128
130
  return 0
129
-
131
+
130
132
  # Default: diff working tree against HEAD
131
133
  head_commit = repo.get_head_commit()
132
134
  if not head_commit:
133
135
  print("No commits yet. Nothing to diff.")
134
136
  return 0
135
-
137
+
136
138
  # Get working directory files
137
139
  working_files = {}
138
140
  for root, dirs, files in os.walk(repo.current_dir):
139
- dirs[:] = [d for d in dirs if not d.startswith('.')]
141
+ dirs[:] = [d for d in dirs if not d.startswith(".")]
140
142
  for filename in files:
141
143
  full_path = Path(root) / filename
142
144
  rel_path = str(full_path.relative_to(repo.current_dir))
143
145
  working_files[rel_path] = full_path.read_bytes()
144
-
145
- tree_diff = engine.diff_working_dir(
146
- head_commit.store(repo.object_store),
147
- working_files
148
- )
149
-
146
+
147
+ tree_diff = engine.diff_working_dir(head_commit.store(repo.object_store), working_files)
148
+
150
149
  if args.stat:
151
150
  print(f" {tree_diff.added_count} file(s) added")
152
151
  print(f" {tree_diff.deleted_count} file(s) deleted")
153
152
  print(f" {tree_diff.modified_count} file(s) modified")
154
153
  else:
155
- print(engine.format_diff(tree_diff, 'HEAD', 'working'))
156
-
154
+ print(engine.format_diff(tree_diff, "HEAD", "working"))
155
+
157
156
  return 0