mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-08 08:52:12 +00:00
* fix(vcs): keep local changes and staging intact across commit, checkout and history edits - Commit: changes left out of a commit keep their staged state. The commit is made from a copy of the index (included files staged there, excluded ones reset to HEAD), so the user's index and other repositories are untouched. - Smart Checkout restores staged changes as staged; untracked files a checkout would overwrite are listed instead of a raw git error. - Edit Commit Message rewrites the commit and its descendants without a rebase, so the working tree and local changes stay as they are. - Edit Commit Message, Drop and Squash offer Undo in their notification while the branch hasn't moved since. - Interactive rebases (Drop, Squash, Interactively Rebase) put local changes aside themselves and bring them back with their staged state once the rebase is over, also after a stop for conflicts. - Undo Commit asks for the target changelist (a new one named after the commit) and puts the commit message back into the commit message field. - Cherry-pick and revert skip commits whose changes are already applied instead of stopping with an empty commit; commits cherry-picked from a protected branch get "(cherry picked from commit …)". - Commit tool window: Commit File…, Accept Yours / Accept Theirs on conflicted files and Copy as Patch to Clipboard. * fix(vcs): restore staged changes after merge, rebase and update; roll back partial multi-repository operations - Merge, rebase, pull / Update Project and the rebase of a diverged checkout put local changes aside themselves and bring them back with --index, so staged changes stay staged (git's --autostash restored them unstaged). An operation stopped on conflicts keeps them aside until it is continued, skipped, aborted or concluded by a commit. - Checkout, New Branch, Merge, Rebase and Delete over several repositories: when one repository fails after others succeeded, ask whether to roll those back (check out the previous branch, delete the new branch, reset to the commit before, restore the branch); not when it stopped on conflicts. - Fixup… / Squash Into…: while the prepared message is in the field, the second commit button is Commit and Rebase, which commits and autosquashes the new commit into its target. - Undo on a history rewrite replaces its notification, so the link is gone once used.
69 lines
3.0 KiB
Rust
69 lines
3.0 KiB
Rust
//! Local changes put aside around merge, rebase and pull, and brought back once the
|
|
//! operation is over: staged changes as staged (git's `--autostash` brings everything
|
|
//! back unstaged). An operation that stops on conflicts keeps them aside until it is
|
|
//! continued, skipped, aborted or concluded by a commit.
|
|
|
|
use crate::Repo;
|
|
use crate::cli::{self, run};
|
|
use crate::write::unmerged_paths;
|
|
use ide_core::{IdeError, IdeResult};
|
|
|
|
/// `.git/<MARKER>` holds the id of the stash the changes went to.
|
|
const MARKER: &str = "openidea-saved-changes";
|
|
|
|
/// Stashes the local changes of tracked files, if any.
|
|
pub(crate) fn save(r: &Repo, message: &str) -> IdeResult<()> {
|
|
let marker = r.git_dir.join(MARKER);
|
|
if marker.exists() {
|
|
// Changes saved by an operation still in progress stay where they are; those of
|
|
// one concluded outside the IDE come back first.
|
|
if r.branch_info()?.operation.is_some() {
|
|
return Ok(());
|
|
}
|
|
restore(r)?;
|
|
}
|
|
if run(r.root(), &["status", "--porcelain", "--untracked-files=no"], None)?.stdout_str().trim().is_empty() {
|
|
return Ok(());
|
|
}
|
|
let before = run(r.root(), &["rev-parse", "-q", "--verify", "refs/stash"], None).map(|o| o.stdout_str()).ok();
|
|
run(r.root(), &["stash", "push", "-q", "-m", message], None)?;
|
|
let id = run(r.root(), &["rev-parse", "refs/stash"], None)?.stdout_str();
|
|
if before.as_deref().map(str::trim) == Some(id.trim()) {
|
|
return Err(IdeError::invalid("could not save the local changes"));
|
|
}
|
|
std::fs::write(marker, id.trim())?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Brings the saved changes back once no operation is in progress. Fails when they come
|
|
/// back with conflicts (they then also stay in the stash).
|
|
pub(crate) fn restore(r: &Repo) -> IdeResult<()> {
|
|
let marker = r.git_dir.join(MARKER);
|
|
let Ok(saved) = std::fs::read_to_string(&marker) else { return Ok(()) };
|
|
if r.branch_info()?.operation.is_some() {
|
|
return Ok(());
|
|
}
|
|
let _ = std::fs::remove_file(&marker);
|
|
let saved = saved.trim();
|
|
let stashes = run(r.root(), &["stash", "list", "--format=%H"], None)?.stdout_str();
|
|
let Some(index) = stashes.lines().position(|id| id.trim() == saved) else { return Ok(()) };
|
|
let entry = format!("stash@{{{index}}}");
|
|
if cli::run_raw(r.root(), &["stash", "pop", "-q", "--index", &entry], None)?.code == Some(0) {
|
|
return Ok(());
|
|
}
|
|
// The staged part no longer applies as it was: bring everything back unstaged.
|
|
if unmerged_paths(r)?.is_empty() && cli::run_raw(r.root(), &["stash", "pop", "-q", &entry], None)?.code == Some(0) {
|
|
return Ok(());
|
|
}
|
|
Err(IdeError::invalid("local changes were restored with conflicts; resolve them (a copy stays in the stash)"))
|
|
}
|
|
|
|
/// Runs `op` with the local changes put aside, and brings them back if it finished.
|
|
pub(crate) fn around<T>(r: &Repo, message: &str, op: impl FnOnce() -> IdeResult<T>) -> IdeResult<T> {
|
|
save(r, message)?;
|
|
let result = op();
|
|
let restored = restore(r);
|
|
let value = result?;
|
|
restored.map(|_| value)
|
|
}
|