1#![cfg_attr(docsrs, feature(doc_auto_cfg))]
2
3mod model;
4
5pub use model::*;
6
7#[allow(unused_imports)] use 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}