git: match native agent workflows
This commit is contained in:
@@ -0,0 +1,227 @@
|
||||
//! 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)
|
||||
}
|
||||
Reference in New Issue
Block a user