1#![feature(path_file_prefix)]
2
3pub extern crate git2;
4
5use std::collections::BTreeMap;
6use std::collections::HashMap;
7use std::fmt;
8use std::fmt::Write;
9use std::fs;
10use std::io;
11use std::path::Path;
12use std::path::PathBuf;
13use std::result;
14use std::slice;
15
16use dir_structure::DirChildSingle;
17use dir_structure::DirChildSingleOpt;
18use dir_structure::DirChildren;
19use dir_structure::DirStructure;
20use dir_structure::Filter;
21use dir_structure::ForceCreateDirChildren;
22use dir_structure::Versioned;
23use dir_structure::file_prefix_filter;
24use dir_structure::json_pretty::JsonPretty;
25use git2::RebaseOperationType;
26use git2::Repository;
27use git2::build::CheckoutBuilder;
28use serde::Deserialize;
29use serde::Serialize;
30
31#[derive(Debug, thiserror::Error)]
32pub enum Error {
33 #[error("Failed to read steps: {0}")]
34 ReadStepsError(#[from] serde_json::Error),
35 #[error("git error: {0}")]
36 GitError(#[from] git2::Error),
37 #[error("dir-structure error: {0}")]
38 DirStructureError(#[from] dir_structure::Error),
39 #[error("IO error: {0}")]
40 IO(#[from] io::Error),
41 #[error("Failed to parse step reference: {0}")]
42 ParseStepReferenceError(String),
43}
44
45pub type Result<T> = result::Result<T, Error>;
46
47#[derive(DirStructure)]
48pub struct Guide {
49 #[dir_structure(path = "steps.json")]
50 steps: Versioned<JsonPretty<Steps>>,
51 #[dir_structure(path = "steps")]
52 step_dirs: ForceCreateDirChildren<StepDir>,
53 code_header: Option<String>,
54 code_footer: Option<String>,
55 #[dir_structure(path = self)]
56 template: DirChildSingle<String, TemplateFilter>,
57
58 self_path: PathBuf,
59}
60
61impl Guide {
62 pub fn new_default(dir: PathBuf, template_extension: &str) -> Self {
63 Self {
64 steps: Versioned::new_dirty(
65 JsonPretty(Steps { steps: vec![] }),
66 dir_structure::resolve_path!([Guide @ dir.clone()].steps),
67 ),
68 step_dirs: ForceCreateDirChildren::new(DirChildren::new()),
69 code_header: Some(String::from("```")),
70 code_footer: Some(String::from("```")),
71 template: DirChildSingle::new(
72 format!("template{template_extension}"),
73 String::from(
74 r#"
75---
76title: My guide
77description: A guide to guides
78---
79
80Beginning of the guide.
81
82And more...
83
84Do not change this next line, it is used to render the steps:
85<__GitVoyageSteps />
86
87End of the guide.
88"#
89 .trim_start(),
90 ),
91 ),
92 self_path: dir,
93 }
94 }
95
96 pub fn add_step(
97 &mut self,
98 step: &StepRef,
99 after_step: Option<&StepRef>,
100 code_extension: Option<Extension>,
101 before_after_extension: Option<Extension>,
102 ) {
103 let (code, code_extension, before_after) = if let Some(after) = after_step {
104 let pos = self
105 .steps
106 .steps
107 .iter()
108 .position(|s| s == after)
109 .expect("Step to add after not found");
110 self.steps.steps.insert(pos + 1, step.clone());
111
112 let code_extension = code_extension
113 .or_else(|| {
114 self.step_dirs
115 .get_value_by_name(&after.0)
116 .map(|dir| dir.code_extension())
117 })
118 .unwrap_or_else(Extension::default_code_extension);
119
120 let before_after = before_after_extension
121 .or_else(|| {
122 self.step_dirs
123 .get_value_by_name(&after.0)
124 .and_then(|dir| dir.before_after_extension())
125 })
126 .unwrap_or_else(Extension::default_before_after_extension);
127
128 (
129 self.step_dirs
130 .get_value_by_name(&after.0)
131 .expect("Step to add after not found")
132 .code
133 .value()
134 .clone(),
135 code_extension,
136 before_after,
137 )
138 } else {
139 let (code, code_extension, before_after) = match self.steps.steps.last() {
140 Some(last_step) => {
141 let code_extension = code_extension
142 .or_else(|| {
143 self.step_dirs
144 .get_value_by_name(&last_step.0)
145 .map(|dir| dir.code_extension())
146 })
147 .unwrap_or_else(Extension::default_code_extension);
148
149 let before_after = before_after_extension
150 .or_else(|| {
151 self.step_dirs
152 .get_value_by_name(&last_step.0)
153 .and_then(|dir| dir.before_after_extension())
154 })
155 .unwrap_or_else(Extension::default_before_after_extension);
156
157 (
158 self.step_dirs
159 .get_value_by_name(&last_step.0)
160 .expect("Last step not found")
161 .code
162 .value()
163 .clone(),
164 code_extension,
165 before_after,
166 )
167 }
168 None => (
169 String::new(),
170 code_extension.unwrap_or_else(Extension::default_code_extension),
171 before_after_extension
172 .unwrap_or_else(Extension::default_before_after_extension),
173 ),
174 };
175
176 self.steps.steps.push(step.clone());
177 (code, code_extension, before_after)
178 };
179
180 let step_dir = StepDir {
181 before: DirChildSingleOpt::Some(DirChildSingle::new(
182 format!("before{before_after}"),
183 String::new(),
184 )),
185 after: DirChildSingleOpt::Some(DirChildSingle::new(
186 format!("after{before_after}"),
187 String::new(),
188 )),
189 code: DirChildSingle::new(format!("code{code_extension}"), code),
190 code_header: None,
191 code_footer: None,
192 self_path: dir_structure::resolve_path!([Guide @ self.self_path.clone()].step_dirs.${&step.0}),
193 };
194 self.step_dirs.push(step.0.clone(), step_dir);
195 }
196
197 pub fn get_step_dir(&self, step: &StepRef) -> Option<&StepDir> {
198 self.step_dirs.get_value_by_name(&step.0)
199 }
200
201 pub fn steps_iter(&self) -> StepsIter<'_> {
202 StepsIter(self.steps.steps.iter(), &self.step_dirs)
203 }
204
205 pub fn template(&self) -> &String {
206 self.template.value()
207 }
208
209 pub fn render_template(&self, steps: String) -> String {
210 self.template().replace("<__GitVoyageSteps />", &steps)
211 }
212
213 pub fn render_guide(&self) -> String {
214 let mut steps = String::new();
215 for (_step, step_dir) in self.steps_iter() {
216 writeln!(&mut steps, "{}", step_dir.render_step(self)).unwrap();
217 }
218 self.render_template(steps)
219 }
220}
221
222#[derive(Debug)]
223pub struct Extension(String);
224
225impl fmt::Display for Extension {
226 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
227 write!(f, "{}", self.0)
228 }
229}
230
231impl Extension {
232 pub const fn new(ext: String) -> Self {
233 Self(ext)
234 }
235
236 pub fn default_code_extension() -> Self {
237 Self::new(String::new())
238 }
239
240 pub fn default_before_after_extension() -> Self {
241 Self::new(String::from(".mdx"))
242 }
243
244 pub fn value(&self) -> &str {
245 &self.0
246 }
247
248 fn guess_from<T, F: Filter>(dir_child: &DirChildSingle<T, F>) -> Self {
249 let s = dir_child.file_name().to_str().unwrap();
250 let p = Path::new(dir_child.file_name());
251 let prefix = if let Some(prefix) = p.file_prefix() {
252 prefix.to_str().unwrap()
253 } else {
254 ""
255 };
256 Self::new(s.strip_prefix(prefix).unwrap().to_owned())
257 }
258}
259
260pub struct StepsIter<'a>(slice::Iter<'a, StepRef>, &'a DirChildren<StepDir>);
261
262impl<'a> Iterator for StepsIter<'a> {
263 type Item = (&'a StepRef, &'a StepDir);
264
265 fn next(&mut self) -> Option<Self::Item> {
266 self.0
267 .next()
268 .and_then(|step| self.1.get_value_by_name(&step.0).map(|dir| (step, dir)))
269 }
270
271 fn size_hint(&self) -> (usize, Option<usize>) {
272 self.0.size_hint()
273 }
274}
275
276impl<'a> ExactSizeIterator for StepsIter<'a> {
277 fn len(&self) -> usize {
278 self.0.len()
279 }
280}
281
282impl<'a> DoubleEndedIterator for StepsIter<'a> {
283 fn next_back(&mut self) -> Option<Self::Item> {
284 self.0
285 .next_back()
286 .and_then(|step| self.1.get_value_by_name(&step.0).map(|dir| (step, dir)))
287 }
288}
289
290#[derive(DirStructure)]
291pub struct StepDir {
292 #[dir_structure(path = self)]
293 before: DirChildSingleOpt<String, BeforeFilter>,
294 #[dir_structure(path = self)]
295 after: DirChildSingleOpt<String, AfterFilter>,
296 #[dir_structure(path = self)]
297 pub code: DirChildSingle<String, CodeFilter>,
298
299 code_header: Option<String>,
300 code_footer: Option<String>,
301 self_path: PathBuf,
302}
303
304impl StepDir {
305 pub fn code_path(&self) -> PathBuf {
306 self.self_path.join(self.code.file_name())
307 }
308
309 pub fn render_step(&self, guide: &Guide) -> String {
310 let mut output = String::new();
311 if let DirChildSingleOpt::Some(before) = &self.before {
312 writeln!(output, "{}", before.value()).unwrap();
313 }
314 if let Some(header) = &self.code_header.as_ref().or(guide.code_header.as_ref()) {
315 writeln!(output, "{header}").unwrap();
316 }
317 writeln!(output, "{}", self.code.value()).unwrap();
318 if let Some(footer) = &self.code_footer.as_ref().or(guide.code_footer.as_ref()) {
319 writeln!(output, "{footer}").unwrap();
320 }
321 if let DirChildSingleOpt::Some(after) = &self.after {
322 writeln!(output, "{}", after.value()).unwrap();
323 }
324
325 output
326 }
327
328 pub fn before_after_extension(&self) -> Option<Extension> {
329 self.before
330 .as_ref()
331 .to_option()
332 .or(self
333 .after
334 .as_ref()
335 .to_option()
336 .map(DirChildSingle::map_filter))
337 .map(|dir| Extension::guess_from(&dir))
338 }
339
340 pub fn code_extension(&self) -> Extension {
341 Extension::guess_from(&self.code)
342 }
343}
344
345file_prefix_filter!(pub TemplateFilter, "template");
346file_prefix_filter!(pub BeforeFilter, "before");
347file_prefix_filter!(pub AfterFilter, "after");
348file_prefix_filter!(pub CodeFilter, "code");
349
350#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
351pub struct Steps {
352 steps: Vec<StepRef>,
353}
354
355#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
356#[repr(transparent)]
357pub struct StepRef(String);
358
359impl StepRef {
360 pub fn new(path: String) -> Self {
361 Self(path)
362 }
363
364 pub fn path(&self) -> &str {
365 &self.0
366 }
367}
368
369trait DbgGitErr {
370 fn dbg_git_err(self) -> Self;
371}
372
373impl<T> DbgGitErr for result::Result<T, git2::Error> {
374 fn dbg_git_err(self) -> Self {
375 match self {
376 Ok(v) => Ok(v),
377 Err(e) => {
378 eprintln!("Git error: {}", e);
379 eprintln!("Class of error: {:?}", e.class());
380 eprintln!("Message: {}", e.message());
381 eprintln!("Code: {:?}", e.code());
382 Err(e)
383 }
384 }
385 }
386}
387
388fn perform_patchup(
389 guide: &Guide,
390 step: &StepRef,
391 new_code: &str,
392 repo_root: &Path,
393 repo: &Repository,
394 mut resolve_conflict: impl FnMut(&Path) -> Result<()>,
395) -> Result<BTreeMap<String, String>> {
396 let code_file = repo_root.join("code");
397 fs::write(&code_file, b"")?;
398
399 let mut index = repo.index()?;
400 index.add_all(["code"].iter(), git2::IndexAddOption::DEFAULT, None)?;
401 index.write()?;
402 let tree_id = index.write_tree()?;
403 let signature = repo.signature()?;
404 let c = repo.commit(
405 Some("HEAD"),
406 &signature,
407 &signature,
408 "Initial commit",
409 &repo.find_tree(tree_id)?,
410 &[],
411 )?;
412
413 let mut current_commit = repo.find_commit(c)?;
414 let mut before_ed_branch = None;
415 let mut edit_branch = None;
416
417 for s in &guide.steps.steps {
418 let mut index = repo.index()?;
419 let code = &*guide.step_dirs.get_name(&s.0).unwrap().value().code;
420 fs::write(&code_file, code)?;
421 index.add_all(["code"], git2::IndexAddOption::DEFAULT, None)?;
422 index.write()?;
423 let tree_id = index.write_tree()?;
424 let c = repo.commit(
425 Some("HEAD"),
426 &signature,
427 &signature,
428 &format!("step: {}", s.0),
429 &repo.find_tree(tree_id)?,
430 &[¤t_commit],
431 )?;
432
433 current_commit = repo.find_commit(c)?;
434
435 if s == step {
436 before_ed_branch = Some(repo.branch("before-edit", ¤t_commit, true)?);
437 fs::write(&code_file, new_code)?;
438
439 index.add_all(["code"], git2::IndexAddOption::DEFAULT, None)?;
440 index.write()?;
441 let tree_id = index.write_tree()?;
442 let edit_c = repo.commit(
443 Some("HEAD"),
444 &signature,
445 &signature,
446 &format!("patchup: {}", s.0),
447 &repo.find_tree(tree_id)?,
448 &[¤t_commit],
449 )?;
450
451 let ed = repo.find_commit(edit_c)?;
452
453 edit_branch = Some(repo.branch("edit", &ed, true)?);
454
455 repo.reset(
456 current_commit.as_object(),
457 git2::ResetType::Hard,
458 Some(CheckoutBuilder::new().path("code").force()),
459 )?;
460 }
461 }
462
463 let final_branch = repo.branch("final", ¤t_commit, true)?;
464
465 let before_edit =
466 repo.reference_to_annotated_commit(&before_ed_branch.unwrap().into_reference())?;
467 let edit = repo.reference_to_annotated_commit(&edit_branch.unwrap().into_reference())?;
468 let f = repo.reference_to_annotated_commit(&final_branch.into_reference())?;
469
470 let mut merge_opts = git2::MergeOptions::new();
471 merge_opts.file_favor(git2::FileFavor::Normal);
472 merge_opts.diff3_style(true);
473 merge_opts.fail_on_conflict(false);
474
475 let mut rebase = repo
476 .rebase(
477 Some(&f),
478 Some(&before_edit),
479 Some(&edit),
480 Some(git2::RebaseOptions::new().merge_options(merge_opts)),
481 )
482 .dbg_git_err()?;
483
484 let mut empty_steps = BTreeMap::new();
485 let mut parent = None;
486
487 while let Some(info) = rebase.next() {
488 let info = info.dbg_git_err()?;
489 let index = repo.index().dbg_git_err()?;
490 let len = repo
491 .diff_index_to_workdir(None, None)
492 .dbg_git_err()?
493 .deltas()
494 .len();
495 println!("Index has conflicts: {}", index.has_conflicts());
496 if len > 0 {
497 eprintln!("Found {} changes in commit: {}", len, info.id());
498 resolve_conflict(&code_file)?;
499 repo.index()?.add_path(Path::new("code"))?;
500 }
501 match info.kind() {
502 Some(RebaseOperationType::Exec) => {}
503 _ => match rebase.commit(None, &signature, None) {
504 Ok(oid) => {
505 parent = Some(oid);
506 }
507 Err(e) if e.code() == git2::ErrorCode::Applied => {
508 let rebasing_commit = repo.find_commit(info.id())?;
509 let original = repo.find_commit(parent.unwrap())?;
510 let Some(message) = rebasing_commit.message() else {
511 return Err(Error::ParseStepReferenceError(
512 "Commit message is not valid UTF-8".to_owned(),
513 ));
514 };
515 let Some(reference) = message
516 .strip_prefix("step: ")
517 .or_else(|| message.strip_prefix("patchup: "))
518 else {
519 return Err(Error::ParseStepReferenceError(
520 "Commit message does not start with 'step: ' or 'patchup: '".to_owned(),
521 ));
522 };
523
524 let code_file = original.tree()?.get_path(Path::new("code"))?;
525 let code_blob = code_file.to_object(repo)?.peel_to_blob()?;
526 let code_content =
527 String::from_utf8(code_blob.content().to_vec()).map_err(|_| {
528 Error::ParseStepReferenceError(
529 "Code content is not valid UTF-8".to_owned(),
530 )
531 })?;
532
533 empty_steps.insert(reference.to_owned(), code_content);
534 }
535 Err(e) => {
536 Err(e).dbg_git_err()?;
537 }
538 },
539 }
540 }
541
542 rebase.finish(None)?;
543
544 Ok(empty_steps)
545}
546
547pub fn patchup(
548 guide: &mut Guide,
549 dir: &Path,
550 step: &StepRef,
551 new_code: &str,
552 resolve_conflict: impl FnMut(&Path) -> Result<()>,
553) -> Result<()> {
554 let repo_root = dir.join(".repo");
555 if repo_root.exists() {
556 fs::remove_dir_all(&repo_root)?;
557 }
558 fs::create_dir_all(&repo_root)?;
559 let repo = Repository::init(&repo_root)?;
560 match perform_patchup(guide, step, new_code, &repo_root, &repo, resolve_conflict) {
561 Ok(empty_commits) => {
562 repatch(guide, dir, &empty_commits)?;
563 }
564 Err(e) => {
565 eprintln!("Failed to patchup: {}", e);
566 return Err(e);
567 }
568 }
569
570 Ok(())
571}
572
573pub fn repatch(
574 guide: &mut Guide,
575 dir: &Path,
576 empty_commits: &BTreeMap<String, String>,
577) -> Result<()> {
578 let repo_root = dir.join(".repo");
579 if !repo_root.exists() {
580 return Err(Error::IO(io::Error::new(
581 io::ErrorKind::NotFound,
582 "Repository not found",
583 )));
584 }
585 let repo = Repository::open(&repo_root)?;
586 let code_file = repo_root.join("code");
587
588 if !code_file.exists() {
589 return Err(Error::IO(io::Error::new(
590 io::ErrorKind::NotFound,
591 "Code file not found",
592 )));
593 }
594
595 let mut files = HashMap::new();
596
597 let mut commit = repo.head()?.peel_to_commit()?;
598
599 loop {
600 let message = commit.message().unwrap();
601 let contents = commit
602 .tree()?
603 .get_path(Path::new("code"))?
604 .to_object(&repo)?
605 .peel_to_blob()?
606 .content()
607 .to_vec();
608 if let Some(step_name) = message.strip_prefix("step: ") {
609 if !files.contains_key(step_name) {
612 files.insert(step_name.to_string(), contents);
613 }
614 } else if let Some(step_name) = message.strip_prefix("patchup: ") {
615 files.insert(step_name.to_string(), contents);
616 }
617
618 if commit.parent_count() == 0 {
619 break;
620 }
621
622 commit = commit.parent(0)?;
623 }
624
625 for step_dir in guide.step_dirs.iter_mut() {
626 let step_name = step_dir.file_name().clone();
627 if let Some(contents) = files.get(step_name.to_str().unwrap()) {
628 let c = &mut step_dir.value_mut().code;
629 *c = DirChildSingle::new(c.file_name(), String::from_utf8(contents.clone()).unwrap());
630 eprintln!("Repatched step: {}", step_name.to_str().unwrap());
631 } else if let Some(contents) = empty_commits.get(step_name.to_str().unwrap()) {
632 let c = &mut step_dir.value_mut().code;
633 *c = DirChildSingle::new(c.file_name(), contents.clone());
634 eprintln!(
635 "Repatched step (empty commit): {}",
636 step_name.to_str().unwrap()
637 );
638 } else {
639 panic!(
640 "No patch found for step: {}\nSomething went terribly wrong.",
641 step_name.to_str().unwrap()
642 );
643 }
644 }
645
646 Ok(())
647}