use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::Instant;
use anyhow::{Context, Result, bail};
use crate::core::hunk_select;
use crate::core::msg;
use crate::trace as loom_trace;
pub fn apply_patch(workdir: &Path, patch: &str) -> Result<()> {
apply_patch_with_flags(workdir, patch, &[])
}
pub fn apply_patch_reverse(workdir: &Path, patch: &str) -> Result<()> {
apply_patch_with_flags(workdir, patch, &["--reverse"])
}
pub fn apply_patch_to_worktree(workdir: &Path, patch: &str) -> Result<()> {
let Err(plain) = apply_patch(workdir, patch) else {
return Ok(());
};
let Ok(before) = super::worktree_dirty_paths(workdir) else {
return Err(plain);
};
if apply_patch_three_way(workdir, patch)?.is_err() {
let written: Vec<String> = super::worktree_dirty_paths(workdir)
.unwrap_or_default()
.difference(&before)
.cloned()
.collect();
restore_from_index(workdir, &written);
return Err(plain);
}
Ok(())
}
fn apply_patch_three_way(workdir: &Path, patch: &str) -> Result<Result<()>> {
with_scratch_index(workdir, "apply", |scratch| {
run_apply(
workdir,
patch,
&["-c", "rerere.enabled=false"],
&["--3way"],
Some(scratch),
)
})
}
fn with_scratch_index(
workdir: &Path,
name: &str,
apply: impl FnOnce(&Path) -> Result<()>,
) -> Result<Result<()>> {
let index = super::git_path(workdir, "index")?;
let scratch = index.with_file_name(format!("loom-{name}-index-{}", std::process::id()));
std::fs::copy(&index, &scratch).with_context(|| {
format!(
"Failed to copy '{}' to '{}'",
index.display(),
scratch.display()
)
})?;
let _cleanup = ScratchIndex(&scratch);
Ok(apply(&scratch))
}
struct ScratchIndex<'a>(&'a Path);
impl Drop for ScratchIndex<'_> {
fn drop(&mut self) {
let _ = std::fs::remove_file(self.0);
let _ = std::fs::remove_file(format!("{}.lock", self.0.display()));
}
}
fn restore_from_index(workdir: &Path, paths: &[String]) {
if paths.is_empty() {
return;
}
let refs: Vec<&str> = paths.iter().map(|p| p.as_str()).collect();
let tracked = super::ls_files(workdir, &refs).unwrap_or_default();
if !tracked.is_empty() {
let tracked_refs: Vec<&str> = tracked.iter().map(|p| p.as_str()).collect();
let _ = super::checkout_index_force(workdir, &tracked_refs);
}
for path in paths {
if !tracked.contains(path) {
let _ = std::fs::remove_file(workdir.join(path));
}
}
}
pub fn apply_cached_patch(workdir: &Path, patch: &str) -> Result<()> {
apply_patch_with_flags(workdir, patch, &["--cached"])
}
pub fn apply_cached_patch_reverse(workdir: &Path, patch: &str) -> Result<()> {
apply_patch_with_flags(workdir, patch, &["--cached", "--reverse"])
}
pub fn apply_patch_with_index(workdir: &Path, patch: &str) -> Result<()> {
apply_patch_with_flags(workdir, patch, &["--index"])
}
pub fn apply_patch_with_index_reverse(workdir: &Path, patch: &str) -> Result<()> {
apply_patch_with_flags(workdir, patch, &["--index", "--reverse"])
}
fn apply_patch_with_flags(workdir: &Path, patch: &str, flags: &[&str]) -> Result<()> {
run_apply(workdir, patch, &[], flags, None)
}
fn run_apply(
workdir: &Path,
patch: &str,
config: &[&str],
flags: &[&str],
index_file: Option<&Path>,
) -> Result<()> {
let mut args = vec!["apply"];
args.extend(flags);
let start = Instant::now();
let mut command = Command::new("git");
command
.current_dir(workdir)
.args(super::FORCED_CONFIG)
.args(config)
.args(&args)
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped());
if let Some(index) = index_file {
command.env("GIT_INDEX_FILE", index);
}
let mut child = command.spawn()?;
if let Some(mut stdin) = child.stdin.take() {
stdin.write_all(patch.as_bytes())?;
}
let output = child.wait_with_output()?;
let duration_ms = start.elapsed().as_millis();
let stderr = String::from_utf8_lossy(&output.stderr);
let mut logged: Vec<&str> = config.to_vec();
logged.extend(&args);
let index_note = index_file.map(|i| format!("GIT_INDEX_FILE={}", i.display()));
if let Some(note) = &index_note {
logged.insert(0, note);
}
loom_trace::log_command(
"git",
&logged.join(" "),
duration_ms,
output.status.success(),
&stderr,
);
if !output.status.success() {
bail!("git apply failed");
}
Ok(())
}
pub fn restore_staged_after_rebase(workdir: &Path, patch: &str) {
if patch.is_empty() {
return;
}
if super::has_unmerged_paths(workdir) {
msg::warn(
"the index has unmerged paths, so your staged changes could not go back \
— resolve them, then replay the patch below",
);
park(workdir, patch);
return;
}
match rehearse_cached_three_way(workdir, patch) {
Ok(Ok(())) => {
if let Err(e) = cached_three_way(workdir, patch, None) {
msg::warn(&format!("could not restore your staged changes: {e}"));
if let Some(hint) = unmerged_reset_hint(workdir) {
msg::warn(&hint);
}
park(workdir, patch);
}
}
Ok(Err(e)) => {
if super::diff_cached(workdir).is_ok_and(|current| current == patch) {
return;
}
msg::warn(&format!(
"your staged changes no longer apply over what is in the index now: {e}"
));
park(workdir, patch);
}
Err(e) => {
msg::warn(&format!(
"could not test whether your staged changes still apply: {e}"
));
park(workdir, patch);
}
}
}
fn unmerged_pathspecs(workdir: &Path) -> Vec<String> {
super::unmerged_paths(workdir)
.iter()
.map(|p| format!(":(top,literal){p}"))
.collect()
}
fn unmerged_reset_hint(workdir: &Path) -> Option<String> {
reset_hint_for(&unmerged_pathspecs(workdir))
}
fn reset_hint_for(specs: &[String]) -> Option<String> {
if specs.is_empty() {
return None;
}
if specs
.iter()
.any(|s| s.contains(char::REPLACEMENT_CHARACTER))
{
return Some(
"it left unmerged entries in the index — `git status` names them, and a \
`git reset` limited to those paths clears them"
.to_string(),
);
}
let quoted: Vec<String> = specs.iter().map(|s| hunk_select::quoted(s)).collect();
Some(format!(
"it left unmerged entries in the index — `git reset -- {}` clears them",
quoted.join(" ")
))
}
pub fn restore_loom_unstaged(workdir: &Path, patch: &str) {
if patch.is_empty() {
return;
}
if super::rebase_is_over(workdir) {
restore_staged_after_rebase(workdir, patch);
} else {
msg::warn("the rebase is still on disk, so your staged changes could not be put back");
park(workdir, patch);
}
}
pub fn restore_or_park_after_abort(workdir: &Path, patch: &str, err: &anyhow::Error) {
if super::rebase_never_started(err) {
return;
}
restore_loom_unstaged(workdir, patch);
}
fn park(workdir: &Path, patch: &str) {
save_or_warn(workdir, "unrestored-staged", patch, Replay::CachedThreeWay);
}
fn cached_three_way(workdir: &Path, patch: &str, index_file: Option<&Path>) -> Result<()> {
run_apply(
workdir,
patch,
&["-c", "rerere.enabled=false"],
&["--cached", "--3way"],
index_file,
)
}
fn rehearse_cached_three_way(workdir: &Path, patch: &str) -> Result<Result<()>> {
with_scratch_index(workdir, "restage", |scratch| {
cached_three_way(workdir, patch, Some(scratch))
})
}
pub fn restore_staged_patch(workdir: &Path, patch: &str) {
if !patch.is_empty()
&& let Err(e) = apply_cached_patch(workdir, patch)
{
msg::warn(&format!(
"could not restore pre-existing staged changes: {e}"
));
save_or_warn(workdir, "unrestored-staged", patch, Replay::Cached);
}
}
pub fn save_patch_aside(workdir: &Path, name: &str, patch: &str) -> Result<PathBuf> {
let dir = super::git_path(workdir, "loom")?;
std::fs::create_dir_all(&dir)
.with_context(|| format!("Failed to create '{}'", dir.display()))?;
if let Some(parent) = dir.parent() {
let _ = std::fs::File::open(parent).and_then(|d| d.sync_all());
}
for attempt in 0..1000 {
let path = dir.join(format!("{name}-{attempt}.patch"));
match std::fs::File::create_new(&path) {
Ok(mut file) => {
let written = file
.write_all(patch.as_bytes())
.and_then(|()| file.sync_all())
.with_context(|| format!("Failed to save '{}'", path.display()));
if let Err(e) = written {
let _ = std::fs::remove_file(&path);
return Err(e);
}
let _ = std::fs::File::open(&dir).and_then(|d| d.sync_all());
return Ok(path);
}
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => continue,
Err(e) => {
return Err(e).with_context(|| format!("Failed to create '{}'", path.display()));
}
}
}
bail!(
"'{}' already holds 1000 saved {name} patches",
dir.display()
)
}
#[derive(Clone, Copy, Debug)]
pub enum Replay {
Worktree,
Cached,
CachedThreeWay,
}
impl Replay {
fn command(self, path: &Path) -> String {
let path = path.display();
match self {
Replay::Worktree => format!("git apply {path}"),
Replay::Cached => format!("git apply --cached {path}"),
Replay::CachedThreeWay => {
format!("git -c rerere.enabled=false apply --cached --3way {path}")
}
}
}
}
pub fn save_or_warn(workdir: &Path, name: &str, patch: &str, replay: Replay) {
if patch.is_empty() {
return;
}
match save_patch_aside(workdir, name, patch) {
Ok(path) => msg::warn(&format!(
"those changes are saved as a patch — replay them with `{}`",
replay.command(&path)
)),
Err(e) => msg::warn(&format!(
"the patch of those changes could not be saved either ({e})"
)),
}
}
#[cfg(test)]
#[path = "git_apply_test.rs"]
mod tests;