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/add.py CHANGED
@@ -10,84 +10,82 @@ from ..core.repository import Repository
10
10
 
11
11
 
12
12
  # Default allowed file extensions for memory files
13
- DEFAULT_ALLOWED_EXTENSIONS = {'.md', '.txt', '.json', '.yaml', '.yml'}
13
+ DEFAULT_ALLOWED_EXTENSIONS = {".md", ".txt", ".json", ".yaml", ".yml"}
14
14
 
15
15
  # Binary file signatures (magic bytes) to detect binary files
16
16
  BINARY_SIGNATURES = [
17
- b'\x89PNG', # PNG
18
- b'\xff\xd8\xff', # JPEG
19
- b'GIF8', # GIF
20
- b'%PDF', # PDF
21
- b'PK\x03\x04', # ZIP
22
- b'\x1f\x8b', # GZIP
23
- b'BM', # BMP
24
- b'\x00\x00\x01\x00', # ICO
25
- b'RIFF', # WAV, AVI, etc.
17
+ b"\x89PNG", # PNG
18
+ b"\xff\xd8\xff", # JPEG
19
+ b"GIF8", # GIF
20
+ b"%PDF", # PDF
21
+ b"PK\x03\x04", # ZIP
22
+ b"\x1f\x8b", # GZIP
23
+ b"BM", # BMP
24
+ b"\x00\x00\x01\x00", # ICO
25
+ b"RIFF", # WAV, AVI, etc.
26
26
  ]
27
27
 
28
28
 
29
29
  class AddCommand:
30
30
  """Add files to the staging area."""
31
-
32
- name = 'add'
33
- help = 'Add memory files to staging area'
34
-
31
+
32
+ name = "add"
33
+ help = "Add memory files to staging area"
34
+
35
35
  @staticmethod
36
36
  def add_arguments(parser: argparse.ArgumentParser):
37
+ parser.add_argument("paths", nargs="+", help="Files or directories to stage")
37
38
  parser.add_argument(
38
- 'paths',
39
- nargs='+',
40
- help='Files or directories to stage'
41
- )
42
- parser.add_argument(
43
- '--all', '-A',
44
- action='store_true',
45
- help='Stage all changes (including modifications and deletions)'
39
+ "--all",
40
+ "-A",
41
+ action="store_true",
42
+ help="Stage all changes (including modifications and deletions)",
46
43
  )
47
44
  parser.add_argument(
48
- '--force', '-f',
49
- action='store_true',
50
- help='Force add even if file type is not recommended'
45
+ "--force",
46
+ "-f",
47
+ action="store_true",
48
+ help="Force add even if file type is not recommended",
51
49
  )
52
50
  parser.add_argument(
53
- '--allow-binary',
54
- action='store_true',
55
- help='Allow staging binary files (not recommended)'
51
+ "--allow-binary",
52
+ action="store_true",
53
+ help="Allow staging binary files (not recommended)",
56
54
  )
57
-
55
+
58
56
  @staticmethod
59
57
  def _is_binary_file(filepath: Path) -> bool:
60
58
  """Check if a file is binary by looking at magic bytes."""
61
59
  try:
62
- with open(filepath, 'rb') as f:
60
+ with open(filepath, "rb") as f:
63
61
  header = f.read(16)
64
-
62
+
65
63
  for signature in BINARY_SIGNATURES:
66
64
  if header.startswith(signature):
67
65
  return True
68
-
66
+
69
67
  # Also check for null bytes (common in binary files)
70
- if b'\x00' in header:
68
+ if b"\x00" in header:
71
69
  return True
72
-
70
+
73
71
  return False
74
72
  except Exception:
75
73
  return False
76
-
74
+
77
75
  @staticmethod
78
76
  def _is_allowed_extension(filepath: Path, config: dict) -> bool:
79
77
  """Check if file extension is in allowed list."""
80
- allowed = config.get('allowed_extensions', list(DEFAULT_ALLOWED_EXTENSIONS))
78
+ allowed = config.get("allowed_extensions", list(DEFAULT_ALLOWED_EXTENSIONS))
81
79
  allowed_set = set(allowed)
82
-
80
+
83
81
  ext = filepath.suffix.lower()
84
82
  return ext in allowed_set or not ext # Allow files without extension
85
-
83
+
86
84
  @staticmethod
87
85
  def _validate_file(filepath: Path, config: dict, force: bool, allow_binary: bool) -> tuple:
88
86
  """
89
87
  Validate a file for staging.
90
-
88
+
91
89
  Returns:
92
90
  Tuple of (is_valid, warning_message)
93
91
  """
