terminal_magic/
git.rs

1// Copyright (c) 2022 Patrick Amrein <amrein@ubique.ch>
2//
3// This software is released under the MIT License.
4// https://opensource.org/licenses/MIT
5
6use std::path::{Path, PathBuf};
7
8use colored::Colorize;
9//GIT Section
10use git2::{Commit, Cred, Error, ObjectType, RemoteCallbacks, Repository};
11
12use crate::{
13    models::GlobalConfig,
14    prompts::{boolean_prompt, password_prompt, text_prompt},
15};
16
17pub fn get_callbacks(
18    global_config: &mut GlobalConfig,
19    ssh_key: Option<PathBuf>,
20    key_needs_pw: bool,
21) -> RemoteCallbacks {
22    update_git_repo_path(global_config).expect("Could not create git repo directory");
23    let mut callbacks = RemoteCallbacks::new();
24    callbacks.credentials(move |_url, username_from_url, allowed_types| {
25        let mut username = String::from("");
26        if let Some(name_from_url) = username_from_url {
27            username = String::from(name_from_url);
28        } else if let Some(user) = text_prompt("Git Username: ") {
29            username = user;
30        }
31
32        if allowed_types.is_user_pass_plaintext() {
33            if let Some(password) = password_prompt("Git Password: ") {
34                Cred::userpass_plaintext(&username, &password)
35            } else {
36                Cred::default()
37            }
38        } else if allowed_types.is_ssh_key() {
39            if let Some(ssh_key) = &ssh_key {
40                if key_needs_pw {
41                    if let Some(key_pw) = password_prompt("SSH key password: ") {
42                        Cred::ssh_key(&username, None, ssh_key, Some(&key_pw))
43                    } else {
44                        Cred::default()
45                    }
46                } else {
47                    Cred::ssh_key(&username, None, ssh_key, None)
48                }
49            } else if let Some(key_path) = text_prompt("SSH Key Path: ") {
50                let key_path = shellexpand::tilde(&key_path).to_string();
51                global_config.ssh_key = Some(key_path.clone());
52                let key_path = Path::new(&key_path);
53                let key_needs_pw = boolean_prompt("Is the key password protected? ");
54                global_config.key_needs_pw = key_needs_pw;
55                if key_needs_pw {
56                    if let Some(key_pw) = password_prompt("SSH key password: ") {
57                        Cred::ssh_key(&username, None, key_path, Some(&key_pw))
58                    } else {
59                        Cred::default()
60                    }
61                } else {
62                    Cred::ssh_key(&username, None, key_path, None)
63                }
64            } else {
65                Cred::default()
66            }
67        } else {
68            Cred::default()
69        }
70    });
71    callbacks
72}
73
74pub fn check_out_modules_with_key(
75    global_config: &mut GlobalConfig,
76    remote: &str,
77    ssh_key: &Path,
78) -> Result<(), Error> {
79    let key_needs_pw = boolean_prompt("Does key need password? ");
80    global_config.key_needs_pw = key_needs_pw;
81    global_config.ssh_key = Some(String::from(ssh_key.to_string_lossy()));
82    let git_modules = global_config.home.join("git_modules");
83    let callbacks = get_callbacks(global_config, Some(ssh_key.into()), false);
84    check_out(git_modules, remote, callbacks)?;
85    Ok(())
86}
87
88pub fn check_out_modules_with_pw(
89    global_config: &mut GlobalConfig,
90    remote: &str,
91) -> Result<(), Error> {
92    let git_modules = global_config.home.join("git_modules");
93    let callbacks = get_callbacks(global_config, None, false);
94    check_out(git_modules, remote, callbacks)?;
95    Ok(())
96}
97
98pub fn update_git_repo_path(global_config: &mut GlobalConfig) -> Result<(), Error> {
99    if Path::new(&global_config.git_repo).exists() {
100        return Ok(());
101    }
102    if !global_config.home.join("git_modules").exists() {
103        match std::fs::create_dir_all(global_config.home.join("git_modules")) {
104            Ok(_) => {}
105            Err(_) => {
106                eprintln!("Could not create git_modules");
107                std::process::exit(1);
108            }
109        }
110    }
111    global_config.git_repo = global_config
112        .home
113        .join("git_modules")
114        .to_string_lossy()
115        .to_string();
116    if global_config.save().is_err() {
117        eprintln!("{}", "Could not write config".red());
118    }
119    Ok(())
120}
121
122pub fn check_out<P: AsRef<Path>>(
123    git_modules: P,
124    remote: &str,
125    callbacks: RemoteCallbacks,
126) -> Result<(), Error> {
127    let mut fo = git2::FetchOptions::new();
128    fo.remote_callbacks(callbacks);
129
130    let mut builder = git2::build::RepoBuilder::new();
131    builder.fetch_options(fo);
132
133    builder.clone(remote, git_modules.as_ref())?;
134
135    Ok(())
136}
137
138pub fn update_modules(global_config: &mut GlobalConfig) -> Result<(), Error> {
139    let mut fo = git2::FetchOptions::new();
140    let mut ssh_key: Option<PathBuf> = None;
141    if let Some(key) = global_config.ssh_key.clone() {
142        ssh_key = Some(Path::new(&key).into());
143    }
144    let git_repo = global_config.git_repo.clone();
145    let branch = global_config.git_main_branch.clone();
146    let callbacks = get_callbacks(global_config, ssh_key, global_config.key_needs_pw);
147    fo.remote_callbacks(callbacks);
148    match Repository::open(shellexpand::tilde(&git_repo).to_string()) {
149        Ok(repo) => {
150            fetch_origin_master(&repo, fo, &branch)?;
151            fast_forward(&repo, &branch)?;
152            println!("{}", "Updated repo to newest revision".green());
153        }
154        Err(e) => {
155            eprintln!("Could not update repo, manual update needed: {:?}", e);
156        }
157    }
158    Ok(())
159}
160
161pub fn fetch_origin_master(
162    repo: &git2::Repository,
163    mut opts: git2::FetchOptions,
164    branch: &str,
165) -> Result<(), git2::Error> {
166    repo.find_remote("origin")?
167        .fetch(&[branch], Some(&mut opts), None)
168}
169
170pub fn fast_forward(repo: &Repository, branch: &str) -> Result<(), Error> {
171    let fetch_head = repo.find_reference("FETCH_HEAD")?;
172    let fetch_commit = repo.reference_to_annotated_commit(&fetch_head)?;
173    let analysis = repo.merge_analysis(&[&fetch_commit])?;
174    if analysis.0.is_up_to_date() {
175        Ok(())
176    } else if analysis.0.is_fast_forward() {
177        let refname = format!("refs/heads/{}", branch);
178        let mut reference = repo.find_reference(&refname)?;
179        reference.set_target(fetch_commit.id(), "Fast-Forward")?;
180        repo.set_head(&refname)?;
181        repo.checkout_head(Some(git2::build::CheckoutBuilder::default().force()))
182    } else {
183        Err(Error::from_str("Fast-forward only!"))
184    }
185}
186pub fn find_last_commit(repo: &Repository) -> Result<Commit, git2::Error> {
187    let obj = repo.head()?.resolve()?.peel(ObjectType::Commit)?;
188    match obj.into_commit() {
189        Ok(c) => Ok(c),
190        Err(_) => Err(Error::from_str("commit error")),
191    }
192}