loom-code 0.1.2__tar.gz → 0.1.3__tar.gz
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.
- {loom_code-0.1.2 → loom_code-0.1.3}/PKG-INFO +2 -2
- {loom_code-0.1.2 → loom_code-0.1.3}/loom_code/__init__.py +1 -1
- {loom_code-0.1.2 → loom_code-0.1.3}/loom_code/checkpoint.py +15 -8
- {loom_code-0.1.2 → loom_code-0.1.3}/loom_code/loominit/repomap.py +62 -12
- {loom_code-0.1.2 → loom_code-0.1.3}/loom_code.egg-info/PKG-INFO +2 -2
- {loom_code-0.1.2 → loom_code-0.1.3}/loom_code.egg-info/requires.txt +1 -1
- {loom_code-0.1.2 → loom_code-0.1.3}/pyproject.toml +8 -3
- {loom_code-0.1.2 → loom_code-0.1.3}/tests/test_checkpoint.py +35 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/LICENSE +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/README.md +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/loom_code/_post_commit.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/loom_code/agent.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/loom_code/approval.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/loom_code/browse/__init__.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/loom_code/browse/act.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/loom_code/browse/observe.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/loom_code/browse/session.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/loom_code/browse/verify.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/loom_code/cli.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/loom_code/code_index.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/loom_code/compact.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/loom_code/consent.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/loom_code/credentials.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/loom_code/edit_tool.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/loom_code/extensions.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/loom_code/file_history.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/loom_code/file_tools.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/loom_code/git_hook.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/loom_code/grep_tool.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/loom_code/hooks.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/loom_code/loominit/__init__.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/loom_code/loominit/_ast_walk.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/loom_code/loominit/_files.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/loom_code/loominit/_graph.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/loom_code/loominit/_resolve.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/loom_code/loominit/_tests_map.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/loom_code/loominit/extractor.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/loom_code/loominit/schema.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/loom_code/lsp_tools.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/loom_code/mcp_host.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/loom_code/operator.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/loom_code/paste.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/loom_code/paths.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/loom_code/permissions.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/loom_code/project.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/loom_code/prompts.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/loom_code/render.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/loom_code/repl.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/loom_code/rules.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/loom_code/sandboxed_bash.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/loom_code/scribe.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/loom_code/skills/__init__.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/loom_code/skills/graphify/SKILL.md +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/loom_code/skills/graphify/tools.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/loom_code/trust.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/loom_code/turn.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/loom_code/web_fetch.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/loom_code/workers.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/loom_code/worktree.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/loom_code.egg-info/SOURCES.txt +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/loom_code.egg-info/dependency_links.txt +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/loom_code.egg-info/entry_points.txt +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/loom_code.egg-info/top_level.txt +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/setup.cfg +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/tests/test_agent.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/tests/test_antipoison_gate.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/tests/test_approval.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/tests/test_approval_danger.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/tests/test_approval_integration.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/tests/test_code_index.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/tests/test_compact.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/tests/test_credentials.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/tests/test_edit_tool.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/tests/test_extensions.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/tests/test_file_boundary.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/tests/test_file_history.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/tests/test_git_hook.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/tests/test_graphify_file_discovery.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/tests/test_graphify_query_tiers.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/tests/test_graphify_wiring.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/tests/test_grep_tool.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/tests/test_learned_notes.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/tests/test_loom_hooks.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/tests/test_lsp_tools.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/tests/test_mcp.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/tests/test_paste.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/tests/test_permissions.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/tests/test_pricing.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/tests/test_project.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/tests/test_prompts.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/tests/test_render.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/tests/test_repl_guards.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/tests/test_resume_migration.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/tests/test_resume_preview.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/tests/test_routing.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/tests/test_rules.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/tests/test_sandboxed_bash.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/tests/test_stream_liveness.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/tests/test_turn_economy.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/tests/test_web_fetch.py +0 -0
- {loom_code-0.1.2 → loom_code-0.1.3}/tests/test_workers.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: loom-code
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: loom-code — a loomflow-native terminal coding agent
|
|
5
5
|
Author: Anupam Nautiyal
|
|
6
6
|
License: MIT License
|
|
@@ -39,7 +39,7 @@ Classifier: Topic :: Software Development :: Code Generators
|
|
|
39
39
|
Requires-Python: >=3.11
|
|
40
40
|
Description-Content-Type: text/markdown
|
|
41
41
|
License-File: LICENSE
|
|
42
|
-
Requires-Dist: loomflow[litellm,mcp,web]<0.11,>=0.10.
|
|
42
|
+
Requires-Dist: loomflow[litellm,mcp,web]<0.11,>=0.10.30
|
|
43
43
|
Requires-Dist: rich>=13
|
|
44
44
|
Requires-Dist: prompt-toolkit>=3.0
|
|
45
45
|
Requires-Dist: anthropic
|
|
@@ -134,18 +134,25 @@ def _snapshot_commit(root: Path) -> tuple[str | None, str]:
|
|
|
134
134
|
|
|
135
135
|
# Temp index OUTSIDE the worktree so it never appears in the
|
|
136
136
|
# snapshot (an in-tree temp index leaked itself into the tree —
|
|
137
|
-
# verified).
|
|
138
|
-
#
|
|
137
|
+
# verified).
|
|
138
|
+
#
|
|
139
|
+
# Seed it from HEAD's TREE via read-tree — NOT by copying the real
|
|
140
|
+
# index file. A copied index carries the real index's STAT CACHE,
|
|
141
|
+
# and a same-size edit whose mtime matches the cached stat makes
|
|
142
|
+
# ``git add`` trust the cache and skip re-hashing — the snapshot
|
|
143
|
+
# silently captures the OLD content and /undo restores stale data
|
|
144
|
+
# (agents write fast; hit for real on CI). read-tree writes
|
|
145
|
+
# entries with no stat information, so the add below re-stats and
|
|
146
|
+
# re-hashes every path — the trap can't fire.
|
|
139
147
|
tmp_fd, tmp_index = tempfile.mkstemp(prefix="loom-ckpt-index-")
|
|
140
148
|
os.close(tmp_fd)
|
|
141
149
|
try:
|
|
142
|
-
real_index = root / ".git" / "index"
|
|
143
|
-
if real_index.is_file():
|
|
144
|
-
try:
|
|
145
|
-
Path(tmp_index).write_bytes(real_index.read_bytes())
|
|
146
|
-
except OSError:
|
|
147
|
-
pass # start from empty temp index
|
|
148
150
|
env = {"GIT_INDEX_FILE": tmp_index}
|
|
151
|
+
if parent:
|
|
152
|
+
rc, _o, err = _git(root, ["read-tree", parent], env=env)
|
|
153
|
+
if rc != 0:
|
|
154
|
+
return None, f"read-tree failed: {err.strip()}"
|
|
155
|
+
# No HEAD yet → empty temp index; add stages everything fresh.
|
|
149
156
|
# Stage tracked changes + untracked files. .loom is excluded so
|
|
150
157
|
# the snapshot doesn't churn on our own state files (it's also
|
|
151
158
|
# usually gitignored, but be explicit).
|
|
@@ -134,23 +134,61 @@ _SKIP_DIRS = frozenset(
|
|
|
134
134
|
|
|
135
135
|
# Cache: root -> (signature, rendered map). The signature is cheap to
|
|
136
136
|
# recompute; the expensive AST walk only re-runs when it changes.
|
|
137
|
-
|
|
137
|
+
# ``_UNMAPPABLE`` in the signature slot marks a root we refuse to map
|
|
138
|
+
# (drive root / home dir / walk blew the budget) — cached so later
|
|
139
|
+
# turns skip instantly instead of re-attempting the walk.
|
|
140
|
+
_UNMAPPABLE = object()
|
|
141
|
+
_REPO_MAP_CACHE: dict[str, tuple[object, str | None]] = {}
|
|
142
|
+
|
|
143
|
+
# Walk guards. The signature walk runs at EVERY turn start, on the
|
|
144
|
+
# REPL's event loop — it must never be able to stall a turn. A user
|
|
145
|
+
# launched loom-code at ``D:\`` (drive root, no git → cwd becomes the
|
|
146
|
+
# project root) and the first turn hung walking the whole drive.
|
|
147
|
+
_SIG_MAX_FILES = 50_000 # .py files — far beyond any sane repo
|
|
148
|
+
_SIG_TIME_BUDGET_S = 3.0 # wall-clock cap for the whole walk
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _unmappable_root(root: Path) -> bool:
|
|
152
|
+
"""Roots where a repo map makes no sense and the walk is dangerous:
|
|
153
|
+
a filesystem/drive root (``/``, ``C:\\``, ``D:\\``) or the user's
|
|
154
|
+
home directory. These aren't projects — they're the world."""
|
|
155
|
+
try:
|
|
156
|
+
return root == Path(root.anchor) or root == Path.home()
|
|
157
|
+
except OSError:
|
|
158
|
+
return True # can't even resolve — don't walk it
|
|
138
159
|
|
|
139
160
|
|
|
140
|
-
def _tree_signature(root: Path) -> tuple[float, int]:
|
|
161
|
+
def _tree_signature(root: Path) -> tuple[float, int] | None:
|
|
141
162
|
"""A cheap freshness key: (newest .py mtime, file count). Either
|
|
142
163
|
moving means the tree changed → rebuild. Count catches add/delete
|
|
143
|
-
that don't bump the newest mtime.
|
|
164
|
+
that don't bump the newest mtime.
|
|
165
|
+
|
|
166
|
+
``os.walk`` with pruning (NOT ``rglob``, which descends into
|
|
167
|
+
node_modules/.venv and filters after the fact) plus a file-count
|
|
168
|
+
and wall-clock budget. Returns ``None`` when the budget is
|
|
169
|
+
breached — the tree is too big to map; the caller caches the
|
|
170
|
+
refusal."""
|
|
171
|
+
import os
|
|
172
|
+
import time as _time
|
|
173
|
+
|
|
174
|
+
deadline = _time.monotonic() + _SIG_TIME_BUDGET_S
|
|
144
175
|
newest = 0.0
|
|
145
176
|
count = 0
|
|
146
|
-
for
|
|
147
|
-
if
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
177
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
178
|
+
if _time.monotonic() > deadline:
|
|
179
|
+
return None
|
|
180
|
+
dirnames[:] = [d for d in dirnames if d not in _SKIP_DIRS]
|
|
181
|
+
for fn in filenames:
|
|
182
|
+
if not fn.endswith(".py"):
|
|
183
|
+
continue
|
|
184
|
+
try:
|
|
185
|
+
st = os.stat(os.path.join(dirpath, fn))
|
|
186
|
+
except OSError:
|
|
187
|
+
continue
|
|
188
|
+
newest = max(newest, st.st_mtime)
|
|
151
189
|
count += 1
|
|
152
|
-
|
|
153
|
-
|
|
190
|
+
if count > _SIG_MAX_FILES:
|
|
191
|
+
return None
|
|
154
192
|
return (newest, count)
|
|
155
193
|
|
|
156
194
|
|
|
@@ -161,11 +199,23 @@ def _repo_map_cached(
|
|
|
161
199
|
True when the source tree changed and we re-walked, False on a
|
|
162
200
|
cache hit. Freshness-keyed (newest .py mtime + file count), so it
|
|
163
201
|
only re-parses when something actually moved. Mirrors the map to
|
|
164
|
-
``<root>/.loom/repomap.md`` on every rebuild for inspection.
|
|
202
|
+
``<root>/.loom/repomap.md`` on every rebuild for inspection.
|
|
203
|
+
|
|
204
|
+
Unmappable roots (drive root / home / budget-breaching trees)
|
|
205
|
+
return ``(None, False)`` and are cached as such — the agent simply
|
|
206
|
+
runs without a repo map there, and no later turn pays the walk."""
|
|
165
207
|
root_p = Path(root).resolve()
|
|
166
208
|
key = str(root_p)
|
|
167
|
-
sig = _tree_signature(root_p)
|
|
168
209
|
cached = _REPO_MAP_CACHE.get(key)
|
|
210
|
+
if cached is not None and cached[0] is _UNMAPPABLE:
|
|
211
|
+
return None, False
|
|
212
|
+
if _unmappable_root(root_p):
|
|
213
|
+
_REPO_MAP_CACHE[key] = (_UNMAPPABLE, None)
|
|
214
|
+
return None, False
|
|
215
|
+
sig = _tree_signature(root_p)
|
|
216
|
+
if sig is None: # walk breached the budget — tree too big
|
|
217
|
+
_REPO_MAP_CACHE[key] = (_UNMAPPABLE, None)
|
|
218
|
+
return None, False
|
|
169
219
|
if cached is not None and cached[0] == sig:
|
|
170
220
|
return cached[1], False
|
|
171
221
|
rendered = repo_map_for_root(root_p, max_tokens=max_tokens)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: loom-code
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: loom-code — a loomflow-native terminal coding agent
|
|
5
5
|
Author: Anupam Nautiyal
|
|
6
6
|
License: MIT License
|
|
@@ -39,7 +39,7 @@ Classifier: Topic :: Software Development :: Code Generators
|
|
|
39
39
|
Requires-Python: >=3.11
|
|
40
40
|
Description-Content-Type: text/markdown
|
|
41
41
|
License-File: LICENSE
|
|
42
|
-
Requires-Dist: loomflow[litellm,mcp,web]<0.11,>=0.10.
|
|
42
|
+
Requires-Dist: loomflow[litellm,mcp,web]<0.11,>=0.10.30
|
|
43
43
|
Requires-Dist: rich>=13
|
|
44
44
|
Requires-Dist: prompt-toolkit>=3.0
|
|
45
45
|
Requires-Dist: anthropic
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "loom-code"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.3"
|
|
4
4
|
description = "loom-code — a loomflow-native terminal coding agent"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = { file = "LICENSE" }
|
|
@@ -54,11 +54,16 @@ dependencies = [
|
|
|
54
54
|
# plugin to use MCP. Discovery degrades gracefully if it's ever absent.
|
|
55
55
|
# >=0.10.25 carries the tool-call arg coercion + PEP 563 schema-typing
|
|
56
56
|
# fix that loom-code's typed tools (edit replace_all=bool, etc.) rely on.
|
|
57
|
+
# >=0.10.30 floor: the tool-transcript capture fix — earlier
|
|
58
|
+
# versions leaked rehydrated prose (a prior answer + the run's own
|
|
59
|
+
# prompt) into persisted transcripts on resumed sessions,
|
|
60
|
+
# corrupting rehydration and the /resume preview. loom-code's
|
|
61
|
+
# /resume scrubs old data; the floor stops NEW corruption.
|
|
57
62
|
# <0.11 ceiling: loomflow is pre-1.0, so a minor bump may break the
|
|
58
63
|
# composed surfaces (Team kwargs, stop hooks, working blocks). Raise
|
|
59
64
|
# the ceiling deliberately after testing, not by accident on a
|
|
60
65
|
# user's machine mid-`pipx upgrade`.
|
|
61
|
-
"loomflow[litellm,web,mcp]>=0.10.
|
|
66
|
+
"loomflow[litellm,web,mcp]>=0.10.30,<0.11",
|
|
62
67
|
"rich>=13",
|
|
63
68
|
# prompt_toolkit drives the REPL line editor — gives us inline
|
|
64
69
|
# slash-command autocomplete (a menu pops the moment the user
|
|
@@ -129,7 +134,7 @@ select = ["E", "F", "I", "B", "UP", "ASYNC"]
|
|
|
129
134
|
# publishing — no tokens). Daily flow stays plain git; ``make release
|
|
130
135
|
# BUMP=patch|minor|major`` is the explicit "this is a release" flag.
|
|
131
136
|
[tool.bumpversion]
|
|
132
|
-
current_version = "0.1.
|
|
137
|
+
current_version = "0.1.3"
|
|
133
138
|
search = "{current_version}"
|
|
134
139
|
replace = "{new_version}"
|
|
135
140
|
commit = true
|
|
@@ -155,3 +155,38 @@ def test_no_checkpoints_to_restore(tmp_path: Path) -> None:
|
|
|
155
155
|
restored, err = cp.restore(root)
|
|
156
156
|
assert restored is None
|
|
157
157
|
assert "no checkpoint" in err.lower()
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def test_checkpoint_sees_same_second_same_size_edit(
|
|
161
|
+
tmp_path: Path,
|
|
162
|
+
) -> None:
|
|
163
|
+
"""Regression: the racy-git stat trap. The temp snapshot index was
|
|
164
|
+
seeded by COPYING the real index — inheriting its stat cache. An
|
|
165
|
+
edit that keeps the same SIZE and lands in the same mtime SECOND
|
|
166
|
+
as the last index write (agents write fast; CI is faster) made
|
|
167
|
+
``git add`` trust the cached hash and skip the file, so the
|
|
168
|
+
snapshot captured the OLD content and /undo restored stale data.
|
|
169
|
+
Forge the collision deterministically with os.utime."""
|
|
170
|
+
import os
|
|
171
|
+
import time
|
|
172
|
+
|
|
173
|
+
root = tmp_path
|
|
174
|
+
_git(root, "init", "-q")
|
|
175
|
+
_git(root, "config", "user.email", "t@t.co")
|
|
176
|
+
_git(root, "config", "user.name", "t")
|
|
177
|
+
(root / ".loom").mkdir()
|
|
178
|
+
a = root / "a.py"
|
|
179
|
+
past = time.time() - 10 # older than the temp index's mtime →
|
|
180
|
+
a.write_text("v1\n") # the entry is NOT "racy", so git's
|
|
181
|
+
os.utime(a, (past, past)) # racy-protection can't save us
|
|
182
|
+
_git(root, "add", "-A")
|
|
183
|
+
_git(root, "commit", "-qm", "init")
|
|
184
|
+
a.write_text("v2\n") # same 3-byte size as "v1\n"
|
|
185
|
+
os.utime(a, (past, past)) # identical stat → cache trusted
|
|
186
|
+
|
|
187
|
+
snap, err = cp.checkpoint(root, summary="racy edit")
|
|
188
|
+
assert snap is not None, err
|
|
189
|
+
in_snapshot = _git(root, "show", f"{snap.sha}:a.py")
|
|
190
|
+
assert in_snapshot == "v2\n", (
|
|
191
|
+
f"snapshot captured stale content: {in_snapshot!r}"
|
|
192
|
+
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|