Skip to main content

jk_core/command_history/
record.rs

1use std::time::{Duration, SystemTime};
2
3use super::{
4    CommandExecutionContext, CommandIdentity, CommandRecordId, CommandResultSummary, CommandSource,
5    DEFAULT_STREAM_LIMIT, ExitStatusSummary, OutputRetention, StreamSummary,
6};
7use crate::{ExecutionMode, JjCommandSpec, RefreshPlan, SafetyClass};
8
9/// One retained command-history record.
10#[derive(Clone, Debug, Eq, PartialEq)]
11pub struct CommandRecord {
12    /// Stable id for this in-memory history.
13    pub id: CommandRecordId,
14    /// Command identity captured from a typed spec.
15    pub command: CommandIdentity,
16    /// View and action that triggered the command.
17    pub source: CommandSource,
18    /// Execution context captured before running the command.
19    pub context: CommandExecutionContext,
20    /// Start and finish timing.
21    pub timing: CommandTiming,
22    /// Process or spawn result summary.
23    pub result: CommandResultSummary,
24    /// Output retention policy used for this record.
25    pub retention: OutputRetention,
26    /// Refresh behavior requested by the command spec.
27    pub refresh: RefreshPlan,
28    /// Safety classification requested by the command spec.
29    pub safety: SafetyClass,
30    /// Execution mode requested by the command spec.
31    pub execution_mode: ExecutionMode,
32    /// Operation id reported by `jj`, when cheaply available.
33    pub operation_id: Option<String>,
34}
35
36impl CommandRecord {
37    pub(super) fn started(id: CommandRecordId, input: CommandRecordStart) -> Self {
38        Self {
39            id,
40            command: input.command,
41            source: input.source,
42            context: input.context,
43            timing: CommandTiming {
44                started_at: input.started_at,
45                ended_at: None,
46                duration: None,
47            },
48            result: CommandResultSummary::default(),
49            retention: input.retention,
50            refresh: input.refresh,
51            safety: input.safety,
52            execution_mode: input.execution_mode,
53            operation_id: None,
54        }
55    }
56
57    pub(super) fn finish(&mut self, mut finish: CommandRecordFinish) -> bool {
58        if self.timing.ended_at.is_some() {
59            return false;
60        }
61
62        let duration = finish
63            .ended_at
64            .duration_since(self.timing.started_at)
65            .unwrap_or_default();
66        finish.result.apply_retention(&self.retention);
67        self.timing.ended_at = Some(finish.ended_at);
68        self.timing.duration = Some(duration);
69        self.result = finish.result;
70        self.operation_id = finish.operation_id;
71        true
72    }
73}
74
75/// Fields required when a command starts.
76#[derive(Clone, Debug, Eq, PartialEq)]
77pub struct CommandRecordStart {
78    /// Command identity captured from a typed spec.
79    pub command: CommandIdentity,
80    /// View and action that triggered the command.
81    pub source: CommandSource,
82    /// Execution context captured before running the command.
83    pub context: CommandExecutionContext,
84    /// Start time.
85    pub started_at: SystemTime,
86    /// Output retention policy for this record.
87    pub retention: OutputRetention,
88    /// Refresh behavior requested by the command spec.
89    pub refresh: RefreshPlan,
90    /// Safety classification requested by the command spec.
91    pub safety: SafetyClass,
92    /// Execution mode requested by the command spec.
93    pub execution_mode: ExecutionMode,
94}
95
96impl CommandRecordStart {
97    /// Creates start data from a typed `jj` command spec and source.
98    #[must_use]
99    pub fn from_spec(spec: &JjCommandSpec, source: CommandSource) -> Self {
100        Self {
101            command: CommandIdentity::from_spec(spec),
102            source,
103            context: CommandExecutionContext::from_spec(spec),
104            started_at: SystemTime::now(),
105            retention: OutputRetention::summary_only(DEFAULT_STREAM_LIMIT, DEFAULT_STREAM_LIMIT),
106            refresh: spec.refresh_plan(),
107            safety: spec.safety(),
108            execution_mode: spec.mode(),
109        }
110    }
111
112    /// Sets the start time for deterministic tests or caller-owned timing.
113    #[must_use]
114    pub const fn with_started_at(mut self, started_at: SystemTime) -> Self {
115        self.started_at = started_at;
116        self
117    }
118
119    /// Sets the output retention policy.
120    #[must_use]
121    pub fn with_retention(mut self, retention: OutputRetention) -> Self {
122        self.retention = retention;
123        self
124    }
125}
126
127/// Fields required when a command finishes.
128#[derive(Clone, Debug, Eq, PartialEq)]
129pub struct CommandRecordFinish {
130    /// Finish time.
131    pub ended_at: SystemTime,
132    /// Process or spawn result summary.
133    pub result: CommandResultSummary,
134    /// Operation id reported by `jj`, when cheaply available.
135    pub operation_id: Option<String>,
136}
137
138impl CommandRecordFinish {
139    /// Creates finish data for a process result.
140    #[must_use]
141    pub const fn from_result(
142        result: CommandResultSummary,
143        operation_id: Option<String>,
144        ended_at: SystemTime,
145    ) -> Self {
146        Self {
147            ended_at,
148            result,
149            operation_id,
150        }
151    }
152
153    /// Creates finish data for a process exit code and captured output.
154    #[must_use]
155    pub fn from_exit_code(
156        code: i32,
157        stdout: impl AsRef<[u8]>,
158        stderr: impl AsRef<[u8]>,
159        ended_at: SystemTime,
160    ) -> Self {
161        Self::from_result(
162            CommandResultSummary {
163                exit_status: Some(ExitStatusSummary::code(code)),
164                spawn_error: None,
165                stdout: StreamSummary::from_bytes(stdout.as_ref(), DEFAULT_STREAM_LIMIT),
166                stderr: StreamSummary::from_bytes(stderr.as_ref(), DEFAULT_STREAM_LIMIT),
167            },
168            None,
169            ended_at,
170        )
171    }
172
173    /// Creates finish data for a command that failed before producing an exit status.
174    #[must_use]
175    pub fn from_spawn_error(
176        error: impl Into<String>,
177        stdout: impl AsRef<[u8]>,
178        stderr: impl AsRef<[u8]>,
179        ended_at: SystemTime,
180    ) -> Self {
181        Self::from_result(
182            CommandResultSummary {
183                exit_status: None,
184                spawn_error: Some(error.into()),
185                stdout: StreamSummary::from_bytes(stdout.as_ref(), DEFAULT_STREAM_LIMIT),
186                stderr: StreamSummary::from_bytes(stderr.as_ref(), DEFAULT_STREAM_LIMIT),
187            },
188            None,
189            ended_at,
190        )
191    }
192}
193
194/// Timing captured for a command.
195#[derive(Clone, Debug, Eq, PartialEq)]
196pub struct CommandTiming {
197    /// Time at which the command was recorded as started.
198    pub started_at: SystemTime,
199    /// Time at which the command finished.
200    pub ended_at: Option<SystemTime>,
201    /// Duration between start and finish.
202    pub duration: Option<Duration>,
203}