//! 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 { 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, revision: Option, annotated: bool, message: Option, 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(()) }