@@ -96,13 +94,16 @@ class AddCommand:
96
94
  if allow_binary:
97
95
  return True, f"Warning: {filepath} appears to be binary"
98
96
  else:
99
- return False, f"Rejected: {filepath} is a binary file. Use --allow-binary to override."
100
-
97
+ return (
98
+ False,
99
+ f"Rejected: {filepath} is a binary file. Use --allow-binary to override.",
100
+ )
101
+
101
102
  # Check extension
102
103
  if not AddCommand._is_allowed_extension(filepath, config):
103
- ext = filepath.suffix or '(no extension)'
104
- allowed = config.get('allowed_extensions', list(DEFAULT_ALLOWED_EXTENSIONS))
105
-
104
+ ext = filepath.suffix or "(no extension)"
105
+ allowed = config.get("allowed_extensions", list(DEFAULT_ALLOWED_EXTENSIONS))
106
+
106
107
  if force:
107
108
  return True, f"Warning: {filepath} has extension '{ext}' which may not be optimal"
108
109
  else:
@@ -111,9 +112,9 @@ class AddCommand:
111
112
  f" Recommended: {', '.join(sorted(allowed))}\n"
112
113
  f" Use --force to override."
113
114
  )
114
-
115
+
115
116
  return True, None
116
-
117
+
117
118
  @staticmethod
118
119
  def execute(args) -> int:
119
120
  repo, code = require_repo()
@@ -123,19 +124,19 @@ class AddCommand:
123
124
  staged_count = 0
124
125
  rejected_count = 0
125
126
  config = repo.get_config()
126
-
127
+
127
128
  for path_str in args.paths:
128
129
  path = Path(path_str)
129
-
130
+
130
131
  # Handle '.' to stage all
131
- if path_str == '.':
132
+ if path_str == ".":
132
133
  staged, rejected = AddCommand._stage_directory_with_validation(
133
134
  repo, None, config, args.force, args.allow_binary
134
135
  )
135
136
  staged_count += staged
136
137
  rejected_count += rejected
137
138
  continue
138
-
139
+
139
140
  # Resolve path relative to current/
140
141
  if path.is_absolute():
141
142
  try:
@@ -156,85 +157,91 @@ class AddCommand:
156
157
  else:
157
158
  print(f"Error: Path not found: {path}")
158
159
  continue
159
-
160
+
160
161
  full_path = repo.current_dir / rel_path
161
-
162
+
162
163
  if not full_path.exists():
163
164
  print(f"Error: Path not found: {path}")
164
165
  continue
165
-
166
+
166
167
  if full_path.is_file():
167
168
  # Validate file
168
169
  is_valid, message = AddCommand._validate_file(
169
170
  full_path, config, args.force, args.allow_binary
170
171
  )
171
-
172
+
172
173
  if not is_valid:
173
174
  print(message)
174
175
  rejected_count += 1
175
176
  continue
176
-
177
+
177
178
  if message: # Warning
178
179
  print(message)
179
-
180
+
180
181
  try:
181
182
  blob_hash = repo.stage_file(str(rel_path))
182
183
  print(f" staged: {rel_path}")
183
184
  staged_count += 1
184
185
  except Exception as e:
185
186
  print(f"Error staging {rel_path}: {e}")
186
-
187
+
187
188
  elif full_path.is_dir():
188
189
  staged, rejected = AddCommand._stage_directory_with_validation(
189
190
  repo, str(rel_path), config, args.force, args.allow_binary
190
191
  )
191
192
  staged_count += staged
192
193
  rejected_count += rejected
193
-
194
+
194
195
  if staged_count > 0 or rejected_count > 0:
195
196
  print(f"\nStaged {staged_count} file(s)")
196
197
  if rejected_count > 0:
197
198
  print(f"Rejected {rejected_count} file(s) - use --force to override")
198
199
  if staged_count > 0:
199
200
  print("Run 'agmem commit -m \"message\"' to save snapshot")
201
+ try:
202
+ from ..core.audit import append_audit
203
+
204
+ append_audit(repo.mem_dir, "add", {"staged_count": staged_count})
205
+ except Exception:
206
+ pass
200
207
  else:
201
208
  print("No files staged")
202
-
209
+
203
210
  return 0
204
-
211
+
205
212
  @staticmethod
206
- def _stage_directory_with_validation(repo, subdir: str, config: dict, force: bool, allow_binary: bool) -> tuple:
213
+ def _stage_directory_with_validation(
214
+ repo, subdir: str, config: dict, force: bool, allow_binary: bool
215
+ ) -> tuple:
207
216
  """
208
217
  Stage a directory with file validation.
209
-
218
+
210
219
  Returns:
211
220
  Tuple of (staged_count, rejected_count)
212
221
  """
