flowey_lib_hvlite/
install_git_credential_manager.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Globally install the git credential manager
5
6use flowey::node::prelude::*;
7use flowey_lib_common::_util::wslpath;
8
9flowey_request! {
10    pub enum Request {
11        /// Automatically configure the user's global config manager
12        AutoConfigure,
13        /// WSL2 will use linux git credential manager if true, windows version if false.
14        UseNativeLinuxOnWsl2,
15        /// Ensure that git was configured
16        EnsureConfigured(WriteVar<SideEffect>),
17    }
18}
19
20new_flow_node!(struct Node);
21
22impl FlowNode for Node {
23    type Request = Request;
24
25    fn imports(dep: &mut ImportCtx<'_>) {
26        dep.import::<flowey_lib_common::install_git::Node>();
27        dep.import::<flowey_lib_common::check_needs_relaunch::Node>();
28    }
29
30    fn emit(requests: Vec<Self::Request>, ctx: &mut NodeCtx<'_>) -> anyhow::Result<()> {
31        if !matches!(ctx.backend(), FlowBackend::Local) {
32            anyhow::bail!("only supported on the local backend at this time");
33        }
34
35        let mut use_native_linux_on_wsl2 = None;
36        let mut auto_configure = None;
37        let mut ensure_configured = Vec::new();
38
39        for req in requests {
40            match req {
41                Request::AutoConfigure => {
42                    same_across_all_reqs("AutoConfigure", &mut auto_configure, true)?
43                }
44                Request::UseNativeLinuxOnWsl2 => same_across_all_reqs(
45                    "UseNativeLinuxOnWsl2",
46                    &mut use_native_linux_on_wsl2,
47                    true,
48                )?,
49                Request::EnsureConfigured(v) => ensure_configured.push(v),
50            }
51        }
52
53        let use_native_linux_on_wsl2 = use_native_linux_on_wsl2.unwrap_or(false);
54        let auto_configure = auto_configure.unwrap_or(false);
55
56        // -- end of req processing -- //
57
58        let git_ensure_installed =
59            ctx.reqv(flowey_lib_common::install_git::Request::EnsureInstalled);
60
61        let (read_env, write_env) = ctx.new_var();
62        ctx.req(flowey_lib_common::check_needs_relaunch::Params {
63            check: read_env,
64            done: ensure_configured,
65        });
66
67        ctx.emit_rust_step("configure git credential manager", move |ctx| {
68            git_ensure_installed.clone().claim(ctx);
69            let write_env = write_env.claim(ctx);
70
71            move |rt: &mut RustRuntimeServices<'_>| {
72                let mut env_to_write = None;
73                let sh = xshell::Shell::new()?;
74
75                let existing_credman = xshell::cmd!(sh, "git config --global credential.helper").ignore_status().read()?;
76                log::info!("existing credentials helper: {existing_credman}");
77
78                if !existing_credman.is_empty() {
79                    if existing_credman.contains("git-credential-manager")
80                        || existing_credman.contains("credential-manager-core")
81                        || existing_credman.contains("manager")
82                    {
83                        log::info!("existing credentials helper matches a known-good credential helper.");
84                        rt.write(write_env, &None);
85                        return Ok(())
86                    } else {
87                        log::warn!("existing credentials helper isn't any of the known-good credential helpers.");
88                        log::warn!("assume the user knows what they're doing?");
89                        rt.write(write_env, &None);
90                        return Ok(())
91                    }
92                }
93
94                // give the user an option to maunally install the cred manager
95                if existing_credman.is_empty() && !auto_configure {
96                    log::info!("Could not detect an existing Git Credential helper.");
97                    log::info!("Press <y> to automatically configure an appropriate --global configuration helper, or any other key to abort the run.");
98                    let mut input = String::new();
99                    std::io::stdin().read_line(&mut input)?;
100                    if input.trim() != "y" {
101                        anyhow::bail!("aborting...")
102                    }
103                }
104
105                if flowey_lib_common::_util::running_in_wsl(rt) && !use_native_linux_on_wsl2 {
106                    let windows_user_profile_path_windows = xshell::cmd!(sh, "cmd.exe /c echo %UserProfile%").read().map_err(|_| anyhow::anyhow!("Unable to run cmd.exe, please restart WSL by running `wsl --shutdown` in powershell and try again."))?;
107                    let windows_user_profile_path = wslpath::win_to_linux(windows_user_profile_path_windows);
108                    let gcm_path_opt_1 = windows_user_profile_path.join("AppData/Local/Programs/Git Credential Manager/git-credential-manager.exe");
109                    let gcm_path_opt_2 = wslpath::win_to_linux(r#"C:\Program Files\Git\mingw64\bin\git-credential-manager.exe"#);
110                    let gcm_path_opt_3 = wslpath::win_to_linux(r#"C:\Program Files\Git\mingw64\libexec\git-core\git-credential-manager.exe"#);
111                    let gcm_path_opt_4 = wslpath::win_to_linux(r#"C:\Program Files (x86)\Git Credential Manager\git-credential-manager.exe"#);
112
113                    let gcm_path = if sh.path_exists(&gcm_path_opt_1) {
114                        &gcm_path_opt_1
115                    } else if sh.path_exists(&gcm_path_opt_2) {
116                        &gcm_path_opt_2
117                    } else if sh.path_exists(&gcm_path_opt_3) {
118                        &gcm_path_opt_3
119                    } else if sh.path_exists(&gcm_path_opt_4) {
120                        &gcm_path_opt_4
121                    } else {
122                        anyhow::bail!("Git Credential Manager not found, please install it manually.");
123                    };
124
125                    if gcm_path == &gcm_path_opt_1 || gcm_path == &gcm_path_opt_4 {
126                        let mut wslenv = sh.var("WSLENV")?;
127                        if !wslenv.contains("GIT_EXEC_PATH/wp") {
128                            log::info!("Standalone Git Credential Manager has been detected.");
129                            log::info!("Please run the following from an administrator command prompt to configure it:");
130                            log::info!("SETX WSLENV %WSLENV%:GIT_EXEC_PATH/wp");
131                            log::info!("This command shares `GIT_EXEC_PATH`, an environment variable which determines where Git looks for its sub-programs,");
132                            log::info!(" with WSL processes spawned from Win32 and vice versa. `/wp` are flags specifying how `GIT_EXEC_PATH` gets translated.");
133                            log::info!("Please refer to <https://devblogs.microsoft.com/commandline/share-environment-vars-between-wsl-and-windows/>");
134                            log::info!("and <https://git-scm.com/book/en/v2/Git-Internals-Environment-Variables> for more details.");
135
136                            let do_config = if !auto_configure {
137                                wslenv.push_str(":GIT_EXEC_PATH/wp");
138
139                                log::info!("Please press <y> to automatically configure it, or any other key to continue after manual configuration.");
140                                let mut input = String::new();
141                                std::io::stdin().read_line(&mut input)?;
142                                input.trim() == "y"
143                            } else {
144                                true
145                            };
146
147                            if do_config {
148                                xshell::cmd!(sh, "setx.exe WSLENV {wslenv}").run()?;
149                            }
150
151                            env_to_write = Some(flowey_lib_common::check_needs_relaunch::BinOrEnv::Env("WSLENV".to_string(), "GIT_EXEC_PATH/wp".to_string()));
152                        }
153                    }
154
155                    // Have to do this weird string business due to requiring the escaped space character in Program\ Files
156                    let gcm_path_str = gcm_path.to_str().expect("Invalid git credential manager path").to_string().replace(' ', "\\ ");
157                    xshell::cmd!(sh, "git config --global credential.helper {gcm_path_str}").run()?;
158                    xshell::cmd!(sh, "git config --global credential.https://dev.azure.com.useHttpPath true").run()?;
159                } else if matches!(rt.platform(), FlowPlatform::Windows) {
160                    xshell::cmd!(sh, "git config --global credential.helper manager").run()?;
161                } else {
162                    anyhow::bail!("git credential manager configuration only supported for windows/wsl2 at this time")
163                }
164
165                rt.write(write_env, &env_to_write);
166                Ok(())
167            }
168        });
169
170        Ok(())
171    }
172}