228 lines
7.8 KiB
Rust
228 lines
7.8 KiB
Rust
//! Stashes are standard two-parent Git commits with a refs/stash reflog.
|
|
use crate::{
|
|
Result, branches, changes, checkout, cherry_pick, local, output, repo::error, staging,
|
|
};
|
|
use gix::{
|
|
bstr::ByteSlice,
|
|
refs::{
|
|
Target,
|
|
transaction::{Change, LogChange, PreviousValue, RefEdit},
|
|
},
|
|
};
|
|
fn log_path(repo: &gix::Repository) -> std::path::PathBuf {
|
|
repo.common_dir().join("logs/refs/stash")
|
|
}
|
|
fn logs(repo: &gix::Repository) -> Result<Vec<String>> {
|
|
match std::fs::read_to_string(log_path(repo)) {
|
|
Ok(text) => Ok(text.lines().map(str::to_owned).collect()),
|
|
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(vec![]),
|
|
Err(e) => Err(error("stash log", e)),
|
|
}
|
|
}
|
|
fn commit(
|
|
repo: &gix::Repository,
|
|
tree: gix::ObjectId,
|
|
parents: Vec<gix::ObjectId>,
|
|
message: &str,
|
|
) -> Result<gix::ObjectId> {
|
|
let sig = staging::signature(None, None)?;
|
|
repo.write_object(&gix::objs::Commit {
|
|
tree,
|
|
parents: parents.into_iter().collect(),
|
|
author: sig.clone(),
|
|
committer: sig,
|
|
encoding: None,
|
|
message: message.into(),
|
|
extra_headers: vec![],
|
|
})
|
|
.map(|id| id.detach())
|
|
.map_err(|e| error("stash commit", e))
|
|
}
|
|
fn update(repo: &gix::Repository, id: gix::ObjectId, message: &str) -> Result<()> {
|
|
branches::edit_refs(
|
|
repo,
|
|
vec![RefEdit {
|
|
name: "refs/stash".try_into().unwrap(),
|
|
deref: false,
|
|
change: Change::Update {
|
|
expected: PreviousValue::Any,
|
|
new: Target::Object(id),
|
|
log: LogChange {
|
|
message: message.into(),
|
|
force_create_reflog: true,
|
|
..Default::default()
|
|
},
|
|
},
|
|
}],
|
|
)?;
|
|
Ok(())
|
|
}
|
|
fn drop_entry(repo: &gix::Repository, n: usize, id: gix::ObjectId, quiet: bool) -> Result<()> {
|
|
let mut rows = logs(repo)?;
|
|
let offset = rows
|
|
.len()
|
|
.checked_sub(n + 1)
|
|
.ok_or_else(|| error("stash", "no such stash"))?;
|
|
rows.remove(offset);
|
|
if rows.is_empty() {
|
|
repo.find_reference("refs/stash")
|
|
.map_err(|e| error("stash ref", e))?
|
|
.delete()
|
|
.map_err(|e| error("stash drop", e))?;
|
|
} else {
|
|
let tip = rows.last().unwrap().split_whitespace().nth(1).unwrap();
|
|
update(
|
|
repo,
|
|
gix::ObjectId::from_hex(tip.as_bytes()).map_err(|e| error("stash id", e))?,
|
|
"stash drop",
|
|
)?;
|
|
std::fs::write(log_path(repo), format!("{}\n", rows.join("\n")))
|
|
.map_err(|e| error("stash log", e))?;
|
|
}
|
|
if !quiet {
|
|
println!("Dropped refs/stash@{{{n}}} ({id})");
|
|
}
|
|
Ok(())
|
|
}
|
|
pub(crate) fn run(
|
|
repo: &gix::Repository,
|
|
command: &str,
|
|
message: Option<String>,
|
|
entry: Option<String>,
|
|
quiet: bool,
|
|
) -> Result<i32> {
|
|
if !matches!(command, "push" | "list" | "apply" | "pop" | "drop") {
|
|
return Err(error("stash", "unsupported subcommand"));
|
|
}
|
|
if command == "push" {
|
|
crate::merge::ensure_idle(repo)?;
|
|
let status = changes::status(repo)?;
|
|
if status.staged.is_empty() && status.unstaged.is_empty() {
|
|
if !quiet {
|
|
println!("No local changes to save");
|
|
}
|
|
return Ok(0);
|
|
}
|
|
let head = branches::resolve(repo, "HEAD")?;
|
|
let original = repo.find_commit(head).map_err(|e| error("stash HEAD", e))?;
|
|
let subject = original
|
|
.decode()
|
|
.map_err(|e| error("stash message", e))?
|
|
.message
|
|
.to_str_lossy()
|
|
.lines()
|
|
.next()
|
|
.unwrap_or("")
|
|
.to_owned();
|
|
let branch = crate::head_info(repo)?
|
|
.branch
|
|
.unwrap_or_else(|| "(no branch)".into())
|
|
.trim_start_matches("refs/heads/")
|
|
.to_owned();
|
|
let description = format!("{branch}: {} {subject}", &head.to_string()[..7]);
|
|
let index = repo.index_or_empty().map_err(|e| error("stash index", e))?;
|
|
let index_tree = staging::tree_from_index(repo, &index)?;
|
|
let index_commit = commit(
|
|
repo,
|
|
index_tree,
|
|
vec![head],
|
|
&format!("index on {description}\n"),
|
|
)?;
|
|
let work_tree = staging::tracked_tree(repo)?;
|
|
let message = message
|
|
.map(|m| format!("On {branch}: {m}"))
|
|
.unwrap_or_else(|| format!("WIP on {description}"));
|
|
let stash = commit(repo, work_tree, vec![head, index_commit], &message)?;
|
|
update(repo, stash, &message)?;
|
|
checkout::write_tree_to_worktree(repo, None, local::tree(repo, "HEAD")?, true)?;
|
|
if !quiet {
|
|
println!("Saved working directory and index state {message}");
|
|
}
|
|
return Ok(0);
|
|
}
|
|
let rows = logs(repo)?;
|
|
if command == "list" {
|
|
for (n, row) in rows.iter().rev().enumerate() {
|
|
println!(
|
|
"stash@{{{n}}}: {}",
|
|
row.split_once('\t').map(|(_, m)| m).unwrap_or("")
|
|
);
|
|
}
|
|
return Ok(0);
|
|
}
|
|
let n = entry
|
|
.as_deref()
|
|
.map(|s| {
|
|
s.strip_prefix("stash@{")
|
|
.and_then(|s| s.strip_suffix('}'))
|
|
.unwrap_or(s)
|
|
.parse::<usize>()
|
|
.map_err(|e| error("stash entry", e))
|
|
})
|
|
.transpose()?
|
|
.unwrap_or(0);
|
|
let Some(row) = rows.iter().rev().nth(n) else {
|
|
eprintln!("No stash entries found.");
|
|
return Ok(1);
|
|
};
|
|
let id = gix::ObjectId::from_hex(row.split_whitespace().nth(1).unwrap().as_bytes())
|
|
.map_err(|e| error("stash id", e))?;
|
|
if command == "drop" {
|
|
drop_entry(repo, n, id, quiet)?;
|
|
return Ok(0);
|
|
}
|
|
if !matches!(command, "apply" | "pop") {
|
|
eprintln!("usage: git stash [push|list|pop|apply|drop]");
|
|
return Ok(129);
|
|
}
|
|
let stash = repo.find_commit(id).map_err(|e| error("stash commit", e))?;
|
|
let decoded = stash.decode().map_err(|e| error("stash decode", e))?;
|
|
let base = local::tree(repo, &decoded.parents[0].to_string())?;
|
|
let old_index = repo.index_or_empty().map_err(|e| error("stash index", e))?;
|
|
let ours = staging::tracked_tree(repo)?;
|
|
let (merged, mut index, conflicts) = cherry_pick::merge_trees(
|
|
repo,
|
|
base,
|
|
ours,
|
|
decoded.tree(),
|
|
"Updated upstream",
|
|
"Stashed changes",
|
|
)?;
|
|
checkout::write_tree_to_worktree(repo, Some(ours), merged, true)?;
|
|
if conflicts.is_empty() {
|
|
// Without --index, tracked modifications remain unstaged. Stashed additions stay staged.
|
|
let base_index = gix::index::State::from_tree(&base, &repo.objects, Default::default())
|
|
.map_err(|e| error("stash base", e))?;
|
|
let added = index
|
|
.entries()
|
|
.iter()
|
|
.filter(|e| {
|
|
!base_index
|
|
.entries()
|
|
.iter()
|
|
.any(|b| b.path(&base_index) == e.path(&index))
|
|
})
|
|
.map(|e| e.path(&index).to_str_lossy().into_owned())
|
|
.collect::<std::collections::BTreeSet<_>>();
|
|
index.remove_entries(|_, path, _| !added.contains(path.to_str_lossy().as_ref()));
|
|
for e in old_index.entries() {
|
|
if !added.contains(e.path(&old_index).to_str_lossy().as_ref()) {
|
|
index.dangerously_push_entry(e.stat, e.id, e.flags, e.mode, e.path(&old_index));
|
|
}
|
|
}
|
|
}
|
|
staging::publish(repo, index)?;
|
|
let repo = crate::repo::open(&repo.workdir().unwrap().to_string_lossy())?;
|
|
output::conflicts(&conflicts, false);
|
|
if !quiet {
|
|
output::long_status(&changes::status(&repo)?);
|
|
}
|
|
if !conflicts.is_empty() {
|
|
return Ok(1);
|
|
}
|
|
if command == "pop" {
|
|
drop_entry(&repo, n, id, quiet)?;
|
|
}
|
|
Ok(0)
|
|
}
|