mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-11-26 12:24:26 +03:00
feat: add support for default and git credentials helper as preferred SSH key options in the UI
This commit is contained in:
parent
9b00b5fe7d
commit
4ddc970747
@ -148,7 +148,6 @@ fn main() {
|
||||
commands::git_set_global_config,
|
||||
commands::git_get_global_config,
|
||||
commands::project_flush_and_push,
|
||||
commands::test_git_connection,
|
||||
zip::commands::get_logs_archive_path,
|
||||
zip::commands::get_project_archive_path,
|
||||
zip::commands::get_project_data_archive_path,
|
||||
|
@ -155,59 +155,3 @@ pub async fn project_flush_and_push(handle: tauri::AppHandle, id: &str) -> Resul
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
#[instrument(skip(handle))]
|
||||
pub async fn test_git_connection(
|
||||
handle: tauri::AppHandle,
|
||||
project_id: &str,
|
||||
) -> Result<serde_json::Value, Error> {
|
||||
let project_id = project_id.parse().map_err(|_| Error::UserError {
|
||||
code: Code::Validation,
|
||||
message: "Malformed project id".into(),
|
||||
})?;
|
||||
|
||||
let users = handle.state::<users::Controller>().inner().clone();
|
||||
let projects = handle.state::<projects::Controller>().inner().clone();
|
||||
let local_data_dir = DataDir::try_from(&handle)?;
|
||||
|
||||
let project = projects.get(&project_id).context("failed to get project")?;
|
||||
let user = users.get_user()?;
|
||||
let project_repository = project_repository::Repository::open(&project)?;
|
||||
let gb_repo =
|
||||
gb_repository::Repository::open(&local_data_dir, &project_repository, user.as_ref())
|
||||
.context("failed to open repository")?;
|
||||
|
||||
let default_target = gb_repo
|
||||
.default_target()
|
||||
.context("failed to get default target")?
|
||||
.ok_or_else(|| Error::UserError {
|
||||
code: Code::Branches,
|
||||
message: "No default target set".into(),
|
||||
})?;
|
||||
|
||||
let remote_name = default_target.branch.remote();
|
||||
|
||||
let helper = git::credentials::Helper::from(&handle);
|
||||
|
||||
let mut results = HashMap::new();
|
||||
let refspec = &format!("+refs/heads/*:refs/remotes/{}/*", remote_name);
|
||||
for (mut remote, credentials) in helper.enumerate(&project_repository, remote_name)? {
|
||||
let remote_url = remote.url().context("failed to get remote url")?.unwrap();
|
||||
let results = results
|
||||
.entry(remote_url.to_string())
|
||||
.or_insert_with(Vec::new);
|
||||
for callback in credentials {
|
||||
let mut fetch_opts = git2::FetchOptions::new();
|
||||
fetch_opts.remote_callbacks(callback.clone().into());
|
||||
fetch_opts.prune(git2::FetchPrune::On);
|
||||
|
||||
results.push(serde_json::json!({
|
||||
"callback": format!("{:?}", callback),
|
||||
"error": remote.fetch(&[refspec], Some(&mut fetch_opts)).err().map(|e| e.to_string()),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(serde_json::json!(results))
|
||||
}
|
||||
|
@ -15,7 +15,6 @@ pub enum SshCredential {
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum HttpsCredential {
|
||||
UsernamePassword { username: String, password: String },
|
||||
CredentialHelper { username: String, password: String },
|
||||
GitHubToken(String),
|
||||
}
|
||||
@ -53,12 +52,6 @@ impl From<Credential> for git2::RemoteCallbacks<'_> {
|
||||
git2::Cred::ssh_key_from_memory("git", None, &key.to_string(), None)
|
||||
});
|
||||
}
|
||||
Credential::Https(HttpsCredential::UsernamePassword { username, password }) => {
|
||||
remote_callbacks.credentials(move |url, _username_from_url, _allowed_types| {
|
||||
tracing::info!("authenticating with {url} as '{username}' with password");
|
||||
git2::Cred::userpass_plaintext(&username, &password)
|
||||
});
|
||||
}
|
||||
Credential::Https(HttpsCredential::CredentialHelper { username, password }) => {
|
||||
remote_callbacks.credentials(move |url, _username_from_url, _allowed_types| {
|
||||
tracing::info!("authenticating with {url} as '{username}' with password using credential helper");
|
||||
@ -147,87 +140,6 @@ impl Helper {
|
||||
}
|
||||
}
|
||||
|
||||
/// returns all possible credentials for a remote, without trying to be smart.
|
||||
pub fn enumerate<'a>(
|
||||
&'a self,
|
||||
project_repository: &'a project_repository::Repository,
|
||||
remote: &str,
|
||||
) -> Result<Vec<(super::Remote, Vec<Credential>)>, HelpError> {
|
||||
let remote = project_repository.git_repository.find_remote(remote)?;
|
||||
let remote_url = remote.url()?.ok_or(HelpError::NoUrlSet)?;
|
||||
|
||||
let mut flow = vec![];
|
||||
|
||||
if let projects::AuthKey::Local {
|
||||
private_key_path,
|
||||
passphrase,
|
||||
} = &project_repository.project().preferred_key
|
||||
{
|
||||
let ssh_remote = if remote_url.scheme == super::Scheme::Ssh {
|
||||
project_repository
|
||||
.git_repository
|
||||
.remote_anonymous(&remote_url)
|
||||
} else {
|
||||
let ssh_url = remote_url.as_ssh()?;
|
||||
project_repository.git_repository.remote_anonymous(&ssh_url)
|
||||
}?;
|
||||
flow.push((
|
||||
ssh_remote,
|
||||
vec![Credential::Ssh(SshCredential::Keyfile {
|
||||
key_path: private_key_path.clone(),
|
||||
passphrase: passphrase.clone(),
|
||||
})],
|
||||
));
|
||||
}
|
||||
|
||||
// is github is authenticated, only try github.
|
||||
if remote_url.is_github() {
|
||||
if let Some(github_access_token) = self
|
||||
.users
|
||||
.get_user()?
|
||||
.and_then(|user| user.github_access_token)
|
||||
{
|
||||
let https_remote = if remote_url.scheme == super::Scheme::Https {
|
||||
project_repository
|
||||
.git_repository
|
||||
.remote_anonymous(&remote_url)
|
||||
} else {
|
||||
let ssh_url = remote_url.as_ssh()?;
|
||||
project_repository.git_repository.remote_anonymous(&ssh_url)
|
||||
}?;
|
||||
flow.push((
|
||||
https_remote,
|
||||
vec![Credential::Https(HttpsCredential::GitHubToken(
|
||||
github_access_token,
|
||||
))],
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(https_url) = remote_url.as_https() {
|
||||
flow.push((
|
||||
project_repository
|
||||
.git_repository
|
||||
.remote_anonymous(&https_url)?,
|
||||
Self::https_flow(project_repository, &https_url)?
|
||||
.into_iter()
|
||||
.map(Credential::Https)
|
||||
.collect(),
|
||||
));
|
||||
}
|
||||
|
||||
if let Ok(ssh_url) = remote_url.as_ssh() {
|
||||
flow.push((
|
||||
project_repository
|
||||
.git_repository
|
||||
.remote_anonymous(&ssh_url)?,
|
||||
self.ssh_flow()?.into_iter().map(Credential::Ssh).collect(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(flow)
|
||||
}
|
||||
|
||||
pub fn help<'a>(
|
||||
&'a self,
|
||||
project_repository: &'a project_repository::Repository,
|
||||
@ -241,146 +153,174 @@ impl Helper {
|
||||
return Ok(vec![(remote, vec![Credential::Noop])]);
|
||||
}
|
||||
|
||||
// if prefernce set, only try that.
|
||||
if let projects::AuthKey::Local {
|
||||
private_key_path,
|
||||
passphrase,
|
||||
} = &project_repository.project().preferred_key
|
||||
{
|
||||
let ssh_remote = if remote_url.scheme == super::Scheme::Ssh {
|
||||
Ok(remote)
|
||||
} else {
|
||||
let ssh_url = remote_url.as_ssh()?;
|
||||
project_repository.git_repository.remote_anonymous(&ssh_url)
|
||||
}?;
|
||||
match &project_repository.project().preferred_key {
|
||||
projects::AuthKey::Local {
|
||||
private_key_path,
|
||||
passphrase,
|
||||
} => {
|
||||
let ssh_remote = if remote_url.scheme == super::Scheme::Ssh {
|
||||
Ok(remote)
|
||||
} else {
|
||||
let ssh_url = remote_url.as_ssh()?;
|
||||
project_repository.git_repository.remote_anonymous(&ssh_url)
|
||||
}?;
|
||||
|
||||
return Ok(vec![(
|
||||
ssh_remote,
|
||||
vec![Credential::Ssh(SshCredential::Keyfile {
|
||||
key_path: private_key_path.clone(),
|
||||
passphrase: passphrase.clone(),
|
||||
})],
|
||||
)]);
|
||||
}
|
||||
Ok(vec![(
|
||||
ssh_remote,
|
||||
vec![Credential::Ssh(SshCredential::Keyfile {
|
||||
key_path: private_key_path.clone(),
|
||||
passphrase: passphrase.clone(),
|
||||
})],
|
||||
)])
|
||||
}
|
||||
projects::AuthKey::Generated => {
|
||||
let ssh_remote = if remote_url.scheme == super::Scheme::Ssh {
|
||||
Ok(remote)
|
||||
} else {
|
||||
let ssh_url = remote_url.as_ssh()?;
|
||||
project_repository.git_repository.remote_anonymous(&ssh_url)
|
||||
}?;
|
||||
|
||||
// is github is authenticated, only try github.
|
||||
if remote_url.is_github() {
|
||||
if let Some(github_access_token) = self
|
||||
.users
|
||||
.get_user()?
|
||||
.and_then(|user| user.github_access_token)
|
||||
{
|
||||
let key = self.keys.get_or_create()?;
|
||||
Ok(vec![(
|
||||
ssh_remote,
|
||||
vec![Credential::Ssh(SshCredential::GitButlerKey(Box::new(key)))],
|
||||
)])
|
||||
}
|
||||
projects::AuthKey::GitCredentialsHelper => {
|
||||
let https_remote = if remote_url.scheme == super::Scheme::Https {
|
||||
Ok(remote)
|
||||
} else {
|
||||
let url = remote_url.as_https()?;
|
||||
project_repository.git_repository.remote_anonymous(&url)
|
||||
}?;
|
||||
return Ok(vec![(
|
||||
https_remote,
|
||||
vec![Credential::Https(HttpsCredential::GitHubToken(
|
||||
github_access_token,
|
||||
))],
|
||||
)]);
|
||||
}
|
||||
}
|
||||
|
||||
match remote_url.scheme {
|
||||
super::Scheme::Https => {
|
||||
let mut flow = vec![];
|
||||
|
||||
let https_flow = Self::https_flow(project_repository, &remote_url)?
|
||||
let flow = Self::https_flow(project_repository, &remote_url)?
|
||||
.into_iter()
|
||||
.map(Credential::Https)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if !https_flow.is_empty() {
|
||||
flow.push((remote, https_flow));
|
||||
}
|
||||
|
||||
if let Ok(ssh_url) = remote_url.as_ssh() {
|
||||
let ssh_flow = self
|
||||
.ssh_flow()?
|
||||
.into_iter()
|
||||
.map(Credential::Ssh)
|
||||
.collect::<Vec<_>>();
|
||||
if !ssh_flow.is_empty() {
|
||||
flow.push((
|
||||
project_repository
|
||||
.git_repository
|
||||
.remote_anonymous(&ssh_url)?,
|
||||
ssh_flow,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(flow)
|
||||
Ok(vec![(https_remote, flow)])
|
||||
}
|
||||
super::Scheme::Ssh => {
|
||||
let mut flow = vec![];
|
||||
|
||||
let ssh_flow = self
|
||||
.ssh_flow()?
|
||||
.into_iter()
|
||||
.map(Credential::Ssh)
|
||||
.collect::<Vec<_>>();
|
||||
if !ssh_flow.is_empty() {
|
||||
flow.push((remote, ssh_flow));
|
||||
}
|
||||
|
||||
if let Ok(https_url) = remote_url.as_https() {
|
||||
let https_flow = Self::https_flow(project_repository, &https_url)?
|
||||
.into_iter()
|
||||
.map(Credential::Https)
|
||||
.collect::<Vec<_>>();
|
||||
if !https_flow.is_empty() {
|
||||
flow.push((
|
||||
project_repository
|
||||
.git_repository
|
||||
.remote_anonymous(&https_url)?,
|
||||
https_flow,
|
||||
));
|
||||
projects::AuthKey::Default => {
|
||||
// is github is authenticated, only try github.
|
||||
if remote_url.is_github() {
|
||||
if let Some(github_access_token) = self
|
||||
.users
|
||||
.get_user()?
|
||||
.and_then(|user| user.github_access_token)
|
||||
{
|
||||
let https_remote = if remote_url.scheme == super::Scheme::Https {
|
||||
Ok(remote)
|
||||
} else {
|
||||
let url = remote_url.as_https()?;
|
||||
project_repository.git_repository.remote_anonymous(&url)
|
||||
}?;
|
||||
return Ok(vec![(
|
||||
https_remote,
|
||||
vec![Credential::Https(HttpsCredential::GitHubToken(
|
||||
github_access_token,
|
||||
))],
|
||||
)]);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(flow)
|
||||
}
|
||||
_ => {
|
||||
let mut flow = vec![];
|
||||
match remote_url.scheme {
|
||||
super::Scheme::Https => {
|
||||
let mut flow = vec![];
|
||||
|
||||
if let Ok(https_url) = remote_url.as_https() {
|
||||
let https_flow = Self::https_flow(project_repository, &https_url)?
|
||||
.into_iter()
|
||||
.map(Credential::Https)
|
||||
.collect::<Vec<_>>();
|
||||
let https_flow = Self::https_flow(project_repository, &remote_url)?
|
||||
.into_iter()
|
||||
.map(Credential::Https)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if !https_flow.is_empty() {
|
||||
flow.push((
|
||||
project_repository
|
||||
.git_repository
|
||||
.remote_anonymous(&https_url)?,
|
||||
https_flow,
|
||||
));
|
||||
if !https_flow.is_empty() {
|
||||
flow.push((remote, https_flow));
|
||||
}
|
||||
|
||||
if let Ok(ssh_url) = remote_url.as_ssh() {
|
||||
let ssh_flow = self
|
||||
.ssh_flow()?
|
||||
.into_iter()
|
||||
.map(Credential::Ssh)
|
||||
.collect::<Vec<_>>();
|
||||
if !ssh_flow.is_empty() {
|
||||
flow.push((
|
||||
project_repository
|
||||
.git_repository
|
||||
.remote_anonymous(&ssh_url)?,
|
||||
ssh_flow,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(flow)
|
||||
}
|
||||
super::Scheme::Ssh => {
|
||||
let mut flow = vec![];
|
||||
|
||||
let ssh_flow = self
|
||||
.ssh_flow()?
|
||||
.into_iter()
|
||||
.map(Credential::Ssh)
|
||||
.collect::<Vec<_>>();
|
||||
if !ssh_flow.is_empty() {
|
||||
flow.push((remote, ssh_flow));
|
||||
}
|
||||
|
||||
if let Ok(https_url) = remote_url.as_https() {
|
||||
let https_flow = Self::https_flow(project_repository, &https_url)?
|
||||
.into_iter()
|
||||
.map(Credential::Https)
|
||||
.collect::<Vec<_>>();
|
||||
if !https_flow.is_empty() {
|
||||
flow.push((
|
||||
project_repository
|
||||
.git_repository
|
||||
.remote_anonymous(&https_url)?,
|
||||
https_flow,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(flow)
|
||||
}
|
||||
_ => {
|
||||
let mut flow = vec![];
|
||||
|
||||
if let Ok(https_url) = remote_url.as_https() {
|
||||
let https_flow = Self::https_flow(project_repository, &https_url)?
|
||||
.into_iter()
|
||||
.map(Credential::Https)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if !https_flow.is_empty() {
|
||||
flow.push((
|
||||
project_repository
|
||||
.git_repository
|
||||
.remote_anonymous(&https_url)?,
|
||||
https_flow,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(ssh_url) = remote_url.as_ssh() {
|
||||
let ssh_flow = self
|
||||
.ssh_flow()?
|
||||
.into_iter()
|
||||
.map(Credential::Ssh)
|
||||
.collect::<Vec<_>>();
|
||||
if !ssh_flow.is_empty() {
|
||||
flow.push((
|
||||
project_repository
|
||||
.git_repository
|
||||
.remote_anonymous(&ssh_url)?,
|
||||
ssh_flow,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(flow)
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(ssh_url) = remote_url.as_ssh() {
|
||||
let ssh_flow = self
|
||||
.ssh_flow()?
|
||||
.into_iter()
|
||||
.map(Credential::Ssh)
|
||||
.collect::<Vec<_>>();
|
||||
if !ssh_flow.is_empty() {
|
||||
flow.push((
|
||||
project_repository
|
||||
.git_repository
|
||||
.remote_anonymous(&ssh_url)?,
|
||||
ssh_flow,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(flow)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,9 @@ use crate::{git, id::Id, types::default_true::DefaultTrue};
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum AuthKey {
|
||||
#[default]
|
||||
Default,
|
||||
Generated,
|
||||
GitCredentialsHelper,
|
||||
Local {
|
||||
private_key_path: path::PathBuf,
|
||||
passphrase: Option<String>,
|
||||
|
@ -12,7 +12,9 @@ import {
|
||||
} from 'rxjs';
|
||||
|
||||
export type Key =
|
||||
| 'default'
|
||||
| 'generated'
|
||||
| 'gitCredentialsHelper'
|
||||
| {
|
||||
local: { private_key_path: string; passphrase?: string };
|
||||
};
|
||||
|
@ -24,12 +24,28 @@
|
||||
sshKey = key;
|
||||
});
|
||||
|
||||
let selectedOption = project.preferred_key === 'generated' ? 'generated' : 'local';
|
||||
let selectedOption =
|
||||
project.preferred_key === 'generated'
|
||||
? 'generated'
|
||||
: project.preferred_key === 'default'
|
||||
? 'default'
|
||||
: project.preferred_key === 'gitCredentialsHelper'
|
||||
? 'gitCredentialsHelper'
|
||||
: 'local';
|
||||
|
||||
let privateKeyPath =
|
||||
project.preferred_key === 'generated' ? '' : project.preferred_key.local.private_key_path;
|
||||
project.preferred_key === 'generated' ||
|
||||
project.preferred_key === 'default' ||
|
||||
project.preferred_key === 'gitCredentialsHelper'
|
||||
? ''
|
||||
: project.preferred_key.local.private_key_path;
|
||||
let privateKeyPassphrase =
|
||||
project.preferred_key === 'generated' ? '' : project.preferred_key.local.passphrase;
|
||||
project.preferred_key === 'generated' ||
|
||||
project.preferred_key === 'default' ||
|
||||
project.preferred_key === 'gitCredentialsHelper'
|
||||
? ''
|
||||
: project.preferred_key.local.passphrase;
|
||||
|
||||
function setLocalKey() {
|
||||
dispatch('updated', {
|
||||
preferred_key: {
|
||||
@ -44,6 +60,18 @@
|
||||
});
|
||||
}
|
||||
|
||||
function setGitCredentialsHelperKey() {
|
||||
dispatch('updated', {
|
||||
preferred_key: 'gitCredentialsHelper'
|
||||
});
|
||||
}
|
||||
|
||||
function setDefaultKey() {
|
||||
dispatch('updated', {
|
||||
preferred_key: 'default'
|
||||
});
|
||||
}
|
||||
|
||||
function setGeneratedKey() {
|
||||
dispatch('updated', {
|
||||
preferred_key: 'generated'
|
||||
@ -52,7 +80,7 @@
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<p>Preferred SSH Key</p>
|
||||
<p>Git Authentication</p>
|
||||
<div class="pr-8 text-sm text-light-700 dark:text-dark-200">
|
||||
<div>
|
||||
Select the SSH key that GitButler will use to authenticate with your Git provider. These keys
|
||||
@ -61,8 +89,44 @@
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-2" style="grid-template-columns: max-content 1fr;">
|
||||
<input type="radio" bind:group={selectedOption} value="generated" on:input={setGeneratedKey} />
|
||||
<input type="radio" bind:group={selectedOption} value="default" on:input={setDefaultKey} />
|
||||
<div class="flex flex-col space-y-2">
|
||||
<div>Default</div>
|
||||
|
||||
{#if selectedOption === 'default'}
|
||||
<div class="pr-8">
|
||||
<div>
|
||||
We will try all of: your local SHH keys, git credentials helper and local GitButler key.
|
||||
</div>
|
||||
<div>
|
||||
It is recommended to select a specific authenticatation flow for a better expirience.
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="radio"
|
||||
bind:group={selectedOption}
|
||||
value="gitCredentialsHelper"
|
||||
on:input={setGitCredentialsHelperKey}
|
||||
/>
|
||||
<div class="flex flex-col space-y-2">
|
||||
<div class="pr-8">
|
||||
<div>
|
||||
Use <a href="https://git-scm.com/doc/credential-helpers">git credentials helper</a>
|
||||
</div>
|
||||
</div>
|
||||
{#if selectedOption === 'gitCredentialsHelper'}
|
||||
<div>
|
||||
<div>
|
||||
We will use the system git credentials helper to authenticate with your Git provider.
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<input type="radio" bind:group={selectedOption} value="generated" on:input={setGeneratedKey} />
|
||||
<div class="flex flex-col space-y-2">
|
||||
<div class="pr-8">
|
||||
<div>Use locally generated SSH key</div>
|
||||
@ -103,7 +167,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<input type="radio" bind:group={selectedOption} value="local" on:input={() => setLocalKey} />
|
||||
<input type="radio" bind:group={selectedOption} value="local" on:input={setLocalKey} />
|
||||
<div class="flex flex-col space-y-2">
|
||||
<div>Use existing SSH key</div>
|
||||
|
||||
@ -121,7 +185,7 @@
|
||||
<label for="path">Path to private key</label>
|
||||
|
||||
<TextBox
|
||||
placeholder="/absolute/path/id_rsa"
|
||||
placeholder="~/.ssh/id_rsa"
|
||||
bind:value={privateKeyPath}
|
||||
on:change={setLocalKey}
|
||||
/>
|
||||
|
Loading…
Reference in New Issue
Block a user