cli/cli/commands/
operator_loop.rs1use anyhow::Result;
3use repo::{GitOverlayImportHint, GitRemoteTrackingStatus, RepositoryOperationStatus};
4
5use super::{
6 operator_core::{
7 OperatorCommandOutput, abort_operator, open_operator_repo_from_path, recommend_next_action,
8 run_git_control,
9 },
10 workflow::cmd_sync,
11};
12use crate::{
13 bridge::{GitBridge, git_import::import_selected_refs},
14 cli::{Cli, cli_args::SyncArgs, should_output_json, style},
15};
16
17pub async fn cmd_continue(cli: &Cli) -> Result<()> {
18 let current_dir = std::env::current_dir()?;
19 let cwd = cli.repo.as_ref().unwrap_or(¤t_dir);
20 let repo = open_operator_repo_from_path(cwd)?;
21 let output = super::operator_core::continue_operator(&repo)?;
22 emit(cli, output)
23}
24
25pub fn cmd_abort(cli: &Cli) -> Result<()> {
26 let current_dir = std::env::current_dir()?;
27 let cwd = cli.repo.as_ref().unwrap_or(¤t_dir);
28 let repo = open_operator_repo_from_path(cwd)?;
29 let output = abort_operator(&repo)?;
30 emit(cli, output)
31}
32
33pub async fn cmd_sync_smart(cli: &Cli, args: SyncArgs) -> Result<()> {
34 let current_dir = std::env::current_dir()?;
35 let cwd = cli.repo.as_ref().unwrap_or(¤t_dir);
36 let repo = open_operator_repo_from_path(cwd)?;
37 if repo.operation_status()?.is_some() || repo.merge_state_manager().is_merge_in_progress() {
38 return emit(
39 cli,
40 OperatorCommandOutput {
41 status: "blocked".to_string(),
42 action: "sync".to_string(),
43 message: "Finish the in-progress operation before syncing".to_string(),
44 blockers: Vec::new(),
45 warnings: Vec::new(),
46 next_action: Some("heddle continue".to_string()),
47 recommended_action: Some("heddle continue".to_string()),
48 },
49 );
50 }
51
52 if let Some(remote) = repo.git_remote_tracking_status()? {
53 if remote.behind > 0 {
54 run_git_control(&repo, &["pull", "--rebase"])?;
55 let mut bridge = GitBridge::new(&repo);
56 import_selected_refs(
57 &mut bridge,
58 Some(repo.root()),
59 std::slice::from_ref(&remote.branch),
60 )?;
61 return emit(
62 cli,
63 OperatorCommandOutput {
64 status: "synced".to_string(),
65 action: "sync".to_string(),
66 message: format!(
67 "Synced branch '{}' with upstream '{}'",
68 remote.branch, remote.upstream
69 ),
70 blockers: Vec::new(),
71 warnings: Vec::new(),
72 next_action: None,
73 recommended_action: None,
74 },
75 );
76 }
77 if remote.ahead > 0 {
78 return emit(
79 cli,
80 OperatorCommandOutput {
81 status: "ahead".to_string(),
82 action: "sync".to_string(),
83 message: remote.message,
84 blockers: Vec::new(),
85 warnings: Vec::new(),
86 next_action: Some("heddle push".to_string()),
87 recommended_action: Some("heddle push".to_string()),
88 },
89 );
90 }
91 }
92
93 cmd_sync(cli, args).await
94}
95
96fn emit(cli: &Cli, output: OperatorCommandOutput) -> Result<()> {
97 if should_output_json(cli, None) {
98 println!("{}", serde_json::to_string(&output)?);
99 } else {
100 let message = match output.status.as_str() {
101 "blocked" => style::warn(&output.message),
102 "aborted" => style::warn(&output.message),
103 "continued" | "completed" | "synced" => style::accent(&output.message),
104 _ => output.message.clone(),
105 };
106 println!("{}", message);
107 if !output.blockers.is_empty() {
108 println!("{}", style::warn("Blocked by"));
109 for blocker in &output.blockers {
110 println!(" - {}", style::warn(blocker));
111 }
112 }
113 if let Some(next) = output.recommended_action.or(output.next_action) {
114 println!("Next step: {}", style::bold(&next));
115 }
116 }
117 Ok(())
118}
119
120pub(crate) fn primary_next_action(
121 operation: Option<&RepositoryOperationStatus>,
122 remote_tracking: Option<&GitRemoteTrackingStatus>,
123 import_hint: Option<&GitOverlayImportHint>,
124 fallback: Option<&str>,
125) -> String {
126 recommend_next_action(operation, remote_tracking, import_hint, fallback)
127}