Files
sausage/userland/git/src/local.rs
T

284 lines
9.4 KiB
Rust

//! 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(())
}