git: match native agent workflows
This commit is contained in:
@@ -0,0 +1,283 @@
|
||||
//! Local reset, path restoration, tags, remotes, and branch renames use ordinary Git metadata.
|
||||
use crate::{Result, branches, changes, checkout, head_info, remotes, repo::error, staging};
|
||||
use gix::bstr::ByteSlice;
|
||||
use gix::index::State;
|
||||
#[cfg(unix)]
|
||||
use gix::index::entry::Mode;
|
||||
use gix::refs::{
|
||||
Target,
|
||||
transaction::{Change, PreviousValue, RefEdit},
|
||||
};
|
||||
|
||||
pub(crate) fn tree(repo: &gix::Repository, revision: &str) -> Result<gix::ObjectId> {
|
||||
Ok(repo
|
||||
.rev_parse_single(revision)
|
||||
.map_err(|e| error("revision", e))?
|
||||
.object()
|
||||
.map_err(|e| error("revision", e))?
|
||||
.peel_to_tree()
|
||||
.map_err(|e| error("tree", e))?
|
||||
.id)
|
||||
}
|
||||
fn selected(path: &str, paths: &[String]) -> bool {
|
||||
paths
|
||||
.iter()
|
||||
.any(|p| p == "." || path == p || path.strip_prefix(p).is_some_and(|s| s.starts_with('/')))
|
||||
}
|
||||
pub(crate) fn restore(
|
||||
repo: &gix::Repository,
|
||||
revision: Option<&str>,
|
||||
paths: &[String],
|
||||
staged: bool,
|
||||
) -> Result<()> {
|
||||
let source = match revision {
|
||||
Some(revision) => {
|
||||
State::from_tree(&tree(repo, revision)?, &repo.objects, Default::default())
|
||||
.map_err(|e| error("restore tree", e))?
|
||||
}
|
||||
None => staging::load(repo)?,
|
||||
};
|
||||
let mut state = staging::load(repo)?;
|
||||
if staged {
|
||||
state.remove_entries(|_, p, _| selected(p.to_str_lossy().as_ref(), paths));
|
||||
for e in source
|
||||
.entries()
|
||||
.iter()
|
||||
.filter(|e| selected(e.path(&source).to_str_lossy().as_ref(), paths))
|
||||
{
|
||||
state.dangerously_push_entry(e.stat, e.id, e.flags, e.mode, e.path(&source));
|
||||
}
|
||||
staging::publish(repo, state)?;
|
||||
} else {
|
||||
let root = crate::repo::worktree(repo)?;
|
||||
for path in paths {
|
||||
if path != "." {
|
||||
crate::repo::validate_path(path)?;
|
||||
}
|
||||
}
|
||||
for e in source
|
||||
.entries()
|
||||
.iter()
|
||||
.filter(|e| selected(e.path(&source).to_str_lossy().as_ref(), paths))
|
||||
{
|
||||
if e.stage_raw() != 0 {
|
||||
return Err(error("restore", "unmerged path"));
|
||||
}
|
||||
staging::safe(root, e.path(&source).to_str_lossy().as_ref())?;
|
||||
let path = root.join(e.path(&source).to_str_lossy().as_ref());
|
||||
let bytes = repo
|
||||
.find_blob(e.id)
|
||||
.map_err(|e| error("restore blob", e))?
|
||||
.data
|
||||
.to_vec();
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent).map_err(|e| error("restore directory", e))?;
|
||||
}
|
||||
std::fs::write(&path, bytes).map_err(|e| error("restore file", e))?;
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
std::fs::set_permissions(
|
||||
&path,
|
||||
std::fs::Permissions::from_mode(if e.mode == Mode::FILE_EXECUTABLE {
|
||||
0o755
|
||||
} else {
|
||||
0o644
|
||||
}),
|
||||
)
|
||||
.map_err(|e| error("restore mode", e))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
pub(crate) fn reset(
|
||||
repo: &gix::Repository,
|
||||
revision: &str,
|
||||
paths: &[String],
|
||||
soft: bool,
|
||||
hard: bool,
|
||||
quiet: bool,
|
||||
) -> Result<()> {
|
||||
if !paths.is_empty() {
|
||||
restore(repo, Some(revision), paths, true)?;
|
||||
} else {
|
||||
let id = branches::resolve(repo, revision)?;
|
||||
let old = head_info(repo)?.id;
|
||||
if hard {
|
||||
checkout::write_tree_to_worktree(repo, None, tree(repo, revision)?, true)?;
|
||||
} else if !soft {
|
||||
let state = State::from_tree(&tree(repo, revision)?, &repo.objects, Default::default())
|
||||
.map_err(|e| error("reset index", e))?;
|
||||
staging::publish(repo, state)?;
|
||||
}
|
||||
if let Some(old) = old {
|
||||
std::fs::write(repo.git_dir().join("ORIG_HEAD"), format!("{old}\n"))
|
||||
.map_err(|e| error("reset ORIG_HEAD", e))?;
|
||||
staging::update_head(
|
||||
repo,
|
||||
Some(gix::ObjectId::from_hex(old.as_bytes()).map_err(|e| error("reset HEAD", e))?),
|
||||
id,
|
||||
&staging::signature(None, None)?,
|
||||
"reset",
|
||||
)?;
|
||||
}
|
||||
if hard && !quiet {
|
||||
let commit = repo.find_commit(id).map_err(|e| error("reset commit", e))?;
|
||||
println!(
|
||||
"HEAD is now at {} {}",
|
||||
&id.to_string()[..7],
|
||||
commit
|
||||
.decode()
|
||||
.map_err(|e| error("reset message", e))?
|
||||
.message
|
||||
.to_str_lossy()
|
||||
.lines()
|
||||
.next()
|
||||
.unwrap_or("")
|
||||
);
|
||||
}
|
||||
}
|
||||
if !soft && !hard && !quiet {
|
||||
let status = changes::status(&crate::repo::open(
|
||||
&repo.workdir().unwrap().to_string_lossy(),
|
||||
)?)?;
|
||||
if !status.unstaged.is_empty() {
|
||||
println!("Unstaged changes after reset:");
|
||||
for c in status.unstaged {
|
||||
println!("{}\t{}", crate::code(&c.status), c.path);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
pub(crate) fn rename_branch(repo: &gix::Repository, old: Option<&str>, new: &str) -> Result<()> {
|
||||
let head = head_info(repo)?;
|
||||
let old = branches::branch_name(
|
||||
old.unwrap_or(
|
||||
head.branch
|
||||
.as_deref()
|
||||
.unwrap_or("HEAD")
|
||||
.trim_start_matches("refs/heads/"),
|
||||
),
|
||||
)?;
|
||||
let new = branches::branch_name(new)?;
|
||||
crate::worktrees::ensure_available(repo, &old)?;
|
||||
let mut edits = vec![
|
||||
RefEdit {
|
||||
name: new
|
||||
.as_str()
|
||||
.try_into()
|
||||
.map_err(|e| error("branch name", e))?,
|
||||
deref: false,
|
||||
change: Change::Update {
|
||||
expected: PreviousValue::MustNotExist,
|
||||
new: Target::Object(branches::resolve(repo, &old)?),
|
||||
log: Default::default(),
|
||||
},
|
||||
},
|
||||
RefEdit {
|
||||
name: old
|
||||
.as_str()
|
||||
.try_into()
|
||||
.map_err(|e| error("branch name", e))?,
|
||||
deref: false,
|
||||
change: Change::Delete {
|
||||
expected: PreviousValue::MustExist,
|
||||
log: gix::refs::transaction::RefLog::AndReference,
|
||||
},
|
||||
},
|
||||
];
|
||||
if head.branch.as_deref() == Some(old.as_str()) {
|
||||
edits.push(RefEdit {
|
||||
name: "HEAD".try_into().unwrap(),
|
||||
deref: false,
|
||||
change: Change::Update {
|
||||
expected: PreviousValue::Any,
|
||||
new: Target::Symbolic(new.as_str().try_into().unwrap()),
|
||||
log: Default::default(),
|
||||
},
|
||||
});
|
||||
}
|
||||
branches::edit_refs(repo, edits)?;
|
||||
let mut config = remotes::load(repo)?;
|
||||
let old_short = old.trim_start_matches("refs/heads/");
|
||||
if config.section("branch", old_short).is_ok() {
|
||||
config
|
||||
.rename_section(
|
||||
"branch",
|
||||
old_short,
|
||||
"branch",
|
||||
new.trim_start_matches("refs/heads/"),
|
||||
)
|
||||
.map_err(|e| error("branch config", e))?;
|
||||
remotes::save(repo, &config)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
pub(crate) fn tag(
|
||||
repo: &gix::Repository,
|
||||
name: Option<String>,
|
||||
revision: Option<String>,
|
||||
annotated: bool,
|
||||
message: Option<String>,
|
||||
delete: bool,
|
||||
) -> Result<()> {
|
||||
let Some(name) = name else {
|
||||
for reference in repo
|
||||
.references()
|
||||
.map_err(|e| error("tags", e))?
|
||||
.all()
|
||||
.map_err(|e| error("tags", e))?
|
||||
{
|
||||
let reference = reference.map_err(|e| error("tag", e))?;
|
||||
if reference.name().as_bstr().starts_with(b"refs/tags/") {
|
||||
println!("{}", reference.name().shorten());
|
||||
}
|
||||
}
|
||||
return Ok(());
|
||||
};
|
||||
let full = format!("refs/tags/{name}");
|
||||
if delete {
|
||||
let reference = repo
|
||||
.find_reference(full.as_str())
|
||||
.map_err(|e| error("tag", e))?;
|
||||
let id = reference
|
||||
.target()
|
||||
.try_id()
|
||||
.ok_or_else(|| error("tag", "symbolic tag"))?
|
||||
.to_string();
|
||||
reference.delete().map_err(|e| error("tag delete", e))?;
|
||||
println!("Deleted tag '{name}' (was {})", &id[..7]);
|
||||
} else {
|
||||
let id = branches::resolve(repo, revision.as_deref().unwrap_or("HEAD"))?;
|
||||
let id = if annotated || message.is_some() {
|
||||
let object = gix::objs::Tag {
|
||||
target: id,
|
||||
target_kind: gix::objs::Kind::Commit,
|
||||
name: name.clone().into(),
|
||||
tagger: Some(staging::signature(None, None)?),
|
||||
message: format!("{}\n", message.unwrap_or_default().trim_end()).into(),
|
||||
signature: None,
|
||||
};
|
||||
repo.write_object(&object)
|
||||
.map_err(|e| error("tag object", e))?
|
||||
.detach()
|
||||
} else {
|
||||
id
|
||||
};
|
||||
branches::edit_refs(
|
||||
repo,
|
||||
vec![RefEdit {
|
||||
name: full.as_str().try_into().map_err(|e| error("tag name", e))?,
|
||||
deref: false,
|
||||
change: Change::Update {
|
||||
expected: PreviousValue::MustNotExist,
|
||||
new: Target::Object(id),
|
||||
log: Default::default(),
|
||||
},
|
||||
}],
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user