Skip to main content

rskit_git/testutil/
builder.rs

1use std::fs;
2use std::path::{Path, PathBuf};
3
4use rskit_errors::{AppError, AppResult};
5use tempfile::TempDir;
6
7use crate::{CheckoutManager, Committer, ConfigReader, Differ, IndexManager, RefManager, Repo};
8
9/// Builder for creating test repositories with specific states.
10pub struct RepoBuilder {
11    _tempdir: TempDir,
12    root: PathBuf,
13    repo: Repo,
14}
15
16impl RepoBuilder {
17    /// Creates a new test repository in a temporary directory.
18    pub fn new() -> AppResult<Self> {
19        let tempdir = TempDir::new().map_err(AppError::internal)?;
20        let root = tempdir.path().to_path_buf();
21        let repo = crate::init(&root)?;
22        repo.config_set("user.name", "Test User")?;
23        repo.config_set("user.email", "test@example.com")?;
24        Ok(Self {
25            _tempdir: tempdir,
26            root,
27            repo,
28        })
29    }
30
31    /// Creates or overwrites a file in the working tree.
32    #[must_use = "builder methods return the updated builder; chain or bind the result"]
33    pub fn with_file(self, path: &str, content: &str) -> AppResult<Self> {
34        let full_path = self.root.join(path);
35        if let Some(parent) = full_path.parent() {
36            fs::create_dir_all(parent).map_err(AppError::internal)?;
37        }
38        fs::write(full_path, content).map_err(AppError::internal)?;
39        Ok(self)
40    }
41
42    /// Stages all changes and creates a commit.
43    #[must_use = "builder methods return the updated builder; chain or bind the result"]
44    pub fn with_commit(self, message: &str) -> AppResult<Self> {
45        let paths = self
46            .repo
47            .status()?
48            .into_iter()
49            .map(|entry| entry.path)
50            .collect::<Vec<_>>();
51        let refs = paths.iter().map(String::as_str).collect::<Vec<_>>();
52        if !refs.is_empty() {
53            self.repo.stage(&refs)?;
54        }
55        self.repo.commit(message, None)?;
56        Ok(self)
57    }
58
59    /// Creates a new branch at HEAD.
60    #[must_use = "builder methods return the updated builder; chain or bind the result"]
61    pub fn with_branch(self, name: &str) -> AppResult<Self> {
62        self.repo.create_branch(name, "HEAD")?;
63        Ok(self)
64    }
65
66    /// Switches to the named branch.
67    #[must_use = "builder methods return the updated builder; chain or bind the result"]
68    pub fn with_checkout(self, branch: &str) -> AppResult<Self> {
69        self.repo.checkout(branch, None)?;
70        Ok(self)
71    }
72
73    /// Creates a tag at HEAD. `Some(message)` makes an annotated tag; `None`
74    /// makes a lightweight tag.
75    #[must_use = "builder methods return the updated builder; chain or bind the result"]
76    pub fn with_tag(self, name: &str, message: Option<&str>) -> AppResult<Self> {
77        self.repo.create_tag(name, "HEAD", message)?;
78        Ok(self)
79    }
80
81    /// Returns a reference to the repository.
82    pub fn repo(&self) -> &Repo {
83        &self.repo
84    }
85
86    /// Returns the repository root path.
87    pub fn root(&self) -> &Path {
88        &self.root
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use crate::{BranchFilter, LogOptions, LogReader, RefManager, Repository};
95
96    use super::RepoBuilder;
97
98    #[test]
99    fn repo_builder_creates_commits_and_refs() {
100        let builder = RepoBuilder::new()
101            .unwrap()
102            .with_file("README.md", "# repo\n")
103            .unwrap()
104            .with_commit("initial commit")
105            .unwrap()
106            .with_branch("feature")
107            .unwrap()
108            .with_tag("v1.0.0", Some("release"))
109            .unwrap()
110            .with_checkout("feature")
111            .unwrap();
112
113        assert_eq!(
114            builder.repo().root().canonicalize().unwrap(),
115            builder.root().canonicalize().unwrap()
116        );
117
118        let commits = builder
119            .repo()
120            .log(Some(&LogOptions {
121                max_count: Some(1),
122                ..Default::default()
123            }))
124            .unwrap();
125        assert_eq!(commits[0].message, "initial commit");
126
127        let branches = builder.repo().list_branches(BranchFilter::Local).unwrap();
128        assert!(branches.iter().any(|branch| branch.name == "feature"));
129        let tags = builder.repo().list_tags().unwrap();
130        assert!(tags.iter().any(|tag| tag.name == "v1.0.0"));
131    }
132
133    #[test]
134    fn repo_builder_writes_files() {
135        let builder = RepoBuilder::new()
136            .unwrap()
137            .with_file("nested/file.txt", "hello\n")
138            .unwrap();
139
140        assert_eq!(
141            std::fs::read_to_string(builder.root().join("nested/file.txt")).unwrap(),
142            "hello\n"
143        );
144    }
145}