1#![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)] use 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}