Replace git_author_name/email parameter pairs with GitAuthor struct

Consolidates the two separate String parameters (git_author_name,
git_author_email) threaded through ~10 function signatures into a
single GitAuthor struct with Default providing "arc"/"arc@local".
Also quotes git config values in parallel.rs shell commands.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-03-07 17:29:32 -05:00
parent c9265749eb
commit f36ec7aab4
10 changed files with 250 additions and 409 deletions

View file

@ -136,14 +136,14 @@ pub async fn serve_command(args: ServeArgs, styles: &'static Styles) -> anyhow::
(auth_mode, client_auth, max_concurrent_runs)
};
let (git_author_name, git_author_email) = {
let git_author = {
let cfg = shared_config.read().expect("config lock poisoned");
(
cfg.git.author.name.clone().unwrap_or_else(|| "arc".into()),
cfg.git.author.email.clone().unwrap_or_else(|| "arc@local".into()),
arc_workflows::git::GitAuthor::from_options(
cfg.git.author.name.clone(),
cfg.git.author.email.clone(),
)
};
let state = crate::server::create_app_state_with_options(db, factory, dry_run_mode, max_concurrent_runs, git_author_name, git_author_email);
let state = crate::server::create_app_state_with_options(db, factory, dry_run_mode, max_concurrent_runs, git_author);
crate::server::spawn_scheduler(Arc::clone(&state));
let router = build_router(state, auth_mode);

View file

@ -106,8 +106,7 @@ pub struct AppState {
max_concurrent_runs: usize,
scheduler_notify: tokio::sync::Notify,
pub hook_config: arc_workflows::hook::HookConfig,
git_author_name: String,
git_author_email: String,
git_author: arc_workflows::git::GitAuthor,
}
/// Build the axum Router with all run endpoints.
@ -367,7 +366,7 @@ pub fn create_app_state(
db: sqlx::SqlitePool,
registry_factory: impl Fn(Arc<dyn Interviewer>) -> HandlerRegistry + Send + Sync + 'static,
) -> Arc<AppState> {
create_app_state_with_options(db, registry_factory, false, 5, "arc".into(), "arc@local".into())
create_app_state_with_options(db, registry_factory, false, 5, arc_workflows::git::GitAuthor::default())
}
/// Create an `AppState` with the given database pool, registry factory, dry-run flag, and concurrency limit.
@ -376,8 +375,7 @@ pub fn create_app_state_with_options(
registry_factory: impl Fn(Arc<dyn Interviewer>) -> HandlerRegistry + Send + Sync + 'static,
dry_run: bool,
max_concurrent_runs: usize,
git_author_name: String,
git_author_email: String,
git_author: arc_workflows::git::GitAuthor,
) -> Arc<AppState> {
Arc::new(AppState {
runs: Mutex::new(HashMap::new()),
@ -388,8 +386,7 @@ pub fn create_app_state_with_options(
max_concurrent_runs,
scheduler_notify: tokio::sync::Notify::new(),
hook_config: arc_workflows::hook::HookConfig::default(),
git_author_name,
git_author_email,
git_author,
})
}
@ -585,8 +582,7 @@ async fn execute_run(state: Arc<AppState>, run_id: String) {
labels: std::collections::HashMap::new(),
checkpoint_exclude_globs: Vec::new(),
github_app: None,
git_author_name: state.git_author_name.clone(),
git_author_email: state.git_author_email.clone(),
git_author: state.git_author.clone(),
};
let result = tokio::select! {
@ -1143,7 +1139,7 @@ mod tests {
#[tokio::test]
async fn test_model_dry_run_returns_ok() {
let state = create_app_state_with_options(test_db().await, test_registry, true, 5, "arc".into(), "arc@local".into());
let state = create_app_state_with_options(test_db().await, test_registry, true, 5, arc_workflows::git::GitAuthor::default());
let app = build_router(state, AuthMode::Disabled);
let req = Request::builder()
@ -1163,7 +1159,7 @@ mod tests {
#[tokio::test]
async fn test_model_dry_run_unknown_returns_404() {
let state = create_app_state_with_options(test_db().await, test_registry, true, 5, "arc".into(), "arc@local".into());
let state = create_app_state_with_options(test_db().await, test_registry, true, 5, arc_workflows::git::GitAuthor::default());
let app = build_router(state, AuthMode::Disabled);
let req = Request::builder()
@ -1844,7 +1840,7 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn concurrency_limit_respected() {
let state = create_app_state_with_options(test_db().await, test_registry, false, 1, "arc".into(), "arc@local".into());
let state = create_app_state_with_options(test_db().await, test_registry, false, 1, arc_workflows::git::GitAuthor::default());
let app = test_app_with_scheduler(state);
// Submit two runs with max_concurrent_runs=1

View file

@ -183,24 +183,21 @@ async fn main() -> Result<()> {
let github_app = build_github_app_credentials(&server_config);
let cli_author = cli_config.git.as_ref().map(|g| &g.author);
let git_author_name = cli_author
.and_then(|a| a.name.as_deref())
.or(server_config.git.author.name.as_deref())
.unwrap_or("arc")
.to_string();
let git_author_email = cli_author
.and_then(|a| a.email.as_deref())
.or(server_config.git.author.email.as_deref())
.unwrap_or("arc@local")
.to_string();
let git_author = arc_workflows::git::GitAuthor::from_options(
cli_author
.and_then(|a| a.name.clone())
.or_else(|| server_config.git.author.name.clone()),
cli_author
.and_then(|a| a.email.clone())
.or_else(|| server_config.git.author.email.clone()),
);
arc_workflows::cli::run::run_command(
args,
server_config.run_defaults,
styles,
github_app,
git_author_name,
git_author_email,
git_author,
)
.await?;
}

View file

@ -181,12 +181,11 @@ pub async fn run_command(
run_defaults: RunDefaults,
styles: &'static Styles,
github_app: Option<crate::github_app::GitHubAppCredentials>,
git_author_name: String,
git_author_email: String,
git_author: crate::git::GitAuthor,
) -> anyhow::Result<()> {
// Handle --run-branch resume: read everything from git metadata
if let Some(branch) = args.run_branch.clone() {
return run_from_branch(args, &branch, styles, git_author_name, git_author_email).await;
return run_from_branch(args, &branch, styles, git_author).await;
}
let workflow_path = args
@ -697,8 +696,7 @@ pub async fn run_command(
.collect(),
checkpoint_exclude_globs,
github_app: github_app.clone(),
git_author_name,
git_author_email,
git_author,
};
let run_start = Instant::now();
@ -930,8 +928,7 @@ async fn run_from_branch(
args: RunArgs,
run_branch: &str,
styles: &'static Styles,
git_author_name: String,
git_author_email: String,
git_author: crate::git::GitAuthor,
) -> anyhow::Result<()> {
// Extract run_id from branch name: "arc/run/{run_id}" -> "{run_id}"
let run_id = run_branch
@ -1068,8 +1065,7 @@ async fn run_from_branch(
labels: HashMap::new(),
checkpoint_exclude_globs: Vec::new(),
github_app: None,
git_author_name,
git_author_email,
git_author,
};
let run_start = Instant::now();

View file

@ -521,8 +521,7 @@ pub struct GitState {
pub run_branch: Option<String>,
pub meta_branch: Option<String>,
pub checkpoint_exclude_globs: Vec<String>,
pub git_author_name: String,
pub git_author_email: String,
pub git_author: crate::git::GitAuthor,
}
/// How git checkpointing should be performed for a workflow run.
@ -544,8 +543,7 @@ pub async fn git_checkpoint_host(
completed_count: usize,
shadow_sha: Option<String>,
exclude_globs: Vec<String>,
author_name: String,
author_email: String,
author: crate::git::GitAuthor,
) -> Option<String> {
match tokio::task::spawn_blocking(move || {
crate::git::checkpoint_commit(
@ -556,8 +554,7 @@ pub async fn git_checkpoint_host(
completed_count,
shadow_sha.as_deref(),
&exclude_globs,
&author_name,
&author_email,
&author,
)
})
.await
@ -593,8 +590,7 @@ pub async fn git_checkpoint_remote(
completed_count: usize,
shadow_sha: Option<String>,
exclude_globs: &[String],
author_name: &str,
author_email: &str,
author: &crate::git::GitAuthor,
) -> Option<String> {
// Stage everything (with optional excludes)
let add_cmd = if exclude_globs.is_empty() {
@ -646,7 +642,9 @@ pub async fn git_checkpoint_remote(
// Commit with configured identity using the message file
let commit_cmd = format!(
"{GIT_REMOTE} -c user.name={author_name} -c user.email={author_email} commit --allow-empty -F /tmp/arc-commit-msg"
"{GIT_REMOTE} -c user.name={name} -c user.email={email} commit --allow-empty -F /tmp/arc-commit-msg",
name = author.name,
email = author.email,
);
let commit_result = sandbox
.exec_command(&commit_cmd, 30_000, None, None, None)
@ -838,10 +836,8 @@ pub struct RunConfig {
pub checkpoint_exclude_globs: Vec<String>,
/// GitHub App credentials for pushing metadata branches to origin.
pub github_app: Option<crate::github_app::GitHubAppCredentials>,
/// Git author name for checkpoint commits.
pub git_author_name: String,
/// Git author email for checkpoint commits.
pub git_author_email: String,
/// Git author identity for checkpoint commits.
pub git_author: crate::git::GitAuthor,
}
/// The workflow run execution engine.
@ -1220,8 +1216,7 @@ impl WorkflowRunEngine {
run_branch: config.run_branch.clone(),
meta_branch: config.meta_branch.clone(),
checkpoint_exclude_globs: config.checkpoint_exclude_globs.clone(),
git_author_name: config.git_author_name.clone(),
git_author_email: config.git_author_email.clone(),
git_author: config.git_author.clone(),
})),
_ => None,
};
@ -1272,11 +1267,7 @@ impl WorkflowRunEngine {
None => None,
};
if let Some(repo_path) = store_path {
let store = crate::git::MetadataStore::new(
repo_path,
&config.git_author_name,
&config.git_author_email,
);
let store = crate::git::MetadataStore::new(repo_path, &config.git_author);
let manifest_bytes = serde_json::to_vec_pretty(&manifest).unwrap_or_default();
let dot_source =
std::fs::read(config.logs_root.join("graph.dot")).unwrap_or_default();
@ -1900,11 +1891,7 @@ impl WorkflowRunEngine {
let repo_path = match mode {
GitCheckpointMode::Host(ref p) | GitCheckpointMode::Remote(ref p) => p,
};
let store = crate::git::MetadataStore::new(
repo_path,
&config.git_author_name,
&config.git_author_email,
);
let store = crate::git::MetadataStore::new(repo_path, &config.git_author);
serde_json::to_vec_pretty(&checkpoint)
.ok()
.and_then(|cp_json| {
@ -1953,8 +1940,7 @@ impl WorkflowRunEngine {
completed_count,
shadow_sha,
config.checkpoint_exclude_globs.clone(),
config.git_author_name.clone(),
config.git_author_email.clone(),
config.git_author.clone(),
)
.await
}
@ -1967,8 +1953,7 @@ impl WorkflowRunEngine {
completed_count,
shadow_sha,
&config.checkpoint_exclude_globs,
&config.git_author_name,
&config.git_author_email,
&config.git_author,
)
.await
}
@ -2836,8 +2821,7 @@ mod tests {
labels: HashMap::new(),
checkpoint_exclude_globs: Vec::new(),
github_app: None,
git_author_name: "arc".into(),
git_author_email: "arc@local".into(),
git_author: crate::git::GitAuthor::default(),
};
let outcome = engine.run(&g, &config).await.unwrap();
assert_eq!(outcome.status, StageStatus::Success);
@ -2861,8 +2845,7 @@ mod tests {
labels: HashMap::new(),
checkpoint_exclude_globs: Vec::new(),
github_app: None,
git_author_name: "arc".into(),
git_author_email: "arc@local".into(),
git_author: crate::git::GitAuthor::default(),
};
engine.run(&g, &config).await.unwrap();
let checkpoint_path = dir.path().join("checkpoint.json");
@ -2894,8 +2877,7 @@ mod tests {
labels: HashMap::new(),
checkpoint_exclude_globs: Vec::new(),
github_app: None,
git_author_name: "arc".into(),
git_author_email: "arc@local".into(),
git_author: crate::git::GitAuthor::default(),
};
engine.run(&g, &config).await.unwrap();
@ -2923,8 +2905,7 @@ mod tests {
labels: HashMap::new(),
checkpoint_exclude_globs: Vec::new(),
github_app: None,
git_author_name: "arc".into(),
git_author_email: "arc@local".into(),
git_author: crate::git::GitAuthor::default(),
};
let result = engine.run(&g, &config).await;
assert!(result.is_err());
@ -2948,8 +2929,7 @@ mod tests {
labels: HashMap::new(),
checkpoint_exclude_globs: Vec::new(),
github_app: None,
git_author_name: "arc".into(),
git_author_email: "arc@local".into(),
git_author: crate::git::GitAuthor::default(),
};
engine.run(&g, &config).await.unwrap();
@ -2986,8 +2966,7 @@ mod tests {
labels: HashMap::new(),
checkpoint_exclude_globs: Vec::new(),
github_app: None,
git_author_name: "arc".into(),
git_author_email: "arc@local".into(),
git_author: crate::git::GitAuthor::default(),
};
let outcome = engine.run(&g, &config).await.unwrap();
assert_eq!(outcome.status, StageStatus::Success);
@ -3048,8 +3027,7 @@ mod tests {
labels: HashMap::new(),
checkpoint_exclude_globs: Vec::new(),
github_app: None,
git_author_name: "arc".into(),
git_author_email: "arc@local".into(),
git_author: crate::git::GitAuthor::default(),
};
engine.run(&g, &config).await.unwrap();
@ -3137,8 +3115,7 @@ mod tests {
labels: HashMap::new(),
checkpoint_exclude_globs: Vec::new(),
github_app: None,
git_author_name: "arc".into(),
git_author_email: "arc@local".into(),
git_author: crate::git::GitAuthor::default(),
};
engine.run(&g, &config).await.unwrap();
@ -3169,8 +3146,7 @@ mod tests {
labels: HashMap::from([("env".into(), "test".into())]),
checkpoint_exclude_globs: Vec::new(),
github_app: None,
git_author_name: "arc".into(),
git_author_email: "arc@local".into(),
git_author: crate::git::GitAuthor::default(),
};
engine.run(&g, &config).await.unwrap();
@ -3197,8 +3173,7 @@ mod tests {
labels: HashMap::new(),
checkpoint_exclude_globs: Vec::new(),
github_app: None,
git_author_name: "arc".into(),
git_author_email: "arc@local".into(),
git_author: crate::git::GitAuthor::default(),
};
engine.run(&g, &config).await.unwrap();
@ -3225,8 +3200,7 @@ mod tests {
labels: HashMap::new(),
checkpoint_exclude_globs: Vec::new(),
github_app: None,
git_author_name: "arc".into(),
git_author_email: "arc@local".into(),
git_author: crate::git::GitAuthor::default(),
};
engine.run(&g, &config).await.unwrap();
@ -3256,8 +3230,7 @@ mod tests {
labels: HashMap::new(),
checkpoint_exclude_globs: Vec::new(),
github_app: None,
git_author_name: "arc".into(),
git_author_email: "arc@local".into(),
git_author: crate::git::GitAuthor::default(),
};
engine.run(&g, &config).await.unwrap();
@ -3415,8 +3388,7 @@ mod tests {
labels: HashMap::new(),
checkpoint_exclude_globs: Vec::new(),
github_app: None,
git_author_name: "arc".into(),
git_author_email: "arc@local".into(),
git_author: crate::git::GitAuthor::default(),
};
engine.run(&g, &config).await.unwrap();
@ -3457,8 +3429,7 @@ mod tests {
labels: HashMap::new(),
checkpoint_exclude_globs: Vec::new(),
github_app: None,
git_author_name: "arc".into(),
git_author_email: "arc@local".into(),
git_author: crate::git::GitAuthor::default(),
};
engine.run(&g, &config).await.unwrap();
@ -3517,8 +3488,7 @@ mod tests {
labels: HashMap::new(),
checkpoint_exclude_globs: Vec::new(),
github_app: None,
git_author_name: "arc".into(),
git_author_email: "arc@local".into(),
git_author: crate::git::GitAuthor::default(),
};
let outcome = engine.run(&g, &config).await.unwrap();
@ -3580,8 +3550,7 @@ mod tests {
labels: HashMap::new(),
checkpoint_exclude_globs: Vec::new(),
github_app: None,
git_author_name: "arc".into(),
git_author_email: "arc@local".into(),
git_author: crate::git::GitAuthor::default(),
};
let result = engine.run(&g, &config).await;
@ -3647,8 +3616,7 @@ mod tests {
labels: HashMap::new(),
checkpoint_exclude_globs: Vec::new(),
github_app: None,
git_author_name: "arc".into(),
git_author_email: "arc@local".into(),
git_author: crate::git::GitAuthor::default(),
};
let result = engine.run(&g, &config).await;
assert!(result.is_ok());
@ -3703,8 +3671,7 @@ mod tests {
labels: HashMap::new(),
checkpoint_exclude_globs: Vec::new(),
github_app: None,
git_author_name: "arc".into(),
git_author_email: "arc@local".into(),
git_author: crate::git::GitAuthor::default(),
};
let outcome = engine.run(&g, &config).await.unwrap();
assert_eq!(outcome.status, StageStatus::Success);
@ -3760,8 +3727,7 @@ mod tests {
labels: HashMap::new(),
checkpoint_exclude_globs: Vec::new(),
github_app: None,
git_author_name: "arc".into(),
git_author_email: "arc@local".into(),
git_author: crate::git::GitAuthor::default(),
};
let outcome = engine.run(&g, &config).await.unwrap();
@ -3792,8 +3758,7 @@ mod tests {
labels: HashMap::new(),
checkpoint_exclude_globs: Vec::new(),
github_app: None,
git_author_name: "arc".into(),
git_author_email: "arc@local".into(),
git_author: crate::git::GitAuthor::default(),
};
let outcome = engine.run(&g, &config).await.unwrap();
assert_eq!(outcome.status, StageStatus::Success);
@ -3820,8 +3785,7 @@ mod tests {
labels: HashMap::new(),
checkpoint_exclude_globs: Vec::new(),
github_app: None,
git_author_name: "arc".into(),
git_author_email: "arc@local".into(),
git_author: crate::git::GitAuthor::default(),
};
let result = engine.run(&g, &config).await;
assert!(result.is_err());
@ -3847,8 +3811,7 @@ mod tests {
labels: HashMap::new(),
checkpoint_exclude_globs: Vec::new(),
github_app: None,
git_author_name: "arc".into(),
git_author_email: "arc@local".into(),
git_author: crate::git::GitAuthor::default(),
};
let outcome = engine.run(&g, &config).await.unwrap();
assert_eq!(outcome.status, StageStatus::Success);
@ -3887,8 +3850,7 @@ mod tests {
labels: HashMap::new(),
checkpoint_exclude_globs: Vec::new(),
github_app: None,
git_author_name: "arc".into(),
git_author_email: "arc@local".into(),
git_author: crate::git::GitAuthor::default(),
};
// Set cancel after a short delay (while the slow handler is running)
@ -3964,8 +3926,7 @@ mod tests {
labels: HashMap::new(),
checkpoint_exclude_globs: Vec::new(),
github_app: None,
git_author_name: "arc".into(),
git_author_email: "arc@local".into(),
git_author: crate::git::GitAuthor::default(),
};
let result = engine.run(&g, &config).await;
assert!(result.is_err());
@ -3994,8 +3955,7 @@ mod tests {
labels: HashMap::new(),
checkpoint_exclude_globs: Vec::new(),
github_app: None,
git_author_name: "arc".into(),
git_author_email: "arc@local".into(),
git_author: crate::git::GitAuthor::default(),
};
let result = engine.run(&g, &config).await;
assert!(result.is_err());
@ -4026,8 +3986,7 @@ mod tests {
labels: HashMap::new(),
checkpoint_exclude_globs: Vec::new(),
github_app: None,
git_author_name: "arc".into(),
git_author_email: "arc@local".into(),
git_author: crate::git::GitAuthor::default(),
};
let result = engine.run(&g, &config).await;
assert!(result.is_err());
@ -4063,8 +4022,7 @@ mod tests {
labels: HashMap::new(),
checkpoint_exclude_globs: Vec::new(),
github_app: None,
git_author_name: "arc".into(),
git_author_email: "arc@local".into(),
git_author: crate::git::GitAuthor::default(),
};
let result = engine.run(&g, &config).await;
assert!(result.is_err());
@ -4098,8 +4056,7 @@ mod tests {
labels: HashMap::new(),
checkpoint_exclude_globs: Vec::new(),
github_app: None,
git_author_name: "arc".into(),
git_author_email: "arc@local".into(),
git_author: crate::git::GitAuthor::default(),
};
let result = engine.run(&g, &config).await;
assert!(result.is_err());
@ -4130,8 +4087,7 @@ mod tests {
labels: HashMap::new(),
checkpoint_exclude_globs: Vec::new(),
github_app: None,
git_author_name: "arc".into(),
git_author_email: "arc@local".into(),
git_author: crate::git::GitAuthor::default(),
};
let result = engine.run(&g, &config).await;
assert!(result.is_err());
@ -4223,8 +4179,7 @@ mod tests {
labels: HashMap::new(),
checkpoint_exclude_globs: Vec::new(),
github_app: None,
git_author_name: "arc".into(),
git_author_email: "arc@local".into(),
git_author: crate::git::GitAuthor::default(),
};
// The engine returns Err because the Fail outcome has no outgoing fail edge,
@ -4431,8 +4386,7 @@ mod tests {
labels: HashMap::new(),
checkpoint_exclude_globs: Vec::new(),
github_app: None,
git_author_name: "arc".into(),
git_author_email: "arc@local".into(),
git_author: crate::git::GitAuthor::default(),
};
let result = engine.run(&g, &config).await;
assert!(result.is_err());
@ -4466,8 +4420,7 @@ mod tests {
labels: HashMap::new(),
checkpoint_exclude_globs: Vec::new(),
github_app: None,
git_author_name: "arc".into(),
git_author_email: "arc@local".into(),
git_author: crate::git::GitAuthor::default(),
};
let result = engine.run(&g, &config).await;
assert!(result.is_err());
@ -4508,8 +4461,7 @@ mod tests {
labels: HashMap::new(),
checkpoint_exclude_globs: Vec::new(),
github_app: None,
git_author_name: "arc".into(),
git_author_email: "arc@local".into(),
git_author: crate::git::GitAuthor::default(),
};
let result = engine.run(&g, &config).await;
assert!(result.is_err());
@ -4590,8 +4542,7 @@ mod tests {
labels: HashMap::new(),
checkpoint_exclude_globs: Vec::new(),
github_app: None,
git_author_name: "arc".into(),
git_author_email: "arc@local".into(),
git_author: crate::git::GitAuthor::default(),
};
let result = engine.run(&g, &config).await;
assert!(result.is_err());
@ -4683,8 +4634,7 @@ mod tests {
labels: HashMap::new(),
checkpoint_exclude_globs: Vec::new(),
github_app: None,
git_author_name: "arc".into(),
git_author_email: "arc@local".into(),
git_author: crate::git::GitAuthor::default(),
};
let result = engine.run(&g, &config).await;
assert!(result.is_err());
@ -4753,8 +4703,7 @@ mod tests {
labels: HashMap::new(),
checkpoint_exclude_globs: Vec::new(),
github_app: None,
git_author_name: "arc".into(),
git_author_email: "arc@local".into(),
git_author: crate::git::GitAuthor::default(),
};
let outcome = engine.run(&g, &config).await.unwrap();
assert_eq!(outcome.status, StageStatus::Success);
@ -4810,8 +4759,7 @@ mod tests {
labels: HashMap::new(),
checkpoint_exclude_globs: Vec::new(),
github_app: None,
git_author_name: "arc".into(),
git_author_email: "arc@local".into(),
git_author: crate::git::GitAuthor::default(),
};
let outcome = engine.run(&g, &config).await.unwrap();
assert_eq!(outcome.status, StageStatus::Success);
@ -4868,8 +4816,7 @@ mod tests {
labels: HashMap::new(),
checkpoint_exclude_globs: Vec::new(),
github_app: None,
git_author_name: "arc".into(),
git_author_email: "arc@local".into(),
git_author: crate::git::GitAuthor::default(),
};
let _outcome = engine.run(&g, &config).await.unwrap();

View file

@ -9,6 +9,34 @@ use git2::{Repository, Signature};
use crate::checkpoint::Checkpoint;
use crate::error::{ArcError, Result};
/// Resolved git author identity for checkpoint commits.
#[derive(Debug, Clone, PartialEq)]
pub struct GitAuthor {
pub name: String,
pub email: String,
}
impl Default for GitAuthor {
fn default() -> Self {
Self {
name: "arc".into(),
email: "arc@local".into(),
}
}
}
impl GitAuthor {
/// Create a `GitAuthor` from optional name/email, falling back to defaults.
pub fn from_options(name: Option<String>, email: Option<String>) -> Self {
let defaults = Self::default();
Self {
name: name.unwrap_or(defaults.name),
email: email.unwrap_or(defaults.email),
}
}
}
fn git_error(msg: impl Into<String>) -> ArcError {
ArcError::engine(msg.into())
}
@ -145,8 +173,7 @@ pub fn checkpoint_commit(
completed_count: usize,
shadow_sha: Option<&str>,
excludes: &[String],
author_name: &str,
author_email: &str,
author: &GitAuthor,
) -> Result<String> {
tracing::debug!(path = %work_dir.display(), node_id, "Creating git checkpoint commit");
// Stage everything (with optional excludes)
@ -186,8 +213,8 @@ pub fn checkpoint_commit(
let message = trailerlink::format_message(&subject, "", &trailers);
// Commit with configured identity (works even if user.name/email not configured)
let name_cfg = format!("user.name={author_name}");
let email_cfg = format!("user.email={author_email}");
let name_cfg = format!("user.name={}", author.name);
let email_cfg = format!("user.email={}", author.email);
let output = git_cmd(work_dir)
.args([
"-c",
@ -287,16 +314,14 @@ pub fn sanitize_ref_component(s: &str) -> String {
/// (`arc/{run_id}`) so that runs can be resumed from git alone.
pub struct MetadataStore {
repo_path: std::path::PathBuf,
author_name: String,
author_email: String,
author: GitAuthor,
}
impl MetadataStore {
pub fn new(repo_path: impl Into<std::path::PathBuf>, author_name: &str, author_email: &str) -> Self {
pub fn new(repo_path: impl Into<std::path::PathBuf>, author: &GitAuthor) -> Self {
Self {
repo_path: repo_path.into(),
author_name: author_name.to_string(),
author_email: author_email.to_string(),
author: author.clone(),
}
}
@ -309,7 +334,7 @@ impl MetadataStore {
let repo = Repository::discover(&self.repo_path)
.map_err(|e| git_error(format!("failed to open repo: {e}")))?;
let store = Store::new(repo);
let sig = Signature::now(&self.author_name, &self.author_email)
let sig = Signature::now(&self.author.name, &self.author.email)
.map_err(|e| git_error(format!("failed to create signature: {e}")))?;
Ok((store, sig))
}
@ -526,7 +551,7 @@ mod tests {
let wt = dir.path().join("ff-wt");
add_worktree(dir.path(), &wt, "ff-branch").unwrap();
fs::write(wt.join("new.txt"), "data").unwrap();
checkpoint_commit(&wt, "run", "node", "ok", 1, None, &[], "arc", "arc@local").unwrap();
checkpoint_commit(&wt, "run", "node", "ok", 1, None, &[], &GitAuthor::default()).unwrap();
let advanced_sha = head_sha(&wt).unwrap();
remove_worktree(dir.path(), &wt).unwrap();
@ -598,7 +623,7 @@ mod tests {
// Simulate a shadow commit SHA
let shadow_sha = "abcdef1234567890abcdef1234567890abcdef12";
let sha =
checkpoint_commit(&wt_path, "run1", "nodeA", "success", 3, Some(shadow_sha), &[], "arc", "arc@local").unwrap();
checkpoint_commit(&wt_path, "run1", "nodeA", "success", 3, Some(shadow_sha), &[], &GitAuthor::default()).unwrap();
assert_eq!(sha.len(), 40);
assert!(sha.chars().all(|c| c.is_ascii_hexdigit()));
@ -637,7 +662,7 @@ mod tests {
let wt_path = dir.path().join("worktree");
add_worktree(dir.path(), &wt_path, "run-branch2").unwrap();
let sha = checkpoint_commit(&wt_path, "run2", "nodeB", "completed", 1, None, &[], "arc", "arc@local").unwrap();
let sha = checkpoint_commit(&wt_path, "run2", "nodeB", "completed", 1, None, &[], &GitAuthor::default()).unwrap();
assert_eq!(sha.len(), 40);
// Verify Arc-Completed trailer present but no Arc-Meta
@ -681,7 +706,7 @@ mod tests {
let wt_path = dir.path().join("worktree");
add_worktree(dir.path(), &wt_path, "fallback-branch").unwrap();
let sha = checkpoint_commit(&wt_path, "run2", "nodeB", "completed", 0, None, &[], "arc", "arc@local").unwrap();
let sha = checkpoint_commit(&wt_path, "run2", "nodeB", "completed", 0, None, &[], &GitAuthor::default()).unwrap();
assert_eq!(sha.len(), 40);
remove_worktree(dir.path(), &wt_path).unwrap();
@ -735,7 +760,7 @@ mod tests {
let dir = tempfile::tempdir().unwrap();
init_repo(dir.path());
let store = MetadataStore::new(dir.path(), "arc", "arc@local");
let store = MetadataStore::new(dir.path(), &GitAuthor::default());
let manifest = br#"{"run_id":"RUN1","workflow_name":"test","goal":"g","start_time":"2025-01-01T00:00:00Z","node_count":2,"edge_count":1}"#;
let dot = b"digraph { start -> end }";
store.init_run("RUN1", manifest, dot).unwrap();
@ -757,7 +782,7 @@ mod tests {
let dir = tempfile::tempdir().unwrap();
init_repo(dir.path());
let store = MetadataStore::new(dir.path(), "arc", "arc@local");
let store = MetadataStore::new(dir.path(), &GitAuthor::default());
store.init_run("RUN2", b"{}", b"digraph {}").unwrap();
let ctx = crate::context::Context::new();
@ -792,7 +817,7 @@ mod tests {
let dir = tempfile::tempdir().unwrap();
init_repo(dir.path());
let store = MetadataStore::new(dir.path(), "arc", "arc@local");
let store = MetadataStore::new(dir.path(), &GitAuthor::default());
store.init_run("RUN3", b"{}", b"digraph {}").unwrap();
let ctx = crate::context::Context::new();
@ -843,7 +868,7 @@ mod tests {
let dir = tempfile::tempdir().unwrap();
init_repo(dir.path());
let store = MetadataStore::new(dir.path(), "arc", "arc@local");
let store = MetadataStore::new(dir.path(), &GitAuthor::default());
store.init_run("RUN4", b"{}", b"digraph {}").unwrap();
let artifact_data = br#"{"large_output":"some data"}"#;
@ -966,7 +991,7 @@ mod tests {
fs::write(wt_path.join("node_modules/pkg/index.js"), "module").unwrap();
let excludes = vec!["**/node_modules/**".to_string()];
checkpoint_commit(&wt_path, "run", "node", "ok", 1, None, &excludes, "arc", "arc@local").unwrap();
checkpoint_commit(&wt_path, "run", "node", "ok", 1, None, &excludes, &GitAuthor::default()).unwrap();
// Verify kept.txt was committed
let output = Command::new("git")
@ -999,14 +1024,14 @@ mod tests {
// Create and commit a file in the excluded dir first
fs::create_dir_all(wt_path.join(".cache")).unwrap();
fs::write(wt_path.join(".cache/data.bin"), "v1").unwrap();
checkpoint_commit(&wt_path, "run", "setup", "ok", 0, None, &[], "arc", "arc@local").unwrap();
checkpoint_commit(&wt_path, "run", "setup", "ok", 0, None, &[], &GitAuthor::default()).unwrap();
// Now modify the tracked excluded file and add a new non-excluded file
fs::write(wt_path.join(".cache/data.bin"), "v2").unwrap();
fs::write(wt_path.join("result.txt"), "done").unwrap();
let excludes = vec!["**/.cache/**".to_string()];
checkpoint_commit(&wt_path, "run", "step", "ok", 1, None, &excludes, "arc", "arc@local").unwrap();
checkpoint_commit(&wt_path, "run", "step", "ok", 1, None, &excludes, &GitAuthor::default()).unwrap();
let output = Command::new("git")
.args(["show", "--name-only", "--format=", "HEAD"])

View file

@ -150,8 +150,7 @@ impl Handler for SubWorkflowHandler {
labels: HashMap::new(),
checkpoint_exclude_globs: Vec::new(),
github_app: None,
git_author_name: git_state.as_ref().map(|gs| gs.git_author_name.clone()).unwrap_or_else(|| "arc".into()),
git_author_email: git_state.as_ref().map(|gs| gs.git_author_email.clone()).unwrap_or_else(|| "arc@local".into()),
git_author: git_state.as_ref().map(|gs| gs.git_author.clone()).unwrap_or_default(),
};
// Clone parent context for child; inject parent preamble

View file

@ -244,8 +244,7 @@ impl Handler for ParallelHandler {
0,
None,
gs.checkpoint_exclude_globs.clone(),
gs.git_author_name.clone(),
gs.git_author_email.clone(),
gs.git_author.clone(),
)
.await
}
@ -258,8 +257,7 @@ impl Handler for ParallelHandler {
0,
None,
&gs.checkpoint_exclude_globs,
&gs.git_author_name,
&gs.git_author_email,
&gs.git_author,
)
.await
}
@ -401,8 +399,7 @@ impl Handler for ParallelHandler {
let sem = Arc::clone(&semaphore);
let has_git = git_state.is_some();
let run_id = git_state.as_ref().map(|gs| gs.run_id.clone());
let git_author_name = git_state.as_ref().map(|gs| gs.git_author_name.clone()).unwrap_or_else(|| "arc".into());
let git_author_email = git_state.as_ref().map(|gs| gs.git_author_email.clone()).unwrap_or_else(|| "arc@local".into());
let git_author = git_state.as_ref().map(|gs| gs.git_author.clone()).unwrap_or_default();
let handle = tokio::spawn(async move {
let _permit = sem
@ -468,7 +465,9 @@ impl Handler for ParallelHandler {
if add_result.as_ref().is_ok_and(|r| r.exit_code == 0) {
let msg = format!("arc({rid}): {nid} ({status_str})");
let commit_cmd = format!(
"{git_r} -c user.name={git_author_name} -c user.email={git_author_email} commit --allow-empty -m '{msg}'"
"{git_r} -c 'user.name={name}' -c 'user.email={email}' commit --allow-empty -m '{msg}'",
name = git_author.name,
email = git_author.email,
);
let _ = setup
.sandbox

View file

@ -333,8 +333,7 @@ async fn daytona_pipeline_artifact_offload_and_sync() {
labels: std::collections::HashMap::new(),
checkpoint_exclude_globs: Vec::new(),
github_app: None,
git_author_name: "arc".into(),
git_author_email: "arc@local".into(),
git_author: arc_workflows::git::GitAuthor::default(),
};
let outcome = engine
@ -528,8 +527,7 @@ async fn daytona_git_checkpoint_remote_emits_events() {
labels: std::collections::HashMap::new(),
checkpoint_exclude_globs: Vec::new(),
github_app: None,
git_author_name: "arc".into(),
git_author_email: "arc@local".into(),
git_author: arc_workflows::git::GitAuthor::default(),
};
let outcome = engine
@ -712,8 +710,7 @@ async fn daytona_parallel_git_branching_e2e() {
labels: std::collections::HashMap::new(),
checkpoint_exclude_globs: Vec::new(),
github_app: None,
git_author_name: "arc".into(),
git_author_email: "arc@local".into(),
git_author: arc_workflows::git::GitAuthor::default(),
};
let outcome = engine
@ -1041,8 +1038,7 @@ async fn daytona_git_checkpoint_with_shadow_branch() {
labels: std::collections::HashMap::new(),
checkpoint_exclude_globs: Vec::new(),
github_app: None,
git_author_name: "arc".into(),
git_author_email: "arc@local".into(),
git_author: arc_workflows::git::GitAuthor::default(),
};
let outcome = engine
@ -1182,8 +1178,7 @@ async fn daytona_asset_collection() {
labels: std::collections::HashMap::new(),
checkpoint_exclude_globs: Vec::new(),
github_app: None,
git_author_name: "arc".into(),
git_author_email: "arc@local".into(),
git_author: arc_workflows::git::GitAuthor::default(),
};
let outcome = engine
@ -1416,8 +1411,7 @@ async fn daytona_git_push_run_branch_to_origin() {
labels: std::collections::HashMap::new(),
checkpoint_exclude_globs: Vec::new(),
github_app: None,
git_author_name: "arc".into(),
git_author_email: "arc@local".into(),
git_author: arc_workflows::git::GitAuthor::default(),
};
let outcome = engine

File diff suppressed because it is too large Load diff