Файли
openidea/src-tauri/src/commands/ssh.rs
T
arookieofcтаa-rookie-of-C-language e85ffa2ae8 feat(git): remember HTTPS credentials in the app secret store (#321)
Git no longer asks for the HTTPS user name and password on every fetch, pull and push. The password prompt offers "Remember for this host"; the credential is saved in the app's secret store (next to the saved SSH passwords, never in git's global configuration) only after the git command that used it succeeds, and a stored credential the server rejects is forgotten so the next command asks again. Settings › Version Control › Git lists saved credentials and forgets them.

Each git child gets its own id, passed to the askpass helper, so concurrent commands never save or forget each other's credentials. ide-vcs reports command completion through an observer installed at start-up instead of depending on ide-ssh. Credentials are keyed by scheme, host and port, and only git's authentication errors (Authentication failed, HTTP 401/403 responses, invalid username or password) forget a stored credential.

Fixes #272

Co-authored-by: a-rookie-of-C-language <[email protected]>
2026-10-30 10:48:29 +08:00

113 рядки
3.9 KiB
Rust
Неформатований Анотація Історія

Цей файл містить неоднозначні символи Юнікоду
Цей файл містить символи Юнікоду, які можна сплутати з іншими символами. Якщо ви вважаєте, що це зроблено навмисно, можете сміливо ігнорувати це попередження. Щоб показати їх, скористайтеся кнопкою Escape.
use crate::state::AppState;
use ide_core::remote_settings::SshConfiguration;
use ide_core::{IdeError, IdeEvent, IdeResult};
use ide_ssh::SshTarget;
use serde::{Deserialize, Serialize};
use specta::Type;
use tauri::State;
/// Settings › SSH Configurations: the configurations in effect.
pub(crate) fn apply_ssh_settings(state: &AppState) {
state.ssh.set_configs(state.global_settings.effective().ssh.configs);
}
/// Test Connection: connects with `config` as edited (not necessarily saved).
#[tauri::command]
#[specta::specta]
pub async fn ssh_test(
state: State<'_, AppState>,
config: SshConfiguration,
secret: Option<String>,
) -> IdeResult<String> {
let ssh = state.ssh.clone();
ssh.test(&config, secret).await
}
/// Remembers the password / passphrase of configuration `id` (`None` forgets it).
#[tauri::command]
#[specta::specta]
pub fn ssh_set_secret(state: State<'_, AppState>, id: String, secret: Option<String>, save: bool) {
state.ssh.set_secret(&id, secret, save);
}
/// Whether connecting with configuration `id` needs a password first.
#[tauri::command]
#[specta::specta]
pub fn ssh_needs_secret(state: State<'_, AppState>, id: String) -> IdeResult<bool> {
Ok(state.ssh.needs_secret(&state.ssh.config(&id)?))
}
/// Trusts the host key that failed the last connection of configuration `id`.
#[tauri::command]
#[specta::specta]
pub async fn ssh_trust_host(state: State<'_, AppState>, id: String) -> IdeResult<()> {
let ssh = state.ssh.clone();
ssh.trust_host(&id).await
}
/// Ids of the configurations with an open connection.
#[tauri::command]
#[specta::specta]
pub fn ssh_connected(state: State<'_, AppState>) -> Vec<String> {
state.ssh.connected()
}
#[tauri::command]
#[specta::specta]
pub fn ssh_disconnect(state: State<'_, AppState>, id: String) {
state.ssh.disconnect(&id);
}
/// Answers an `IdeEvent::Askpass` prompt (`None`: cancelled); every window drops it.
#[tauri::command]
#[specta::specta]
pub fn ssh_askpass_answer(state: State<'_, AppState>, id: u64, answer: Option<String>, remember: bool) {
if let Some(server) = &state.askpass {
server.answer(id, answer, remember);
}
state.sink.emit(IdeEvent::AskpassDone { id });
}
/// Settings › Git › Saved credentials: `scheme://host` of every saved entry.
#[tauri::command]
#[specta::specta]
pub fn git_credentials(_state: State<'_, AppState>) -> Vec<String> {
ide_ssh::git_creds::get().map(|s| s.list()).unwrap_or_default()
}
/// Forgets the credential of `key` (a `git_credentials` entry).
#[tauri::command]
#[specta::specta]
pub fn git_credential_forget(_state: State<'_, AppState>, key: String) {
if let Some(store) = ide_ssh::git_creds::get() {
store.forget(&key);
}
}
#[derive(Serialize, Deserialize, Type)]
#[serde(rename_all = "camelCase")]
pub struct SshTerminalCommand {
pub shell: String,
pub args: Vec<String>,
pub title: String,
}
/// Tools › Start SSH Session: the system `ssh` command line for configuration `id`
/// (run in a terminal tab, so prompts and host-key questions are answered there).
#[tauri::command]
#[specta::specta]
pub fn ssh_terminal_command(state: State<'_, AppState>, id: String) -> IdeResult<SshTerminalCommand> {
let config = state.ssh.config(&id)?;
if config.host.trim().is_empty() {
return Err(IdeError::invalid(format!("SSH configuration {} has no host", config.name)));
}
let path = std::env::var_os("PATH").unwrap_or_default();
let shell = ide_run::which::find_executable("ssh", &path)
.map(|p| p.to_string_lossy().into_owned())
.ok_or_else(|| IdeError::unavailable("ssh is not installed (OpenSSH client not found on PATH)"))?;
let mut args = vec!["-t".to_owned()];
args.extend(ide_ssh::openssh_args(&SshTarget::from(&config)));
let title = if config.name.trim().is_empty() { config.label() } else { config.name.clone() };
Ok(SshTerminalCommand { shell, args, title })
}