use quilt_rs::flow::UserMeta;
use quilt_rs::io::remote::HostConfig;
use quilt_rs::io::remote::WorkflowIntent;
use quilt_rs::lineage::CommitState;
use quilt_uri::Namespace;
use tracing::log;
use crate::cli::Error;
use crate::cli::model::Commands;
use crate::cli::output::Render;
use crate::cli::output::Std;
#[derive(Clone, Debug)]
pub struct Input {
pub message: String,
pub namespace: Namespace,
pub user_meta: UserMeta,
pub workflow: WorkflowIntent,
pub host_config: Option<HostConfig>,
}
#[derive(Debug)]
pub struct Output {
pub commit: CommitState,
}
impl std::fmt::Display for Output {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, r#"New commit "{}" created"#, self.commit.hash)
}
}
impl Render for Output {
fn to_json(&self) -> serde_json::Value {
serde_json::json!({ "hash": &self.commit.hash })
}
}
pub async fn command(m: impl Commands, args: Input) -> Std {
Std::from_result(m.commit(args).await)
}
async fn commit_package(
local_domain: &quilt_rs::LocalDomain,
namespace: Namespace,
message: String,
user_meta: UserMeta,
workflow: WorkflowIntent,
host_config: Option<HostConfig>,
) -> Result<CommitState, Error> {
match local_domain.get_installed_package(&namespace).await? {
Some(installed_package) => {
let named_id = match &workflow {
WorkflowIntent::Named(id) => Some(id.clone()),
WorkflowIntent::BucketDefault | WorkflowIntent::NoWorkflow => None,
};
let workflow = installed_package.resolve_workflow(workflow).await?;
if workflow.is_none()
&& let Some(id) = named_id
{
log::warn!(
"workflow '{id}' not applied: package has no remote yet; choose it at push/set-remote."
);
}
Ok(installed_package
.commit(message, user_meta, workflow, host_config)
.await?)
}
None => Err(Error::NamespaceNotFound(namespace)),
}
}
pub async fn model(
local_domain: &quilt_rs::LocalDomain,
Input {
message,
namespace,
user_meta,
workflow,
host_config,
}: Input,
) -> Result<Output, Error> {
let commit = commit_package(
local_domain,
namespace,
message,
user_meta,
workflow,
host_config,
)
.await?;
Ok(Output { commit })
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
use test_log::test;
use crate::cli::create;
use crate::cli::model::create_model_in_temp_dir;
use crate::cli::model::install_package_into_temp_dir;
use crate::cli::output::Render;
use quilt_rs::io::storage::ByteStream;
use quilt_rs::io::storage::LocalStorage;
use quilt_rs::io::storage::Storage;
#[test(tokio::test)]
async fn live_commit_package_with_message_and_null_workflow() -> Result<(), Error> {
use crate::cli::fixtures::packages::workflow_null as pkg;
let uri = pkg::URI;
let (m, _installed_package, _tempdir) = install_package_into_temp_dir(uri).await?;
{
let local_domain = m.get_local_domain();
let output = model(
local_domain,
Input {
message: pkg::MESSAGE.to_string(),
namespace: pkg::NAMESPACE.into(),
user_meta: UserMeta::Keep,
workflow: WorkflowIntent::NoWorkflow,
host_config: None,
},
)
.await?;
assert_eq!(output.commit.hash, pkg::TOP_HASH);
}
Ok(())
}
#[test(tokio::test)]
async fn live_commit_package_bucket_default_equals_null_workflow() -> Result<(), Error> {
use crate::cli::fixtures::packages::workflow_null as pkg;
let uri = pkg::URI;
let (m, _installed_package, _tempdir) = install_package_into_temp_dir(uri).await?;
{
let local_domain = m.get_local_domain();
let output = model(
local_domain,
Input {
message: pkg::MESSAGE.to_string(),
namespace: pkg::NAMESPACE.into(),
user_meta: UserMeta::Keep,
workflow: WorkflowIntent::BucketDefault,
host_config: None,
},
)
.await?;
assert_eq!(output.commit.hash, pkg::TOP_HASH);
}
Ok(())
}
#[test(tokio::test)]
async fn live_commit_rejected_by_workflow_surfaces_clear_error() -> Result<(), Error> {
use crate::cli::fixtures::packages::my_workflow as pkg;
let uri = pkg::URI;
let (m, _installed_package, _tempdir) = install_package_into_temp_dir(uri).await?;
let local_domain = m.get_local_domain();
let err = model(
local_domain,
Input {
message: pkg::MESSAGE.to_string(),
namespace: pkg::NAMESPACE.into(),
user_meta: UserMeta::Clear,
workflow: WorkflowIntent::Named("my-workflow".to_string()),
host_config: None,
},
)
.await
.unwrap_err();
let message = err.to_string();
assert!(
message.starts_with("quilt_rs error: package does not satisfy the workflow"),
"message must announce a workflow rejection, got: {message}"
);
assert!(
message.contains("metadata_schema"),
"message must name the failing rule, got: {message}"
);
for field in ["Date", "Name", "Owner", "Type"] {
assert!(
message.contains(field),
"message must name the missing field {field}, got: {message}"
);
}
Ok(())
}
#[test(tokio::test)]
async fn live_commit_package_with_workflow_and_meta() -> Result<(), Error> {
use crate::cli::fixtures::packages::my_workflow as pkg;
let uri = pkg::URI;
let (m, _installed_package, _tempdir) = install_package_into_temp_dir(uri).await?;
{
let local_domain = m.get_local_domain();
let output = model(
local_domain,
Input {
message: pkg::MESSAGE.to_string(),
namespace: pkg::NAMESPACE.into(),
user_meta: UserMeta::Set(serde_json::json!({
"Date": "2025-12-31",
"Name": "Foo",
"Owner": "Kevin",
"Type": "NGS"
})),
workflow: WorkflowIntent::Named("my-workflow".to_string()),
host_config: None,
},
)
.await?;
assert_eq!(output.commit.hash, pkg::TOP_HASH);
}
Ok(())
}
#[test(tokio::test)]
async fn live_commit_package_with_message_only() -> Result<(), Error> {
use crate::cli::fixtures::packages::no_workflows_message_only as pkg;
let uri = pkg::URI;
let (m, _installed_package, _tempdir) = install_package_into_temp_dir(uri).await?;
{
let local_domain = m.get_local_domain();
let output = model(
local_domain,
Input {
message: pkg::MESSAGE.to_string(),
namespace: pkg::NAMESPACE.into(),
user_meta: UserMeta::Keep,
workflow: WorkflowIntent::NoWorkflow,
host_config: None,
},
)
.await?;
assert_eq!(output.commit.hash, pkg::TOP_HASH);
}
Ok(())
}
#[test(tokio::test)]
async fn live_throwing_error_when_workflow_set_but_no_workflows_config() -> Result<(), Error> {
use crate::cli::fixtures::packages::no_workflows_message_only as pkg;
let uri = pkg::URI;
let (m, _installed_package, _tempdir) = install_package_into_temp_dir(uri).await?;
{
let local_domain = m.get_local_domain();
let output = model(
local_domain,
Input {
message: pkg::MESSAGE.to_string(),
namespace: pkg::NAMESPACE.into(),
user_meta: UserMeta::Keep,
workflow: WorkflowIntent::Named("Anything".to_string()),
host_config: None,
},
)
.await;
assert_eq!(
output.unwrap_err().to_string(),
r#"quilt_rs error: Workflow error: There is no workflows config, but the workflow "Anything" is set"#
);
}
Ok(())
}
#[test(tokio::test)]
async fn live_commit_package_with_meta_only() -> Result<(), Error> {
use crate::cli::fixtures::packages::no_workflows_with_meta as pkg;
let uri = pkg::URI;
let (m, _installed_package, _tempdir) = install_package_into_temp_dir(uri).await?;
{
let local_domain = m.get_local_domain();
let output = model(
local_domain,
Input {
message: "Initial".to_string(),
namespace: pkg::NAMESPACE.into(),
user_meta: UserMeta::Set(serde_json::json!({
"C": "D",
"c": "d",
"a": "b",
"A": "B",
"e": 123,
"f": null
})),
workflow: WorkflowIntent::NoWorkflow,
host_config: None,
},
)
.await?;
assert_eq!(output.commit.hash, pkg::TOP_HASH);
}
Ok(())
}
#[test(tokio::test)]
async fn test_commit_named_workflow_without_remote_drops_workflow() -> Result<(), Error> {
let (m, _temp_dir) = create_model_in_temp_dir().await?;
let namespace: Namespace = ("local", "no_remote").into();
let create_output = m
.create(create::Input {
namespace: namespace.clone(),
source: None,
message: None,
})
.await?;
let output = model(
m.get_local_domain(),
Input {
message: "local commit".to_string(),
namespace: namespace.clone(),
user_meta: UserMeta::Keep,
workflow: WorkflowIntent::Named("some-workflow".to_string()),
host_config: None,
},
)
.await?;
assert!(!output.commit.hash.is_empty(), "commit should succeed");
let manifest = create_output.installed_package.manifest().await?;
assert!(
manifest.header.workflow.is_none(),
"remote-less Named intent must not stamp a workflow"
);
Ok(())
}
#[test(tokio::test)]
async fn live_model() -> Result<(), Error> {
let uri = "quilt+s3://udp-spec#package=spec/quilt-rs@11c5f6dbd1bf1d8675c18aaaa963b2f0dced2f892c7406fa36c9cd17d3d31b73";
let timestamp_logical_key = PathBuf::from("timestamp.txt");
let (m, installed_package, _temp_dir) = install_package_into_temp_dir(uri).await?;
let first_input = Input {
message: "Test message".to_string(),
namespace: ("spec", "quilt-rs").into(),
user_meta: UserMeta::Clear,
workflow: WorkflowIntent::NoWorkflow,
host_config: None,
};
let hash_for_initial_test_commit =
"d6e62c3c43ddd30447d99eede1c7280c017b15cc716037b74af7bb5230fbb61a";
{
let local_domain = m.get_local_domain();
let output = model(local_domain, first_input.clone())
.await
.expect("Failed to commit");
assert_eq!(
format!("{output}"),
format!("New commit \"{}\" created", hash_for_initial_test_commit)
);
}
{
let local_domain = m.get_local_domain();
let second_commit = model(local_domain, first_input)
.await
.expect("Failed to commit second commit which is identical to the first one");
assert_eq!(second_commit.commit.hash, hash_for_initial_test_commit);
assert_eq!(
second_commit.commit.prev_hashes,
vec![hash_for_initial_test_commit]
);
}
{
let local_domain = m.get_local_domain();
let third_commit = model(
local_domain,
Input {
message: "New commit message".to_string(),
namespace: ("spec", "quilt-rs").into(),
user_meta: UserMeta::Set(serde_json::json!({"key": "value"})),
workflow: WorkflowIntent::NoWorkflow,
host_config: None,
},
)
.await
.expect("Failed to commit third commit different from the first one");
assert_eq!(
third_commit.commit.hash,
"e2a86408670c7a33f78758d72166333e4a96b6aadbb3b03d25fd6e209dc6e0b3"
);
assert_eq!(
third_commit.commit.prev_hashes,
vec![hash_for_initial_test_commit, hash_for_initial_test_commit]
);
}
{
let local_domain = m.get_local_domain();
let not_found = model(
local_domain,
Input {
message: "Anything".to_string(),
namespace: ("a", "b").into(),
user_meta: UserMeta::Keep,
workflow: WorkflowIntent::NoWorkflow,
host_config: None,
},
)
.await;
assert_eq!(not_found.unwrap_err().to_string(), "Package a/b not found");
}
let working_dir = installed_package.package_home().await?;
let storage = LocalStorage::new();
storage
.write_byte_stream(
working_dir.join(timestamp_logical_key),
ByteStream::from_static(b"1697916638"),
)
.await
.expect("Failed to write timestamp.txt to the installed package working directory");
{
let local_domain = m.get_local_domain();
let commit_the_same_file = model(
local_domain,
Input {
message: "Test message".to_string(),
namespace: ("spec", "quilt-rs").into(),
user_meta: UserMeta::Clear,
workflow: WorkflowIntent::NoWorkflow,
host_config: None,
},
)
.await
.expect("Failed to commit the same file ensuring the commit hash will persist");
assert_eq!(
commit_the_same_file.commit.hash,
hash_for_initial_test_commit
);
}
Ok(())
}
#[test(tokio::test)]
async fn live_model_keep_inherits_fixture_meta() -> Result<(), Error> {
let uri = "quilt+s3://udp-spec#package=spec/quilt-rs@11c5f6dbd1bf1d8675c18aaaa963b2f0dced2f892c7406fa36c9cd17d3d31b73";
let (m, _installed_package, _temp_dir) = install_package_into_temp_dir(uri).await?;
let local_domain = m.get_local_domain();
let output = model(
local_domain,
Input {
message: "Test message".to_string(),
namespace: ("spec", "quilt-rs").into(),
user_meta: UserMeta::Keep,
workflow: WorkflowIntent::NoWorkflow,
host_config: None,
},
)
.await?;
assert_eq!(
output.commit.hash,
"b9853f32d9b87b28a617a51acbdb3dffdb0974053a3415dedfef3950c6e239ef"
);
Ok(())
}
#[test]
fn json_carries_the_hash_and_no_timestamp() {
let output = Output {
commit: CommitState {
timestamp: std::time::SystemTime::UNIX_EPOCH.into(),
hash: "abc123".to_string(),
prev_hashes: vec!["older".to_string()],
},
};
assert_eq!(output.to_json().to_string(), r#"{"hash":"abc123"}"#);
}
}