#[cfg(any(target_os = "linux", target_os = "macos"))]
use std::collections::{BTreeMap, BTreeSet};
use std::io::Write as _;
use std::path::{Path, PathBuf};
use std::sync::atomic::AtomicBool;
use crate::index::{self, Entry, Mode, Stage};
use crate::pathspec::Pathspec;
use crate::refs::{Dereference, Expected, RefEdit, RefName, Reflog, Target};
use crate::{EntryMode, ObjectId, Repository, Signature, TreeCompareLimits};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum ResetMode {
Soft,
Mixed,
Keep,
Merge,
Hard,
}
#[derive(Clone, Debug)]
pub struct ResetOptions {
committer: Signature,
target: Vec<u8>,
index: index::Limits,
trees: TreeCompareLimits,
}
impl ResetOptions {
pub fn new(committer: Signature, target: impl Into<Vec<u8>>) -> Self {
Self {
committer,
target: target.into(),
index: index::Limits::default(),
trees: TreeCompareLimits::default(),
}
}
pub fn with_index_limits(mut self, limits: index::Limits) -> Self {
self.index = limits;
self
}
pub fn with_tree_limits(mut self, limits: TreeCompareLimits) -> Self {
self.trees = limits;
self
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ResetError {
#[error("a mixed or path reset needs a working tree")]
Bare,
#[error("cannot do a soft reset in the middle of a merge")]
MergeInProgress,
#[error("updating working files: {0}")]
Checkout(#[source] Box<crate::checkout::Failure>),
#[error("a keep reset needs a commit on HEAD")]
NoCommit,
#[error(
"{} has local changes that the reset would overwrite",
String::from_utf8_lossy(path)
)]
LocalChanges {
path: Vec<u8>,
},
#[error("reset does not support {0} yet")]
Unsupported(&'static str),
#[error("pathspecs with attr: magic are not supported for reset")]
PathspecAttributes,
#[error(transparent)]
Commit(#[from] crate::ReadCommitError),
#[error(transparent)]
Tree(#[from] crate::TreeCompareError),
#[error(transparent)]
IndexStorage(#[from] index::StorageError),
#[error(transparent)]
Index(#[from] index::Error),
#[error(transparent)]
Head(#[from] crate::refs::ReferenceError),
#[error("writing ORIG_HEAD: {0}")]
OrigHead(#[source] std::io::Error),
#[error("moving HEAD (index updated: {index_updated}): {source}")]
Reference {
index_updated: bool,
#[source]
source: crate::refs::TransactionError,
},
#[error("removing {}: {source}", path.display())]
OperationFile {
path: PathBuf,
#[source]
source: std::io::Error,
},
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
struct Selected {
paths: BTreeSet<Vec<u8>>,
clean: BTreeSet<Vec<u8>>,
replacement: Vec<Entry>,
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
impl Selected {
fn selection(&self) -> crate::checkout::Selection<'_> {
crate::checkout::Selection {
paths: &self.paths,
clean: &self.clean,
replacement: &self.replacement,
}
}
}
const OPERATION_FILES: &[&str] = &[
"MERGE_HEAD",
"MERGE_MODE",
"MERGE_MSG",
"AUTO_MERGE",
"MERGE_RR",
"SQUASH_MSG",
"CHERRY_PICK_HEAD",
"REVERT_HEAD",
];
impl Repository {
pub fn reset(
&self,
commit: ObjectId,
mode: ResetMode,
options: &ResetOptions,
) -> Result<Option<ObjectId>, ResetError> {
self.reset_with(commit, mode, options, None)
}
pub fn reset_with_filters(
&self,
commit: ObjectId,
mode: ResetMode,
options: &ResetOptions,
filters: &mut crate::filter::Pipeline,
) -> Result<Option<ObjectId>, ResetError> {
self.reset_with(commit, mode, options, Some(filters))
}
fn reset_with(
&self,
commit: ObjectId,
mode: ResetMode,
options: &ResetOptions,
filters: Option<&mut crate::filter::Pipeline>,
) -> Result<Option<ObjectId>, ResetError> {
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
{
let _ = filters;
if matches!(mode, ResetMode::Hard | ResetMode::Keep | ResetMode::Merge) {
return Err(ResetError::Unsupported("hard reset on this platform"));
}
}
if self.reference_backend() == crate::refs::Backend::Reftable {
return Err(ResetError::Unsupported("reftable repositories"));
}
if mode == ResetMode::Soft && self.git_dir().join("MERGE_HEAD").exists() {
return Err(ResetError::MergeInProgress);
}
if mode != ResetMode::Soft && self.worktree().is_none() {
return Err(ResetError::Bare);
}
let objects = self
.objects()
.map_err(|source| ResetError::Commit(crate::ReadCommitError::Read(source)))?;
let tree = objects.read_commit(commit)?.tree();
if mode == ResetMode::Soft
&& let Some(index) = self.read_index(options.index)?
&& index
.entries()
.iter()
.any(|entry| entry.stage != Stage::Normal)
{
return Err(ResetError::MergeInProgress);
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
let selected = match mode {
ResetMode::Keep | ResetMode::Merge => {
Some(self.select_local(&objects, Some(tree), mode, options)?)
}
_ => None,
};
let mut edit = match mode {
ResetMode::Soft | ResetMode::Hard | ResetMode::Keep | ResetMode::Merge => None,
ResetMode::Mixed => {
let mut edit = self.edit_index(options.index)?;
refuse_sparse(edit.index().entries())?;
let entries = tree_entries(&objects, Some(tree), &Pathspec::all(), options.trees)?;
let entries = keep_unchanged(edit.index().entries(), entries);
edit.replace_entries_reusing_stat(entries)?;
Some(edit)
}
};
let references = self.references()?;
let previous = references.resolve(&RefName::HEAD)?.id;
let message = [b"reset: moving to ".as_slice(), &options.target].concat();
let reflog = Reflog::Append {
committer: options.committer.clone(),
message,
};
let update = match references.read(&RefName::HEAD)? {
Some(Target::Symbolic(branch)) if previous == Some(commit) => RefEdit::new(
RefName::HEAD,
Some(Target::Symbolic(branch.clone())),
Expected::Value(Target::Symbolic(branch)),
reflog,
)
.with_dereference(Dereference::Stored),
_ => {
let expected =
previous.map_or(Expected::Absent, |id| Expected::Value(Target::Direct(id)));
RefEdit::new(
RefName::HEAD,
Some(Target::Direct(commit)),
expected,
reflog,
)
.with_dereference(Dereference::Follow)
}
};
let prepared = references
.prepare_transaction(&[update])
.map_err(|source| ResetError::Reference {
index_updated: false,
source,
})?;
let index_updated = edit.is_some();
if let Some(edit) = edit.take() {
edit.commit()?;
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
let index_updated = if mode == ResetMode::Hard || selected.is_some() {
let selection = selected.as_ref().map(Selected::selection);
hard(self, tree, filters, selection.as_ref(), options)?;
true
} else {
index_updated
};
if let Some(previous) = previous {
write_orig_head(self.git_dir(), previous).map_err(ResetError::OrigHead)?;
}
prepared.publish().map_err(|source| ResetError::Reference {
index_updated,
source,
})?;
remove_operation_files(self.git_dir())?;
Ok(previous)
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
fn select_local(
&self,
objects: &crate::Objects,
tree: Option<ObjectId>,
mode: ResetMode,
options: &ResetOptions,
) -> Result<Selected, ResetError> {
let current = self
.read_index(options.index)?
.map(|index| index.entries().to_vec())
.unwrap_or_default();
refuse_sparse(¤t)?;
let unmerged: BTreeSet<Vec<u8>> = current
.iter()
.filter(|entry| entry.stage != Stage::Normal)
.map(|entry| entry.path.clone())
.collect();
if mode == ResetMode::Keep && !unmerged.is_empty() {
return Err(ResetError::MergeInProgress);
}
let target = tree_entries(objects, tree, &Pathspec::all(), options.trees)?;
let leaves = |entries: &[Entry]| -> BTreeMap<Vec<u8>, (Mode, ObjectId)> {
entries
.iter()
.filter(|entry| entry.stage == Stage::Normal)
.map(|entry| (entry.path.clone(), (entry.mode, entry.id)))
.collect()
};
let index_leaves = leaves(¤t);
let target_leaves = leaves(&target);
let mut paths: BTreeSet<Vec<u8>> = index_leaves
.keys()
.chain(target_leaves.keys())
.filter(|path| index_leaves.get(*path) != target_leaves.get(*path))
.cloned()
.collect();
paths.extend(unmerged.iter().cloned());
if mode == ResetMode::Keep {
let head_tree = match self.head()?.id {
Some(commit) => Some(objects.read_commit(commit)?.tree()),
None => None,
};
let head_leaves = leaves(&tree_entries(
objects,
head_tree,
&Pathspec::all(),
options.trees,
)?);
paths.retain(|path| head_leaves.get(path) != target_leaves.get(path));
if let Some(path) = paths
.iter()
.find(|path| index_leaves.get(*path) != head_leaves.get(*path))
{
return Err(ResetError::LocalChanges { path: path.clone() });
}
}
if let Some(entry) = current
.iter()
.find(|entry| entry.intent_to_add && paths.contains(&entry.path))
{
return Err(ResetError::LocalChanges {
path: entry.path.clone(),
});
}
let clean = paths.difference(&unmerged).cloned().collect();
let replacement = keep_unchanged(¤t, target);
Ok(Selected {
paths,
clean,
replacement,
})
}
pub fn reset_unborn(&self, mode: ResetMode, limits: &index::Limits) -> Result<(), ResetError> {
match mode {
ResetMode::Soft => Ok(()),
ResetMode::Mixed => self.reset_paths(None, &Pathspec::all(), limits),
#[cfg(any(target_os = "linux", target_os = "macos"))]
ResetMode::Hard => {
if self.worktree().is_none() {
return Err(ResetError::Bare);
}
let checkout = crate::checkout::Limits::default().with_index(*limits);
hard_to(self, None, None, None, checkout)
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
ResetMode::Merge => {
if self.worktree().is_none() {
return Err(ResetError::Bare);
}
let objects = self
.objects()
.map_err(|source| ResetError::Commit(crate::ReadCommitError::Read(source)))?;
let options = ResetOptions {
committer: Signature {
name: Vec::new(),
email: Vec::new(),
seconds: 0,
offset_minutes: 0,
},
target: Vec::new(),
index: *limits,
trees: TreeCompareLimits::default(),
};
let selected = self.select_local(&objects, None, ResetMode::Merge, &options)?;
let checkout = crate::checkout::Limits::default().with_index(*limits);
hard_to(self, None, None, Some(&selected.selection()), checkout)
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
ResetMode::Hard | ResetMode::Merge => {
Err(ResetError::Unsupported("hard reset on this platform"))
}
ResetMode::Keep => Err(ResetError::NoCommit),
}
}
pub fn reset_paths(
&self,
tree: Option<ObjectId>,
paths: &Pathspec,
limits: &index::Limits,
) -> Result<(), ResetError> {
if self.worktree().is_none() {
return Err(ResetError::Bare);
}
let objects = self
.objects()
.map_err(|source| ResetError::Commit(crate::ReadCommitError::Read(source)))?;
let mut edit = self.edit_index(*limits)?;
let current = edit.index().entries();
let mut kept = Vec::with_capacity(current.len());
for entry in current {
if !paths
.matches(&entry.path)
.map_err(|_| ResetError::PathspecAttributes)?
{
kept.push(entry.clone());
continue;
}
refuse_sparse(std::slice::from_ref(entry))?;
}
let added = tree_entries(&objects, tree, paths, TreeCompareLimits::default())?;
let added = keep_unchanged(current, added);
let added_paths: std::collections::BTreeSet<&[u8]> =
added.iter().map(|entry| entry.path.as_slice()).collect();
kept.retain(|entry| {
!added_paths
.iter()
.any(|path| is_inside(path, &entry.path) || is_inside(&entry.path, path))
});
let mut entries = kept;
entries.extend(added);
entries.sort_by(|a, b| (&a.path, a.stage).cmp(&(&b.path, b.stage)));
edit.replace_entries_reusing_stat(entries)?;
edit.commit()?;
Ok(())
}
}
fn keep_unchanged(current: &[Entry], replacement: Vec<Entry>) -> Vec<Entry> {
replacement
.into_iter()
.map(|new| {
current
.binary_search_by(|old| {
(old.path.as_slice(), old.stage).cmp(&(&new.path, Stage::Normal))
})
.ok()
.map(|at| ¤t[at])
.filter(|old| old.id == new.id && old.mode == new.mode && !old.intent_to_add)
.cloned()
.unwrap_or(new)
})
.collect()
}
fn is_inside(path: &[u8], directory: &[u8]) -> bool {
path.len() > directory.len() && path.starts_with(directory) && path[directory.len()] == b'/'
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
fn hard(
repository: &Repository,
tree: ObjectId,
filters: Option<&mut crate::filter::Pipeline>,
selection: Option<&crate::checkout::Selection<'_>>,
options: &ResetOptions,
) -> Result<(), ResetError> {
let limits = crate::checkout::Limits::default()
.with_index(options.index)
.with_trees(options.trees);
hard_to(repository, Some(tree), filters, selection, limits)
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
fn hard_to(
repository: &Repository,
tree: Option<ObjectId>,
filters: Option<&mut crate::filter::Pipeline>,
selection: Option<&crate::checkout::Selection<'_>>,
limits: crate::checkout::Limits,
) -> Result<(), ResetError> {
crate::checkout::run_discarding(
repository,
tree,
filters,
selection,
limits,
&AtomicBool::new(false),
)
.map(drop)
.map_err(|failure| match &*failure.cause {
crate::checkout::Error::Refused { path, reason }
if selection.is_some()
&& [
crate::checkout::LOCAL_CHANGES,
"untracked obstruction",
"untracked ancestor obstruction",
]
.contains(reason) =>
{
ResetError::LocalChanges { path: path.clone() }
}
_ => ResetError::Checkout(Box::new(failure)),
})
}
fn refuse_sparse(entries: &[Entry]) -> Result<(), ResetError> {
for entry in entries {
if entry.mode == Mode::SparseDirectory {
return Err(ResetError::Unsupported("sparse index directories"));
}
if entry.skip_worktree {
return Err(ResetError::Unsupported("skip-worktree entries"));
}
}
Ok(())
}
fn tree_entries(
objects: &crate::Objects,
tree: Option<ObjectId>,
paths: &Pathspec,
limits: TreeCompareLimits,
) -> Result<Vec<Entry>, ResetError> {
let leaves =
objects.compare_trees_matching(None, tree, paths, limits, &AtomicBool::new(false))?;
Ok(leaves
.into_iter()
.filter_map(|change| {
let leaf = change.new?;
let mode = match leaf.mode {
EntryMode::Blob => Mode::Regular,
EntryMode::Executable => Mode::Executable,
EntryMode::Symlink => Mode::Symlink,
EntryMode::Gitlink => Mode::Gitlink,
_ => return None,
};
Some(Entry::new(change.path, mode, leaf.id).with_stage(Stage::Normal))
})
.collect())
}
fn write_orig_head(git_dir: &Path, id: ObjectId) -> std::io::Result<()> {
let path = git_dir.join("ORIG_HEAD");
let lock = git_dir.join("ORIG_HEAD.lock");
let mut file = std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&lock)?;
let written = writeln!(file, "{id}").and_then(|()| file.sync_all());
drop(file);
match written.and_then(|()| std::fs::rename(&lock, &path)) {
Ok(()) => Ok(()),
Err(error) => {
let _ = std::fs::remove_file(&lock);
Err(error)
}
}
}
fn remove_operation_files(git_dir: &Path) -> Result<(), ResetError> {
for name in OPERATION_FILES {
let path = git_dir.join(name);
match std::fs::remove_file(&path) {
Ok(()) => {}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
Err(source) => return Err(ResetError::OperationFile { path, source }),
}
}
Ok(())
}