feat(git): add stash push with optional paths and per-file scope

The stash push feature now supports limiting the stash to
selected files via an optional paths parameter.
The UI passes file paths to stash_push and adds a
per-file context menu for scoped stash operations.

- Extend stash API to accept optional paths for scoped stashes
- Implement per-file stash actions via a status panel context menu
- Update tests and docs to reflect scoped stash behavior
This commit is contained in:
Christoph Brandau
2026-08-17 22:24:22 +02:00
parent e7fbaadf5e
commit b5d15f9190
8 changed files with 328 additions and 20 deletions
+62 -12
View File
@@ -1243,25 +1243,45 @@ pub async fn stash_push(
path: String,
message: Option<String>,
include_untracked: bool,
paths: Option<Vec<String>>,
) -> Result<GitStatus, String> {
run_git_task("Could not stash changes", move || {
let repo = resolve_repo(&path)?;
let trimmed_message = message.unwrap_or_default().trim().to_string();
let mut args: Vec<OsString> = vec![OsString::from("stash"), OsString::from("push")];
if include_untracked {
args.push(OsString::from("--include-untracked"));
}
if !trimmed_message.is_empty() {
args.push(OsString::from("-m"));
args.push(OsString::from(trimmed_message));
}
run_git(&repo, args)?;
status_for_repo(&repo)
stash_push_for_repo(
&repo,
message.as_deref(),
include_untracked,
paths.as_deref().unwrap_or_default(),
)
})
.await
}
fn stash_push_for_repo(
repo: &Path,
message: Option<&str>,
include_untracked: bool,
paths: &[String],
) -> Result<GitStatus, String> {
validate_files(paths)?;
let trimmed_message = message.unwrap_or_default().trim();
let mut args: Vec<OsString> = vec![OsString::from("stash"), OsString::from("push")];
if include_untracked {
args.push(OsString::from("--include-untracked"));
}
if !trimmed_message.is_empty() {
args.push(OsString::from("-m"));
args.push(OsString::from(trimmed_message));
}
if !paths.is_empty() {
args.push(OsString::from("--"));
args.extend(paths.iter().map(OsString::from));
}
run_git(repo, args)?;
status_for_repo(repo)
}
#[tauri::command]
pub async fn stash_apply(path: String, selector: String) -> Result<GitStatus, String> {
run_git_task("Could not apply stash", move || {
@@ -7075,6 +7095,36 @@ mod tests {
assert!(status.files.is_empty());
}
#[test]
fn stash_push_can_be_limited_to_selected_files() {
let repo = init_temp_repo("scoped_stash");
fs::write(repo.path.join("selected.txt"), "initial selected\n")
.expect("selected file should be written");
fs::write(repo.path.join("remaining.txt"), "initial remaining\n")
.expect("remaining file should be written");
run_git_test(&repo.path, ["add", "selected.txt", "remaining.txt"]);
run_git_test(&repo.path, ["commit", "-q", "-m", "initial files"]);
fs::write(repo.path.join("selected.txt"), "stashed change\n")
.expect("selected change should be written");
fs::write(repo.path.join("remaining.txt"), "remaining change\n")
.expect("remaining change should be written");
let paths = vec!["selected.txt".to_string()];
let status = stash_push_for_repo(&repo.path, Some("selected file"), false, &paths)
.expect("selected file should be stashed");
assert!(status.files.iter().all(|file| file.path != "selected.txt"));
assert!(status.files.iter().any(|file| file.path == "remaining.txt"));
assert_eq!(
git_output_test(
&repo.path,
["stash", "show", "--name-only", "--format=", "stash@{0}"],
),
"selected.txt"
);
}
#[test]
fn branches_report_configured_upstream_and_local_only_state() {
let repo = init_temp_repo("branch_upstream_state");