git_fixture/
lib.rs

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