feat(editor): add :enew and :delete with real git-staging (v0.5 slice 3)
:enew <name> creates a new file (empty, dirty, added to the palette list); :delete unlinks the current file via a new Effect::Delete and switches to a parked buffer or scratch. Scope is read from the path (local/x, repo/x) rather than a modal prompt, and the /sd prefix is now optional in resolve_path. On-device testing showed deletions never reached the remote: add_all(["*"]) alone does not stage a removal on this libgit2, so the tree came back unchanged and the push was a silent no-op. stage_and_commit now runs add_all then update_all(["*"]) (git add -u) — together git add -A. The :delete snackbar now confirms the scoped file and flags that a Tracked file is local until :sync.
This commit is contained in:
@@ -254,13 +254,20 @@ fn publish_once() -> Result<PublishOutcome> {
|
||||
/// (nothing to publish). Called on the first attempt and again to replay the note
|
||||
/// after a reconcile.
|
||||
///
|
||||
/// `add_all` runs a per-path filter that drops macOS AppleDouble sidecars
|
||||
/// (`._name`) and `.DS_Store` that Finder/Spotlight sprinkle onto the FAT card
|
||||
/// whenever it's mounted on a Mac — without it, a blind add --all sweeps them into
|
||||
/// the commit (it did once: 07d87772 shipped `._.git`, `._README.md`,
|
||||
/// `._notes.md`). Filtering here fixes it for *every* repo at the device level, so
|
||||
/// no per-repo `.gitignore` is needed. (add --all also stages deletions, for a
|
||||
/// future note-delete.)
|
||||
/// Staging is `add --all` **plus** `add -u`, which together equal `git add -A`.
|
||||
/// `add_all` stages new + modified files; `update_all` re-syncs already-tracked
|
||||
/// entries to the working tree, which is what actually removes an index entry
|
||||
/// whose file was deleted. Spike 14 found `add_all` alone did **not** stage a
|
||||
/// `:delete`d file's removal on this libgit2 build (the tree came back unchanged,
|
||||
/// so the publish was a silent no-op), so the `update_all` pass is load-bearing,
|
||||
/// not belt-and-braces — do not drop it.
|
||||
///
|
||||
/// Both run a per-path filter that drops macOS AppleDouble sidecars (`._name`)
|
||||
/// and `.DS_Store` that Finder/Spotlight sprinkle onto the FAT card whenever it's
|
||||
/// mounted on a Mac — without it, a blind add --all sweeps them into the commit
|
||||
/// (it did once: 07d87772 shipped `._.git`, `._README.md`, `._notes.md`).
|
||||
/// Filtering here fixes it for *every* repo at the device level, so no per-repo
|
||||
/// `.gitignore` is needed.
|
||||
fn stage_and_commit(repo: &Repository) -> Result<Option<git2::Oid>> {
|
||||
let mut index = repo.index().context("opening index")?;
|
||||
let mut skip_macos_cruft = |path: &Path, _matched: &[u8]| -> i32 {
|
||||
@@ -271,7 +278,12 @@ fn stage_and_commit(repo: &Repository) -> Result<Option<git2::Oid>> {
|
||||
};
|
||||
index
|
||||
.add_all(["*"], IndexAddOption::DEFAULT, Some(&mut skip_macos_cruft))
|
||||
.context("staging (add --all)")?;
|
||||
.context("staging new/modified (add --all)")?;
|
||||
// Stage deletions: update_all removes index entries whose working-tree file is
|
||||
// gone. add_all does not do this reliably here (Spike 14), so this is required.
|
||||
index
|
||||
.update_all(["*"], Some(&mut skip_macos_cruft))
|
||||
.context("staging deletions (add -u)")?;
|
||||
index.write().context("writing index")?;
|
||||
let tree = repo.find_tree(index.write_tree().context("writing tree")?)?;
|
||||
|
||||
|
||||
@@ -203,6 +203,7 @@ fn main() -> anyhow::Result<()> {
|
||||
// and no-op for now.
|
||||
ed.set_notice("pull: not wired yet (v0.7)");
|
||||
}
|
||||
Effect::Delete { path, scope } => delete_buffer(&storage, &mut ed, path, scope),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -421,6 +422,33 @@ fn open_buffer(storage: &Storage, ed: &mut Editor, path: String, scope: Scope) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Unlink a file from the card (`:delete`). The editor has already dropped it
|
||||
/// from its model and switched away, so this is pure IO plus the snackbar. For a
|
||||
/// Tracked file the removal is left in the git working copy — the next `:sync`'s
|
||||
/// `add --all` stages the deletion — so nothing git-specific happens here. A
|
||||
/// failure keeps the file on disk and says so; the buffer has still switched, so
|
||||
/// the file is recoverable by re-opening it.
|
||||
fn delete_buffer(storage: &Storage, ed: &mut Editor, path: String, scope: Scope) {
|
||||
// Scope-qualified label (`repo/notes.md`), so the snackbar names exactly which
|
||||
// file left the card — and, for a Tracked file, that the removal is only local
|
||||
// until the next `:sync` publishes it (deleting from the card alone never
|
||||
// touches the remote — that mirrors how a Save is local until Publish).
|
||||
let label = path.strip_prefix("/sd/").unwrap_or(&path);
|
||||
match storage.delete_path(&path) {
|
||||
Ok(()) => {
|
||||
log::info!("deleted {path} ({scope:?})");
|
||||
ed.set_notice(match scope {
|
||||
Scope::Tracked => format!("deleted {label} - :sync to publish"),
|
||||
Scope::Local => format!("deleted {label}"),
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("delete {path} FAILED ({e:#})");
|
||||
ed.set_notice(format!("delete FAILED: {label}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Enumerate the palette's openable files: the top-level regular files in
|
||||
/// `/sd/repo` and `/sd/local`, as absolute paths. Skips dotfiles (so `.git`,
|
||||
/// `.typoena.toml`, and the like never show) and anything that isn't a plain
|
||||
|
||||
@@ -308,6 +308,20 @@ impl Storage {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Unlink a file under `/sd` (`:delete`). Tolerates a missing target — an
|
||||
/// already-gone file is a success, so the call is idempotent. Also clears a
|
||||
/// stray `{path}.tmp` best-effort, so a crash-interrupted save can't leave the
|
||||
/// file half-present after a delete. For a Tracked file this leaves the
|
||||
/// working copy short one file; the next publish's `add --all` stages it.
|
||||
pub fn delete_path(&self, path: &str) -> Result<()> {
|
||||
let _ = fs::remove_file(format!("{path}.tmp"));
|
||||
match fs::remove_file(path) {
|
||||
Ok(()) => Ok(()),
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
|
||||
Err(e) => Err(e).with_context(|| format!("unlink {path}")),
|
||||
}
|
||||
}
|
||||
|
||||
/// Reconcile a leftover `notes.md.tmp` at boot. The save sequence is
|
||||
/// write-tmp → fsync → unlink-target → rename, so a lingering tmp means the
|
||||
/// last save was interrupted. Which way to recover depends on whether the
|
||||
|
||||
Reference in New Issue
Block a user