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]>
Цей коміт міститься в:
arookieofcтаa-rookie-of-C-language authored and GitHub committed 2026-10-30 10:48:29 +08:00
1 джерело 2c10b63f76
коміт e85ffa2ae8
17 files changed
+666 -34

No files matched your search

+2
Переглянути файл
@@ -424,6 +424,8 @@ pub fn builder() -> tauri_specta::Builder<tauri::Wry> {
ssh::ssh_connected,
ssh::ssh_disconnect,
ssh::ssh_askpass_answer,
ssh::git_credentials,
ssh::git_credential_forget,
ssh::ssh_terminal_command,
deploy::deploy_upload,
deploy::deploy_download,
+18 -2
Переглянути файл
@@ -61,13 +61,29 @@ pub fn ssh_disconnect(state: State<'_, AppState>, id: String) {
/// 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>) {
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);
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 {
+17 -1
Переглянути файл
@@ -66,6 +66,8 @@ impl AppState {
let data_dir = app.path().app_data_dir()?;
std::fs::create_dir_all(&config_dir)?;
std::fs::create_dir_all(&data_dir)?;
// Saved git HTTPS credentials (askpass answers them silently).
ide_ssh::git_creds::init(data_dir.join("git-credentials.json"));
// File › Invalidate Caches… requested at the last run.
let removed = ide_workspace::caches::apply_pending(&data_dir);
if !removed.is_empty() {
@@ -158,10 +160,24 @@ fn start_askpass(app: &AppHandle) -> Option<ide_ssh::askpass::AskpassServer> {
let on_prompt = Arc::new(move |p: ide_ssh::askpass::AskpassPrompt| {
// A focused Settings window hands the prompt to its project window.
let window = crate::windows::target_window(&handle).map(|w| AppState::owner_window(w.label()).to_owned());
TauriSink { app: handle.clone(), window }.emit(IdeEvent::Askpass { id: p.id, prompt: p.prompt, kind: p.kind });
let remember = ide_ssh::askpass::remembers(&p.prompt);
TauriSink { app: handle.clone(), window }.emit(IdeEvent::Askpass {
id: p.id,
prompt: p.prompt,
kind: p.kind,
remember,
});
});
match ide_ssh::askpass::AskpassServer::start(exe, on_prompt) {
Ok(server) => {
ide_vcs::set_git_command_observer(
ide_ssh::askpass::COMMAND_ENV,
Arc::new(|id, success, stderr| {
if let Some(store) = ide_ssh::git_creds::get() {
store.command_finished(id, success, stderr);
}
}),
);
ide_vcs::set_askpass_env(server.env());
Some(server)
}