213
222
  staged_count = 0
214
223
  rejected_count = 0
215
-
224
+
216
225
  if subdir:
217
226
  dir_path = repo.current_dir / subdir
218
227
  else:
219
228
  dir_path = repo.current_dir
220
-
229
+
221
230
  if not dir_path.exists():
222
231
  return 0, 0
223
-
224
- for file_path in dir_path.rglob('*'):
232
+
233
+ for file_path in dir_path.rglob("*"):
225
234
  if not file_path.is_file():
226
235
  continue
227
-
236
+
228
237
  # Skip hidden files and .mem directory
229
238
  rel_to_current = file_path.relative_to(repo.current_dir)
230
- if any(part.startswith('.') for part in rel_to_current.parts):
239
+ if any(part.startswith(".") for part in rel_to_current.parts):
231
240
  continue
232
-
241
+
233
242
  # Validate file
234
- is_valid, message = AddCommand._validate_file(
235
- file_path, config, force, allow_binary
236
- )
237
-
243
+ is_valid, message = AddCommand._validate_file(file_path, config, force, allow_binary)
244
+
238
245
  if not is_valid:
239
246
  if not force:
240
247
  # Only print first few rejections to avoid spam
@@ -244,15 +251,15 @@ class AddCommand:
244
251
  print(" ... (more files rejected)")
245
252
  rejected_count += 1
246
253
  continue
247
-
254
+
248
255
  if message: # Warning
249
256
  print(message)
250
-
257
+
251
258
  try:
252
259
  repo.stage_file(str(rel_to_current))
253
260
  print(f" staged: {rel_to_current}")
254
261
  staged_count += 1
255
262
  except Exception as e:
256
263
  print(f"Error staging {rel_to_current}: {e}")
257
-
264
+
258
265
  return staged_count, rejected_count
@@ -0,0 +1,59 @@
1
+ """
2
+ agmem audit - Tamper-evident audit trail.
3
+
4
+ Read and verify the append-only audit log.
5
+ """
6
+
7
+ import argparse
8
+
9
+ from ..commands.base import require_repo
10
+ from ..core.audit import read_audit, verify_audit
11
+
12
+
13
+ class AuditCommand:
14
+ """Show and verify the tamper-evident audit log."""
15
+
16
+ name = "audit"
17
+ help = "Show and verify the tamper-evident audit log"
18
+
19
+ @staticmethod
20
+ def add_arguments(parser: argparse.ArgumentParser):
21
+ parser.add_argument(
22
+ "-n",
23
+ "--max",
24
+ type=int,
25
+ default=50,
26
+ metavar="N",
27
+ help="Show at most N entries (default 50)",
28
+ )
29
+ parser.add_argument(
30
+ "--verify",
31
+ action="store_true",
32
+ help="Verify the audit chain and report first tampering point",
33
+ )
34
+
35
+ @staticmethod
36
+ def execute(args) -> int:
37
+ repo, code = require_repo()
38
+ if code != 0:
39
+ return code
40
+
41
+ if args.verify:
42
+ valid, first_bad = verify_audit(repo.mem_dir)
43
+ if valid:
44
+ print("Audit log chain is valid.")
45
+ return 0
46
+ print(f"Audit log chain invalid at entry index {first_bad}.")
47
+ return 1
48
+
49
+ entries = read_audit(repo.mem_dir, max_entries=args.max)
50
+ if not entries:
51
+ print("No audit entries.")
52
+ return 0
53
+ for e in entries:
54
+ ts = e.get("timestamp", "")
55
+ op = e.get("operation", "")
56
+ details = e.get("details", {})
57
+ detail_str = " ".join(f"{k}={v}" for k, v in sorted(details.items()) if v is not None)
58
+ print(f"{ts} {op} {detail_str}")
59
+ return 0
memvcs/commands/blame.py CHANGED
@@ -12,53 +12,46 @@ from ..core.objects import Commit, Tree, Blob
12
12
 
13
13
  class BlameCommand:
14
14
  """Show author and commit for each line of a file, or trace semantic facts."""
15
-
16
- name = 'blame'
17
- help = 'Show who changed each line of a memory file, or trace semantic facts'
18
-
15
+
16
+ name = "blame"
17
+ help = "Show who changed each line of a memory file, or trace semantic facts"
18
+
19
19
  @staticmethod
20
20
  def add_arguments(parser: argparse.ArgumentParser):
