Skip to main content

changeset_git/repository/
commit.rs

1use crate::{CommitInfo, GitError, Result};
2
3use super::Repository;
4
5impl Repository {
6    /// Performs a soft reset to the parent of HEAD (HEAD~1).
7    ///
8    /// This undoes the last commit while keeping changes staged.
9    ///
10    /// # Errors
11    ///
12    /// Returns an error if:
13    /// - HEAD cannot be resolved
14    /// - HEAD has no parent (initial commit)
15    /// - The reset operation fails
16    pub fn reset_to_parent(&self) -> Result<()> {
17        let head_commit = self.inner.head()?.peel_to_commit()?;
18        let parent = head_commit
19            .parent(0)
20            .map_err(|source| GitError::NoParentCommit { source })?;
21        self.inner
22            .reset(parent.as_object(), git2::ResetType::Soft, None)?;
23        Ok(())
24    }
25
26    /// # Errors
27    ///
28    /// Returns an error if the commit cannot be created.
29    pub fn commit(&self, message: &str) -> Result<CommitInfo> {
30        let sig = self.inner.signature()?;
31        let mut index = self.inner.index()?;
32        let tree_id = index.write_tree()?;
33        let tree = self.inner.find_tree(tree_id)?;
34
35        let parent = self.inner.head().ok().and_then(|h| h.peel_to_commit().ok());
36
37        let parents: Vec<&git2::Commit<'_>> = parent.iter().collect();
38
39        let commit_oid = self
40            .inner
41            .commit(Some("HEAD"), &sig, &sig, message, &tree, &parents)?;
42
43        let sha = commit_oid.to_string();
44
45        Ok(CommitInfo {
46            sha,
47            message: message.to_string(),
48        })
49    }
50}
51
52#[cfg(test)]
53mod tests {
54    use super::super::tests::setup_test_repo;
55    use crate::GitError;
56    use std::fs;
57    use std::path::Path;
58
59    #[test]
60    fn create_commit() -> anyhow::Result<()> {
61        let (dir, repo) = setup_test_repo()?;
62
63        fs::write(dir.path().join("file.txt"), "content")?;
64        repo.stage_files(&[Path::new("file.txt")])?;
65
66        let commit_info = repo.commit("Test commit message")?;
67
68        assert!(!commit_info.sha.is_empty());
69        assert_eq!(commit_info.message, "Test commit message");
70
71        let head = repo.inner.head()?.peel_to_commit()?;
72        assert_eq!(head.id().to_string(), commit_info.sha);
73
74        Ok(())
75    }
76
77    #[test]
78    fn commit_with_multiline_message() -> anyhow::Result<()> {
79        let (dir, repo) = setup_test_repo()?;
80
81        fs::write(dir.path().join("file.txt"), "content")?;
82        repo.stage_files(&[Path::new("file.txt")])?;
83
84        let message = "Summary line\n\nDetailed description\nwith multiple lines";
85        let commit_info = repo.commit(message)?;
86
87        let head = repo.inner.head()?.peel_to_commit()?;
88        assert_eq!(head.message(), Some(message));
89        assert_eq!(commit_info.message, message);
90
91        Ok(())
92    }
93
94    #[test]
95    fn reset_to_parent_undoes_last_commit() -> anyhow::Result<()> {
96        let (dir, repo) = setup_test_repo()?;
97
98        let initial_head = repo.inner.head()?.peel_to_commit()?.id();
99
100        fs::write(dir.path().join("file.txt"), "content")?;
101        repo.stage_files(&[Path::new("file.txt")])?;
102        repo.commit("Second commit")?;
103
104        let after_commit_head = repo.inner.head()?.peel_to_commit()?.id();
105        assert_ne!(initial_head, after_commit_head);
106
107        repo.reset_to_parent()?;
108
109        let after_reset_head = repo.inner.head()?.peel_to_commit()?.id();
110        assert_eq!(initial_head, after_reset_head);
111
112        Ok(())
113    }
114
115    #[test]
116    fn reset_to_parent_on_initial_commit_fails() -> anyhow::Result<()> {
117        let (_dir, repo) = setup_test_repo()?;
118
119        let result = repo.reset_to_parent();
120
121        assert!(result.is_err());
122        let err = result.expect_err("expected NoParentCommit error");
123        assert!(matches!(err, GitError::NoParentCommit { .. }));
124
125        Ok(())
126    }
127}