#![cfg(feature = "git")]
use bashkit::{Bash, GitConfig};
fn create_git_bash() -> Bash {
Bash::builder()
.git(GitConfig::new().author("Test User", "test@example.com"))
.build()
}
mod init {
use super::*;
#[tokio::test]
async fn test_git_init_creates_repository() {
let mut bash = create_git_bash();
let result = bash.exec("git init /repo").await.unwrap();
assert_eq!(result.exit_code, 0);
assert!(result.stdout.contains("Initialized empty Git repository"));
}
#[tokio::test]
async fn test_git_init_in_current_directory() {
let mut bash = create_git_bash();
let result = bash
.exec("mkdir -p /myrepo && cd /myrepo && git init")
.await
.unwrap();
assert_eq!(result.exit_code, 0);
assert!(result.stdout.contains("Initialized empty Git repository"));
}
#[tokio::test]
async fn test_git_init_reinitialize() {
let mut bash = create_git_bash();
bash.exec("git init /repo").await.unwrap();
let result = bash.exec("git init /repo").await.unwrap();
assert_eq!(result.exit_code, 0);
assert!(
result
.stdout
.contains("Reinitialized existing Git repository")
);
}
}
mod config {
use super::*;
#[tokio::test]
async fn test_git_config_get_user_name() {
let mut bash = create_git_bash();
bash.exec("git init /repo").await.unwrap();
let result = bash.exec("cd /repo && git config user.name").await.unwrap();
assert_eq!(result.exit_code, 0);
assert_eq!(result.stdout.trim(), "Test User");
}
#[tokio::test]
async fn test_git_config_get_user_email() {
let mut bash = create_git_bash();
bash.exec("git init /repo").await.unwrap();
let result = bash
.exec("cd /repo && git config user.email")
.await
.unwrap();
assert_eq!(result.exit_code, 0);
assert_eq!(result.stdout.trim(), "test@example.com");
}
#[tokio::test]
async fn test_git_config_set() {
let mut bash = create_git_bash();
bash.exec("git init /repo").await.unwrap();
bash.exec("cd /repo && git config user.name 'New Name'")
.await
.unwrap();
let result = bash.exec("cd /repo && git config user.name").await.unwrap();
assert_eq!(result.exit_code, 0);
assert_eq!(result.stdout.trim(), "New Name");
}
#[tokio::test]
async fn test_git_config_not_in_repo() {
let mut bash = create_git_bash();
let result = bash.exec("cd /tmp && git config user.name").await.unwrap();
assert_ne!(result.exit_code, 0);
assert!(result.stderr.contains("not a git repository"));
}
}
mod add_commit {
use super::*;
#[tokio::test]
async fn test_git_add_single_file() {
let mut bash = create_git_bash();
bash.exec("git init /repo && cd /repo && echo 'hello' > test.txt")
.await
.unwrap();
let result = bash.exec("cd /repo && git add test.txt").await.unwrap();
assert_eq!(result.exit_code, 0);
let status = bash.exec("cd /repo && git status").await.unwrap();
assert!(status.stdout.contains("test.txt"));
assert!(status.stdout.contains("Changes to be committed"));
}
#[tokio::test]
async fn test_git_add_all() {
let mut bash = create_git_bash();
bash.exec("git init /repo && cd /repo && echo 'a' > a.txt && echo 'b' > b.txt")
.await
.unwrap();
let result = bash.exec("cd /repo && git add .").await.unwrap();
assert_eq!(result.exit_code, 0);
let status = bash.exec("cd /repo && git status").await.unwrap();
assert!(status.stdout.contains("a.txt"));
assert!(status.stdout.contains("b.txt"));
}
#[tokio::test]
async fn test_git_commit_with_message() {
let mut bash = create_git_bash();
bash.exec("git init /repo && cd /repo && echo 'hello' > test.txt && git add test.txt")
.await
.unwrap();
let result = bash
.exec("cd /repo && git commit -m 'Initial commit'")
.await
.unwrap();
assert_eq!(result.exit_code, 0);
assert!(result.stdout.contains("[master"));
assert!(result.stdout.contains("Initial commit"));
}
#[tokio::test]
async fn test_git_commit_nothing_to_commit() {
let mut bash = create_git_bash();
bash.exec("git init /repo").await.unwrap();
let result = bash
.exec("cd /repo && git commit -m 'Empty'")
.await
.unwrap();
assert_ne!(result.exit_code, 0);
assert!(result.stderr.contains("nothing to commit"));
}
#[tokio::test]
async fn test_git_commit_requires_message() {
let mut bash = create_git_bash();
bash.exec("git init /repo && cd /repo && echo 'x' > x.txt && git add x.txt")
.await
.unwrap();
let result = bash.exec("cd /repo && git commit").await.unwrap();
assert_ne!(result.exit_code, 0);
assert!(result.stderr.contains("requires a value"));
}
}
mod status {
use super::*;
#[tokio::test]
async fn test_git_status_clean() {
let mut bash = create_git_bash();
bash.exec("git init /repo").await.unwrap();
let result = bash.exec("cd /repo && git status").await.unwrap();
assert_eq!(result.exit_code, 0);
assert!(result.stdout.contains("On branch master"));
assert!(result.stdout.contains("nothing to commit"));
}
#[tokio::test]
async fn test_git_status_untracked() {
let mut bash = create_git_bash();
bash.exec("git init /repo && cd /repo && echo 'x' > new.txt")
.await
.unwrap();
let result = bash.exec("cd /repo && git status").await.unwrap();
assert_eq!(result.exit_code, 0);
assert!(result.stdout.contains("Untracked files"));
assert!(result.stdout.contains("new.txt"));
}
#[tokio::test]
async fn test_git_status_staged() {
let mut bash = create_git_bash();
bash.exec("git init /repo && cd /repo && echo 'x' > new.txt && git add new.txt")
.await
.unwrap();
let result = bash.exec("cd /repo && git status").await.unwrap();
assert_eq!(result.exit_code, 0);
assert!(result.stdout.contains("Changes to be committed"));
assert!(result.stdout.contains("new.txt"));
}
#[tokio::test]
async fn test_git_status_not_in_repo() {
let mut bash = create_git_bash();
let result = bash.exec("cd /tmp && git status").await.unwrap();
assert_ne!(result.exit_code, 0);
assert!(result.stderr.contains("not a git repository"));
}
}
mod log {
use super::*;
#[tokio::test]
async fn test_git_log_after_commit() {
let mut bash = create_git_bash();
bash.exec("git init /repo && cd /repo && echo 'x' > x.txt && git add x.txt && git commit -m 'First'").await.unwrap();
let result = bash.exec("cd /repo && git log").await.unwrap();
assert_eq!(result.exit_code, 0);
assert!(result.stdout.contains("commit"));
assert!(result.stdout.contains("Author: Test User"));
assert!(result.stdout.contains("First"));
}
#[tokio::test]
async fn test_git_log_multiple_commits() {
let mut bash = create_git_bash();
bash.exec("git init /repo && cd /repo").await.unwrap();
bash.exec("cd /repo && echo 'a' > a.txt && git add a.txt && git commit -m 'First'")
.await
.unwrap();
bash.exec("cd /repo && echo 'b' > b.txt && git add b.txt && git commit -m 'Second'")
.await
.unwrap();
let result = bash.exec("cd /repo && git log").await.unwrap();
assert_eq!(result.exit_code, 0);
assert!(result.stdout.contains("First"));
assert!(result.stdout.contains("Second"));
}
#[tokio::test]
async fn test_git_log_limit() {
let mut bash = create_git_bash();
bash.exec("git init /repo && cd /repo").await.unwrap();
bash.exec("cd /repo && echo 'a' > a.txt && git add a.txt && git commit -m 'First'")
.await
.unwrap();
bash.exec("cd /repo && echo 'b' > b.txt && git add b.txt && git commit -m 'Second'")
.await
.unwrap();
bash.exec("cd /repo && echo 'c' > c.txt && git add c.txt && git commit -m 'Third'")
.await
.unwrap();
let result = bash.exec("cd /repo && git log -n 1").await.unwrap();
assert_eq!(result.exit_code, 0);
assert!(result.stdout.contains("Third"));
let first_count = result.stdout.matches("First").count();
assert_eq!(first_count, 0);
}
#[tokio::test]
async fn test_git_log_no_commits() {
let mut bash = create_git_bash();
bash.exec("git init /repo").await.unwrap();
let result = bash.exec("cd /repo && git log").await.unwrap();
assert_ne!(result.exit_code, 0);
assert!(result.stderr.contains("does not have any commits"));
}
}
mod workflow {
use super::*;
#[tokio::test]
async fn test_full_workflow() {
let mut bash = create_git_bash();
let result = bash.exec("git init /project").await.unwrap();
assert_eq!(result.exit_code, 0);
bash.exec("cd /project && echo '# My Project' > README.md")
.await
.unwrap();
bash.exec("cd /project && echo 'fn main() {}' > main.rs")
.await
.unwrap();
let status = bash.exec("cd /project && git status").await.unwrap();
assert!(status.stdout.contains("Untracked files"));
let result = bash.exec("cd /project && git add .").await.unwrap();
assert_eq!(result.exit_code, 0);
let status = bash.exec("cd /project && git status").await.unwrap();
assert!(status.stdout.contains("Changes to be committed"));
let result = bash
.exec("cd /project && git commit -m 'Initial project setup'")
.await
.unwrap();
assert_eq!(result.exit_code, 0);
let status = bash.exec("cd /project && git status").await.unwrap();
assert!(status.stdout.contains("nothing to commit"));
let log = bash.exec("cd /project && git log").await.unwrap();
assert!(log.stdout.contains("Initial project setup"));
}
}
mod error_handling {
use super::*;
#[tokio::test]
async fn test_git_unknown_command() {
let mut bash = create_git_bash();
let result = bash.exec("git unknown").await.unwrap();
assert_ne!(result.exit_code, 0);
assert!(result.stderr.contains("is not a git command"));
}
#[tokio::test]
async fn test_git_no_subcommand() {
let mut bash = create_git_bash();
let result = bash.exec("git").await.unwrap();
assert_ne!(result.exit_code, 0);
assert!(result.stderr.contains("usage: git"));
}
#[tokio::test]
async fn test_git_add_nothing() {
let mut bash = create_git_bash();
bash.exec("git init /repo").await.unwrap();
let result = bash.exec("cd /repo && git add").await.unwrap();
assert!(result.stderr.contains("Nothing specified"));
}
}
mod not_configured {
use bashkit::Bash;
#[tokio::test]
async fn test_git_not_configured() {
let mut bash = Bash::new();
let result = bash.exec("git init /repo").await.unwrap();
assert_ne!(result.exit_code, 0);
assert!(result.stderr.contains("not configured"));
}
}
#[tokio::test]
async fn git_init_options_are_parsed_not_taken_as_directory() {
let mut bash = create_git_bash();
let r = bash
.exec("git init -q /repo; echo \"[$?]\"; ls /repo/.git/HEAD; git init -b main /r2 >/dev/null; cat /r2/.git/HEAD; git init --initial-branch=dev -q /r3; cat /r3/.git/HEAD; ls /home/user")
.await
.unwrap();
assert_eq!(
r.stdout,
"[0]\n/repo/.git/HEAD\nref: refs/heads/main\nref: refs/heads/dev\n"
);
let r = bash.exec("git init --bare /x").await.unwrap();
assert_eq!(r.exit_code, 129);
let r = bash.exec("git init -b 'a b' /y").await.unwrap();
assert_eq!(r.exit_code, 128);
}
mod stage_all {
use super::*;
#[tokio::test]
async fn test_git_add_all_flags_stage_everything() {
for flag in ["-A", "--all"] {
let mut bash = create_git_bash();
let script = format!(
"mkdir -p /r/d && cd /r && git init -q && echo a > f && echo b > d/g && \
git add {flag} && git commit -m init && git ls-files"
);
let result = bash.exec(&script).await.unwrap();
assert_eq!(result.exit_code, 0, "{flag}: {}", result.stderr);
assert!(result.stdout.contains("d/g"), "{flag}: {}", result.stdout);
assert!(result.stdout.contains("f\n"), "{flag}: {}", result.stdout);
}
}
#[tokio::test]
async fn test_git_commit_am_stages_tracked_files_only() {
let mut bash = create_git_bash();
let result = bash
.exec(
"mkdir -p /r && cd /r && git init -q && echo a > f && git add . && \
git commit -q -m init && echo b >> f && echo n > untracked && \
git commit -am 'fix: y'",
)
.await
.unwrap();
assert_eq!(result.exit_code, 0, "{}", result.stderr);
assert!(result.stdout.contains("fix: y"), "{}", result.stdout);
let log = bash.exec("cd /r && git log").await.unwrap();
assert!(log.stdout.contains("fix: y"), "{}", log.stdout);
let files = bash.exec("cd /r && git ls-files").await.unwrap();
assert!(!files.stdout.contains("untracked"), "{}", files.stdout);
let split = bash
.exec("cd /r && echo c >> f && git commit -a -m 'second'")
.await
.unwrap();
assert_eq!(split.exit_code, 0, "{}", split.stderr);
}
}
mod clone_local {
use super::*;
const UPSTREAM: &str = "mkdir -p /srv/git/calc/lib && cd /srv/git/calc && git init -q && \
echo 'mul() { :; }' > lib/calc.sh && ln -s lib/calc.sh entry && \
git add . && git commit -q -m 'initial import' && cd /";
#[tokio::test]
async fn test_git_clone_local_path_copies_tree_and_history() {
let mut bash = create_git_bash();
bash.exec(UPSTREAM).await.unwrap();
let r = bash
.exec("mkdir -p /work && cd /work && git clone /srv/git/calc")
.await
.unwrap();
assert_eq!(r.exit_code, 0, "{}", r.stderr);
assert!(r.stderr.contains("Cloning into 'calc'"), "{}", r.stderr);
let r = bash
.exec("cd /work/calc && cat lib/calc.sh && readlink entry && git log && git remote -v")
.await
.unwrap();
assert_eq!(r.exit_code, 0, "{}", r.stderr);
assert!(r.stdout.contains("mul()"), "{}", r.stdout);
assert!(r.stdout.contains("lib/calc.sh\n"), "{}", r.stdout);
assert!(r.stdout.contains("initial import"), "{}", r.stdout);
assert!(r.stdout.contains("origin\t/srv/git/calc"), "{}", r.stdout);
bash.exec("cd /work/calc && echo changed > lib/calc.sh")
.await
.unwrap();
let up = bash.exec("cat /srv/git/calc/lib/calc.sh").await.unwrap();
assert!(up.stdout.contains("mul()"), "{}", up.stdout);
}
#[tokio::test]
async fn test_git_clone_local_explicit_dest_and_file_url() {
let mut bash = create_git_bash();
bash.exec(UPSTREAM).await.unwrap();
let r = bash
.exec("git clone -q file:///srv/git/calc /w/c2 && cat /w/c2/lib/calc.sh")
.await
.unwrap();
assert_eq!(r.exit_code, 0, "{}", r.stderr);
assert!(r.stdout.contains("mul()"), "{}", r.stdout);
}
#[tokio::test]
async fn test_git_clone_local_errors() {
let mut bash = create_git_bash();
bash.exec(UPSTREAM).await.unwrap();
let r = bash.exec("git clone /srv/git/nope /w/x").await.unwrap();
assert_eq!(r.exit_code, 128);
assert!(r.stderr.contains("does not exist"), "{}", r.stderr);
let r = bash
.exec("mkdir -p /w/full && touch /w/full/f && git clone /srv/git/calc /w/full")
.await
.unwrap();
assert_eq!(r.exit_code, 128);
assert!(r.stderr.contains("already exists"), "{}", r.stderr);
let r = bash
.exec("git clone /srv/git/calc /srv/git/calc/inner")
.await
.unwrap();
assert_eq!(r.exit_code, 128, "{}", r.stderr);
}
}