Skip to content

Fix detection of dirty working directory #43

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
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 @@ -64,7 +64,7 @@
}

fn merge_conflict(branch: String, upstream: String, message: Option<String>) -> Self {
let mut error_msg = format!("Merge conflict between {} and {}", upstream, branch);

Check warning on line 67 in src/main.rs

View workflow job for this annotation

GitHub Actions / Clippy (beta)

variables can be used directly in the `format!` string

Check warning on line 67 in src/main.rs

View workflow job for this annotation

GitHub Actions / Clippy (nightly)

variables can be used directly in the `format!` string
if let Some(details) = message {
error_msg.push('\n');
error_msg.push_str(&details);
Expand All @@ -73,7 +73,7 @@
}

fn git_command_failed(command: String, status: i32, stdout: String, stderr: String) -> Self {
let error_msg = format!(

Check warning on line 76 in src/main.rs

View workflow job for this annotation

GitHub Actions / Clippy (beta)

variables can be used directly in the `format!` string

Check warning on line 76 in src/main.rs

View workflow job for this annotation

GitHub Actions / Clippy (nightly)

variables can be used directly in the `format!` string
"Git command failed: {}\nStatus: {}\nStdout: {}\nStderr: {}",
command, status, stdout, stderr
);
Expand Down Expand Up @@ -132,21 +132,21 @@
if name.starts_with("git-") && name.len() > 4 {
let tmp: Vec<String> = name.split("git-").map(|x| x.to_string()).collect();
let git_cmd = &tmp[1];
return format!("git {}", git_cmd);

Check warning on line 135 in src/main.rs

View workflow job for this annotation

GitHub Actions / Clippy (beta)

variables can be used directly in the `format!` string

Check warning on line 135 in src/main.rs

View workflow job for this annotation

GitHub Actions / Clippy (nightly)

variables can be used directly in the `format!` string
}
name
}

fn chain_name_key(branch_name: &str) -> String {
format!("branch.{}.chain-name", branch_name)

Check warning on line 141 in src/main.rs

View workflow job for this annotation

GitHub Actions / Clippy (beta)

variables can be used directly in the `format!` string

Check warning on line 141 in src/main.rs

View workflow job for this annotation

GitHub Actions / Clippy (nightly)

variables can be used directly in the `format!` string
}

fn chain_order_key(branch_name: &str) -> String {
format!("branch.{}.chain-order", branch_name)

Check warning on line 145 in src/main.rs

View workflow job for this annotation

GitHub Actions / Clippy (beta)

variables can be used directly in the `format!` string

Check warning on line 145 in src/main.rs

View workflow job for this annotation

GitHub Actions / Clippy (nightly)

variables can be used directly in the `format!` string
}

fn root_branch_key(branch_name: &str) -> String {
format!("branch.{}.root-branch", branch_name)

Check warning on line 149 in src/main.rs

View workflow job for this annotation

GitHub Actions / Clippy (beta)

variables can be used directly in the `format!` string

Check warning on line 149 in src/main.rs

View workflow job for this annotation

GitHub Actions / Clippy (nightly)

variables can be used directly in the `format!` string
}

fn generate_chain_order() -> String {
Expand Down Expand Up @@ -195,7 +195,7 @@
branch.bold(),
upstream_branch.bold()
);
eprintln!(

Check warning on line 198 in src/main.rs

View workflow job for this annotation

GitHub Actions / Clippy (beta)

variables can be used directly in the `format!` string

Check warning on line 198 in src/main.rs

View workflow job for this annotation

GitHub Actions / Clippy (nightly)

variables can be used directly in the `format!` string
"⚠️ Resolve any rebase merge conflicts, and then run {} rebase",
executable_name
);
Expand Down Expand Up @@ -532,7 +532,7 @@
let mut branches = Chain::get_branches_for_chain(git_chain, chain_name)?;

if branches.is_empty() {
return Err(Error::from_str(&format!(

Check warning on line 535 in src/main.rs

View workflow job for this annotation

GitHub Actions / Clippy (beta)

variables can be used directly in the `format!` string

Check warning on line 535 in src/main.rs

View workflow job for this annotation

GitHub Actions / Clippy (nightly)

variables can be used directly in the `format!` string
"Unable to get branches attached to chain: {}",
chain_name
)));
Expand Down Expand Up @@ -579,10 +579,10 @@
let status = match ahead_behind {
(0, 0) => "".to_string(),
(ahead, 0) => {
format!("{} ahead", ahead)

Check warning on line 582 in src/main.rs

View workflow job for this annotation

GitHub Actions / Clippy (beta)

variables can be used directly in the `format!` string

Check warning on line 582 in src/main.rs

View workflow job for this annotation

GitHub Actions / Clippy (nightly)

variables can be used directly in the `format!` string
}
(0, behind) => {
format!("{} behind", behind)

Check warning on line 585 in src/main.rs

View workflow job for this annotation

GitHub Actions / Clippy (beta)

variables can be used directly in the `format!` string

Check warning on line 585 in src/main.rs

View workflow job for this annotation

GitHub Actions / Clippy (nightly)

variables can be used directly in the `format!` string
}
(ahead, behind) => {
format!("{} ahead ⦁ {} behind", ahead, behind)
Expand Down Expand Up @@ -1381,24 +1381,22 @@
}

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 @@
}

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 @@
}

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 @@

// 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);
}
Loading