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:
+62
-12
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user