monochange_core 0.4.2

Manage versions and releases for your multiplatform, multilanguage monorepo
Documentation
use std::fs;
use std::io;

use super::*;

#[test]
fn git_command_reuses_stable_path_for_child_processes() {
	let command = git_command(Path::new("."));
	let path = command
		.get_envs()
		.find_map(|(key, value)| (key == "PATH").then_some(value))
		.flatten()
		.unwrap_or_else(|| panic!("expected PATH override"));

	assert!(path_contains_git(path));
}

#[test]
fn resolve_process_path_prefers_current_path_when_it_contains_git() {
	let current = temp_path_with_git();
	let fallback = tempdir("create fallback dir");

	let path = resolve_process_path(
		Some(current.path().as_os_str().to_owned()),
		Some(fallback.path().as_os_str().to_owned()),
	)
	.unwrap_or_else(|| panic!("expected current PATH"));

	assert_eq!(path, current.path().as_os_str());
}

#[test]
fn resolve_process_path_uses_fallback_when_current_path_lacks_git() {
	let current = tempdir("create current dir");
	let fallback = temp_path_with_git();

	let path = resolve_process_path(
		Some(current.path().as_os_str().to_owned()),
		Some(fallback.path().as_os_str().to_owned()),
	)
	.unwrap_or_else(|| panic!("expected fallback PATH"));

	assert_eq!(path, fallback.path().as_os_str());
}

#[test]
fn resolve_process_path_keeps_current_path_when_no_path_contains_git() {
	let current = tempdir("create current dir");
	let fallback = tempdir("create fallback dir");

	let path = resolve_process_path(
		Some(current.path().as_os_str().to_owned()),
		Some(fallback.path().as_os_str().to_owned()),
	)
	.unwrap_or_else(|| panic!("expected current PATH"));

	assert_eq!(path, current.path().as_os_str());
}

fn temp_path_with_git() -> tempfile::TempDir {
	let directory = tempdir("create PATH dir");
	fs::write(directory.path().join(git_executable_name()), "")
		.unwrap_or_else(|error| panic!("write git shim: {error}"));
	directory
}

fn tempdir(context: &str) -> tempfile::TempDir {
	tempfile::tempdir().unwrap_or_else(|error| panic!("{context}: {error}"))
}

fn git(root: &Path, args: &[&str]) {
	let status = git_command(root)
		.args(args)
		.status()
		.unwrap_or_else(|error| panic!("git {args:?}: {error}"));
	assert!(status.success(), "git {args:?} failed with {status}");
}

fn git_stdout(root: &Path, args: &[&str]) -> String {
	let output = git_command(root)
		.args(args)
		.output()
		.unwrap_or_else(|error| panic!("git {args:?}: {error}"));
	assert!(
		output.status.success(),
		"git {args:?} failed with {output:?}"
	);
	String::from_utf8_lossy(&output.stdout).to_string()
}

#[test]
fn git_commit_message_text_renders_subject_and_body() {
	let message = CommitMessage {
		subject: "chore(release): prepare release".to_string(),
		body: Some("Prepare release.\n\n<!-- release record -->".to_string()),
	};

	assert_eq!(
		git_commit_message_text(&message),
		"chore(release): prepare release\n\nPrepare release.\n\n<!-- release record -->"
	);
}

#[test]
fn git_commit_message_text_renders_subject_without_body() {
	let message = CommitMessage {
		subject: "chore(release): prepare release".to_string(),
		body: None,
	};

	assert_eq!(
		git_commit_message_text(&message),
		"chore(release): prepare release"
	);
}

#[test]
fn git_commit_paths_stdin_command_reads_message_from_stdin() {
	let command = git_commit_paths_stdin_command(Path::new("."), true);
	let args = command
		.get_args()
		.map(|arg| arg.to_string_lossy().into_owned())
		.collect::<Vec<_>>();

	assert_eq!(args, vec!["commit", "--no-verify", "--file", "-"]);
}

#[test]
fn git_commit_file_command_reads_message_from_file() {
	let command = git_commit_file_command(Path::new("."), Path::new("message.txt"), true);
	let args = command
		.get_args()
		.map(|arg| arg.to_string_lossy().into_owned())
		.collect::<Vec<_>>();

	assert_eq!(args, vec!["commit", "--no-verify", "--file", "message.txt"]);
}

