feat(sync): skip media paths in the pull apply

A pulled image blob is materialized whole in RAM before landing on the
card — history holds 16MB PNGs and a 38MB mp3 against 8MB PSRAM, the
last OOM path in :gl. The device never renders media, and the splice's
explicit-path staging means a missing image can't be committed as a
deletion, so media become invisible to apply_tree_diff: never written,
deleted, or belt-hashed. Blobs still arrive in .git via fetch; the
card's working copy stays text-only and goes deliberately image-stale.
This commit is contained in:
Julien Calixte
2026-07-14 10:05:41 +02:00
parent 3da31de0f2
commit 026eaea69a

View File

@@ -910,6 +910,17 @@ fn pull_once() -> Result<PullOutcome> {
/// power-pull mid-apply leaves at worst a `.gltmp` orphan and a half-applied
/// working copy with the ref NOT yet moved — the next `:gl` re-applies
/// idempotently on the same diff.
///
/// Media paths are invisible to both passes (skip-media-in-apply, 2026-07-14;
/// see docs/notes/git-sync-images-and-repo-size.md). The device never renders
/// them, and writing one means materializing the whole blob in RAM — history
/// holds 16 MB PNGs and a 38 MB mp3 against 8 MB of PSRAM, so a pulled image
/// was the one OOM path left in `:gl`. The blobs still arrive in `.git` via
/// the fetch (streamed, cheap); only the working-tree copy is skipped, so the
/// card's media files go stale/absent relative to HEAD. That's safe on the
/// commit side because the splice stages explicit journal paths — a missing
/// image can never be committed as a deletion. The belt hash skips them too:
/// hashing a stale 16 MB image would be the same OOM by another door.
fn apply_tree_diff(repo: &Repository, head: Oid, theirs: Oid) -> Result<usize> {
use git2::Delta;
@@ -933,6 +944,9 @@ fn apply_tree_diff(repo: &Repository, head: Oid, theirs: Oid) -> Result<usize> {
let Some(rel) = path.and_then(|p| p.to_str()) else {
continue;
};
if is_media_path(rel) {
continue;
}
let abs = format!("{REPO_DIR}/{rel}");
match fs::read(&abs) {
Ok(bytes) => {
@@ -949,12 +963,17 @@ fn apply_tree_diff(repo: &Repository, head: Oid, theirs: Oid) -> Result<usize> {
// Pass 2 — apply.
let mut changed = 0usize;
let mut media_skipped = 0usize;
for d in diff.deltas() {
match d.status() {
Delta::Added | Delta::Modified | Delta::Typechange => {
let Some(rel) = d.new_file().path().and_then(|p| p.to_str()) else {
continue;
};
if is_media_path(rel) {
media_skipped += 1;
continue;
}
let abs = format!("{REPO_DIR}/{rel}");
if let Some(dir) = std::path::Path::new(&abs).parent() {
fs::create_dir_all(dir).with_context(|| format!("mkdir for {rel}"))?;
@@ -972,6 +991,10 @@ fn apply_tree_diff(repo: &Repository, head: Oid, theirs: Oid) -> Result<usize> {
let Some(rel) = d.old_file().path().and_then(|p| p.to_str()) else {
continue;
};
if is_media_path(rel) {
media_skipped += 1;
continue;
}
match fs::remove_file(format!("{REPO_DIR}/{rel}")) {
Ok(()) => changed += 1,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
@@ -981,9 +1004,29 @@ fn apply_tree_diff(repo: &Repository, head: Oid, theirs: Oid) -> Result<usize> {
_ => {}
}
}
if media_skipped > 0 {
log::info!(
"pull: skipped {media_skipped} media file(s) — blobs live in .git, the card's working copy stays text-only"
);
}
Ok(changed)
}
/// Paths [`apply_tree_diff`] never writes, deletes, or belt-hashes: binary
/// media the device can't render and can't afford to hold in RAM. Matched by
/// extension, case-insensitive. Text-ish assets (svg, csv…) stay eligible —
/// the criterion is blob size risk, not "is it a note".
fn is_media_path(rel: &str) -> bool {
const MEDIA_EXT: &[&str] = &[
"png", "jpg", "jpeg", "gif", "bmp", "webp", "heic", "tiff", "ico", "pdf", "mp3", "mp4",
"m4a", "wav", "mov", "avi", "mkv", "zip",
];
std::path::Path::new(rel)
.extension()
.and_then(|e| e.to_str())
.is_some_and(|e| MEDIA_EXT.iter().any(|m| e.eq_ignore_ascii_case(m)))
}
/// Auth + cert callbacks shared by fetch and push. Captures only the baked
/// consts, so a fresh set can be built per operation. The PAT is handed to
/// libgit2 here and never logged.