Skip to main content

mz_deploy/cli/commands/
log.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10//! History command - show deployment history in chronological order.
11
12use crate::cli::CliError;
13use crate::client::{Client, DeploymentHistoryEntry};
14use crate::config::Settings;
15use crate::{info, info_nonl, log};
16use chrono::{DateTime, Local};
17use owo_colors::{OwoColorize, Stream, Style};
18use std::fmt;
19use std::io::{IsTerminal, Write};
20use std::process::{Command, Stdio};
21
22/// Render struct for deployment history — single source of truth for formatting.
23#[derive(serde::Serialize)]
24#[serde(transparent)]
25struct HistoryOutput {
26    entries: Vec<DeploymentHistoryEntry>,
27}
28
29impl fmt::Display for HistoryOutput {
30    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31        writeln!(f, "Deployment history (promoted):\n")?;
32
33        let deployment_style = Style::new().yellow().bold();
34        for entry in &self.entries {
35            let datetime: DateTime<Local> = entry.promoted_at.with_timezone(&Local);
36            let date_str = datetime.format("%a %b %d %H:%M:%S %Y %z").to_string();
37
38            writeln!(
39                f,
40                "{} {} [{}]",
41                "deployment".if_supports_color(Stream::Stderr, |t| deployment_style.style(t)),
42                entry
43                    .deploy_id
44                    .if_supports_color(Stream::Stderr, |t| t.cyan()),
45                entry
46                    .kind
47                    .to_string()
48                    .if_supports_color(Stream::Stderr, |t| t.dimmed())
49            )?;
50            if let Some(commit_sha) = &entry.git_commit {
51                writeln!(
52                    f,
53                    "{}: {}",
54                    "Commit".if_supports_color(Stream::Stderr, |t| t.dimmed()),
55                    commit_sha
56                )?;
57            }
58            writeln!(
59                f,
60                "{}: {}",
61                "Promoted by".if_supports_color(Stream::Stderr, |t| t.dimmed()),
62                entry
63                    .deployed_by
64                    .if_supports_color(Stream::Stderr, |t| t.yellow())
65            )?;
66            writeln!(
67                f,
68                "{}:   {}",
69                "Date".if_supports_color(Stream::Stderr, |t| t.dimmed()),
70                date_str
71            )?;
72            writeln!(f)?;
73
74            for sq in &entry.schemas {
75                writeln!(
76                    f,
77                    "    {}.{}",
78                    sq.database
79                        .if_supports_color(Stream::Stderr, |t| t.dimmed()),
80                    sq.schema
81                )?;
82            }
83            writeln!(f)?;
84        }
85
86        Ok(())
87    }
88}
89
90/// Show deployment history in chronological order (promoted deployments only).
91///
92/// This command:
93/// - Queries all promoted deployments (promoted_at IS NOT NULL) ordered by promoted_at DESC
94/// - Groups deployments by environment and promotion time
95/// - Lists all schemas included in each deployment
96///
97/// Similar to `git log` - shows historical production deployment activity with
98/// each deployment showing the "commit message" (schemas changed).
99///
100/// # Arguments
101/// * `settings` - Resolved CLI settings (profile, project directory, etc.)
102/// * `limit` - Optional limit on number of deployments to show
103///
104/// # Returns
105/// Ok(()) if listing succeeds
106///
107/// # Errors
108/// Returns `CliError::Connection` for database errors
109pub async fn run(settings: &Settings, limit: Option<usize>) -> Result<(), CliError> {
110    let profile = settings.connection();
111    let client = Client::connect_with_profile(profile.clone())
112        .await
113        .map_err(CliError::Connection)?;
114
115    super::setup::verify(&client, settings.emulator()).await?;
116    super::setup::validate_connection(&client, settings.emulator()).await?;
117    let history = client.deployments().list_deployment_history(limit).await?;
118
119    let output = HistoryOutput { entries: history };
120
121    if log::json_output_enabled() {
122        log::output(&output);
123    } else if output.entries.is_empty() {
124        info!("No deployment history found.");
125        info!();
126        info!("To create and promote a deployment, run:");
127        info!(
128            "  {} {} {}",
129            "mz-deploy".if_supports_color(Stream::Stderr, |t| t.cyan()),
130            "stage".if_supports_color(Stream::Stderr, |t| t.cyan()),
131            ".".if_supports_color(Stream::Stderr, |t| t.cyan())
132        );
133        info!(
134            "  {} {} {}",
135            "mz-deploy".if_supports_color(Stream::Stderr, |t| t.cyan()),
136            "apply".if_supports_color(Stream::Stderr, |t| t.cyan()),
137            "--staging-env <name>".if_supports_color(Stream::Stderr, |t| t.cyan())
138        );
139    } else {
140        let formatted = format!("{output}");
141        if std::io::stderr().is_terminal() {
142            display_with_pager(&formatted);
143        } else {
144            info_nonl!("{}", formatted);
145        }
146    }
147
148    Ok(())
149}
150
151/// Display output through a pager (less) if available, otherwise print directly.
152fn display_with_pager(content: &str) {
153    // Try to spawn less with flags:
154    // -R: interpret ANSI color codes
155    // -F: exit immediately if content fits on one screen
156    // -X: don't clear screen on exit
157    if let Ok(mut child) = Command::new("less")
158        .args(["-RFX"])
159        .stdin(Stdio::piped())
160        .spawn()
161    {
162        if let Some(mut stdin) = child.stdin.take() {
163            // Write content to less, ignore errors (e.g., broken pipe if user quits early)
164            let _ = stdin.write_all(content.as_bytes());
165        }
166        // Wait for less to exit
167        let _ = child.wait();
168    } else {
169        // Fallback: print directly if less isn't available
170        info_nonl!("{}", content);
171    }
172}