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 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 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 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 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 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 Ok(())
226}
227
228#[cfg(test)]
229mod tests;