Skip to main content

git_voyage/
lib.rs

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            &[&current_commit],
431        )?;
432
433        current_commit = repo.find_commit(c)?;
434
435        if s == step {
436            before_ed_branch = Some(repo.branch("before-edit", &current_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                &[&current_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", &current_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            // most likely original commit, we don't want to overwrite it, if it already exists in the map
610            // (patchup comes after the original commit, and when we explore we walk backwards)
611            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}