1use std::{fs, path::Path};
2
3use anyhow::{bail, Context, Error, Result};
4use git2::build::{CheckoutBuilder, RepoBuilder};
5use once_cell::sync::Lazy;
6use regex::Regex;
7
8use crate::{
9 cfg::{cfg_android, cfg_macos, cfg_unix, cfg_windows},
10 model::Command,
11};
12
13static PAGES_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r#"\n\s*- (.+?):?\n\n?\s*`([^`]+)`"#).unwrap());
15
16pub fn scrape_tldr_github(category: Option<&str>) -> Result<Vec<Command>> {
18 scrape_tldr_repo("https://github.com/tldr-pages/tldr.git", category)
19}
20
21pub fn scrape_tldr_repo(url: impl AsRef<str>, category: Option<&str>) -> Result<Vec<Command>> {
23 let tmp_dir = tempfile::tempdir()?;
24 let repo_path = tmp_dir.path();
25
26 let mut checkout = CheckoutBuilder::default();
27 checkout.path("pages/**");
28
29 RepoBuilder::default()
30 .with_checkout(checkout)
31 .clone(url.as_ref(), repo_path)?;
32
33 let mut result = Vec::new();
34
35 match category {
36 Some(category) => {
37 if !repo_path.join("pages").join(category).exists() {
38 bail!("Category {category} doesn't exist")
39 }
40 result.append(&mut parse_tldr_folder(
41 category,
42 repo_path.join("pages").join(category),
43 )?);
44 }
45 None => {
46 result.append(&mut parse_tldr_folder(
47 "common",
48 repo_path.join("pages").join("common"),
49 )?);
50
51 cfg_android!(
52 result.append(&mut parse_tldr_folder(
53 "android",
54 repo_path.join("pages").join("android"),
55 )?);
56 );
57 cfg_macos!(
58 result.append(&mut parse_tldr_folder(
59 "osx",
60 repo_path.join("pages").join("osx"),
61 )?);
62 );
63 cfg_unix!(
64 result.append(&mut parse_tldr_folder(
65 "linux",
66 repo_path.join("pages").join("linux"),
67 )?);
68 );
69 cfg_windows!(
70 result.append(&mut parse_tldr_folder(
71 "windows",
72 repo_path.join("pages").join("windows"),
73 )?);
74 );
75 }
76 }
77
78 Ok(result)
79}
80
81fn parse_tldr_folder(category: impl Into<String>, path: impl AsRef<Path>) -> Result<Vec<Command>> {
83 let path = path.as_ref();
84 let category = category.into();
85 path.read_dir()
86 .context("Error reading tldr dir")?
87 .map(|r| r.map_err(Error::from))
88 .map(|r| r.map(|e| e.path()))
89 .map(|r| r.and_then(|p| Ok(fs::read_to_string(p)?)))
90 .map(|r| r.map(|r| parse_page(&category, r)))
91 .flat_map(|result| match result {
92 Ok(vec) => vec.into_iter().map(Ok).collect(),
93 Err(er) => vec![Err(er)],
94 })
95 .collect::<Result<Vec<_>>>()
96}
97
98fn parse_page(category: impl Into<String>, str: impl AsRef<str>) -> Vec<Command> {
100 let category = category.into();
101 PAGES_REGEX
102 .captures_iter(str.as_ref())
103 .map(|c| Command::new(category.clone(), &c[2], &c[1]))
104 .collect()
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110
111 #[test]
112 fn test_parse_page() -> Result<()> {
113 let commands = parse_page(
114 "test",
115 r#"# git commit
116
117 > Commit files to the repository.
118 > More information: <https://git-scm.com/docs/git-commit>.
119
120 - Commit staged files to the repository with a message:
121
122 `git commit -m "{{message}}"`
123
124 - Commit staged files with a message read from a file
125
126 `git commit --file {{path/to/commit_message_file}}`
127
128 - Auto stage all modified files and commit with a message;
129
130 `git commit -a -m "{{message}}"`
131
132 - Commit staged files and [S]ign them with the GPG key defined in `~/.gitconfig`
133
134 `git commit -S -m "{{message}}"`
135
136 - Update the last commit by adding the currently staged changes, changing the commit's hash
137
138 `git commit --amend`
139
140 - Commit only specific (already staged) files:
141
142 `git commit {{path/to/file1}} {{path/to/file2}}`
143
144 - Create a commit, even if there are no staged files
145
146 `git commit -m "{{message}}" --allow-empty`
147 "#,
148 );
149
150 assert_eq!(commands.len(), 7);
151 assert_eq!(commands.get(0).unwrap().cmd, r#"git commit -m "{{message}}""#);
152 assert_eq!(
153 commands.get(0).unwrap().description,
154 r#"Commit staged files to the repository with a message"#
155 );
156 assert_eq!(commands.get(3).unwrap().cmd, r#"git commit -S -m "{{message}}""#);
157 assert_eq!(
158 commands.get(3).unwrap().description,
159 r#"Commit staged files and [S]ign them with the GPG key defined in `~/.gitconfig`"#
160 );
161
162 Ok(())
163 }
164}