agmem 0.1.1__py3-none-any.whl → 0.1.2__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.
- {agmem-0.1.1.dist-info → agmem-0.1.2.dist-info}/METADATA +20 -3
- agmem-0.1.2.dist-info/RECORD +86 -0
- memvcs/__init__.py +1 -1
- memvcs/cli.py +35 -31
- memvcs/commands/__init__.py +9 -9
- memvcs/commands/add.py +77 -76
- memvcs/commands/blame.py +46 -53
- memvcs/commands/branch.py +13 -33
- memvcs/commands/checkout.py +27 -32
- memvcs/commands/clean.py +18 -23
- memvcs/commands/clone.py +4 -1
- memvcs/commands/commit.py +40 -39
- memvcs/commands/daemon.py +81 -76
- memvcs/commands/decay.py +77 -0
- memvcs/commands/diff.py +56 -57
- memvcs/commands/distill.py +74 -0
- memvcs/commands/fsck.py +55 -61
- memvcs/commands/garden.py +28 -37
- memvcs/commands/graph.py +41 -48
- memvcs/commands/init.py +16 -24
- memvcs/commands/log.py +25 -40
- memvcs/commands/merge.py +16 -28
- memvcs/commands/pack.py +129 -0
- memvcs/commands/pull.py +4 -1
- memvcs/commands/push.py +4 -2
- memvcs/commands/recall.py +145 -0
- memvcs/commands/reflog.py +13 -22
- memvcs/commands/remote.py +1 -0
- memvcs/commands/repair.py +66 -0
- memvcs/commands/reset.py +23 -33
- memvcs/commands/resurrect.py +82 -0
- memvcs/commands/search.py +3 -4
- memvcs/commands/serve.py +2 -1
- memvcs/commands/show.py +66 -36
- memvcs/commands/stash.py +34 -34
- memvcs/commands/status.py +27 -35
- memvcs/commands/tag.py +23 -47
- memvcs/commands/test.py +30 -44
- memvcs/commands/timeline.py +111 -0
- memvcs/commands/tree.py +26 -27
- memvcs/commands/verify.py +59 -0
- memvcs/commands/when.py +115 -0
- memvcs/core/access_index.py +167 -0
- memvcs/core/config_loader.py +3 -1
- memvcs/core/consistency.py +214 -0
- memvcs/core/decay.py +185 -0
- memvcs/core/diff.py +158 -143
- memvcs/core/distiller.py +277 -0
- memvcs/core/gardener.py +164 -132
- memvcs/core/hooks.py +48 -14
- memvcs/core/knowledge_graph.py +134 -138
- memvcs/core/merge.py +248 -171
- memvcs/core/objects.py +95 -96
- memvcs/core/pii_scanner.py +147 -146
- memvcs/core/refs.py +132 -115
- memvcs/core/repository.py +174 -164
- memvcs/core/schema.py +155 -113
- memvcs/core/staging.py +60 -65
- memvcs/core/storage/__init__.py +20 -18
- memvcs/core/storage/base.py +74 -70
- memvcs/core/storage/gcs.py +70 -68
- memvcs/core/storage/local.py +42 -40
- memvcs/core/storage/s3.py +105 -110
- memvcs/core/temporal_index.py +112 -0
- memvcs/core/test_runner.py +101 -93
- memvcs/core/vector_store.py +41 -35
- memvcs/integrations/mcp_server.py +1 -3
- memvcs/integrations/web_ui/server.py +25 -26
- memvcs/retrieval/__init__.py +22 -0
- memvcs/retrieval/base.py +54 -0
- memvcs/retrieval/pack.py +128 -0
- memvcs/retrieval/recaller.py +105 -0
- memvcs/retrieval/strategies.py +314 -0
- memvcs/utils/__init__.py +3 -3
- memvcs/utils/helpers.py +52 -52
- agmem-0.1.1.dist-info/RECORD +0 -67
- {agmem-0.1.1.dist-info → agmem-0.1.2.dist-info}/WHEEL +0 -0
- {agmem-0.1.1.dist-info → agmem-0.1.2.dist-info}/entry_points.txt +0 -0
- {agmem-0.1.1.dist-info → agmem-0.1.2.dist-info}/licenses/LICENSE +0 -0
- {agmem-0.1.1.dist-info → agmem-0.1.2.dist-info}/top_level.txt +0 -0
memvcs/commands/reset.py
CHANGED
|
@@ -11,34 +11,25 @@ from ..core.repository import Repository
|
|
|
11
11
|
|
|
12
12
|
class ResetCommand:
|
|
13
13
|
"""Reset current HEAD to the specified state."""
|
|
14
|
-
|
|
15
|
-
name =
|
|
16
|
-
help =
|
|
17
|
-
|
|
14
|
+
|
|
15
|
+
name = "reset"
|
|
16
|
+
help = "Reset current HEAD to the specified state"
|
|
17
|
+
|
|
18
18
|
@staticmethod
|
|
19
19
|
def add_arguments(parser: argparse.ArgumentParser):
|
|
20
20
|
parser.add_argument(
|
|
21
|
-
|
|
22
|
-
nargs='?',
|
|
23
|
-
default='HEAD',
|
|
24
|
-
help='Commit to reset to (default: HEAD)'
|
|
21
|
+
"commit", nargs="?", default="HEAD", help="Commit to reset to (default: HEAD)"
|
|
25
22
|
)
|
|
26
23
|
parser.add_argument(
|
|
27
|
-
|
|
28
|
-
action='store_true',
|
|
29
|
-
help='Reset HEAD but keep staged changes'
|
|
24
|
+
"--soft", action="store_true", help="Reset HEAD but keep staged changes"
|
|
30
25
|
)
|
|
31
26
|
parser.add_argument(
|
|
32
|
-
|
|
33
|
-
action='store_true',
|
|
34
|
-
help='Reset HEAD and unstaged changes (default)'
|
|
27
|
+
"--mixed", action="store_true", help="Reset HEAD and unstaged changes (default)"
|
|
35
28
|
)
|
|
36
29
|
parser.add_argument(
|
|
37
|
-
|
|
38
|
-
action='store_true',
|
|
39
|
-
help='Reset HEAD, index, and working tree'
|
|
30
|
+
"--hard", action="store_true", help="Reset HEAD, index, and working tree"
|
|
40
31
|
)
|
|
41
|
-
|
|
32
|
+
|
|
42
33
|
@staticmethod
|
|
43
34
|
def execute(args) -> int:
|
|
44
35
|
# Find repository
|
|
@@ -46,53 +37,52 @@ class ResetCommand:
|
|
|
46
37
|
if code != 0:
|
|
47
38
|
return code
|
|
48
39
|
|
|
49
|
-
|
|
50
40
|
# Determine mode
|
|
51
41
|
if args.soft:
|
|
52
|
-
mode =
|
|
42
|
+
mode = "soft"
|
|
53
43
|
elif args.hard:
|
|
54
|
-
mode =
|
|
44
|
+
mode = "hard"
|
|
55
45
|
else:
|
|
56
|
-
mode =
|
|
57
|
-
|
|
46
|
+
mode = "mixed"
|
|
47
|
+
|
|
58
48
|
# Resolve commit
|
|
59
49
|
commit_hash = repo.resolve_ref(args.commit)
|
|
60
50
|
if not commit_hash:
|
|
61
51
|
print(f"Error: Unknown revision: {args.commit}")
|
|
62
52
|
return 1
|
|
63
|
-
|
|
53
|
+
|
|
64
54
|
# Get current branch
|
|
65
55
|
current_branch = repo.refs.get_current_branch()
|
|
66
|
-
|
|
56
|
+
|
|
67
57
|
try:
|
|
68
|
-
if mode ==
|
|
58
|
+
if mode == "soft":
|
|
69
59
|
# Just move HEAD
|
|
70
60
|
if current_branch:
|
|
71
61
|
repo.refs.set_branch_commit(current_branch, commit_hash)
|
|
72
62
|
else:
|
|
73
63
|
repo.refs.set_head_detached(commit_hash)
|
|
74
64
|
print(f"HEAD is now at {commit_hash[:8]}")
|
|
75
|
-
|
|
76
|
-
elif mode ==
|
|
65
|
+
|
|
66
|
+
elif mode == "mixed":
|
|
77
67
|
# Move HEAD and clear staging
|
|
78
68
|
if current_branch:
|
|
79
69
|
repo.refs.set_branch_commit(current_branch, commit_hash)
|
|
80
70
|
else:
|
|
81
71
|
repo.refs.set_head_detached(commit_hash)
|
|
82
|
-
|
|
72
|
+
|
|
83
73
|
# Keep staged files but mark them as unstaged
|
|
84
74
|
# (In a full implementation, we'd restore the tree state)
|
|
85
75
|
print(f"HEAD is now at {commit_hash[:8]}")
|
|
86
76
|
print("Staged changes have been unstaged.")
|
|
87
|
-
|
|
88
|
-
elif mode ==
|
|
77
|
+
|
|
78
|
+
elif mode == "hard":
|
|
89
79
|
# Move HEAD, clear staging, and restore working tree
|
|
90
80
|
repo.checkout(commit_hash, force=True)
|
|
91
81
|
print(f"HEAD is now at {commit_hash[:8]}")
|
|
92
82
|
print("Working tree has been reset.")
|
|
93
|
-
|
|
83
|
+
|
|
94
84
|
return 0
|
|
95
|
-
|
|
85
|
+
|
|
96
86
|
except Exception as e:
|
|
97
87
|
print(f"Error during reset: {e}")
|
|
98
88
|
return 1
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""
|
|
2
|
+
agmem resurrect - Restore archived (decayed) memories.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import shutil
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from ..commands.base import require_repo
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ResurrectCommand:
|
|
13
|
+
"""Restore memories from .mem/forgetting/."""
|
|
14
|
+
|
|
15
|
+
name = "resurrect"
|
|
16
|
+
help = "Restore archived (decayed) memories from forgetting/"
|
|
17
|
+
|
|
18
|
+
@staticmethod
|
|
19
|
+
def add_arguments(parser: argparse.ArgumentParser):
|
|
20
|
+
parser.add_argument(
|
|
21
|
+
"path",
|
|
22
|
+
nargs="?",
|
|
23
|
+
help="Path or pattern to restore (e.g., semantic/user-prefs.md)",
|
|
24
|
+
)
|
|
25
|
+
parser.add_argument(
|
|
26
|
+
"--list",
|
|
27
|
+
"-l",
|
|
28
|
+
action="store_true",
|
|
29
|
+
help="List archived memories",
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
@staticmethod
|
|
33
|
+
def execute(args) -> int:
|
|
34
|
+
repo, code = require_repo()
|
|
35
|
+
if code != 0:
|
|
36
|
+
return code
|
|
37
|
+
|
|
38
|
+
forgetting_dir = repo.mem_dir / "forgetting"
|
|
39
|
+
if not forgetting_dir.exists():
|
|
40
|
+
print("No forgotten memories found.")
|
|
41
|
+
return 0
|
|
42
|
+
|
|
43
|
+
if args.list:
|
|
44
|
+
for sub in sorted(forgetting_dir.iterdir()):
|
|
45
|
+
if sub.is_dir():
|
|
46
|
+
print(f"\n{sub.name}:")
|
|
47
|
+
for f in sorted(sub.glob("*")):
|
|
48
|
+
if f.is_file():
|
|
49
|
+
print(f" - {f.name}")
|
|
50
|
+
return 0
|
|
51
|
+
|
|
52
|
+
if not args.path:
|
|
53
|
+
print("Error: Path to restore is required.")
|
|
54
|
+
print("Usage: agmem resurrect <path>")
|
|
55
|
+
print(" agmem resurrect --list")
|
|
56
|
+
return 1
|
|
57
|
+
|
|
58
|
+
# Find archived file
|
|
59
|
+
pattern = args.path.replace("/", "_")
|
|
60
|
+
found = []
|
|
61
|
+
for sub in forgetting_dir.iterdir():
|
|
62
|
+
if sub.is_dir():
|
|
63
|
+
for f in sub.glob("*"):
|
|
64
|
+
if f.is_file() and (pattern in f.name or f.name == args.path):
|
|
65
|
+
found.append(f)
|
|
66
|
+
|
|
67
|
+
if not found:
|
|
68
|
+
print(f"No archived memory matching '{args.path}' found.")
|
|
69
|
+
print("Use 'agmem resurrect --list' to see available archives.")
|
|
70
|
+
return 1
|
|
71
|
+
|
|
72
|
+
for archived in found:
|
|
73
|
+
# Restore to current/
|
|
74
|
+
orig_path = (
|
|
75
|
+
archived.name.replace("_", "/", 1) if "_" in archived.name else archived.name
|
|
76
|
+
)
|
|
77
|
+
dest = repo.current_dir / orig_path
|
|
78
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
79
|
+
shutil.copy2(str(archived), str(dest))
|
|
80
|
+
print(f"Restored {archived.name} -> {orig_path}")
|
|
81
|
+
|
|
82
|
+
return 0
|
memvcs/commands/search.py
CHANGED
|
@@ -10,9 +10,7 @@ from pathlib import Path
|
|
|
10
10
|
def _is_vector_unavailable_error(exc: Exception) -> bool:
|
|
11
11
|
"""True if the exception indicates vector deps are missing (fall back to text search)."""
|
|
12
12
|
msg = str(exc).lower()
|
|
13
|
-
return any(
|
|
14
|
-
key in msg for key in ("sqlite-vec", "sentence-transformers", "vector search")
|
|
15
|
-
)
|
|
13
|
+
return any(key in msg for key in ("sqlite-vec", "sentence-transformers", "vector search"))
|
|
16
14
|
|
|
17
15
|
|
|
18
16
|
def _first_line_containing(content: str, query: str, max_len: int = 200) -> str:
|
|
@@ -38,7 +36,8 @@ class SearchCommand:
|
|
|
38
36
|
help="Search query for semantic search",
|
|
39
37
|
)
|
|
40
38
|
parser.add_argument(
|
|
41
|
-
"--limit",
|
|
39
|
+
"--limit",
|
|
40
|
+
"-n",
|
|
42
41
|
type=int,
|
|
43
42
|
default=10,
|
|
44
43
|
help="Maximum results to return (default: 10)",
|
memvcs/commands/serve.py
CHANGED
memvcs/commands/show.py
CHANGED
|
@@ -4,6 +4,7 @@ agmem show - Show various types of objects.
|
|
|
4
4
|
|
|
5
5
|
import argparse
|
|
6
6
|
import json
|
|
7
|
+
import sys
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
|
|
9
10
|
from ..commands.base import require_repo
|
|
@@ -13,28 +14,27 @@ from ..core.repository import Repository
|
|
|
13
14
|
|
|
14
15
|
class ShowCommand:
|
|
15
16
|
"""Show various types of objects."""
|
|
16
|
-
|
|
17
|
-
name =
|
|
18
|
-
help =
|
|
19
|
-
|
|
17
|
+
|
|
18
|
+
name = "show"
|
|
19
|
+
help = "Show various types of objects"
|
|
20
|
+
|
|
20
21
|
@staticmethod
|
|
21
22
|
def add_arguments(parser: argparse.ArgumentParser):
|
|
23
|
+
parser.add_argument("object", help="Object to show (commit, tree, blob, branch)")
|
|
22
24
|
parser.add_argument(
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
"--type",
|
|
26
|
+
"-t",
|
|
27
|
+
choices=["commit", "tree", "blob", "auto"],
|
|
28
|
+
default="auto",
|
|
29
|
+
help="Type of object to show",
|
|
25
30
|
)
|
|
31
|
+
parser.add_argument("--raw", action="store_true", help="Show raw object content")
|
|
26
32
|
parser.add_argument(
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
help='Type of object to show'
|
|
33
|
+
"--at",
|
|
34
|
+
metavar="TIMESTAMP",
|
|
35
|
+
help="View file at specific time (ISO 8601, e.g., 2025-12-01T14:00:00)",
|
|
31
36
|
)
|
|
32
|
-
|
|
33
|
-
'--raw',
|
|
34
|
-
action='store_true',
|
|
35
|
-
help='Show raw object content'
|
|
36
|
-
)
|
|
37
|
-
|
|
37
|
+
|
|
38
38
|
@staticmethod
|
|
39
39
|
def execute(args) -> int:
|
|
40
40
|
# Find repository
|
|
@@ -42,11 +42,41 @@ class ShowCommand:
|
|
|
42
42
|
if code != 0:
|
|
43
43
|
return code
|
|
44
44
|
|
|
45
|
-
|
|
46
45
|
obj_ref = args.object
|
|
47
46
|
obj_hash = None
|
|
48
47
|
obj_type = args.type
|
|
49
|
-
|
|
48
|
+
|
|
49
|
+
# --at: show file at timestamp
|
|
50
|
+
if args.at:
|
|
51
|
+
commit_hash = repo.resolve_ref(args.at)
|
|
52
|
+
if not commit_hash:
|
|
53
|
+
print(f"Error: No commit found at {args.at}", file=sys.stderr)
|
|
54
|
+
return 1
|
|
55
|
+
# Treat object as file path under current/
|
|
56
|
+
file_path = obj_ref.replace("current/", "").lstrip("/")
|
|
57
|
+
tree = repo.get_commit_tree(commit_hash)
|
|
58
|
+
if not tree:
|
|
59
|
+
print(
|
|
60
|
+
f"Error: Tree not found for commit at {args.at}", file=__import__("sys").stderr
|
|
61
|
+
)
|
|
62
|
+
return 1
|
|
63
|
+
# Find blob for path (tree has flat entries with path)
|
|
64
|
+
blob_hash = None
|
|
65
|
+
for entry in tree.entries:
|
|
66
|
+
full_path = (
|
|
67
|
+
(entry.path + "/" + entry.name).lstrip("/") if entry.path else entry.name
|
|
68
|
+
)
|
|
69
|
+
if full_path == file_path:
|
|
70
|
+
blob_hash = entry.hash
|
|
71
|
+
break
|
|
72
|
+
if not blob_hash:
|
|
73
|
+
print(f"Error: File {file_path} not found at {args.at}", file=sys.stderr)
|
|
74
|
+
return 1
|
|
75
|
+
blob = Blob.load(repo.object_store, blob_hash)
|
|
76
|
+
if blob:
|
|
77
|
+
print(blob.content.decode("utf-8", errors="replace"))
|
|
78
|
+
return 0
|
|
79
|
+
|
|
50
80
|
# Try to resolve as reference
|
|
51
81
|
resolved = repo.resolve_ref(obj_ref)
|
|
52
82
|
if resolved:
|
|
@@ -54,34 +84,34 @@ class ShowCommand:
|
|
|
54
84
|
else:
|
|
55
85
|
# Assume it's a hash
|
|
56
86
|
obj_hash = obj_ref
|
|
57
|
-
|
|
87
|
+
|
|
58
88
|
# Try to determine type if auto
|
|
59
|
-
if obj_type ==
|
|
89
|
+
if obj_type == "auto":
|
|
60
90
|
# Try commit first
|
|
61
91
|
commit = Commit.load(repo.object_store, obj_hash)
|
|
62
92
|
if commit:
|
|
63
|
-
obj_type =
|
|
93
|
+
obj_type = "commit"
|
|
64
94
|
else:
|
|
65
95
|
# Try tree
|
|
66
96
|
tree = Tree.load(repo.object_store, obj_hash)
|
|
67
97
|
if tree:
|
|
68
|
-
obj_type =
|
|
98
|
+
obj_type = "tree"
|
|
69
99
|
else:
|
|
70
100
|
# Try blob
|
|
71
101
|
blob = Blob.load(repo.object_store, obj_hash)
|
|
72
102
|
if blob:
|
|
73
|
-
obj_type =
|
|
103
|
+
obj_type = "blob"
|
|
74
104
|
else:
|
|
75
105
|
print(f"Error: Object not found: {obj_ref}")
|
|
76
106
|
return 1
|
|
77
|
-
|
|
107
|
+
|
|
78
108
|
# Display based on type
|
|
79
|
-
if obj_type ==
|
|
109
|
+
if obj_type == "commit":
|
|
80
110
|
commit = Commit.load(repo.object_store, obj_hash)
|
|
81
111
|
if not commit:
|
|
82
112
|
print(f"Error: Commit not found: {obj_ref}")
|
|
83
113
|
return 1
|
|
84
|
-
|
|
114
|
+
|
|
85
115
|
print(f"commit {obj_hash}")
|
|
86
116
|
print(f"Author: {commit.author}")
|
|
87
117
|
print(f"Date: {commit.timestamp}")
|
|
@@ -91,35 +121,35 @@ class ShowCommand:
|
|
|
91
121
|
print(f"tree {commit.tree}")
|
|
92
122
|
if commit.parents:
|
|
93
123
|
print(f"parent {' '.join(commit.parents)}")
|
|
94
|
-
|
|
95
|
-
elif obj_type ==
|
|
124
|
+
|
|
125
|
+
elif obj_type == "tree":
|
|
96
126
|
tree = Tree.load(repo.object_store, obj_hash)
|
|
97
127
|
if not tree:
|
|
98
128
|
print(f"Error: Tree not found: {obj_ref}")
|
|
99
129
|
return 1
|
|
100
|
-
|
|
130
|
+
|
|
101
131
|
print(f"tree {obj_hash}")
|
|
102
132
|
print()
|
|
103
133
|
for entry in sorted(tree.entries, key=lambda e: e.name):
|
|
104
|
-
path = entry.path +
|
|
134
|
+
path = entry.path + "/" + entry.name if entry.path else entry.name
|
|
105
135
|
print(f"{entry.mode} {entry.type} {entry.hash[:8]}\t{path}")
|
|
106
|
-
|
|
107
|
-
elif obj_type ==
|
|
136
|
+
|
|
137
|
+
elif obj_type == "blob":
|
|
108
138
|
blob = Blob.load(repo.object_store, obj_hash)
|
|
109
139
|
if not blob:
|
|
110
140
|
print(f"Error: Blob not found: {obj_ref}")
|
|
111
141
|
return 1
|
|
112
|
-
|
|
142
|
+
|
|
113
143
|
if args.raw:
|
|
114
|
-
print(blob.content.decode(
|
|
144
|
+
print(blob.content.decode("utf-8", errors="replace"))
|
|
115
145
|
else:
|
|
116
146
|
print(f"blob {obj_hash}")
|
|
117
147
|
print(f"Size: {len(blob.content)} bytes")
|
|
118
148
|
print()
|
|
119
|
-
content = blob.content.decode(
|
|
149
|
+
content = blob.content.decode("utf-8", errors="replace")
|
|
120
150
|
if len(content) > 1000:
|
|
121
151
|
print(content[:1000] + "\n... (truncated)")
|
|
122
152
|
else:
|
|
123
153
|
print(content)
|
|
124
|
-
|
|
154
|
+
|
|
125
155
|
return 0
|
memvcs/commands/stash.py
CHANGED
|
@@ -11,52 +11,51 @@ from ..core.repository import Repository
|
|
|
11
11
|
|
|
12
12
|
class StashCommand:
|
|
13
13
|
"""Stash working directory changes."""
|
|
14
|
-
|
|
15
|
-
name =
|
|
16
|
-
help =
|
|
17
|
-
|
|
14
|
+
|
|
15
|
+
name = "stash"
|
|
16
|
+
help = "Stash changes for later (save work in progress)"
|
|
17
|
+
|
|
18
18
|
@staticmethod
|
|
19
19
|
def add_arguments(parser: argparse.ArgumentParser):
|
|
20
|
-
subparsers = parser.add_subparsers(dest=
|
|
21
|
-
|
|
20
|
+
subparsers = parser.add_subparsers(dest="stash_action", help="Stash action")
|
|
21
|
+
|
|
22
22
|
# Default: list (when no subcommand given)
|
|
23
|
-
parser.set_defaults(stash_action=
|
|
24
|
-
|
|
23
|
+
parser.set_defaults(stash_action="list")
|
|
24
|
+
|
|
25
25
|
# stash (push)
|
|
26
|
-
push_p = subparsers.add_parser(
|
|
27
|
-
push_p.add_argument(
|
|
28
|
-
|
|
26
|
+
push_p = subparsers.add_parser("push", help="Stash current changes")
|
|
27
|
+
push_p.add_argument("-m", "--message", default="", help="Stash message")
|
|
28
|
+
|
|
29
29
|
# stash pop
|
|
30
|
-
subparsers.add_parser(
|
|
31
|
-
|
|
30
|
+
subparsers.add_parser("pop", help="Apply and remove most recent stash")
|
|
31
|
+
|
|
32
32
|
# stash list (default)
|
|
33
|
-
subparsers.add_parser(
|
|
34
|
-
|
|
33
|
+
subparsers.add_parser("list", help="List stashes")
|
|
34
|
+
|
|
35
35
|
# stash apply
|
|
36
|
-
apply_p = subparsers.add_parser(
|
|
37
|
-
apply_p.add_argument(
|
|
38
|
-
|
|
36
|
+
apply_p = subparsers.add_parser("apply", help="Apply stash without removing")
|
|
37
|
+
apply_p.add_argument("stash_ref", nargs="?", default="stash@{0}", help="Stash reference")
|
|
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
|
-
|
|
46
|
-
action = getattr(args, 'stash_action', None)
|
|
45
|
+
action = getattr(args, "stash_action", None)
|
|
47
46
|
if action is None:
|
|
48
|
-
action =
|
|
49
|
-
|
|
50
|
-
if action ==
|
|
51
|
-
stash_hash = repo.stash_create(getattr(args,
|
|
47
|
+
action = "list"
|
|
48
|
+
|
|
49
|
+
if action == "push":
|
|
50
|
+
stash_hash = repo.stash_create(getattr(args, "message", "") or "")
|
|
52
51
|
if stash_hash:
|
|
53
52
|
print(f"Stashed changes (stash@{0})")
|
|
54
53
|
return 0
|
|
55
54
|
else:
|
|
56
55
|
print("No local changes to stash.")
|
|
57
56
|
return 0
|
|
58
|
-
|
|
59
|
-
elif action ==
|
|
57
|
+
|
|
58
|
+
elif action == "pop":
|
|
60
59
|
stash_hash = repo.stash_pop(0)
|
|
61
60
|
if stash_hash:
|
|
62
61
|
print(f"Restored stashed changes")
|
|
@@ -64,25 +63,26 @@ class StashCommand:
|
|
|
64
63
|
else:
|
|
65
64
|
print("No stash entries found.")
|
|
66
65
|
return 1
|
|
67
|
-
|
|
68
|
-
elif action ==
|
|
66
|
+
|
|
67
|
+
elif action == "list":
|
|
69
68
|
stashes = repo.refs.stash_list()
|
|
70
69
|
if not stashes:
|
|
71
70
|
print("No stash entries found.")
|
|
72
71
|
return 0
|
|
73
72
|
for i, s in enumerate(stashes):
|
|
74
|
-
msg = s.get(
|
|
75
|
-
h = s.get(
|
|
73
|
+
msg = s.get("message", "WIP")
|
|
74
|
+
h = s.get("hash", "")[:8]
|
|
76
75
|
print(f"stash@{{{i}}}: {h} {msg}")
|
|
77
76
|
return 0
|
|
78
|
-
|
|
79
|
-
elif action ==
|
|
80
|
-
ref = getattr(args,
|
|
77
|
+
|
|
78
|
+
elif action == "apply":
|
|
79
|
+
ref = getattr(args, "stash_ref", "stash@{0}")
|
|
81
80
|
commit_hash = repo.resolve_ref(ref)
|
|
82
81
|
if not commit_hash:
|
|
83
82
|
print(f"Error: Stash not found: {ref}")
|
|
84
83
|
return 1
|
|
85
84
|
from ..core.objects import Tree, Blob
|
|
85
|
+
|
|
86
86
|
tree = repo.get_commit_tree(commit_hash)
|
|
87
87
|
if tree:
|
|
88
88
|
for entry in tree.entries:
|
|
@@ -93,5 +93,5 @@ class StashCommand:
|
|
|
93
93
|
fp.write_bytes(blob.content)
|
|
94
94
|
print("Applied stash (changes in working directory)")
|
|
95
95
|
return 0
|
|
96
|
-
|
|
96
|
+
|
|
97
97
|
return 1
|
memvcs/commands/status.py
CHANGED
|
@@ -10,23 +10,15 @@ from ..core.repository import Repository
|
|
|
10
10
|
|
|
11
11
|
class StatusCommand:
|
|
12
12
|
"""Show the working tree status."""
|
|
13
|
-
|
|
14
|
-
name =
|
|
15
|
-
help =
|
|
16
|
-
|
|
13
|
+
|
|
14
|
+
name = "status"
|
|
15
|
+
help = "Show the working tree status"
|
|
16
|
+
|
|
17
17
|
@staticmethod
|
|
18
18
|
def add_arguments(parser: argparse.ArgumentParser):
|
|
19
|
-
parser.add_argument(
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
help='Show short format'
|
|
23
|
-
)
|
|
24
|
-
parser.add_argument(
|
|
25
|
-
'--branch', '-b',
|
|
26
|
-
action='store_true',
|
|
27
|
-
help='Show branch information'
|
|
28
|
-
)
|
|
29
|
-
|
|
19
|
+
parser.add_argument("--short", "-s", action="store_true", help="Show short format")
|
|
20
|
+
parser.add_argument("--branch", "-b", action="store_true", help="Show branch information")
|
|
21
|
+
|
|
30
22
|
@staticmethod
|
|
31
23
|
def execute(args) -> int:
|
|
32
24
|
repo, code = require_repo()
|
|
@@ -34,35 +26,35 @@ class StatusCommand:
|
|
|
34
26
|
return code
|
|
35
27
|
|
|
36
28
|
status = repo.get_status()
|
|
37
|
-
|
|
29
|
+
|
|
38
30
|
# Show branch info
|
|
39
|
-
branch = status.get(
|
|
40
|
-
head = status.get(
|
|
41
|
-
|
|
31
|
+
branch = status.get("branch")
|
|
32
|
+
head = status.get("head", {})
|
|
33
|
+
|
|
42
34
|
if args.short:
|
|
43
35
|
# Short format: XY filename
|
|
44
|
-
for f in status.get(
|
|
36
|
+
for f in status.get("staged", []):
|
|
45
37
|
print(f"A {f}")
|
|
46
|
-
for f in status.get(
|
|
38
|
+
for f in status.get("modified", []):
|
|
47
39
|
print(f" M {f}")
|
|
48
|
-
for f in status.get(
|
|
40
|
+
for f in status.get("deleted", []):
|
|
49
41
|
print(f" D {f}")
|
|
50
|
-
for f in status.get(
|
|
42
|
+
for f in status.get("untracked", []):
|
|
51
43
|
print(f"?? {f}")
|
|
52
44
|
else:
|
|
53
45
|
# Long format
|
|
54
46
|
if branch:
|
|
55
47
|
print(f"On branch {branch}")
|
|
56
|
-
elif head.get(
|
|
48
|
+
elif head.get("type") == "commit":
|
|
57
49
|
print(f"HEAD detached at {head['value'][:8]}")
|
|
58
|
-
|
|
50
|
+
|
|
59
51
|
# Check for commits
|
|
60
52
|
head_commit = repo.get_head_commit()
|
|
61
53
|
if not head_commit:
|
|
62
54
|
print("\nNo commits yet")
|
|
63
|
-
|
|
55
|
+
|
|
64
56
|
# Staged changes
|
|
65
|
-
staged = status.get(
|
|
57
|
+
staged = status.get("staged", [])
|
|
66
58
|
if staged:
|
|
67
59
|
print(f"\nChanges to be committed:")
|
|
68
60
|
print(f' (use "agmem reset HEAD <file>..." to unstage)')
|
|
@@ -70,9 +62,9 @@ class StatusCommand:
|
|
|
70
62
|
for f in staged:
|
|
71
63
|
print(f" new file: {f}")
|
|
72
64
|
print()
|
|
73
|
-
|
|
65
|
+
|
|
74
66
|
# Modified but not staged
|
|
75
|
-
modified = status.get(
|
|
67
|
+
modified = status.get("modified", [])
|
|
76
68
|
if modified:
|
|
77
69
|
print(f"Changes not staged for commit:")
|
|
78
70
|
print(f' (use "agmem add <file>..." to update what will be committed)')
|
|
@@ -80,9 +72,9 @@ class StatusCommand:
|
|
|
80
72
|
for f in modified:
|
|
81
73
|
print(f" modified: {f}")
|
|
82
74
|
print()
|
|
83
|
-
|
|
75
|
+
|
|
84
76
|
# Deleted but not staged
|
|
85
|
-
deleted = status.get(
|
|
77
|
+
deleted = status.get("deleted", [])
|
|
86
78
|
if deleted:
|
|
87
79
|
print(f"Deleted files:")
|
|
88
80
|
print(f' (use "agmem add <file>..." to stage deletion)')
|
|
@@ -90,9 +82,9 @@ class StatusCommand:
|
|
|
90
82
|
for f in deleted:
|
|
91
83
|
print(f" deleted: {f}")
|
|
92
84
|
print()
|
|
93
|
-
|
|
85
|
+
|
|
94
86
|
# Untracked files
|
|
95
|
-
untracked = status.get(
|
|
87
|
+
untracked = status.get("untracked", [])
|
|
96
88
|
if untracked:
|
|
97
89
|
print(f"Untracked files:")
|
|
98
90
|
print(f' (use "agmem add <file>..." to include in what will be committed)')
|
|
@@ -100,7 +92,7 @@ class StatusCommand:
|
|
|
100
92
|
for f in untracked:
|
|
101
93
|
print(f" {f}")
|
|
102
94
|
print()
|
|
103
|
-
|
|
95
|
+
|
|
104
96
|
# Summary
|
|
105
97
|
total_changes = len(staged) + len(modified) + len(deleted) + len(untracked)
|
|
106
98
|
if total_changes == 0:
|
|
@@ -108,5 +100,5 @@ class StatusCommand:
|
|
|
108
100
|
print("nothing to commit, working tree clean")
|
|
109
101
|
else:
|
|
110
102
|
print('nothing to commit (create/copy files and use "agmem add" to track)')
|
|
111
|
-
|
|
103
|
+
|
|
112
104
|
return 0
|