use std::path::Path;
use super::{Git, GitError, PATHS_PER_CALL};
impl Git {
pub async fn tree_without_yours(
&self,
dir: &Path,
scratch: &Path,
snapshot: &str,
main: &str,
head: &str,
) -> Result<String, GitError> {
let base = format!("--merge-base={snapshot}");
let merged = match self.merge_tree(dir, &[&base, "-Xtheirs"], main, head).await {
Ok(merged) => merged,
Err(_) => self.merge_tree(dir, &[&base], main, head).await?,
};
if merged.both_changed.is_empty() {
return Ok(merged.tree);
}
self.tree_with(dir, scratch, &merged.tree, head, &merged.both_changed)
.await
}
pub async fn follow_beside(
&self,
checkout: &Path,
scratch: &Path,
snapshot: &str,
main: &str,
commit: &str,
) -> Result<(), GitError> {
let changed = self.changed_paths(checkout, main, commit).await?;
if changed.is_empty() {
return Ok(());
}
let staged = self
.staged_apart(checkout, main, snapshot, &changed)
.await?;
let follow: Vec<&str> = changed
.iter()
.filter(|path| !staged.contains(*path))
.map(String::as_str)
.collect();
for chunk in follow.chunks(PATHS_PER_CALL) {
let mut args = vec!["--literal-pathspecs", "reset", "--quiet", commit, "--"];
args.extend(chunk);
self.run(checkout, &args).await?;
}
let as_main = self.holding(checkout, main, &changed).await?;
if !as_main.is_empty() {
let others: Vec<String> = changed
.into_iter()
.filter(|path| !as_main.contains(path))
.collect();
self.write_files(checkout, scratch, main, commit, &others)
.await?;
}
self.output(checkout, &["update-index", "-q", "--refresh"], &[])
.await?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::testutil::{TestRepo, git_test_env};
async fn started_from_your_edits(repo: &TestRepo, git: &Git) -> (String, String, String) {
repo.write("yours.txt", "one\n");
repo.write("both.txt", "a\nb\nc\nd\ne\nf\ng\nh\n");
let main = git.commit_all(&repo.root, "files").await.unwrap().unwrap();
repo.write("yours.txt", "one, edited\n");
repo.write("both.txt", "a, edited\nb\nc\nd\ne\nf\ng\nh\n");
let snapshot = git
.commit_all(&repo.root, "snapshot")
.await
.unwrap()
.unwrap();
repo.write("both.txt", "a, edited\nb\nc\nd\ne\nf\ng\nh, by the run\n");
repo.write("new.txt", "new\n");
let head = git
.commit_all(&repo.root, "the run")
.await
.unwrap()
.unwrap();
repo.git(&["reset", "-q", "--hard", &snapshot]);
repo.git(&["reset", "-q", "--mixed", &main]);
(main, snapshot, head)
}
#[tokio::test]
async fn the_tree_for_your_branch_holds_the_runs_work_and_none_of_your_edits() {
let repo = TestRepo::new();
let git = Git::with_env(git_test_env());
let (main, snapshot, head) = started_from_your_edits(&repo, &git).await;
let scratch = repo.root.join(".git/scratch.index");
let tree = git
.tree_without_yours(&repo.root, &scratch, &snapshot, &main, &head)
.await
.unwrap();
let show = |path: &str| repo.git(&["show", &format!("{tree}:{path}")]);
assert_eq!(show("yours.txt"), "one");
assert_eq!(show("both.txt"), "a\nb\nc\nd\ne\nf\ng\nh, by the run");
assert_eq!(show("new.txt"), "new");
}
#[tokio::test]
async fn lines_the_run_rewrote_over_your_edit_are_the_runs() {
let repo = TestRepo::new();
let git = Git::with_env(git_test_env());
repo.write("both.txt", "a\nb\nc\n");
let main = git.commit_all(&repo.root, "files").await.unwrap().unwrap();
repo.write("both.txt", "a, edited\nb\nc\n");
let snapshot = git
.commit_all(&repo.root, "snapshot")
.await
.unwrap()
.unwrap();
repo.write("both.txt", "a, edited and finished\nb\nc\n");
let head = git
.commit_all(&repo.root, "the run")
.await
.unwrap()
.unwrap();
let scratch = repo.root.join(".git/scratch.index");
let tree = git
.tree_without_yours(&repo.root, &scratch, &snapshot, &main, &head)
.await
.unwrap();
assert_eq!(
repo.git(&["show", &format!("{tree}:both.txt")]),
"a, edited and finished\nb\nc"
);
}
#[tokio::test]
async fn your_index_follows_the_commit_and_your_edits_stay_uncommitted() {
let repo = TestRepo::new();
let git = Git::with_env(git_test_env());
let (main, snapshot, head) = started_from_your_edits(&repo, &git).await;
let scratch = repo.root.join(".git/scratch.index");
let tree = git
.tree_without_yours(&repo.root, &scratch, &snapshot, &main, &head)
.await
.unwrap();
let commit = git
.commit_tree(&repo.root, &tree, &main, "the run's work")
.await
.unwrap();
repo.write("both.txt", "a\nb\nc\nd\ne\nf\ng\nh\n");
git.write_files(
&repo.root,
&scratch,
&snapshot,
&head,
&["both.txt".to_owned()],
)
.await
.unwrap();
git.update_branch(&repo.root, "main", &commit, &main)
.await
.unwrap();
for _ in 0..2 {
git.follow_beside(&repo.root, &scratch, &snapshot, &main, &commit)
.await
.unwrap();
assert_eq!(repo.git(&["status", "--porcelain"]), "M yours.txt");
assert_eq!(
std::fs::read_to_string(repo.root.join("both.txt")).unwrap(),
"a\nb\nc\nd\ne\nf\ng\nh, by the run\n"
);
}
}
}