use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::Path;
use ta_changeset::DraftPackage;
use ta_goal::GoalRun;
use thiserror::Error;
use crate::config::SubmitConfig;
#[derive(Debug, Error)]
pub enum SubmitError {
#[error("Adapter not configured: {0}")]
NotConfigured(String),
#[error("VCS operation failed: {0}")]
VcsError(String),
#[error("IO error: {0}")]
IoError(#[from] std::io::Error),
#[error("Configuration error: {0}")]
ConfigError(String),
#[error("Review creation failed: {0}")]
ReviewError(String),
#[error("Invalid state: {0}")]
InvalidState(String),
#[error("Sync failed: {0}")]
SyncError(String),
#[error("Sync conflict: {conflicts} file(s) in conflict")]
SyncConflict { conflicts: usize },
}
pub type Result<T> = std::result::Result<T, SubmitError>;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommitResult {
pub commit_id: String,
pub message: String,
#[serde(default)]
pub metadata: HashMap<String, String>,
#[serde(default)]
pub ignored_artifacts: Vec<ta_changeset::IgnoredArtifact>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PushResult {
pub remote_ref: String,
pub message: String,
#[serde(default)]
pub metadata: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReviewResult {
pub review_url: String,
pub review_id: String,
pub message: String,
#[serde(default)]
pub metadata: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SyncResult {
pub updated: bool,
pub conflicts: Vec<String>,
pub new_commits: u32,
pub message: String,
#[serde(default)]
pub metadata: HashMap<String, String>,
}
impl SyncResult {
pub fn is_clean(&self) -> bool {
self.conflicts.is_empty()
}
}
pub struct SavedVcsState {
pub adapter: String,
pub data: Box<dyn std::any::Any + Send>,
}
pub trait SourceAdapter: Send + Sync {
fn prepare(&self, goal: &GoalRun, config: &SubmitConfig) -> Result<()>;
fn commit(&self, goal: &GoalRun, pr: &DraftPackage, message: &str) -> Result<CommitResult>;
fn push(&self, goal: &GoalRun) -> Result<PushResult>;
fn open_review(&self, goal: &GoalRun, pr: &DraftPackage) -> Result<ReviewResult>;
fn sync_upstream(&self) -> Result<SyncResult> {
Ok(SyncResult {
updated: false,
conflicts: vec![],
new_commits: 0,
message: "No sync operation (default implementation)".to_string(),
metadata: HashMap::new(),
})
}
fn name(&self) -> &str;
fn exclude_patterns(&self) -> Vec<String> {
vec![]
}
fn save_state(&self) -> Result<Option<SavedVcsState>> {
Ok(None)
}
fn restore_state(&self, _state: Option<SavedVcsState>) -> Result<()> {
Ok(())
}
fn current_branch(&self) -> Result<String> {
Ok("unknown".to_string())
}
fn revision_id(&self) -> Result<String> {
Ok("unknown".to_string())
}
fn check_review(&self, _review_id: &str) -> Result<Option<ReviewStatus>> {
Ok(None)
}
fn merge_review(&self, _review_id: &str) -> Result<MergeResult> {
Ok(MergeResult {
merged: false,
merge_commit: None,
message: "This adapter does not support automatic merging. \
Merge the PR manually in your VCS platform, then run `ta sync`."
.to_string(),
metadata: HashMap::new(),
})
}
fn detect(project_root: &Path) -> bool
where
Self: Sized,
{
let _ = project_root;
false
}
fn protected_submit_targets(&self) -> Vec<String> {
vec![]
}
fn verify_not_on_protected_target(&self) -> Result<()> {
Ok(())
}
fn stage_env(
&self,
_staging_dir: &Path,
_config: &crate::config::VcsAgentConfig,
) -> Result<HashMap<String, String>> {
Ok(HashMap::new())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MergeResult {
pub merged: bool,
pub merge_commit: Option<String>,
pub message: String,
#[serde(default)]
pub metadata: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReviewStatus {
pub state: String,
pub checks_passing: Option<bool>,
}
pub use SourceAdapter as SubmitAdapter;
#[cfg(test)]
mod tests {
use super::*;
struct MockAdapter;
impl SourceAdapter for MockAdapter {
fn prepare(&self, _: &GoalRun, _: &SubmitConfig) -> Result<()> {
Ok(())
}
fn commit(&self, _: &GoalRun, _: &DraftPackage, _: &str) -> Result<CommitResult> {
unimplemented!()
}
fn push(&self, _: &GoalRun) -> Result<PushResult> {
unimplemented!()
}
fn open_review(&self, _: &GoalRun, _: &DraftPackage) -> Result<ReviewResult> {
unimplemented!()
}
fn name(&self) -> &str {
"mock"
}
}
#[test]
fn default_protected_targets_empty() {
let adapter = MockAdapter;
assert!(adapter.protected_submit_targets().is_empty());
}
#[test]
fn default_verify_not_on_protected_target_ok() {
let adapter = MockAdapter;
assert!(adapter.verify_not_on_protected_target().is_ok());
}
#[test]
fn sync_result_is_clean_when_no_conflicts() {
let result = SyncResult {
updated: true,
conflicts: vec![],
new_commits: 3,
message: "ok".to_string(),
metadata: HashMap::new(),
};
assert!(result.is_clean());
}
#[test]
fn sync_result_is_not_clean_with_conflicts() {
let result = SyncResult {
updated: true,
conflicts: vec!["src/main.rs".to_string()],
new_commits: 3,
message: "conflict".to_string(),
metadata: HashMap::new(),
};
assert!(!result.is_clean());
}
#[test]
fn sync_result_serialization_roundtrip() {
let result = SyncResult {
updated: true,
conflicts: vec!["a.rs".to_string()],
new_commits: 5,
message: "synced".to_string(),
metadata: [("branch".to_string(), "main".to_string())]
.into_iter()
.collect(),
};
let json = serde_json::to_string(&result).unwrap();
let restored: SyncResult = serde_json::from_str(&json).unwrap();
assert!(restored.updated);
assert_eq!(restored.conflicts, vec!["a.rs"]);
assert_eq!(restored.new_commits, 5);
}
}