mirror of
https://github.com/caelestia-dots/cli.git
synced 2026-06-18 15:00:00 -05:00
fix: handle user deleted but upstream changed properly
Also catch error reading blob for new files
This commit is contained in:
@@ -3,6 +3,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
from caelestia.utils.dots.manifest import ManifestEntry
|
from caelestia.utils.dots.manifest import ManifestEntry
|
||||||
from caelestia.utils.dots.source import DotsSource, SourceError
|
from caelestia.utils.dots.source import DotsSource, SourceError
|
||||||
|
from caelestia.utils.io import warn
|
||||||
|
|
||||||
|
|
||||||
class _Continue(Exception):
|
class _Continue(Exception):
|
||||||
@@ -15,9 +16,10 @@ class Changeset:
|
|||||||
conflicts: list[tuple[str, Path]] = field(default_factory=list) # (repofile, dest) -> write .new
|
conflicts: list[tuple[str, Path]] = field(default_factory=list) # (repofile, dest) -> write .new
|
||||||
deletes: list[Path] = field(default_factory=list) # We placed it, upstream removed it, unmodified
|
deletes: list[Path] = field(default_factory=list) # We placed it, upstream removed it, unmodified
|
||||||
stale: list[Path] = field(default_factory=list) # Upstream removed it but user modified it
|
stale: list[Path] = field(default_factory=list) # Upstream removed it but user modified it
|
||||||
|
deleted_changed: list[tuple[str, Path]] = field(default_factory=list) # User deleted it, upstream changed -> .new
|
||||||
|
|
||||||
def is_empty(self) -> bool:
|
def is_empty(self) -> bool:
|
||||||
return not (self.place or self.conflicts or self.deletes or self.stale)
|
return not (self.place or self.conflicts or self.deletes or self.stale or self.deleted_changed)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def compute(
|
def compute(
|
||||||
@@ -30,11 +32,18 @@ class Changeset:
|
|||||||
"""Collect all file changes needed into a Changeset."""
|
"""Collect all file changes needed into a Changeset."""
|
||||||
|
|
||||||
has_base = source.has_rev(applied_rev)
|
has_base = source.has_rev(applied_rev)
|
||||||
|
if not has_base:
|
||||||
|
warn(
|
||||||
|
"the previously applied revision is missing from the dots clone; files that differ "
|
||||||
|
"from the latest version will be written as .new instead of updated in place."
|
||||||
|
)
|
||||||
|
|
||||||
changed = set(source.changed_files(applied_rev, tip)) if has_base else set()
|
changed = set(source.changed_files(applied_rev, tip)) if has_base else set()
|
||||||
place: list[tuple[str, Path]] = []
|
place: list[tuple[str, Path]] = []
|
||||||
conflicts: list[tuple[str, Path]] = []
|
conflicts: list[tuple[str, Path]] = []
|
||||||
deletes: list[Path] = []
|
deletes: list[Path] = []
|
||||||
stale: list[Path] = []
|
stale: list[Path] = []
|
||||||
|
deleted_changed: list[tuple[str, Path]] = []
|
||||||
|
|
||||||
# Collect all files to deploy (entry sources can be dirs so we recurse into them)
|
# Collect all files to deploy (entry sources can be dirs so we recurse into them)
|
||||||
to_deploy: dict[Path, str] = {}
|
to_deploy: dict[Path, str] = {}
|
||||||
@@ -69,13 +78,17 @@ class Changeset:
|
|||||||
stale.append(dest_path)
|
stale.append(dest_path)
|
||||||
else: # Still managed; `src` is what we last placed, `new_src` the current source
|
else: # Still managed; `src` is what we last placed, `new_src` the current source
|
||||||
new_src = to_deploy[dest_path]
|
new_src = to_deploy[dest_path]
|
||||||
|
if not dest_path.exists():
|
||||||
|
# User deleted a managed file locally
|
||||||
|
if has_base and new_src == src and new_src not in changed:
|
||||||
|
continue # Respect the deletion; upstream has nothing new to offer
|
||||||
|
# Upstream changed it (or base is unknown): surface as .new, don't restore
|
||||||
|
deleted_changed.append((new_src, dest_path))
|
||||||
|
continue
|
||||||
|
|
||||||
if has_base and new_src == src and new_src not in changed:
|
if has_base and new_src == src and new_src not in changed:
|
||||||
continue # Unchanged upstream
|
continue # Unchanged upstream
|
||||||
|
|
||||||
if not dest_path.exists():
|
|
||||||
place.append((new_src, dest_path))
|
|
||||||
continue
|
|
||||||
|
|
||||||
dest_content = dest_path.read_bytes()
|
dest_content = dest_path.read_bytes()
|
||||||
if try_read(tip, new_src) == dest_content:
|
if try_read(tip, new_src) == dest_content:
|
||||||
continue # File is already up to date
|
continue # File is already up to date
|
||||||
@@ -91,10 +104,18 @@ class Changeset:
|
|||||||
# New files to deploy
|
# New files to deploy
|
||||||
for dest in files_to_deploy - set(Path(d) for d in deployed):
|
for dest in files_to_deploy - set(Path(d) for d in deployed):
|
||||||
src = to_deploy[dest]
|
src = to_deploy[dest]
|
||||||
if not dest.exists() or source.blob_at(tip, src) == dest.read_bytes():
|
try:
|
||||||
|
new_content = source.blob_at(tip, src)
|
||||||
|
except SourceError:
|
||||||
|
# Failed to read the upstream blob; skip rather than abort the whole update
|
||||||
|
warn(f"could not read from source, skipping: {src}")
|
||||||
|
continue
|
||||||
|
if not dest.exists() or new_content == dest.read_bytes():
|
||||||
# Dest nonexistent or already equal to new content
|
# Dest nonexistent or already equal to new content
|
||||||
place.append((src, dest))
|
place.append((src, dest))
|
||||||
else:
|
else:
|
||||||
conflicts.append((src, dest))
|
conflicts.append((src, dest))
|
||||||
|
|
||||||
return Changeset(place=place, conflicts=conflicts, deletes=deletes, stale=stale)
|
return Changeset(
|
||||||
|
place=place, conflicts=conflicts, deletes=deletes, stale=stale, deleted_changed=deleted_changed
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user