21
+ parser.add_argument("file", nargs="?", help="File to blame (path relative to current/)")
21
22
  parser.add_argument(
22
- 'file',
23
- nargs='?',
24
- help='File to blame (path relative to current/)'
25
- )
26
- parser.add_argument(
27
- 'ref',
28
- nargs='?',
29
- default='HEAD',
30
- help='Commit to blame at (default: HEAD)'
23
+ "ref", nargs="?", default="HEAD", help="Commit to blame at (default: HEAD)"
31
24
  )
32
25
  parser.add_argument(
33
- '--query', '-q',
34
- help='Semantic query to trace (e.g., "Why does agent think X?")'
26
+ "--query", "-q", help='Semantic query to trace (e.g., "Why does agent think X?")'
35
27
  )
36
28
  parser.add_argument(
37
- '--limit', '-n',
29
+ "--limit",
30
+ "-n",
38
31
  type=int,
39
32
  default=5,
40
- help='Number of results to show for semantic blame (default: 5)'
33
+ help="Number of results to show for semantic blame (default: 5)",
41
34
  )
42
-
35
+
43
36
  @staticmethod
44
37
  def execute(args) -> int:
45
38
  repo, code = require_repo()
46
39
  if code != 0:
47
40
  return code
48
-
41
+
49
42
  # Semantic blame mode
50
43
  if args.query:
51
44
  return BlameCommand._semantic_blame(repo, args.query, args.limit)
52
-
45
+
53
46
  # File blame mode
54
47
  if not args.file:
55
48
  print("Error: Either --query or a file path is required.")
56
49
  print("Usage: agmem blame <file> [ref]")
57
- print(" agmem blame --query \"Why does agent think X?\"")
50
+ print(' agmem blame --query "Why does agent think X?"')
58
51
  return 1
59
-
52
+
60
53
  return BlameCommand._file_blame(repo, args.file, args.ref)
61
-
54
+
62
55
  @staticmethod
63
56
  def _file_blame(repo, filepath: str, ref: str) -> int:
64
57
  """Traditional file-based blame."""
@@ -66,45 +59,45 @@ class BlameCommand:
66
59
  if not commit_hash:
67
60
  print(f"Error: Unknown revision: {ref}")
68
61
  return 1
69
-
62
+
70
63
  # Get file content at commit
71
64
  tree = repo.get_commit_tree(commit_hash)
72
65
  if not tree:
73
66
  print("Error: Could not load tree.")
74
67
  return 1
75
-
68
+
76
69
  # Find file in tree (support path like semantic/user-prefs.md)
77
70
  blob_hash = None
78
71
  for entry in tree.entries:
79
- path = entry.path + '/' + entry.name if entry.path else entry.name
72
+ path = entry.path + "/" + entry.name if entry.path else entry.name
80
73
  if path == filepath:
81
74
  blob_hash = entry.hash
82
75
  break
83
-
76
+
84
77
  if not blob_hash:
85
78
  print(f"Error: File not found in {ref}: {filepath}")
86
79
  return 1
87
-
80
+
88
81
  blob = Blob.load(repo.object_store, blob_hash)
89
82
  if not blob:
90
83
  print("Error: Could not load file content.")
91
84
  return 1
92
-
93
- lines = blob.content.decode('utf-8', errors='replace').splitlines()
85
+
86
+ lines = blob.content.decode("utf-8", errors="replace").splitlines()
94
87
  commit = Commit.load(repo.object_store, commit_hash)
95
- author_short = commit.author.split('<')[0].strip()[:20] if commit else 'unknown'
88
+ author_short = commit.author.split("<")[0].strip()[:20] if commit else "unknown"
96
89
  hash_short = commit_hash[:8]
97
-
90
+
98
91
  for i, line in enumerate(lines, 1):
99
92
  print(f"{hash_short} ({author_short:20} {i:4}) {line}")
100
-
93
+
101
94
  return 0
102
-
95
+
103
96
  @staticmethod
104
97
  def _semantic_blame(repo, query: str, limit: int) -> int:
105
98
  """
106
99
  Semantic blame - trace which commit introduced a fact.
107
-
100
+
108
101
  Searches the vector store and shows provenance for matching chunks.
109
102
  """
110
103
  try:
@@ -113,34 +106,34 @@ class BlameCommand:
113
106
  print("Error: Vector search requires sqlite-vec.")
114
107
  print("Install with: pip install agmem[vector]")
115
108
  return 1
116
-
109
+
117
110
  try:
118
- vs = VectorStore(repo.root / '.mem')
111
+ vs = VectorStore(repo.root / ".mem")
119
112
  results = vs.search_with_provenance(query, limit=limit)
