pivot 0.3.2__tar.gz → 0.3.4__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.
- {pivot-0.3.2 → pivot-0.3.4}/PKG-INFO +1 -1
- {pivot-0.3.2 → pivot-0.3.4}/pyproject.toml +1 -1
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/cli/checkout.py +18 -9
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/cli/track.py +11 -7
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/exceptions.py +21 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/executor/core.py +1 -1
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/executor/worker.py +3 -9
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/explain.py +4 -8
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/remote/sync.py +2 -4
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/run_history.py +7 -1
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/status.py +1 -1
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/storage/cache.py +310 -46
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/storage/lock.py +38 -15
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/storage/restore.py +4 -17
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/storage/store.py +5 -9
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/storage/track.py +23 -4
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/types.py +13 -3
- {pivot-0.3.2 → pivot-0.3.4}/LICENSE +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/README.md +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/__init__.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/ast_utils.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/cli/AGENTS.md +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/cli/CLAUDE.md +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/cli/__init__.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/cli/_run_common.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/cli/check_ignore.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/cli/commit.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/cli/completion.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/cli/config.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/cli/console.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/cli/dag.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/cli/data.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/cli/decorators.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/cli/doctor.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/cli/errors.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/cli/fingerprint.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/cli/helpers.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/cli/history.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/cli/import_cmd.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/cli/init.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/cli/list.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/cli/metrics.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/cli/params.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/cli/plots.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/cli/remote.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/cli/repro.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/cli/run.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/cli/status.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/cli/targets.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/cli/update.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/cli/verify.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/compose.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/config/__init__.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/config/io.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/config/models.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/dag/__init__.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/dag/render.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/decorators.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/discovery.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/engine/__init__.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/engine/agent_rpc.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/engine/engine.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/engine/graph.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/engine/scheduler.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/engine/sinks.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/engine/sources.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/engine/types.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/engine/watch.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/engine/worker_pool.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/executor/AGENTS.md +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/executor/CLAUDE.md +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/executor/__init__.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/executor/commit.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/fingerprint.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/git.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/ignore.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/import_artifact.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/loaders.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/metrics.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/names.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/outputs.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/parameters.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/path_policy.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/project.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/py.typed +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/registry.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/remote/AGENTS.md +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/remote/__init__.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/remote/config.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/remote/git_archive.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/remote/github.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/remote/storage.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/show/__init__.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/show/common.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/show/data.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/show/metrics.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/show/params.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/show/plots.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/skip.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/stage_def.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/storage/AGENTS.md +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/storage/CLAUDE.md +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/storage/__init__.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/storage/artifact_lock.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/storage/presentation.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/storage/state.py +0 -0
- {pivot-0.3.2 → pivot-0.3.4}/src/pivot/yaml_config.py +0 -0
|
@@ -8,9 +8,10 @@ from typing import TYPE_CHECKING, Literal
|
|
|
8
8
|
|
|
9
9
|
import click
|
|
10
10
|
|
|
11
|
-
from pivot import config, project, registry, types
|
|
11
|
+
from pivot import config, exceptions, project, registry, types
|
|
12
12
|
from pivot.cli import completion
|
|
13
13
|
from pivot.cli import decorators as cli_decorators
|
|
14
|
+
from pivot.cli import errors as cli_errors
|
|
14
15
|
from pivot.cli import helpers as cli_helpers
|
|
15
16
|
from pivot.cli import targets as cli_targets
|
|
16
17
|
from pivot.storage import cache, lock, state, track
|
|
@@ -73,11 +74,15 @@ def _on_disk_matches_expected(
|
|
|
73
74
|
if is_dir_hash(expected_hash):
|
|
74
75
|
if not path.is_dir():
|
|
75
76
|
return False
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
77
|
+
actual = cache.hash_directory(path, cache_dir=cache_dir)
|
|
78
|
+
# A hash pivot 0.3.3 recorded lists no empty directories, and repro still honors it.
|
|
79
|
+
# When only empty directories differ, checkout would change no file, so it matches.
|
|
80
|
+
return actual["hash"] == expected_hash["hash"] or (
|
|
81
|
+
"empty_dirs" not in expected_hash and actual["manifest"] == expected_hash["manifest"]
|
|
82
|
+
)
|
|
83
|
+
if not path.is_file():
|
|
84
|
+
return False
|
|
85
|
+
actual_hash, _ = cache.hash_file(path)
|
|
81
86
|
return actual_hash == expected_hash["hash"]
|
|
82
87
|
|
|
83
88
|
|
|
@@ -229,9 +234,13 @@ async def _checkout_files_async(
|
|
|
229
234
|
for abs_path_str, output_hash in files.items():
|
|
230
235
|
_ = tg.create_task(restore_one(abs_path_str, output_hash))
|
|
231
236
|
except* Exception as eg:
|
|
232
|
-
# Convert unexpected exceptions to friendly error message
|
|
233
|
-
|
|
234
|
-
|
|
237
|
+
# Convert unexpected exceptions to a friendly error message, rendering a PivotError's
|
|
238
|
+
# actionable suggestion the same way the top-level CLI error handler does (cli/errors.py).
|
|
239
|
+
messages = [
|
|
240
|
+
cli_errors.format_pivot_error(e) if isinstance(e, exceptions.PivotError) else str(e)
|
|
241
|
+
for e in eg.exceptions
|
|
242
|
+
]
|
|
243
|
+
raise click.ClickException("\n".join(messages)) from None
|
|
235
244
|
|
|
236
245
|
# Re-raise immediate failures (all of them, aggregated)
|
|
237
246
|
if immediate_errors:
|
|
@@ -154,32 +154,36 @@ def _track_single_path(
|
|
|
154
154
|
|
|
155
155
|
# Hash and cache
|
|
156
156
|
if path.is_dir():
|
|
157
|
-
|
|
157
|
+
dir_hash = cache.hash_directory(
|
|
158
158
|
path, cache_dir=cache_dir, warn_left_out_symlinks=warn_left_out_symlinks
|
|
159
159
|
)
|
|
160
|
+
manifest = dir_hash["manifest"]
|
|
160
161
|
total_size = sum(e["size"] for e in manifest)
|
|
161
162
|
num_files = len(manifest)
|
|
162
163
|
|
|
163
|
-
#
|
|
164
|
+
# copy_to_cache is idempotent and already checks existence itself; letting it see
|
|
165
|
+
# every entry (not just ones missing from the cache) is what lets it republish an
|
|
166
|
+
# object `pivot track --force` finds cached non-executable by an older pivot or a
|
|
167
|
+
# cold `pivot pull`, when this entry's bytes should be executable.
|
|
164
168
|
for entry in manifest:
|
|
165
169
|
file_path = path / entry["relpath"]
|
|
166
170
|
file_cache_path = cache.get_cache_path(cache_dir, entry["hash"])
|
|
167
|
-
|
|
168
|
-
cache.copy_to_cache(file_path, file_cache_path)
|
|
171
|
+
cache.copy_to_cache(file_path, file_cache_path, executable=entry["isexec"])
|
|
169
172
|
|
|
170
173
|
pvt_data: track_mod.PvtData = {
|
|
171
174
|
"path": path.name,
|
|
172
|
-
"hash":
|
|
175
|
+
"hash": dir_hash["hash"],
|
|
173
176
|
"size": total_size,
|
|
174
177
|
"num_files": num_files,
|
|
175
178
|
"manifest": manifest,
|
|
176
179
|
}
|
|
180
|
+
if "empty_dirs" in dir_hash:
|
|
181
|
+
pvt_data["empty_dirs"] = dir_hash["empty_dirs"]
|
|
177
182
|
else:
|
|
178
183
|
file_hash, _ = cache.hash_file(path)
|
|
179
184
|
file_size = path.stat().st_size
|
|
180
185
|
file_cache_path = cache.get_cache_path(cache_dir, file_hash)
|
|
181
|
-
|
|
182
|
-
cache.copy_to_cache(path, file_cache_path)
|
|
186
|
+
cache.copy_to_cache(path, file_cache_path)
|
|
183
187
|
|
|
184
188
|
pvt_data = {
|
|
185
189
|
"path": path.name,
|
|
@@ -214,6 +214,27 @@ class UncachedIncrementalOutputError(CacheError):
|
|
|
214
214
|
"""Raised when an incremental output exists without a cache entry."""
|
|
215
215
|
|
|
216
216
|
|
|
217
|
+
class CacheModeCollisionError(CacheError):
|
|
218
|
+
"""A HARDLINK/SYMLINK checkout shares the cache object's own permissions and found it
|
|
219
|
+
already recorded with the wrong executable bit, with no COPY fallback available.
|
|
220
|
+
|
|
221
|
+
This happens when two files with identical content disagree on their executable bit (the
|
|
222
|
+
cache key is content-only) or when a cache object was populated by a path that does not
|
|
223
|
+
know a file's intended executable bit (e.g. `pivot pull`, which downloads by hash alone).
|
|
224
|
+
"""
|
|
225
|
+
|
|
226
|
+
@override
|
|
227
|
+
def get_suggestion(self) -> str:
|
|
228
|
+
return (
|
|
229
|
+
"Add CheckoutMode.COPY to checkout_modes (cache: checkout_mode: [..., copy] in "
|
|
230
|
+
".pivot/config.yaml) so checkout can fall back to a private, correctly-permissioned "
|
|
231
|
+
"copy instead of sharing the cache object's own permissions; or, if you have the "
|
|
232
|
+
"source bytes in hand, re-running what produced them (`pivot repro --force "
|
|
233
|
+
"<stage>`, `pivot track --force <path>`) republishes the cache object with the "
|
|
234
|
+
"right bit"
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
|
|
217
238
|
class ParamsError(PivotError):
|
|
218
239
|
"""Raised when parameter validation or loading fails."""
|
|
219
240
|
|
|
@@ -409,7 +409,7 @@ def verify_tracked_files(project_root: pathlib.Path, checkout_missing: bool = Fa
|
|
|
409
409
|
if path.is_file():
|
|
410
410
|
current_hash, _ = cache.hash_file(path, state_db)
|
|
411
411
|
elif path.is_dir():
|
|
412
|
-
current_hash
|
|
412
|
+
current_hash = cache.hash_directory(path, state_db, cache_dir=cache_dir)["hash"]
|
|
413
413
|
else:
|
|
414
414
|
# Path doesn't exist
|
|
415
415
|
raise FileNotFoundError(data_path)
|
|
@@ -39,7 +39,6 @@ from pivot.storage import artifact_lock, cache, lock, state
|
|
|
39
39
|
from pivot.storage import store as store_mod
|
|
40
40
|
from pivot.types import (
|
|
41
41
|
DeferredWrites,
|
|
42
|
-
DirHash,
|
|
43
42
|
FileHash,
|
|
44
43
|
HashInfo,
|
|
45
44
|
LockData,
|
|
@@ -631,8 +630,7 @@ def hash_output(
|
|
|
631
630
|
``cache_dir`` is the cache object root whose symlinks count as directory content.
|
|
632
631
|
"""
|
|
633
632
|
if path.is_dir():
|
|
634
|
-
|
|
635
|
-
return DirHash(hash=tree_hash, manifest=manifest)
|
|
633
|
+
return cache.hash_directory(path, state_db, cache_dir=cache_dir)
|
|
636
634
|
file_hash, _ = cache.hash_file(path, state_db)
|
|
637
635
|
return FileHash(hash=file_hash)
|
|
638
636
|
|
|
@@ -1063,12 +1061,9 @@ def _hash_output_paths(
|
|
|
1063
1061
|
if not path.exists():
|
|
1064
1062
|
raise exceptions.OutputMissingError(f"Stage did not produce output: {path}")
|
|
1065
1063
|
if path.is_dir():
|
|
1066
|
-
|
|
1064
|
+
output_hashes[types.identity_key(ref.identity)] = cache.hash_directory(
|
|
1067
1065
|
path, state_db, cache_dir=cache_dir, warn_left_out_symlinks=True
|
|
1068
1066
|
)
|
|
1069
|
-
output_hashes[types.identity_key(ref.identity)] = DirHash(
|
|
1070
|
-
hash=tree_hash, manifest=manifest
|
|
1071
|
-
)
|
|
1072
1067
|
else:
|
|
1073
1068
|
file_hash, _ = cache.hash_file(path, state_db)
|
|
1074
1069
|
output_hashes[types.identity_key(ref.identity)] = FileHash(hash=file_hash)
|
|
@@ -1290,8 +1285,7 @@ def hash_dependencies(
|
|
|
1290
1285
|
path = pathlib.Path(normalized_path)
|
|
1291
1286
|
try:
|
|
1292
1287
|
if path.is_dir():
|
|
1293
|
-
|
|
1294
|
-
hashes[dep] = DirHash(hash=tree_hash, manifest=manifest)
|
|
1288
|
+
hashes[dep] = cache.hash_directory(path, state_db)
|
|
1295
1289
|
else:
|
|
1296
1290
|
file_hash, fs_stat = cache.hash_file(path, state_db)
|
|
1297
1291
|
hashes[dep] = FileHash(hash=file_hash)
|
|
@@ -14,7 +14,7 @@ import pydantic
|
|
|
14
14
|
|
|
15
15
|
from pivot import parameters, project, registry, skip, types
|
|
16
16
|
from pivot.executor import worker
|
|
17
|
-
from pivot.storage import lock, state
|
|
17
|
+
from pivot.storage import lock, state, track
|
|
18
18
|
from pivot.storage import store as store_mod
|
|
19
19
|
from pivot.types import (
|
|
20
20
|
ArtifactIdentity,
|
|
@@ -30,8 +30,6 @@ if TYPE_CHECKING:
|
|
|
30
30
|
|
|
31
31
|
import pygtrie
|
|
32
32
|
|
|
33
|
-
from pivot.storage.track import PvtData
|
|
34
|
-
|
|
35
33
|
OUTPUTS_MISSING_REASON = "Outputs missing from cache"
|
|
36
34
|
"""Reason for a stage whose lock is current but whose cacheable outputs are absent locally.
|
|
37
35
|
|
|
@@ -105,7 +103,7 @@ def _find_tracked_ancestor(dep: Path, tracked_trie: pygtrie.Trie[str]) -> Path |
|
|
|
105
103
|
|
|
106
104
|
def _find_tracked_hash(
|
|
107
105
|
dep: Path,
|
|
108
|
-
tracked_files: dict[str, PvtData],
|
|
106
|
+
tracked_files: dict[str, track.PvtData],
|
|
109
107
|
tracked_trie: pygtrie.Trie[str],
|
|
110
108
|
) -> HashInfo | None:
|
|
111
109
|
"""Find hash for dep from tracked files data.
|
|
@@ -121,9 +119,7 @@ def _find_tracked_hash(
|
|
|
121
119
|
|
|
122
120
|
# Exact match - use top-level hash
|
|
123
121
|
if dep == tracked_path:
|
|
124
|
-
|
|
125
|
-
return {"hash": pvt_data["hash"], "manifest": pvt_data["manifest"]}
|
|
126
|
-
return {"hash": pvt_data["hash"]}
|
|
122
|
+
return track.pvt_to_hash_info(pvt_data)
|
|
127
123
|
|
|
128
124
|
# Nested path - find in manifest
|
|
129
125
|
if "manifest" not in pvt_data:
|
|
@@ -155,7 +151,7 @@ def get_stage_explanation(
|
|
|
155
151
|
lock_data: LockData | None,
|
|
156
152
|
force: bool = False,
|
|
157
153
|
allow_missing: bool = False,
|
|
158
|
-
tracked_files: dict[str, PvtData] | None = None,
|
|
154
|
+
tracked_files: dict[str, track.PvtData] | None = None,
|
|
159
155
|
tracked_trie: pygtrie.Trie[str] | None = None,
|
|
160
156
|
producer_hashes: dict[ArtifactIdentity, HashInfo] | None = None,
|
|
161
157
|
) -> StageExplanation:
|
|
@@ -11,7 +11,7 @@ from pivot.remote import config as remote_config
|
|
|
11
11
|
from pivot.remote import storage as remote_mod
|
|
12
12
|
from pivot.storage import cache, lock, track
|
|
13
13
|
from pivot.storage import store as store_mod
|
|
14
|
-
from pivot.types import
|
|
14
|
+
from pivot.types import HashInfo, RemoteStatus, TransferSummary, is_dir_hash
|
|
15
15
|
|
|
16
16
|
if TYPE_CHECKING:
|
|
17
17
|
from collections.abc import Callable
|
|
@@ -175,9 +175,7 @@ def _get_file_hash_from_pvt(rel_path: str, proj_root: pathlib.Path) -> HashInfo
|
|
|
175
175
|
if track_data is None:
|
|
176
176
|
return None
|
|
177
177
|
|
|
178
|
-
|
|
179
|
-
return DirHash(hash=track_data["hash"], manifest=track_data["manifest"])
|
|
180
|
-
return FileHash(hash=track_data["hash"])
|
|
178
|
+
return track.pvt_to_hash_info(track_data)
|
|
181
179
|
|
|
182
180
|
|
|
183
181
|
def _get_tracked_hashes(proj_root: pathlib.Path) -> set[str]:
|
|
@@ -64,6 +64,7 @@ class OutputHashEntry(TypedDict):
|
|
|
64
64
|
path: str
|
|
65
65
|
hash: str
|
|
66
66
|
manifest: NotRequired[list[DirManifestEntry]]
|
|
67
|
+
empty_dirs: NotRequired[list[str]]
|
|
67
68
|
|
|
68
69
|
|
|
69
70
|
def output_hash_to_entry(path: str, oh: HashInfo) -> OutputHashEntry:
|
|
@@ -71,6 +72,8 @@ def output_hash_to_entry(path: str, oh: HashInfo) -> OutputHashEntry:
|
|
|
71
72
|
entry = OutputHashEntry(path=path, hash=oh["hash"])
|
|
72
73
|
if is_dir_hash(oh):
|
|
73
74
|
entry["manifest"] = oh["manifest"]
|
|
75
|
+
if "empty_dirs" in oh:
|
|
76
|
+
entry["empty_dirs"] = oh["empty_dirs"]
|
|
74
77
|
return entry
|
|
75
78
|
|
|
76
79
|
|
|
@@ -78,7 +81,10 @@ def entry_to_output_hash(entry: OutputHashEntry) -> FileHash | DirHash:
|
|
|
78
81
|
"""Convert serialized OutputHashEntry back to internal HashInfo."""
|
|
79
82
|
# Null check guards against corrupted JSON where manifest could be null
|
|
80
83
|
if "manifest" in entry and entry["manifest"] is not None: # pyright: ignore[reportUnnecessaryComparison]
|
|
81
|
-
|
|
84
|
+
dir_hash = DirHash(hash=entry["hash"], manifest=entry["manifest"])
|
|
85
|
+
if "empty_dirs" in entry and entry["empty_dirs"]:
|
|
86
|
+
dir_hash["empty_dirs"] = entry["empty_dirs"]
|
|
87
|
+
return dir_hash
|
|
82
88
|
return FileHash(hash=entry["hash"])
|
|
83
89
|
|
|
84
90
|
|
|
@@ -356,7 +356,7 @@ def get_tracked_files_status(
|
|
|
356
356
|
|
|
357
357
|
try:
|
|
358
358
|
if path.is_dir():
|
|
359
|
-
current_hash
|
|
359
|
+
current_hash = cache.hash_directory(path, state_db, cache_dir=cache_dir)["hash"]
|
|
360
360
|
else:
|
|
361
361
|
current_hash, _ = cache.hash_file(path, state_db)
|
|
362
362
|
except FileNotFoundError:
|