git-clean 0.8.0

A tool for cleaning old git branches.
Documentation
use support::project;

macro_rules! touch_command {
    ($project:ident, $file_name:literal) => {
        if cfg!(windows) {
            format!(
                "cmd /c copy nul {}\\{}",
                $project.path().display(),
                $file_name
            )
        } else {
            format!("touch {}", $file_name)
        }
    };
}

#[test]
fn test_git_clean_works_with_merged_branches() {
    let project = project("git-clean_squashed_merges").build().setup_remote();

    let touch_command = touch_command!(project, "file2.txt");

    project.batch_setup_commands(&[
        "git checkout -b merged",
        &touch_command,
        "git add .",
        "git commit -am Merged",
        "git checkout main",
        "git merge merged",
    ]);

    let result = project.git_clean_command("-y").run();

    assert!(
        result.is_success(),
        "{}",
        result.failure_message("command to succeed")
    );
    assert!(
        result.stdout().contains("Deleted branch merged"),
        "{}",
        result.failure_message("command to delete merged")
    );
}

#[test]
fn test_git_clean_works_with_squashed_merges() {
    let project = project("git-clean_squashed_merges").build().setup_remote();

    let touch_command = touch_command!(project, "file2.txt");

    project.batch_setup_commands(&[
        "git checkout -b squashed",
        &touch_command,
        "git add .",
        "git commit -am Squash",
        "git checkout main",
        "git merge --ff-only squashed",
    ]);

    let result = project.git_clean_command("-y").run();

    assert!(
        result.is_success(),
        "{}",
        result.failure_message("command to succeed")
    );
    assert!(
        result.stdout().contains("Deleted branch squashed"),
        "{}",
        result.failure_message("command to delete squashed")
    );
}

fn git_clean_does_not_delete_branches_ahead_of_main(flags: &str) {
    let project = project("git-clean_branch_ahead").build().setup_remote();

    let touch_command = touch_command!(project, "file2.txt");

    project.batch_setup_commands(&[
        "git checkout -b ahead",
        &touch_command,
        "git add .",
        "git commit -am Ahead",
        "git push origin HEAD",
        "git checkout main",
    ]);

    let result = project.git_clean_command(flags).run();

    assert!(
        result.is_success(),
        "{}",
        result.failure_message("command to succeed")
    );
    assert!(
        !result.stdout().contains("Deleted branch ahead"),
        "{}",
        result.failure_message("command not to delete ahead")
    );
}

#[test]
fn test_git_clean_does_not_delete_branches_ahead_of_main() {
    git_clean_does_not_delete_branches_ahead_of_main("-y")
}

#[test]
fn test_git_clean_does_not_delete_branches_ahead_of_main_d_flag() {
    git_clean_does_not_delete_branches_ahead_of_main("-y -d")
}

fn git_clean_with_unpushed_ahead_branch(flags: &str, expect_branch_deleted: bool) {
    let project = project("git-clean_branch_ahead").build().setup_remote();

    let touch_command = touch_command!(project, "file2.txt");

    project.batch_setup_commands(&[
        "git checkout -b ahead",
        &touch_command,
        "git add .",
        "git commit -am Ahead",
        "git checkout main",
    ]);

    let result = project.git_clean_command(flags).run();

    assert!(
        result.is_success(),
        "{}",
        result.failure_message("command to succeed")
    );
    if expect_branch_deleted {
        assert!(
            result.stdout().contains("Deleted branch ahead"),
            "{}",
            result.failure_message("command to delete ahead")
        );
    } else {
        assert!(
            !result.stdout().contains("Deleted branch ahead"),
            "{}",
            result.failure_message("command not to delete ahead")
        );
    }
}

#[test]
fn test_git_clean_does_not_delete_unpushed_ahead_branch() {
    git_clean_with_unpushed_ahead_branch("-y", false)
}

#[test]
fn test_git_clean_deletes_unpushed_ahead_branch() {
    git_clean_with_unpushed_ahead_branch("-y -d", true)
}

#[test]
fn test_git_clean_works_with_squashes_with_flag() {
    let project = project("git-clean_github_squashes").build().setup_remote();

    let touch_squash_command = touch_command!(project, "squash.txt");
    let touch_new_command = touch_command!(project, "new.txt");

    // Github squashes function basically like a normal squashed merge, it creates an entirely new
    // commit in which all your changes live. The biggest challenge of this is that your local
    // branch doesn't have any knowledge of this new commit. So if main gets ahead of your local
    // branch, git no longer is able to tell that branch has been merged. These commands simulate
    // this condition.
    project.batch_setup_commands(&[
        "git checkout -b github_squash",
        &touch_squash_command,
        "git add .",
        "git commit -am Commit",
        "git push origin HEAD",
        "git checkout main",
        &touch_squash_command,
        "git add .",
        "git commit -am Squash",
        &touch_new_command,
        "git add .",
        "git commit -am Other",
        "git push origin HEAD",
    ]);

    let result = project.git_clean_command("-y --squashes").run();

    assert!(
        result.is_success(),
        "{}",
        result.failure_message("command to succeed")
    );
    assert!(
        result
            .stdout()
            .contains(" - [deleted]         github_squash"),
        "{}",
        result.failure_message("command to delete github_squash in remote")
    );
    assert!(
        result.stdout().contains("Deleted branch github_squash"),
        "{}",
        result.failure_message("command to delete github_squash locally")
    );
}

#[test]
fn test_git_clean_ignores_squashes_without_flag() {
    let project = project("git-clean_ignores_github_squashes")
        .build()
        .setup_remote();

    let touch_squash_command = touch_command!(project, "squash.txt");
    let touch_new_command = touch_command!(project, "new.txt");

    // Github squashes function basically like a normal squashed merge, it creates an entirely new
    // commit in which all your changes live. The biggest challenge of this is that your local
    // branch doesn't have any knowledge of this new commit. So if main gets ahead of your local
    // branch, git no longer is able to tell that branch has been merged. These commands simulate
    // this condition.
    project.batch_setup_commands(&[
        "git checkout -b github_squash",
        &touch_squash_command,
        "git add .",
        "git commit -am Commit",
        "git push origin HEAD",
        "git checkout main",
        &touch_squash_command,
        "git add .",
        "git commit -am Squash",
        &touch_new_command,
        "git add .",
        "git commit -am Other",
        "git push origin HEAD",
    ]);

    let result = project.git_clean_command("-y").run();

    assert!(
        result.is_success(),
        "{}",
        result.failure_message("command to succeed")
    );
    assert!(
        !result
            .stdout()
            .contains(" - [deleted]         github_squash"),
        "{}",
        result.failure_message("command not to delete github_squash in remote")
    );
    assert!(
        !result.stdout().contains("Deleted branch github_squash"),
        "{}",
        result.failure_message("command not to delete github_squash locally")
    );
}