Skip to main content

cli/cli/commands/
operator_loop.rs

1// SPDX-License-Identifier: Apache-2.0
2use 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(&current_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(&current_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(&current_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}