degit/
lib.rs

1use colored::*;
2use flate2::read::GzDecoder;
3use indicatif::{ProgressBar, ProgressStyle};
4use regex::Regex;
5use std::{error::Error, fmt, path::PathBuf};
6use tar::Archive;
7
8#[derive(Debug, PartialEq)]
9enum Host {
10    Github,
11    Gitlab(String),
12    BitBucket,
13}
14
15#[derive(Debug, PartialEq)]
16struct Repo {
17    host: Host,
18    project: String,
19    owner: String,
20}
21
22impl fmt::Display for Repo {
23    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
24        let owner = self.owner.bold().underline();
25        let project = self.project.red();
26        let host = match self.host {
27            Host::Github => "GitHub".blue(),
28            Host::Gitlab(_) => "GitLab".red(),
29            Host::BitBucket => "BitBucket".green(),
30        };
31        write!(f, "{}/{} from {}", owner, project, host)
32    }
33}
34
35pub fn degit(src: &str, dest: &str) {
36    let repo = parse(src).unwrap();
37    match download(repo, PathBuf::from(dest)) {
38        Err(x) => println!("{}", x),
39        _ => (),
40    }
41}
42
43fn parse(src: &str) -> Result<Repo, Box<dyn Error>> {
44    let repo_match = Regex::new(
45        r"(?x)
46                                (?P<protocol>(git@|https://))
47                                (?P<host>([\w\.@]+))
48                                (/|:)
49                                (?P<owner>[\w,\-,_]+)
50                                /
51                                (?P<repo>[\w,\-,_]+)
52                                (.git)?/?
53                                ",
54    )
55    .unwrap();
56    let shortrepo_match = Regex::new(
57        r"(?x)
58                                (?P<host>(github|gitlab|bitbucket)?)
59                                (?P<colon>(:))?
60                                (?P<owner>[\w,\-,_]+)
61                                /
62                                (?P<repo>[\w,\-,_]+)
63                                ",
64    )
65    .unwrap();
66    if repo_match.is_match(src) {
67        let caps = repo_match.captures(src).unwrap();
68        let host = caps.name("host").unwrap().as_str();
69        // println!("{}",host);
70        let hosten;
71        if host.contains("github") {
72            hosten = Host::Github;
73        } else if host.contains("gitlab") {
74            hosten = Host::Gitlab(host.to_string());
75        } else if host.contains("bitbucket") {
76            hosten = Host::BitBucket;
77        } else {
78            return Err("Git provider not supported.")?;
79        }
80        let res = Repo {
81            owner: caps.name("owner").unwrap().as_str().to_string(),
82            project: caps.name("repo").unwrap().as_str().to_string(),
83            host: hosten,
84        };
85        return Ok(res);
86    }
87    if shortrepo_match.is_match(src) {
88        let caps = shortrepo_match.captures(src).unwrap();
89        let host = caps.name("host").unwrap().as_str();
90        let colon = caps.name("colon");
91        let hosten;
92        if let None = colon {
93            hosten = Host::Github;
94        } else {
95            if host.contains("github") {
96                hosten = Host::Github;
97            } else if host.contains("gitlab") {
98                hosten = Host::Gitlab("gitlab.com".to_string());
99            } else if host.contains("bitbucket") {
100                hosten = Host::BitBucket;
101            } else {
102                return Err("Git provider not supported.")?;
103            }
104        }
105        let res = Repo {
106            owner: caps.name("owner").unwrap().as_str().to_string(),
107            project: caps.name("repo").unwrap().as_str().to_string(),
108            host: hosten,
109        };
110        return Ok(res);
111    }
112    Err("Could not parse repository")?
113}
114
115fn download(repo: Repo, dest: PathBuf) -> Result<(), Box<dyn Error>> {
116    let url = match &repo.host {
117        Host::Github => format!(
118            "https://github.com/{}/{}/archive/HEAD.tar.gz",
119            repo.owner, repo.project
120        ),
121        Host::Gitlab(x) => format!(
122            "https://{}/{}/{}/repository/archive.tar.gz",
123            x, repo.owner, repo.project
124        ),
125        Host::BitBucket => format!(
126            "https://bitbucket.org/{}/{}/get/HEAD.zip",
127            repo.owner, repo.project
128        ),
129    };
130    // println!("{}", url);
131    let client = reqwest::Client::new();
132
133    let request = client.get(&url).send().unwrap();
134    match request.status() {
135        reqwest::StatusCode::OK => (),
136        reqwest::StatusCode::UNAUTHORIZED => {
137            Err("Could not find repository.")?;
138        }
139        s => Err(format!("Received response status: {:?}", s))?,
140    };
141
142    let total_size = request.content_length();
143
144    let pb = match total_size {
145        Some(x) => {
146            let p = ProgressBar::new(x);
147            p.set_style(ProgressStyle::default_bar()
148                     .template("> {wide_msg}\n{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {bytes}/{total_bytes} ({eta})")
149                     .progress_chars("#>-"));
150            p
151        }
152        None => {
153            let p = ProgressBar::new_spinner();
154            p
155        }
156    };
157
158    println!("Downloading {} to {}", repo, dest.display());
159    // println!("{:#?}", request.content_length());
160
161    let tar = GzDecoder::new(pb.wrap_read(request));
162    let mut archive = Archive::new(tar);
163    archive
164        .entries()?
165        .filter_map(|e| e.ok())
166        .map(|mut entry| -> Result<PathBuf, Box<dyn Error>> {
167            let path = entry.path()?;
168            let path = path
169                .strip_prefix(path.components().next().unwrap())?
170                .to_owned();
171            entry.unpack(dest.join(&path))?;
172            Ok(path)
173        })
174        .filter_map(|e| e.ok())
175        .for_each(|x| pb.set_message(&format!("{}", x.display())));
176
177    // archive.unpack(dest).unwrap();
178    pb.finish_with_message("Done...");
179    Ok(())
180}
181
182pub fn validate_src(src: String) -> Result<(), String> {
183    parse(&src).map(|_| ()).map_err(|x| x.to_string())
184}
185
186pub fn validate_dest(dest: String) -> Result<(), String> {
187    let path = PathBuf::from(dest);
188    if path.exists() {
189        if path.is_dir() {
190            let count = std::fs::read_dir(&path).map_err(|x| x.to_string())?.count();
191            if count != 0 {
192                Err("Directory is not empty.")?
193            }
194        } else {
195            Err("Destination is not a directory.")?
196        }
197    }
198    let mut realpath = {
199        if path.is_relative() {
200            let mut realpath = std::fs::canonicalize(std::path::Path::new(".")).unwrap();
201
202            for c in path.components() {
203                // println!("component: {:?}", c);
204                match c {
205                    std::path::Component::ParentDir => {
206                        realpath = realpath.parent().unwrap().to_path_buf()
207                    }
208                    std::path::Component::Normal(c) => realpath.push(c),
209                    _ => (),
210                }
211            }
212            realpath
213        } else {
214            path
215        }
216    };
217    while !realpath.exists(){
218        realpath.pop();
219    }
220    if std::fs::metadata(&realpath).unwrap().permissions().readonly(){
221        Err("Directory is read-only.")?
222    }
223    // println!("realpath: {:?}", realpath);
224
225    Ok(())
226}
227
228#[cfg(test)]
229mod tests;