120
113
  except Exception as e:
121
114
  print(f"Error: Vector search failed: {e}")
122
115
  print("Try running 'agmem search --rebuild' to rebuild the index.")
123
116
  return 1
124
-
117
+
125
118
  if not results:
126
119
  print("No matching facts found in memory.")
127
120
  print("Try rebuilding the index with 'agmem search --rebuild'")
128
121
  return 0
129
-
130
- print(f"Semantic blame for: \"{query}\"")
122
+
123
+ print(f'Semantic blame for: "{query}"')
131
124
  print("=" * 60)
132
-
125
+
133
126
  for i, result in enumerate(results, 1):
134
- path = result['path']
135
- content = result['content']
136
- similarity = result['similarity']
137
- commit_hash = result['commit_hash']
138
- author = result['author']
139
- indexed_at = result['indexed_at']
140
-
127
+ path = result["path"]
128
+ content = result["content"]
129
+ similarity = result["similarity"]
130
+ commit_hash = result["commit_hash"]
131
+ author = result["author"]
132
+ indexed_at = result["indexed_at"]
133
+
141
134
  print(f"\n[{i}] {path}")
142
135
  print(f" Similarity: {similarity:.2%}")
143
-
136
+
144
137
  if commit_hash:
145
138
  # Try to get commit details
146
139
  commit = Commit.load(repo.object_store, commit_hash)
@@ -157,13 +150,13 @@ class BlameCommand:
157
150
  print(" Commit: (not tracked)")
158
151
  if indexed_at:
159
152
  print(f" Indexed: {indexed_at}")
160
-
153
+
161
154
  # Show content preview
162
155
  print(f"\n Content preview:")
163
- for line in content.split('\n')[:5]:
156
+ for line in content.split("\n")[:5]:
164
157
  print(f" {line[:70]}")
165
- if len(content.split('\n')) > 5:
158
+ if len(content.split("\n")) > 5:
166
159
  print(" ...")
167
-
160
+
168
161
  print()
169
162
  return 0
memvcs/commands/branch.py CHANGED
@@ -10,43 +10,23 @@ from ..core.repository import Repository
10
10
 
11
11
  class BranchCommand:
12
12
  """Manage branches."""
13
-
14
- name = 'branch'
15
- help = 'List, create, or delete branches'
16
-
13
+
14
+ name = "branch"
15
+ help = "List, create, or delete branches"
16
+
17
17
  @staticmethod
18
18
  def add_arguments(parser: argparse.ArgumentParser):
19
+ parser.add_argument("name", nargs="?", help="Branch name to create or delete")
20
+ parser.add_argument("--delete", "-d", action="store_true", help="Delete a branch")
19
21
  parser.add_argument(
20
- 'name',
21
- nargs='?',
22
- help='Branch name to create or delete'
23
- )
24
- parser.add_argument(
25
- '--delete', '-d',
26
- action='store_true',
27
- help='Delete a branch'
22
+ "--force", "-D", action="store_true", help="Force delete a branch (even if not merged)"
28
23
  )
24
+ parser.add_argument("--list", "-l", action="store_true", help="List all branches")
29
25
  parser.add_argument(
30
- '--force', '-D',
31
- action='store_true',
32
- help='Force delete a branch (even if not merged)'
26
+ "--all", "-a", action="store_true", help="List all branches including remote"
33
27
  )
34
- parser.add_argument(
35
- '--list', '-l',
36
- action='store_true',
37
- help='List all branches'
38
- )
39
- parser.add_argument(
40
- '--all', '-a',
41
- action='store_true',
42
- help='List all branches including remote'
43
- )
44
- parser.add_argument(
45
- 'start_point',
46
- nargs='?',
47
- help='Commit to start the new branch from'
48
- )
49
-
28
+ parser.add_argument("start_point", nargs="?", help="Commit to start the new branch from")
29
+
50
30
  @staticmethod
51
31
  def execute(args) -> int:
52
32
  repo, code = require_repo()
@@ -60,13 +40,13 @@ class BranchCommand:
60
40
  if not args.name:
61
41
  print("Error: Branch name required for deletion")
62
42
  return 1
63
-
43
+
64
44
  current = repo.refs.get_current_branch()
65
45
  if args.name == current:
66
46
  print(f"Error: Cannot delete current branch '{args.name}'")
67
47
  print("Switch to another branch first.")
68
48
  return 1
69
-
49
+
70
50
  if repo.refs.delete_branch(args.name):
71
51
  print(f"Deleted branch {args.name}")
72
52
  return 0