git_fixture/
lib.rs

1//! > DESCRIPTION
2
3#![cfg_attr(docsrs, feature(doc_auto_cfg))]
4#![warn(clippy::print_stderr)]
5#![warn(clippy::print_stdout)]
6
7#[doc = include_str!("../README.md")]
8#[cfg(doctest)]
9pub struct ReadmeDoctests;
10
11mod model;
12
13pub use model::*;
14
15#[allow(unused_imports)] // Not bothering matching the right features
16use eyre::WrapErr;
17
18impl TodoList {
19    pub fn load(path: &std::path::Path) -> eyre::Result<Self> {
20        match path.extension().and_then(std::ffi::OsStr::to_str) {
21            #[cfg(feature = "yaml")]
22            Some("yaml") | Some("yml") => {
23                let data = std::fs::read_to_string(path)
24                    .wrap_err_with(|| format!("Could not read {}", path.display()))?;
25
26                Self::parse_yaml(&data)
27                    .wrap_err_with(|| format!("Could not parse {}", path.display()))
28            }
29            #[cfg(feature = "json")]
30            Some("json") => {
31                let data = std::fs::read_to_string(path)
32                    .wrap_err_with(|| format!("Could not read {}", path.display()))?;
33
34                Self::parse_json(&data)
35                    .wrap_err_with(|| format!("Could not parse {}", path.display()))
36            }
37            #[cfg(feature = "toml")]
38            Some("toml") => {
39                let data = std::fs::read_to_string(path)
40                    .wrap_err_with(|| format!("Could not read {}", path.display()))?;
41
42                Self::parse_toml(&data)
43                    .wrap_err_with(|| format!("Could not parse {}", path.display()))
44            }
45            Some(other) => Err(eyre::eyre!("Unknown extension: {:?}", other)),
46            None => Err(eyre::eyre!("No extension for {}", path.display())),
47        }
48    }
49
50    pub fn save(&self, path: &std::path::Path) -> eyre::Result<()> {
51        match path.extension().and_then(std::ffi::OsStr::to_str) {
52            #[cfg(feature = "yaml")]
53            Some("yaml") | Some("yml") => {
54                let raw = self
55                    .to_yaml()
56                    .wrap_err_with(|| format!("Could not parse {}", path.display()))?;
57                std::fs::write(path, raw)
58                    .wrap_err_with(|| format!("Could not write {}", path.display()))
59            }
60            #[cfg(feature = "json")]
61            Some("json") => {
62                let raw = self
63                    .to_json()
64                    .wrap_err_with(|| format!("Could not parse {}", path.display()))?;
65                std::fs::write(path, raw)
66                    .wrap_err_with(|| format!("Could not write {}", path.display()))
67            }
68            #[cfg(feature = "toml")]
69            Some("toml") => {
70                let raw = self
71                    .to_toml()
72                    .wrap_err_with(|| format!("Could not parse {}", path.display()))?;
73                std::fs::write(path, raw)
74                    .wrap_err_with(|| format!("Could not write {}", path.display()))
75            }
76            Some(other) => Err(eyre::eyre!("Unknown extension: {:?}", other)),
77            None => Err(eyre::eyre!("No extension for {}", path.display())),
78        }
79    }
80
81    #[cfg(feature = "yaml")]
82    pub fn parse_yaml(data: &str) -> eyre::Result<Self> {
83        serde_yaml::from_str(data).map_err(|err| err.into())
84    }
85
86    #[cfg(feature = "json")]
87    pub fn parse_json(data: &str) -> eyre::Result<Self> {
88        serde_json::from_str(data).map_err(|err| err.into())
89    }
90
91    #[cfg(feature = "toml")]
92    pub fn parse_toml(data: &str) -> eyre::Result<Self> {
93        toml::from_str(data).map_err(|err| err.into())
94    }
95
96    #[cfg(feature = "yaml")]
97    pub fn to_yaml(&self) -> eyre::Result<String> {
98        serde_yaml::to_string(self).map_err(|err| err.into())
99    }
100
101    #[cfg(feature = "json")]
102    pub fn to_json(&self) -> eyre::Result<String> {
103        serde_json::to_string(self).map_err(|err| err.into())
104    }
105
106    #[cfg(feature = "toml")]
107    pub fn to_toml(&self) -> eyre::Result<String> {
108        toml::to_string(self).map_err(|err| err.into())
109    }
110}
111
112impl TodoList {
113    pub fn run(self, cwd: &std::path::Path) -> eyre::Result<()> {
114        let repo = if self.init {
115            git2::Repository::init(cwd)?
116        } else {
117            git2::Repository::open(cwd)?
118        };
119
120        let mut head = None;
121        let mut last_oid = repo
122            .head()
123            .and_then(|h| h.resolve())
124            .ok()
125            .and_then(|r| r.target());
126        let mut labels: std::collections::HashMap<Label, git2::Oid> = Default::default();
127        for (i, event) in self.commands.iter().enumerate() {
128            match event {
129                Command::Label(label) => {
130                    let current_oid = last_oid.ok_or_else(|| eyre::eyre!("no commits yet"))?;
131                    log::trace!("label {}  # {}", label, current_oid);
132                    labels.insert(label.clone(), current_oid);
133                }
134                Command::Reset(label) => {
135                    let current_oid = *labels
136                        .get(label.as_str())
137                        .ok_or_else(|| eyre::eyre!("Label doesn't exist: {:?}", label))?;
138                    log::trace!("reset {}  # {}", label, current_oid);
139                    last_oid = Some(current_oid);
140                }
141                Command::Tree(tree) => {
142                    let mut builder = repo.treebuilder(None)?;
143                    for (relpath, content) in tree.files.iter() {
144                        let relpath = path2bytes(relpath);
145                        let blob_id = repo.blob(content.as_bytes())?;
146                        let mode = 0o100755;
147                        builder.insert(relpath, blob_id, mode)?;
148                    }
149                    let new_tree_oid = builder.write()?;
150                    let new_tree = repo.find_tree(new_tree_oid)?;
151
152                    let sig =
153                        if let Some(author) = tree.author.as_deref().or(self.author.as_deref()) {
154                            git2::Signature::now(author, "")?
155                        } else {
156                            repo.signature()?
157                        };
158                    let message = tree
159                        .message
160                        .clone()
161                        .unwrap_or_else(|| format!("Commit (command {i})"));
162                    let mut parents = Vec::new();
163                    if let Some(last_oid) = last_oid {
164                        parents.push(repo.find_commit(last_oid)?);
165                    }
166                    let parents = parents.iter().collect::<Vec<_>>();
167                    let current_oid =
168                        repo.commit(None, &sig, &sig, &message, &new_tree, &parents)?;
169                    last_oid = Some(current_oid);
170
171                    if let Some(sleep) = self.sleep {
172                        std::thread::sleep(sleep);
173                    }
174                }
175                Command::Merge(merge) => {
176                    let ours_oid = last_oid.ok_or_else(|| eyre::eyre!("no commits yet"))?;
177                    log::trace!(
178                        "merge {}  # {}",
179                        merge
180                            .base
181                            .iter()
182                            .map(|s| s.as_str())
183                            .collect::<Vec<_>>()
184                            .join(" "),
185                        ours_oid
186                    );
187                    let mut parents = Vec::new();
188
189                    let ours_commit = repo.find_commit(ours_oid)?;
190                    let mut ours_tree_oid = ours_commit.tree_id();
191                    parents.push(ours_commit);
192                    for label in &merge.base {
193                        let ours_tree = repo.find_tree(ours_tree_oid)?;
194
195                        let their_oid = *labels
196                            .get(label.as_str())
197                            .ok_or_else(|| eyre::eyre!("Label doesn't exist: {:?}", label))?;
198                        let their_commit = repo.find_commit(their_oid)?;
199                        let their_tree = their_commit.tree()?;
200                        parents.push(their_commit);
201
202                        let base_oid = repo.merge_base(ours_oid, their_oid)?;
203                        let base_commit = repo.find_commit(base_oid)?;
204                        let base_tree = base_commit.tree()?;
205
206                        let mut options = git2::MergeOptions::new();
207                        options.find_renames(true);
208                        options.fail_on_conflict(true);
209                        let mut index =
210                            repo.merge_trees(&base_tree, &ours_tree, &their_tree, Some(&options))?;
211                        ours_tree_oid = index.write_tree()?;
212                    }
213
214                    let sig =
215                        if let Some(author) = merge.author.as_deref().or(self.author.as_deref()) {
216                            git2::Signature::now(author, "")?
217                        } else {
218                            repo.signature()?
219                        };
220                    let message = merge.message.clone().unwrap_or_else(|| {
221                        format!(
222                            "Merged {} (command {i})",
223                            merge
224                                .base
225                                .iter()
226                                .map(|s| s.as_str())
227                                .collect::<Vec<_>>()
228                                .join(" "),
229                        )
230                    });
231                    let ours_tree = repo.find_tree(ours_tree_oid)?;
232                    let parents = parents.iter().collect::<Vec<_>>();
233                    let current_oid =
234                        repo.commit(None, &sig, &sig, &message, &ours_tree, &parents)?;
235                    last_oid = Some(current_oid);
236
237                    if let Some(sleep) = self.sleep {
238                        std::thread::sleep(sleep);
239                    }
240                }
241                Command::Branch(branch) => {
242                    let current_oid = last_oid.ok_or_else(|| eyre::eyre!("no commits yet"))?;
243                    log::trace!("exec git branch --force {}  # {}", branch, current_oid);
244                    let commit = repo.find_commit(current_oid)?;
245                    repo.branch(branch.as_str(), &commit, true)?;
246                }
247                Command::Tag(tag) => {
248                    let current_oid = last_oid.ok_or_else(|| eyre::eyre!("no commits yet"))?;
249                    log::trace!("exec git tag --force -a {}  # {}", tag, current_oid);
250                    let commit = repo.find_commit(current_oid)?;
251                    let sig = if let Some(author) = self.author.as_deref() {
252                        git2::Signature::now(author, "")?
253                    } else {
254                        repo.signature()?
255                    };
256                    let message = format!("Tag (command {i})");
257                    repo.tag(tag.as_str(), commit.as_object(), &sig, &message, true)?;
258                }
259                Command::Head => {
260                    let new_head = if let Some(branch) = self.last_branch(i) {
261                        AnnotatedOid::Branch(branch)
262                    } else {
263                        let current_oid = last_oid.ok_or_else(|| eyre::eyre!("no commits yet"))?;
264                        AnnotatedOid::Commit(current_oid)
265                    };
266                    log::trace!("exec git checkout {}", new_head);
267                    head = Some(new_head);
268                }
269            }
270        }
271
272        let head = if let Some(head) = head {
273            head
274        } else if let Some(branch) = self.last_branch(self.commands.len()) {
275            AnnotatedOid::Branch(branch)
276        } else {
277            let current_oid = last_oid.ok_or_else(|| eyre::eyre!("no commits yet"))?;
278            AnnotatedOid::Commit(current_oid)
279        };
280        match head {
281            AnnotatedOid::Commit(head) => {
282                repo.set_head_detached(head)?;
283            }
284            AnnotatedOid::Branch(head) => {
285                let branch = repo.find_branch(&head, git2::BranchType::Local)?;
286                repo.set_head(branch.get().name().unwrap())?;
287            }
288        }
289        repo.checkout_head(None)?;
290
291        Ok(())
292    }
293
294    fn last_branch(&self, current_index: usize) -> Option<String> {
295        if let Some(Command::Branch(prev)) = self.commands.get(current_index.saturating_sub(1)) {
296            Some(prev.as_str().to_owned())
297        } else {
298            None
299        }
300    }
301}
302
303enum AnnotatedOid {
304    Commit(git2::Oid),
305    Branch(String),
306}
307
308impl std::fmt::Display for AnnotatedOid {
309    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
310        match self {
311            Self::Commit(ann) => ann.fmt(f),
312            Self::Branch(ann) => ann.fmt(f),
313        }
314    }
315}
316
317#[cfg(unix)]
318fn path2bytes(p: &std::path::Path) -> Vec<u8> {
319    use std::os::unix::prelude::*;
320    p.as_os_str().as_bytes().to_vec()
321}
322
323#[cfg(not(unix))]
324fn path2bytes(p: &std::path::Path) -> Vec<u8> {
325    _path2bytes_utf8(p)
326}
327
328fn _path2bytes_utf8(p: &std::path::Path) -> Vec<u8> {
329    let mut v = p.as_os_str().to_str().unwrap().as_bytes().to_vec();
330    for c in &mut v {
331        if *c == b'\\' {
332            *c = b'/';
333        }
334    }
335    v
336}