Skip to main content

changeset_git/repository/
commit.rs

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