Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 28 additions & 22 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1381,24 +1381,22 @@ impl GitChain {
}

fn dirty_working_directory(&self) -> Result<bool, Error> {
// perform equivalent to git diff-index HEAD
let obj = self.repo.revparse_single("HEAD")?;
let tree = obj.peel(ObjectType::Tree)?;
use git2::StatusOptions;

// This is used for diff formatting for diff-index. But we're only interested in the diff stats.
// let mut opts = DiffOptions::new();
// opts.id_abbrev(40);
// Configure status collection so we can detect *any* change
// in the working directory. This mimics `git status --porcelain`
// by including untracked files and directories. Ignored files
// and paths that haven't changed are skipped so the resulting
// status list only contains meaningful modifications.
let mut opts = StatusOptions::new();
opts.include_untracked(true)
.recurse_untracked_dirs(true)
.include_ignored(false)
.include_unmodified(false);

let diff = self
.repo
.diff_tree_to_workdir_with_index(tree.as_tree(), None)?;

let diff_stats = diff.stats()?;
let has_changes = diff_stats.files_changed() > 0
|| diff_stats.insertions() > 0
|| diff_stats.deletions() > 0;

Ok(has_changes)
// If the repository reports no statuses, the working tree is clean.
let statuses = self.repo.statuses(Some(&mut opts))?;
Ok(!statuses.is_empty())
}

fn backup(&self, chain_name: &str) -> Result<(), Error> {
Expand All @@ -1420,11 +1418,15 @@ impl GitChain {
}

if self.dirty_working_directory()? {
let current_branch = self.get_current_branch_name()?;
eprintln!(
"🛑 Unable to back up branches for the chain: {}",
chain.name.bold()
);
eprintln!("You have uncommitted changes in your working directory.");
eprintln!(
"You have uncommitted changes on branch {}.",
current_branch.bold()
);
eprintln!("Please commit or stash them.");
process::exit(1);
}
Expand Down Expand Up @@ -1630,9 +1632,11 @@ impl GitChain {
}

if self.dirty_working_directory()? {
return Err(Error::from_str(
"You have uncommitted changes in your working directory.",
));
let current_branch = self.get_current_branch_name()?;
return Err(Error::from_str(&format!(
"You have uncommitted changes on branch {}.",
current_branch.bold()
)));
}

Ok(())
Expand Down Expand Up @@ -2008,9 +2012,11 @@ impl GitChain {

// Check for uncommitted changes
if self.dirty_working_directory()? {
let current_branch = self.get_current_branch_name()?;
return Err(Error::from_str(&format!(
"🛑 Unable to merge branches for the chain: {}\nYou have uncommitted changes in your working directory.\nPlease commit or stash them.",
chain_name.bold()
"🛑 Unable to merge branches for the chain: {}\nYou have uncommitted changes on branch {}.\nPlease commit or stash them.",
chain_name.bold(),
current_branch.bold()
)));
}

Expand Down
107 changes: 107 additions & 0 deletions tests/untracked_detection.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
#[path = "common/mod.rs"]
pub mod common;

use common::{
checkout_branch, commit_all, create_branch, create_new_file, first_commit_all,
generate_path_to_repo, get_current_branch_name, run_test_bin_expect_err,
run_test_bin_expect_ok, setup_git_repo, teardown_git_repo,
};

#[test]
fn backup_fails_with_untracked_files() {
let repo_name = "backup_fails_with_untracked";
let repo = setup_git_repo(repo_name);
let path_to_repo = generate_path_to_repo(repo_name);

// initial commit on master
create_new_file(&path_to_repo, "initial.txt", "initial");
first_commit_all(&repo, "initial commit");

// create feature branch
create_branch(&repo, "feature");
checkout_branch(&repo, "feature");
create_new_file(&path_to_repo, "feature.txt", "feature");
commit_all(&repo, "feature commit");

// initialize chain with root master
let args = vec!["init", "chain", "master"];
run_test_bin_expect_ok(&path_to_repo, args);

// add untracked file
create_new_file(&path_to_repo, "untracked.txt", "dirty");

// attempt backup and expect failure mentioning branch name
let args = vec!["backup"];
let output = run_test_bin_expect_err(&path_to_repo, args);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains("uncommitted"));
assert!(stderr.contains(&get_current_branch_name(&repo)));

teardown_git_repo(repo_name);
}

#[test]
fn merge_fails_with_untracked_files() {
let repo_name = "merge_fails_with_untracked";
let repo = setup_git_repo(repo_name);
let path_to_repo = generate_path_to_repo(repo_name);

// initial commit on master
create_new_file(&path_to_repo, "initial.txt", "initial");
first_commit_all(&repo, "initial commit");

// create feature branch and commit
create_branch(&repo, "feature");
checkout_branch(&repo, "feature");
create_new_file(&path_to_repo, "feature.txt", "feature");
commit_all(&repo, "feature commit");

// initialize chain with root master
let args = vec!["init", "chain", "master"];
run_test_bin_expect_ok(&path_to_repo, args);

// add untracked file
create_new_file(&path_to_repo, "untracked.txt", "dirty");

// attempt merge and expect failure mentioning branch name
let args = vec!["merge"];
let output = run_test_bin_expect_err(&path_to_repo, args);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains("uncommitted"));
assert!(stderr.contains(&get_current_branch_name(&repo)));

teardown_git_repo(repo_name);
}

#[test]
fn rebase_fails_with_untracked_files() {
let repo_name = "rebase_fails_with_untracked";
let repo = setup_git_repo(repo_name);
let path_to_repo = generate_path_to_repo(repo_name);

// initial commit on master
create_new_file(&path_to_repo, "initial.txt", "initial");
first_commit_all(&repo, "initial commit");

// create feature branch and commit
create_branch(&repo, "feature");
checkout_branch(&repo, "feature");
create_new_file(&path_to_repo, "feature.txt", "feature");
commit_all(&repo, "feature commit");

// initialize chain with root master
let args = vec!["init", "chain", "master"];
run_test_bin_expect_ok(&path_to_repo, args);

// add untracked file
create_new_file(&path_to_repo, "untracked.txt", "dirty");

// attempt rebase and expect failure mentioning branch name
let args = vec!["rebase"];
let output = run_test_bin_expect_err(&path_to_repo, args);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains("uncommitted"));
assert!(stderr.contains(&get_current_branch_name(&repo)));

teardown_git_repo(repo_name);
}