feat(sync): commit by splicing journaled dirty paths onto HEAD
The index pipeline (add_all → index.write → write_tree) is O(N_tree) and cannot commit the real 1179-file / 570 MB-pack clone (index.write measured up to 611 s on FAT's racy-clean re-hash). stage_and_commit is now an O(depth) TreeBuilder splice of exactly the paths the editor saved or deleted — ~2-2.8 s on the real clone — with the working tree as source of truth (existing file → insert, missing → remove). Storage records those repo-relative paths on every save/delete and journals them to /sd/.typoena-dirty (atomic, only on growth), so a power pull can't strand a saved-but-unpublished note now that nothing walks the tree. take_dirty → publish_succeeded/publish_failed settles each publish's snapshot from the UI outcome handler. Also required by / discovered with the splice: - mwindow opts at git-service start (32-bit defaults would OOM PSRAM on the first real-pack access; bench-proven 256 KB / 4 MB). - 16-FD mount for git builds (libgit2 holds pack+idx descriptors open; the editor's 4-FD budget overruns). - reconcile is a soft reset (no index to reset anymore); side win: a remote-only added file is carried by the replay instead of dropped. - stranded-commit recovery: tree-unchanged now pushes anyway when origin/<branch> lacks HEAD (a commit whose push failed used to be silently never retried). - radio-free up-to-date: empty dirty set + origin at HEAD answers without bringing Wi-Fi up. - try_push splits ref rejection (reconcilable) from transport failure (surfaced directly) — the first on-device run burned a doomed reconcile on "unsupported URL protocol" and hid the cause. Deliberate behavior change: files changed on the card outside the editor are never committed anymore (also retires the macOS-cruft filter). Trail: docs/tradeoff-curves/sync-commit-staging.md.
This commit is contained in:
@@ -224,11 +224,21 @@ fn main() -> anyhow::Result<()> {
|
||||
// The outcome returns on `git_rx` and updates the snackbar
|
||||
// (see the idle branch below). The Save that preceded this
|
||||
// in the batch already persisted the buffer, so this is a
|
||||
// pure git push.
|
||||
// pure git publish of the recorded dirty paths — the
|
||||
// outcome decides whether the snapshot is forgotten
|
||||
// (publish_succeeded) or retried (publish_failed).
|
||||
#[cfg(feature = "git")]
|
||||
match git_tx.send(firmware::git_sync::PublishRequest) {
|
||||
Ok(()) => ed.set_notice("syncing..."),
|
||||
Err(_) => ed.set_notice("sync: git thread down"),
|
||||
{
|
||||
let paths = storage.take_dirty();
|
||||
match git_tx.send(firmware::git_sync::PublishRequest { paths }) {
|
||||
Ok(()) => ed.set_notice("syncing..."),
|
||||
Err(_) => {
|
||||
// Thread gone — nothing will report back, so
|
||||
// return the snapshot to pending ourselves.
|
||||
storage.publish_failed();
|
||||
ed.set_notice("sync: git thread down");
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(not(feature = "git"))]
|
||||
log::info!(":sync — saved; light build (no `git` feature) — push skipped");
|
||||
@@ -259,6 +269,13 @@ fn main() -> anyhow::Result<()> {
|
||||
#[cfg(feature = "git")]
|
||||
if let Ok(outcome) = git_rx.try_recv() {
|
||||
use firmware::git_sync::PublishOutcome::*;
|
||||
// Settle the dirty snapshot this publish took: confirmed
|
||||
// published (or up to date) → forget it; failed → back to
|
||||
// pending so the next :sync retries the same paths.
|
||||
match &outcome {
|
||||
Pushed(_) | UpToDate => storage.publish_succeeded(),
|
||||
Failed(_) => storage.publish_failed(),
|
||||
}
|
||||
ed.set_notice(match outcome {
|
||||
Pushed(oid) => format!("synced {oid}"),
|
||||
UpToDate => "up to date".to_string(),
|
||||
@@ -410,7 +427,15 @@ fn main() -> anyhow::Result<()> {
|
||||
/// `main`): the note is the whole point of the appliance, so we refuse to run
|
||||
/// in a state where the next save could destroy it.
|
||||
fn boot_storage(epd: &mut Epd) -> (Storage, String) {
|
||||
let storage = match Storage::mount() {
|
||||
// A git build shares this mount with the git thread, and libgit2 keeps the
|
||||
// pack + idx descriptors open across a publish — that overruns the
|
||||
// editor's tight 4-FD budget, so mount with the 16-FD one (persistence.rs,
|
||||
// MAX_FILES_GIT). The light build keeps the editor's own budget.
|
||||
#[cfg(feature = "git")]
|
||||
let mounted = Storage::mount_for_git();
|
||||
#[cfg(not(feature = "git"))]
|
||||
let mounted = Storage::mount();
|
||||
let storage = match mounted {
|
||||
Ok(s) => s,
|
||||
Err(e) => boot_halt(epd, "SD card not ready", &format!("{e:#}")),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user