284 lines
9.4 KiB
Rust
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(())
|
|
}
|