docker_wrapper/command/
commit.rs

1//! Docker commit command implementation.
2//!
3//! This module provides the `docker commit` command for creating a new image
4//! from a container's changes.
5
6use super::{CommandExecutor, CommandOutput, DockerCommand};
7use crate::error::Result;
8use async_trait::async_trait;
9
10/// Docker commit command builder
11///
12/// Create a new image from a container's changes.
13///
14/// # Example
15///
16/// ```no_run
17/// use docker_wrapper::CommitCommand;
18///
19/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
20/// // Commit container changes to a new image
21/// let image_id = CommitCommand::new("my-container")
22///     .repository("myapp")
23///     .tag("v2.0")
24///     .message("Updated configuration")
25///     .author("Developer <dev@example.com>")
26///     .run()
27///     .await?;
28///
29/// println!("Created image: {}", image_id.image_id());
30/// # Ok(())
31/// # }
32/// ```
33#[derive(Debug, Clone)]
34pub struct CommitCommand {
35    /// Container name or ID
36    container: String,
37    /// Repository name
38    repository: Option<String>,
39    /// Tag name
40    tag: Option<String>,
41    /// Commit message
42    message: Option<String>,
43    /// Author
44    author: Option<String>,
45    /// Pause container during commit
46    pause: bool,
47    /// Dockerfile instructions to apply
48    changes: Vec<String>,
49    /// Command executor
50    pub executor: CommandExecutor,
51}
52
53impl CommitCommand {
54    /// Create a new commit command
55    ///
56    /// # Example
57    ///
58    /// ```
59    /// use docker_wrapper::CommitCommand;
60    ///
61    /// let cmd = CommitCommand::new("my-container");
62    /// ```
63    #[must_use]
64    pub fn new(container: impl Into<String>) -> Self {
65        Self {
66            container: container.into(),
67            repository: None,
68            tag: None,
69            message: None,
70            author: None,
71            pause: true, // Docker default is true
72            changes: Vec::new(),
73            executor: CommandExecutor::new(),
74        }
75    }
76
77    /// Set the repository name for the new image
78    ///
79    /// # Example
80    ///
81    /// ```
82    /// use docker_wrapper::CommitCommand;
83    ///
84    /// let cmd = CommitCommand::new("my-container")
85    ///     .repository("myapp");
86    /// ```
87    #[must_use]
88    pub fn repository(mut self, repository: impl Into<String>) -> Self {
89        self.repository = Some(repository.into());
90        self
91    }
92
93    /// Set the tag for the new image
94    ///
95    /// # Example
96    ///
97    /// ```
98    /// use docker_wrapper::CommitCommand;
99    ///
100    /// let cmd = CommitCommand::new("my-container")
101    ///     .repository("myapp")
102    ///     .tag("v2.0");
103    /// ```
104    #[must_use]
105    pub fn tag(mut self, tag: impl Into<String>) -> Self {
106        self.tag = Some(tag.into());
107        self
108    }
109
110    /// Set the commit message
111    #[must_use]
112    pub fn message(mut self, message: impl Into<String>) -> Self {
113        self.message = Some(message.into());
114        self
115    }
116
117    /// Set the author
118    ///
119    /// # Example
120    ///
121    /// ```
122    /// use docker_wrapper::CommitCommand;
123    ///
124    /// let cmd = CommitCommand::new("my-container")
125    ///     .author("John Doe <john@example.com>");
126    /// ```
127    #[must_use]
128    pub fn author(mut self, author: impl Into<String>) -> Self {
129        self.author = Some(author.into());
130        self
131    }
132
133    /// Do not pause the container during commit
134    #[must_use]
135    pub fn no_pause(mut self) -> Self {
136        self.pause = false;
137        self
138    }
139
140    /// Apply Dockerfile instruction to the created image
141    ///
142    /// # Example
143    ///
144    /// ```
145    /// use docker_wrapper::CommitCommand;
146    ///
147    /// let cmd = CommitCommand::new("my-container")
148    ///     .change("ENV VERSION=2.0")
149    ///     .change("EXPOSE 8080")
150    ///     .change("CMD [\"app\", \"--production\"]");
151    /// ```
152    #[must_use]
153    pub fn change(mut self, change: impl Into<String>) -> Self {
154        self.changes.push(change.into());
155        self
156    }
157
158    /// Execute the commit command
159    ///
160    /// # Errors
161    /// Returns an error if:
162    /// - The Docker daemon is not running
163    /// - The container doesn't exist
164    /// - The repository/tag format is invalid
165    ///
166    /// # Example
167    ///
168    /// ```no_run
169    /// use docker_wrapper::CommitCommand;
170    ///
171    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
172    /// let result = CommitCommand::new("my-container")
173    ///     .repository("myapp")
174    ///     .tag("snapshot")
175    ///     .run()
176    ///     .await?;
177    ///
178    /// println!("New image ID: {}", result.image_id());
179    /// # Ok(())
180    /// # }
181    /// ```
182    pub async fn run(&self) -> Result<CommitResult> {
183        let output = self.execute().await?;
184
185        // Parse image ID from output
186        let image_id = output.stdout.trim().to_string();
187
188        Ok(CommitResult { output, image_id })
189    }
190}
191
192#[async_trait]
193impl DockerCommand for CommitCommand {
194    type Output = CommandOutput;
195
196    fn build_command_args(&self) -> Vec<String> {
197        let mut args = vec!["commit".to_string()];
198
199        if let Some(ref author) = self.author {
200            args.push("--author".to_string());
201            args.push(author.clone());
202        }
203
204        for change in &self.changes {
205            args.push("--change".to_string());
206            args.push(change.clone());
207        }
208
209        if let Some(ref message) = self.message {
210            args.push("--message".to_string());
211            args.push(message.clone());
212        }
213
214        if !self.pause {
215            args.push("--pause=false".to_string());
216        }
217
218        // Add container name/ID
219        args.push(self.container.clone());
220
221        // Add repository[:tag] if specified
222        if let Some(ref repo) = self.repository {
223            let mut image_name = repo.clone();
224            if let Some(ref tag) = self.tag {
225                image_name.push(':');
226                image_name.push_str(tag);
227            }
228            args.push(image_name);
229        }
230
231        args.extend(self.executor.raw_args.clone());
232        args
233    }
234
235    fn get_executor(&self) -> &CommandExecutor {
236        &self.executor
237    }
238
239    fn get_executor_mut(&mut self) -> &mut CommandExecutor {
240        &mut self.executor
241    }
242
243    async fn execute(&self) -> Result<Self::Output> {
244        let args = self.build_command_args();
245        let command_name = args[0].clone();
246        let command_args = args[1..].to_vec();
247        self.executor
248            .execute_command(&command_name, command_args)
249            .await
250    }
251}
252
253/// Result from the commit command
254#[derive(Debug, Clone)]
255pub struct CommitResult {
256    /// Raw command output
257    pub output: CommandOutput,
258    /// ID of the created image
259    pub image_id: String,
260}
261
262impl CommitResult {
263    /// Check if the commit was successful
264    #[must_use]
265    pub fn success(&self) -> bool {
266        self.output.success && !self.image_id.is_empty()
267    }
268
269    /// Get the created image ID
270    #[must_use]
271    pub fn image_id(&self) -> &str {
272        &self.image_id
273    }
274}
275
276#[cfg(test)]
277mod tests {
278    use super::*;
279
280    #[test]
281    fn test_commit_basic() {
282        let cmd = CommitCommand::new("test-container");
283        let args = cmd.build_command_args();
284        assert_eq!(args, vec!["commit", "test-container"]);
285    }
286
287    #[test]
288    fn test_commit_with_repository() {
289        let cmd = CommitCommand::new("test-container").repository("myapp");
290        let args = cmd.build_command_args();
291        assert_eq!(args, vec!["commit", "test-container", "myapp"]);
292    }
293
294    #[test]
295    fn test_commit_with_repository_and_tag() {
296        let cmd = CommitCommand::new("test-container")
297            .repository("myapp")
298            .tag("v2.0");
299        let args = cmd.build_command_args();
300        assert_eq!(args, vec!["commit", "test-container", "myapp:v2.0"]);
301    }
302
303    #[test]
304    fn test_commit_with_message_and_author() {
305        let cmd = CommitCommand::new("test-container")
306            .message("Updated config")
307            .author("Dev <dev@example.com>")
308            .repository("myapp");
309        let args = cmd.build_command_args();
310        assert_eq!(
311            args,
312            vec![
313                "commit",
314                "--author",
315                "Dev <dev@example.com>",
316                "--message",
317                "Updated config",
318                "test-container",
319                "myapp"
320            ]
321        );
322    }
323
324    #[test]
325    fn test_commit_with_changes() {
326        let cmd = CommitCommand::new("test-container")
327            .change("ENV VERSION=2.0")
328            .change("EXPOSE 8080")
329            .repository("myapp");
330        let args = cmd.build_command_args();
331        assert_eq!(
332            args,
333            vec![
334                "commit",
335                "--change",
336                "ENV VERSION=2.0",
337                "--change",
338                "EXPOSE 8080",
339                "test-container",
340                "myapp"
341            ]
342        );
343    }
344
345    #[test]
346    fn test_commit_no_pause() {
347        let cmd = CommitCommand::new("test-container")
348            .no_pause()
349            .repository("myapp");
350        let args = cmd.build_command_args();
351        assert_eq!(
352            args,
353            vec!["commit", "--pause=false", "test-container", "myapp"]
354        );
355    }
356
357    #[test]
358    fn test_commit_all_options() {
359        let cmd = CommitCommand::new("test-container")
360            .repository("myapp")
361            .tag("v2.0")
362            .message("Commit message")
363            .author("Author Name")
364            .no_pause()
365            .change("ENV FOO=bar");
366        let args = cmd.build_command_args();
367        assert_eq!(
368            args,
369            vec![
370                "commit",
371                "--author",
372                "Author Name",
373                "--change",
374                "ENV FOO=bar",
375                "--message",
376                "Commit message",
377                "--pause=false",
378                "test-container",
379                "myapp:v2.0"
380            ]
381        );
382    }
383}