use crate::{
git::{self, Generation, GitRef, GitRemote, RepoDetails},
git_dir::StoragePathType,
s, webhook, BranchName, ForgeAlias, ForgeConfig, ForgeType, GitDir, Hostname, RemoteUrl,
RepoAlias, RepoBranches, RepoConfig, RepoConfigSource, RepoPath, ServerRepoConfig,
};
use assert2::let_assert;
use secrecy::ExposeSecret;
use std::{
collections::BTreeMap,
path::{Path, PathBuf},
};
type TestResult = Result<(), Box<dyn std::error::Error>>;
mod commit {
use super::*;
#[test]
fn should_return_sha() {
let sha = given::a_commit_sha();
let commit = given::a_commit_with_sha(&sha);
assert_eq!(commit.sha(), &sha);
}
#[test]
fn should_return_message() {
let message = given::a_commit_message();
let commit = given::a_commit_with_message(&message);
assert_eq!(commit.message(), &message);
}
#[test]
fn should_convert_from_push() {
let sha = given::a_commit_sha();
let message = given::a_commit_message();
let push = given::a_webhook_push(&sha, &message);
let commit = git::Commit::from(push);
let expected = git::Commit::new(
git::commit::Sha::new(sha),
git::commit::Message::new(message),
);
assert_eq!(commit, expected);
}
}
mod generation {
use super::*;
#[test]
fn should_increment() {
let mut g = Generation::default();
assert_eq!(s!(g), "0");
g.inc();
assert_eq!(s!(g), "1");
}
}
mod gitref {
use super::*;
#[test]
fn should_convert_from_commit() {
let commit = git::Commit::new(
git::commit::Sha::new("sha"),
git::commit::Message::new("message"),
);
let gitref = GitRef::from(commit);
assert_eq!(s!(gitref), "sha");
}
}
mod gitremote {
use super::*;
#[test]
fn should_return_hostname() {
let host = Hostname::new("localhost");
let repo_path = RepoPath::new(s!("kemitix/git-next"));
let gr = GitRemote::new(host.clone(), repo_path);
assert_eq!(gr.host(), &host);
}
#[test]
fn should_return_repo_path() {
let host = Hostname::new("localhost");
let repo_path = RepoPath::new(s!("kemitix/git-next"));
let gr = GitRemote::new(host, repo_path.clone());
assert_eq!(gr.repo_path(), &repo_path);
}
}
mod push {
use super::*;
#[test]
fn force_no_should_display() {
assert_eq!(s!(git::push::Force::No), "fast-forward");
}
#[test]
fn force_from_should_display() {
let sha = given::a_name();
let commit = given::a_commit_with_sha(&git::commit::Sha::new(sha.clone()));
assert_eq!(
s!(git::push::Force::From(GitRef::from(commit))),
format!("force-if-from:{sha}")
);
}
mod reset {
use super::*;
#[test]
fn should_perform_a_fetch_then_push() {
let mut open_repository = git::repository::open::mock();
let mut seq = mockall::Sequence::new();
open_repository
.expect_fetch()
.times(1)
.in_sequence(&mut seq)
.returning(|| Ok(()));
open_repository
.expect_push()
.times(1)
.in_sequence(&mut seq)
.returning(|_repo_details, _branch_name, _gitref, _force| Ok(()));
let fs = given::a_filesystem();
let repo_details = given::repo_details(&fs);
let branch_name = &repo_details.branch;
let commit = given::a_commit();
let gitref = GitRef::from(commit);
let_assert!(
Ok(()) = git::push::reset(
&*open_repository,
&repo_details,
branch_name,
&gitref,
&git::push::Force::No
)
);
}
}
}
mod repo_details {
use super::*;
#[test]
fn should_return_origin() {
let rd = RepoDetails::new(
Generation::default(),
&RepoAlias::new("foo"),
&ServerRepoConfig::new(s!("repo"), s!("branch"), None, None, None, None),
&ForgeAlias::new("default"),
&ForgeConfig::new(
ForgeType::MockForge,
s!("host"),
s!("user"),
s!("token"),
given::maybe_a_number(), BTreeMap::new(),
),
GitDir::new(PathBuf::default().join("foo"), StoragePathType::Internal),
);
assert_eq!(
rd.origin().expose_secret(),
"https://user:token@host/repo.git"
);
}
}
pub mod given {
use crate::ForgeDetails;
use super::*;
pub fn repo_branches() -> RepoBranches {
RepoBranches::new(
format!("main-{}", a_name()),
format!("next-{}", a_name()),
format!("dev-{}", a_name()),
)
}
pub fn a_forge_alias() -> ForgeAlias {
ForgeAlias::new(a_name())
}
pub fn a_repo_alias() -> RepoAlias {
RepoAlias::new(a_name())
}
pub fn a_pathbuf() -> PathBuf {
PathBuf::from(given::a_name())
}
pub fn a_name() -> String {
use rand::Rng;
use std::iter;
fn generate(len: usize) -> String {
const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
let mut rng = rand::thread_rng();
let one_char = || CHARSET[rng.gen_range(0..CHARSET.len())] as char;
iter::repeat_with(one_char).take(len).collect()
}
generate(5)
}
pub fn maybe_a_number() -> Option<u32> {
use rand::Rng;
let mut rng = rand::thread_rng();
if Rng::gen_ratio(&mut rng, 1, 2) {
Some(a_number())
} else {
None
}
}
pub fn a_number() -> u32 {
use rand::Rng;
let mut rng = rand::thread_rng();
rng.gen_range(5..100)
}
pub fn a_branch_name() -> BranchName {
BranchName::new(a_name())
}
pub fn a_git_dir(fs: &kxio::fs::FileSystem) -> GitDir {
let dir_name = a_name();
let dir = fs.base().join(dir_name);
GitDir::new(dir, StoragePathType::Internal)
}
pub fn a_forge_config() -> ForgeConfig {
ForgeConfig::new(
ForgeType::MockForge,
format!("hostname-{}", a_name()),
format!("user-{}", a_name()),
format!("token-{}", a_name()),
given::maybe_a_number(), BTreeMap::default(), )
}
pub fn forge_details() -> ForgeDetails {
(&a_forge_alias(), &a_forge_config()).into()
}
pub fn a_server_repo_config() -> ServerRepoConfig {
let main = a_branch_name().peel();
let next = a_branch_name().peel();
let dev = a_branch_name().peel();
ServerRepoConfig::new(
format!("{}/{}", a_name(), a_name()),
main.clone(),
None,
Some(main),
Some(next),
Some(dev),
)
}
pub fn a_repo_config() -> RepoConfig {
RepoConfig::new(given::repo_branches(), RepoConfigSource::Repo)
}
pub fn a_commit() -> git::Commit {
git::Commit::new(a_commit_sha(), a_commit_message())
}
pub fn a_commit_with_message(message: &git::commit::Message) -> git::Commit {
git::Commit::new(a_commit_sha(), message.to_owned())
}
pub fn a_commit_with_sha(sha: &git::commit::Sha) -> git::Commit {
git::Commit::new(sha.to_owned(), a_commit_message())
}
pub fn a_commit_message() -> git::commit::Message {
git::commit::Message::new(a_name())
}
pub fn a_commit_sha() -> git::commit::Sha {
git::commit::Sha::new(a_name())
}
pub fn a_webhook_push(sha: &git::commit::Sha, message: &git::commit::Message) -> webhook::Push {
let branch = a_branch_name();
webhook::Push::new(branch, s!(sha), s!(message))
}
pub fn a_filesystem() -> kxio::fs::TempFileSystem {
kxio::fs::temp().unwrap_or_else(|e| panic!("{}", e))
}
pub fn a_hostname() -> Hostname {
Hostname::new(given::a_name())
}
pub fn repo_details(fs: &kxio::fs::FileSystem) -> git::RepoDetails {
let generation = git::Generation::default();
let repo_alias = a_repo_alias();
let server_repo_config = a_server_repo_config();
let forge_alias = a_forge_alias();
let forge_config = a_forge_config();
let gitdir = a_git_dir(fs);
RepoDetails::new(
generation,
&repo_alias,
&server_repo_config,
&forge_alias,
&forge_config,
gitdir,
)
}
#[allow(clippy::expect_used)]
pub fn a_bare_repo_with_url(path: &Path, url: &str, fs: &kxio::fs::FileSystem) {
let repo = gix::prepare_clone_bare(url, fs.base()).expect("prepare_clone_bare");
repo.persist();
let file = fs.file(&path.join("config"));
let config_file = file.reader().expect("reader");
let mut config_lines = config_file.lines().expect("lines").collect::<Vec<_>>();
config_lines.push(r#"[remote "origin"]"#);
let url_line = format!(r#" url = "{url}""#);
tracing::info!(?url, %url_line, "writing");
config_lines.push(&url_line);
file.write(config_lines.join("\n").as_str()).expect("write");
}
#[allow(clippy::unwrap_used)]
pub fn a_remote_url() -> RemoteUrl {
let hostname = given::a_hostname();
let owner = given::a_name();
let repo = given::a_name();
RemoteUrl::parse(format!("git@{hostname}:{owner}/{repo}.git")).unwrap()
}
}
pub mod then {
use super::*;
pub fn commit_named_file_to_branch(
file_name: &Path,
contents: &str,
fs: &kxio::fs::FileSystem,
gitdir: &GitDir,
branch_name: &BranchName,
) -> TestResult {
git_checkout_new_branch(branch_name, gitdir)?;
let pathbuf = PathBuf::from(gitdir);
let file = fs.base().join(pathbuf).join(file_name);
#[allow(clippy::expect_used)]
fs.file(&file).write(contents)?;
git_add_file(gitdir, &file)?;
git_commit(gitdir, &file)?;
then::push_branch(fs, gitdir, branch_name)?;
Ok(())
}
pub fn create_a_commit_on_branch(
fs: &kxio::fs::FileSystem,
gitdir: &GitDir,
branch_name: &BranchName,
) -> TestResult {
git_checkout_new_branch(branch_name, gitdir)?;
let word = given::a_name();
let pathbuf = PathBuf::from(gitdir);
let file = fs.base().join(pathbuf).join(&word);
fs.file(&file).write(&word)?;
git_add_file(gitdir, &file)?;
git_commit(gitdir, &file)?;
then::push_branch(fs, gitdir, branch_name)?;
Ok(())
}
fn push_branch(
fs: &kxio::fs::FileSystem,
gitdir: &GitDir,
branch_name: &BranchName,
) -> TestResult {
let gitrefs = fs
.base()
.join(gitdir.to_path_buf())
.join(".git")
.join("refs");
let local_branch = gitrefs.join("heads").join(branch_name.as_str());
let origin_heads = gitrefs.join("remotes").join("origin");
let remote_branch = origin_heads.join(branch_name.as_str());
let contents = fs.file(&local_branch).reader()?;
fs.dir(&origin_heads).create_all()?;
fs.file(&remote_branch).write(s!(contents))?;
Ok(())
}
pub fn git_checkout_new_branch(branch_name: &BranchName, gitdir: &GitDir) -> TestResult {
exec(
&format!("git checkout -b {branch_name}"),
std::process::Command::new("/usr/bin/git")
.current_dir(gitdir.to_path_buf())
.args(["checkout", "-b", branch_name.as_str()])
.output(),
)?;
Ok(())
}
pub fn git_switch(branch_name: &BranchName, gitdir: &GitDir) -> TestResult {
exec(
&format!("git switch {branch_name}"),
std::process::Command::new("/usr/bin/git")
.current_dir(gitdir.to_path_buf())
.args(["switch", branch_name.as_str()])
.output(),
)
}
fn exec(label: &str, output: Result<std::process::Output, std::io::Error>) -> TestResult {
println!("== {label}");
match output {
Ok(output) => {
println!(
"\nstdout:\n{}",
String::from_utf8_lossy(output.stdout.as_slice())
);
println!(
"\nstderr:\n{}",
String::from_utf8_lossy(output.stderr.as_slice())
);
println!("=============================");
Ok(())
}
Err(err) => {
println!("ERROR: {err:#?}");
Ok(Err(err)?)
}
}
}
fn git_add_file(gitdir: &GitDir, file: &Path) -> TestResult {
exec(
&format!("git add {file:?}"),
std::process::Command::new("/usr/bin/git")
.current_dir(gitdir.to_path_buf())
.args(["add", s!(file.display()).as_str()])
.output(),
)
}
fn git_commit(gitdir: &GitDir, file: &Path) -> TestResult {
exec(
&format!(r#"git commit -m"Added {file:?}""#),
std::process::Command::new("/usr/bin/git")
.current_dir(gitdir.to_path_buf())
.args(["commit", format!(r#"-m"Added {}"#, file.display()).as_str()])
.output(),
)
}
pub fn git_log_all(gitdir: &GitDir) -> TestResult {
exec(
"git log --all --oneline --decorate --graph",
std::process::Command::new("/usr/bin/git")
.current_dir(gitdir.to_path_buf())
.args(["log", "--all", "--oneline", "--decorate", "--graph"])
.output(),
)
}
pub fn get_sha_for_branch(
fs: &kxio::fs::FileSystem,
gitdir: &GitDir,
branch_name: &BranchName,
) -> Result<git::commit::Sha, Box<dyn std::error::Error>> {
let main_ref = fs
.base()
.join(gitdir.to_path_buf())
.join(".git")
.join("refs")
.join("heads")
.join(branch_name.as_str());
let sha = fs.file(&main_ref).reader()?;
Ok(git::commit::Sha::new(s!(sha).trim()))
}
}