1mod git;
2mod smb_config;
3
4use crate::{
5 account::lib::protected_request,
6 cli::CommandResult,
7 ui::{fail_message, fail_symbol, succeed_message, succeed_symbol},
8};
9use anyhow::Result;
10use console::style;
11use git::remote_deployment_setup;
12use git2::{Cred, PushOptions, RemoteCallbacks, Repository};
13use git_url_parse::{GitUrl, Scheme};
14use indicatif::{ProgressBar, ProgressState, ProgressStyle};
15use smb_config::check_config;
16use smbcloud_networking::environment::Environment;
17use spinners::Spinner;
18use ssh2_config::{ParseRule, SshConfig};
19use std::{cmp::min, fmt::Write, fs::File, io::BufReader};
20
21pub async fn process_deploy(env: Environment) -> Result<CommandResult> {
22 protected_request(env).await?;
23 let repo_name = check_config().await?;
24
25 let mut spinner = Spinner::new(
26 spinners::Spinners::SimpleDotsScrolling,
27 style("Deploying...").green().bold().to_string(),
28 );
29
30 let repo = match Repository::open(".") {
31 Ok(repo) => repo,
32 Err(_) => {
33 spinner.stop_and_persist("😩", "No git repository found.".to_owned());
34 return Ok(CommandResult {
35 spinner,
36 symbol: fail_symbol(),
37 msg: fail_message("No git repository found. Init with `git init` command."),
38 });
39 }
40 };
41
42 let _main_branch = match repo.head() {
43 Ok(branch) => branch,
44 Err(_) => {
45 spinner.stop_and_persist("😩", "No main branch found.".to_owned());
46 return Ok(CommandResult {
47 spinner,
48 symbol: fail_symbol(),
49 msg: fail_message(
50 "No main branch found. Create with `git checkout -b <branch>` command.",
51 ),
52 });
53 }
54 };
55 let mut origin = remote_deployment_setup(&repo, repo_name).await?;
56 let remote_url = match origin.url() {
57 Some(url) => url,
58 None => {
59 spinner.stop_and_persist("😩", "No remote URL found.".to_owned());
60 return Ok(CommandResult {
61 spinner,
62 symbol: fail_symbol(),
63 msg: fail_message(
64 "No remote URL found. Add with `git remote add origin <url>` command.",
65 ),
66 });
67 }
68 };
69 let parsed_url = match GitUrl::parse(remote_url) {
71 Ok(url) => url,
72 Err(e) => {
73 spinner.stop_and_persist("😩", e.to_string());
74 return Ok(CommandResult {
75 spinner,
76 symbol: fail_symbol(),
77 msg: "Invalid remote URL.".to_owned(),
78 });
79 }
80 };
81 match parsed_url.scheme {
83 Scheme::Ssh => {
84 }
86 _ => {
87 return Ok(CommandResult {
89 spinner,
90 symbol: fail_symbol(),
91 msg: fail_message("Only ssh is supported."),
92 });
93 }
94 };
95
96 let host = match parsed_url.host {
98 Some(host) => host,
99 None => {
100 spinner.stop_and_persist("😩", "No host found.".to_owned());
101 return Ok(CommandResult {
102 spinner,
103 symbol: fail_symbol(),
104 msg: fail_message("No host found."),
105 });
106 }
107 };
108
109 let ssh_config_file = match home::home_dir() {
111 Some(home) => {
112 let ssh_config_path = home.join(".ssh/config");
113 if ssh_config_path.exists() {
114 File::open(ssh_config_path.clone()).expect("Unable to open ssh config file")
117 } else {
118 spinner.stop_and_persist("😩", "No ssh config found.".to_owned());
119 return Ok(CommandResult {
120 spinner,
121 symbol: fail_symbol(),
122 msg: fail_message("No ssh config found."),
123 });
124 }
125 }
126 None => {
127 spinner.stop_and_persist("😩", "No home".to_owned());
128 return Ok(CommandResult {
129 spinner,
130 symbol: "😩".to_owned(),
131 msg: "No home directory found.".to_owned(),
132 });
133 }
134 };
135 let mut reader = BufReader::new(ssh_config_file);
137 let config = SshConfig::default()
138 .parse(&mut reader, ParseRule::STRICT)
139 .expect("Failed to parse ssh config file");
140 let host_config = config.query(host);
144 let identity_files = match host_config.identity_file {
147 Some(identity_files) => {
148 identity_files
150 }
151 None => {
152 spinner.stop_and_persist("😩", "No identity file found.".to_owned());
153 return Ok(CommandResult {
154 spinner,
155 symbol: "😩".to_owned(),
156 msg: "No identity files found.".to_owned(),
157 });
158 }
159 };
160 let identity_file = match identity_files.first() {
163 Some(identity_file) => {
164 identity_file
166 }
167 None => {
168 spinner.stop_and_persist("😩", "No identity file found.".to_owned());
169 return Ok(CommandResult {
170 spinner,
171 symbol: "😩".to_owned(),
172 msg: "No identity file found.".to_owned(),
173 });
174 }
175 };
176 let mut push_opts = PushOptions::new();
177 let mut callbacks = RemoteCallbacks::new();
178 callbacks.credentials(|_url, _username_from_url, _allowed_types| {
180 Cred::ssh_key("deploy", None, identity_file, None)
181 });
182 callbacks.push_transfer_progress(|current, total, bytes| {
183 let pb = ProgressBar::new(total.try_into().unwrap());
185 pb.set_style(ProgressStyle::with_template("{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({eta})")
186 .unwrap()
187 .with_key("eta", |state: &ProgressState, w: &mut dyn Write| write!(w, "{:.1}s", state.eta().as_secs_f64()).unwrap())
188 .progress_chars("#>-"));
189 let mut current = current;
190 while current < total {
191 let new = min(current + 223211, total);
192 current = new;
193 pb.set_position(new.try_into().unwrap());
194 }
195
196 pb.finish_with_message("downloaded");
197 });
198 callbacks.pack_progress(|stage, current, total| {
199 println!("pack_progress: current -> {}, total -> {}", current, total);
200 });
201 callbacks.sideband_progress(|x| {
202 return true;
204 });
205 callbacks.push_update_reference(|x, y| match y {
206 Some(e) => {
207 println!("{}", e);
208 Ok(())
209 }
210 None => {
211 println!("Done");
212 Ok(())
213 }
214 });
215 push_opts.remote_callbacks(callbacks);
216
217 return match origin.push(&["refs/heads/main:refs/heads/main"], Some(&mut push_opts)) {
218 Ok(_) => {
219 println!("Succeed!!!");
220 Ok(CommandResult {
221 spinner,
222 symbol: succeed_symbol(),
223 msg: succeed_message("Your app has been deployed successfully."),
224 })
225 }
226 Err(e) => {
227 println!("Failed to push to remote: {:#?}", e);
228 spinner.stop_and_persist("😩", e.to_string());
229 Ok(CommandResult {
230 spinner,
231 symbol: fail_symbol(),
232 msg: fail_message(&e.to_string()),
233 })
234 }
235 };
236}