#[test]
fn git_commit_temp_file_error_helpers_include_phase_diagnostics() {
	let message = CommitMessage {
		subject: "chore(release): prepare release".to_string(),
		body: None,
	};
	let message_text = git_commit_message_text(&message);
	let message_file = Path::new("message.txt");

	let create_error = git_commit_create_temp_file_error(
		Path::new("repo"),
		&message,
		&message_text,
		"commit release pull request changes",
		true,
		"disk unavailable",
	)
	.to_string();
	let write_error = git_commit_write_temp_file_error(
		Path::new("repo"),
		&message,
		&message_text,
		"commit release pull request changes",
		true,
		message_file,
		"write failed",
	)
	.to_string();
	let flush_error = git_commit_flush_temp_file_error(
		Path::new("repo"),
		&message,
		&message_text,
		"commit release pull request changes",
		true,
		message_file,
		"flush failed",
	)
	.to_string();

	assert!(create_error.contains("phase: creating temporary commit message file"));
	assert!(create_error.contains("temporary message file: <not yet created>"));
	assert!(create_error.contains("body preview: <none>"));
	assert!(create_error.contains("no_verify: true"));
	assert!(write_error.contains("phase: writing temporary commit message file"));
	assert!(write_error.contains("temporary message file: message.txt"));
	assert!(flush_error.contains("phase: flushing temporary commit message file"));
	assert!(flush_error.contains("temporary message file: message.txt"));
}

#[test]
fn preview_text_truncates_long_multiline_messages() {
	let preview = preview_text(&format!("{}\nsecond line", "a".repeat(260)));

	assert!(preview.ends_with('…'));
	assert!(!preview.contains('\n'));
}

#[test]
fn run_git_commit_message_reports_create_temp_file_diagnostics() {
	let tempdir = tempdir("create parent");
	let missing_nested_root = tempdir.path().join("missing-parent").join("repo");
	let error = run_git_commit_message(
		&missing_nested_root,
		&CommitMessage {
			subject: "chore(release): prepare release".to_string(),
			body: Some("release body".to_string()),
		},
		"commit release pull request changes",
		false,
	)
	.err()
	.unwrap_or_else(|| panic!("expected commit failure"))
	.to_string();

	assert!(error.contains("could not create temporary git commit message file"));
	assert!(error.contains("phase: creating temporary commit message file"));
	assert!(error.contains("temporary message file: <not yet created>"));
}

#[test]
fn run_git_commit_message_reports_write_and_flush_diagnostics() {
	let tempdir = tempdir("create repo");
	let root = tempdir.path();
	let message = CommitMessage {
		subject: "chore(release): prepare release".to_string(),
		body: Some("release body".to_string()),
	};

	let write_error = run_git_commit_message_with_io(
		root,
		&message,
		"commit release pull request changes",
		false,
		|_, _| Err(io::Error::other("write failed")),
		flush_git_commit_message_file,
	)
	.err()
	.unwrap_or_else(|| panic!("expected write failure"))
	.to_string();
	let flush_error = run_git_commit_message_with_io(
		root,
		&message,
		"commit release pull request changes",
		false,
		|_, _| Ok(()),
		|_| Err(io::Error::other("flush failed")),
	)
	.err()
	.unwrap_or_else(|| panic!("expected flush failure"))
	.to_string();

	assert!(write_error.contains("could not write temporary git commit message file"));
	assert!(write_error.contains("phase: writing temporary commit message file"));
	assert!(write_error.contains("temporary message file:"));
	assert!(flush_error.contains("could not flush temporary git commit message file"));
	assert!(flush_error.contains("phase: flushing temporary commit message file"));
	assert!(flush_error.contains("temporary message file:"));
}

#[test]
fn run_git_commit_message_commits_large_message_from_file() {
	let tempdir = tempdir("create repo");
	let root = tempdir.path();
	git(root, &["init"]);
	git(root, &["config", "user.name", "monochange Tests"]);
	git(root, &["config", "user.email", "monochange@example.com"]);
	fs::write(root.join("release.txt"), "release\n")
		.unwrap_or_else(|error| panic!("write release file: {error}"));
	git(root, &["add", "release.txt"]);

	let body = "release target metadata\n".repeat(30_000);
	run_git_commit_message(
		root,
		&CommitMessage {
			subject: "chore(release): prepare release".to_string(),
			body: Some(body.clone()),
		},
		"commit release pull request changes",
		false,
	)
	.unwrap_or_else(|error| panic!("commit: {error}"));

	let message = git_stdout(root, &["log", "-1", "--pretty=%B"]);
	assert!(message.starts_with("chore(release): prepare release\n\n"));
	assert!(message.contains("release target metadata"));
	assert!(message.len() >= body.len());
}

#[test]
fn run_git_commit_message_reports_message_diagnostics_when_git_cannot_start() {
	let tempdir = tempdir("create parent");
	let missing = tempdir.path().join("missing");
	let error = run_git_commit_message(
		&missing,
		&CommitMessage {
			subject: "chore(release): prepare release".to_string(),
			body: Some("release body".to_string()),
		},
		"commit release pull request changes",
		false,
	)
	.err()
	.unwrap_or_else(|| panic!("expected commit failure"));
	let error = error.to_string();

	assert!(error.contains("failed to commit release pull request changes"));
	assert!(error.contains("phase: spawning git commit with --file"));
	assert!(error.contains("command: git commit --file <temporary-message-file>"));
	assert!(error.contains("subject: chore(release): prepare release"));
	assert!(error.contains("body bytes: 12"));
	assert!(error.contains("full message bytes: 45"));
	assert!(error.contains("write the commit message to a file"));
	assert!(error.contains("avoid passing very large release records"));
}