feat(sync): :gl pull — fetch + fast-forward only on the git thread
The git channel now carries GitRequest::{Publish,Pull} with a shared
ensure_online preamble (Wi-Fi/clock/TLS once per session). pull_once
fetches origin — refreshing the tracking ref so the radio-free
up-to-date check stays honest — and maps four shapes: up to date,
LocalAhead (stranded commit, :sync's job), clean fast-forward,
Diverged (refused; no merge on the device). The fast-forward is a SAFE
checkout then ref move: it refuses to overwrite content that differs
from HEAD, the belt under the UI gate that refuses :gl while the dirty
journal is non-empty. A RAM-dirty buffer doesn't gate — its edits win
(last-writer-wins, like the reconcile).
After a pull the UI drops clean parked buffers, re-reads the clean
active buffer in place (Editor::refresh_active), and re-walks the
palette list. Firmware 0.6.0 -> 0.7.0. On-device verification pending.
This commit is contained in:
@@ -960,6 +960,26 @@ impl Editor {
|
||||
self.set_active(path, scope, contents);
|
||||
}
|
||||
|
||||
/// Replace the active buffer's contents after the file changed on disk
|
||||
/// underneath us — a `:gl` pull fast-forwarded the working copy. Same boot
|
||||
/// posture as a fresh load (Normal, caret on the last char, clean, no undo
|
||||
/// history — the old snapshots reference the replaced text). The host only
|
||||
/// calls this when the buffer is clean; a dirty buffer's RAM edits win
|
||||
/// (last-writer-wins, like the reconcile path).
|
||||
pub fn refresh_active(&mut self, contents: String) {
|
||||
let (path, scope) = (self.path.clone(), self.scope);
|
||||
self.set_active(path, scope, contents);
|
||||
}
|
||||
|
||||
/// Drop every *clean* parked buffer, so the next switch to one re-reads the
|
||||
/// disk ([`Effect::Load`]) instead of resurrecting a stale resident copy —
|
||||
/// a `:gl` pull may have rewritten any tracked file. Dirty parked buffers
|
||||
/// are kept: their unsaved edits win over the pulled state, exactly like
|
||||
/// the active buffer's.
|
||||
pub fn drop_clean_parked(&mut self) {
|
||||
self.parked.retain(|b| b.dirty);
|
||||
}
|
||||
|
||||
pub fn scroll_top(&self) -> usize {
|
||||
self.scroll_top
|
||||
}
|
||||
@@ -6427,4 +6447,38 @@ mod tests {
|
||||
e.handle(Key::Char('a'));
|
||||
let _ = e.draw(true);
|
||||
}
|
||||
|
||||
// --- `:gl` pull support (v0.7) ------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn refresh_active_replaces_text_and_resets_state() {
|
||||
let mut e = over("old text");
|
||||
e.handle(Key::Char('v')); // some transient state to reset
|
||||
e.refresh_active("pulled text".into());
|
||||
assert_eq!(e.text, "pulled text");
|
||||
assert_eq!(e.mode(), Mode::Normal);
|
||||
assert!(!e.dirty());
|
||||
assert!(e.undo.is_empty()); // old snapshots reference the old text
|
||||
assert_eq!(e.path(), "/sd/repo/notes.md"); // same file, new contents
|
||||
assert_eq!(e.caret, 10); // boot posture: caret on the last char
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drop_clean_parked_keeps_only_dirty_buffers() {
|
||||
let mut e = over("one"); // active: notes.md, clean
|
||||
e.handle(Key::Char(':'));
|
||||
for c in "enew /sd/repo/b.md".chars() {
|
||||
e.handle(Key::Char(c));
|
||||
}
|
||||
e.handle(Key::Enter); // notes.md parked (clean); b.md active (dirty by design)
|
||||
e.handle(Key::Char(':'));
|
||||
for c in "enew /sd/repo/c.md".chars() {
|
||||
e.handle(Key::Char(c));
|
||||
}
|
||||
e.handle(Key::Enter); // b.md parked (dirty); c.md active
|
||||
assert_eq!(e.parked.len(), 2);
|
||||
e.drop_clean_parked();
|
||||
let kept: Vec<&str> = e.parked.iter().map(|b| b.path.as_str()).collect();
|
||||
assert_eq!(kept, ["/sd/repo/b.md"]); // clean notes.md dropped, dirty b.md kept
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user