mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2025-01-02 06:25:02 +03:00
Merge pull request #2418 from gitbutlerapp/add-fetch-method
initial setup for new fork/exec authentication system
This commit is contained in:
commit
1c043b3196
20
Cargo.lock
generated
20
Cargo.lock
generated
@ -1876,6 +1876,8 @@ name = "gitbutler-git"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"git2",
|
||||
"nix 0.27.1",
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
@ -2881,7 +2883,7 @@ dependencies = [
|
||||
"combine",
|
||||
"libc",
|
||||
"mach2",
|
||||
"nix",
|
||||
"nix 0.26.4",
|
||||
"sysctl",
|
||||
"thiserror",
|
||||
"widestring",
|
||||
@ -2953,6 +2955,18 @@ dependencies = [
|
||||
"pin-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.27.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053"
|
||||
dependencies = [
|
||||
"bitflags 2.4.0",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"memoffset 0.9.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "no-std-compat"
|
||||
version = "0.4.1"
|
||||
@ -6531,7 +6545,7 @@ version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2769203cd13a0c6015d515be729c526d041e9cf2c0cc478d57faee85f40c6dcd"
|
||||
dependencies = [
|
||||
"nix",
|
||||
"nix 0.26.4",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
@ -6565,7 +6579,7 @@ dependencies = [
|
||||
"futures-sink",
|
||||
"futures-util",
|
||||
"hex",
|
||||
"nix",
|
||||
"nix 0.26.4",
|
||||
"once_cell",
|
||||
"ordered-stream",
|
||||
"rand 0.8.5",
|
||||
|
@ -3,9 +3,22 @@ name = "gitbutler-git"
|
||||
version = "0.0.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
path = "src/lib.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "gitbutler-git-askpass"
|
||||
path = "src/backend/cli/bin/askpass.rs"
|
||||
required-features = ["cli"]
|
||||
|
||||
[[bin]]
|
||||
name = "gitbutler-git-setsid"
|
||||
path = "src/backend/cli/bin/setsid.rs"
|
||||
required-features = ["cli"]
|
||||
|
||||
[features]
|
||||
default = ["git2", "cli", "serde", "tokio"]
|
||||
cli = ["std"]
|
||||
cli = ["std", "dep:nix", "dep:rand"]
|
||||
git2 = ["dep:git2", "std"]
|
||||
serde = ["dep:serde"]
|
||||
std = ["dep:thiserror"]
|
||||
@ -16,6 +29,10 @@ git2 = { workspace = true, optional = true }
|
||||
thiserror = { workspace = true, optional = true }
|
||||
serde = { workspace = true, optional = true }
|
||||
tokio = { workspace = true, optional = true, features = ["process"]}
|
||||
rand = { version = "0.8.5", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { workspace = true, features = ["rt", "rt-multi-thread", "process"]}
|
||||
|
||||
[target."cfg(unix)".dependencies]
|
||||
nix = { version = "0.27.1", optional = true, features = ["process", "socket"] }
|
||||
|
@ -22,7 +22,7 @@ mod tests {
|
||||
.join(test_name);
|
||||
let _ = std::fs::remove_dir_all(&repo_path);
|
||||
std::fs::create_dir_all(&repo_path).unwrap();
|
||||
Repository::open_or_init(executor::tokio::TokioExecutor, repo_path)
|
||||
Repository::open_or_init(executor::tokio::TokioExecutor, repo_path.to_str().unwrap())
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
36
gitbutler-git/src/backend/cli/bin/askpass-unix.rs
Normal file
36
gitbutler-git/src/backend/cli/bin/askpass-unix.rs
Normal file
@ -0,0 +1,36 @@
|
||||
use std::io::{Read, Write};
|
||||
use std::os::unix::net::UnixStream;
|
||||
|
||||
pub fn main(sock_path: &str, secret: &str, prompt: &str) {
|
||||
let mut stream = UnixStream::connect(sock_path).expect("connect():");
|
||||
|
||||
// Set a timer for 10s.
|
||||
stream
|
||||
.set_read_timeout(Some(std::time::Duration::from_secs(10)))
|
||||
.expect("set_read_timeout():");
|
||||
|
||||
// Write the secret.
|
||||
stream
|
||||
.write_all(secret.as_bytes())
|
||||
.expect("write_all(secret):");
|
||||
|
||||
// Write the prompt that Git gave us.
|
||||
stream
|
||||
.write_all(prompt.as_bytes())
|
||||
.expect("write_all(prompt):");
|
||||
|
||||
// Wait for the response.
|
||||
let mut buf = [0; 2048];
|
||||
let n = stream.read(&mut buf).expect("read():");
|
||||
|
||||
// TODO(qix-): Figure out a way to do a single timeout
|
||||
// TODO(qix-): but allow any response size.
|
||||
if n == buf.len() {
|
||||
panic!("response too long");
|
||||
}
|
||||
|
||||
// Write the response back to Git.
|
||||
std::io::stdout()
|
||||
.write_all(&buf[..n])
|
||||
.expect("write_all(stdout):");
|
||||
}
|
15
gitbutler-git/src/backend/cli/bin/askpass.rs
Normal file
15
gitbutler-git/src/backend/cli/bin/askpass.rs
Normal file
@ -0,0 +1,15 @@
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
#[path = "askpass-unix.rs"]
|
||||
mod unix;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
compile_error!("Windows support is not yet implemented.");
|
||||
|
||||
pub fn main() {
|
||||
let pipe_name = std::env::var("GITBUTLER_ASKPASS_PIPE").expect("do not run this binary yourself; it's only meant to be run by GitButler (missing GITBUTLER_ASKPASS_PIPE env var)");
|
||||
let pipe_secret = std::env::var("GITBUTLER_ASKPASS_SECRET").expect("do not run this binary yourself; it's only meant to be run by GitButler (missing GITBUTLER_ASKPASS_SECRET env var)");
|
||||
let prompt = std::env::args().nth(1).expect("do not run this binary yourself; it's only meant to be run by GitButler (missing prompt arg)");
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
unix::main(&pipe_name, &pipe_secret, &prompt);
|
||||
}
|
54
gitbutler-git/src/backend/cli/bin/setsid.rs
Normal file
54
gitbutler-git/src/backend/cli/bin/setsid.rs
Normal file
@ -0,0 +1,54 @@
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
use nix::{
|
||||
libc::{c_int, wait, EXIT_FAILURE, WEXITSTATUS, WIFEXITED, WIFSIGNALED, WTERMSIG},
|
||||
unistd::{fork, setsid, ForkResult},
|
||||
};
|
||||
use std::{os::unix::process::CommandExt, process};
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn main() {
|
||||
panic!("This binary is only meant to be run on Unix-like systems. It exists on Windows only because Cargo cannot switch off bins based on target platform.");
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub fn main() {
|
||||
let has_pipe_var = std::env::var("GITBUTLER_ASKPASS_PIPE")
|
||||
.map(|v| v != "")
|
||||
.unwrap_or(false);
|
||||
if !has_pipe_var {
|
||||
panic!("This binary is only meant to be run by GitButler; please do not use it yourself as it's entirely unstable.");
|
||||
}
|
||||
|
||||
let args = std::env::args().skip(1).collect::<Vec<_>>();
|
||||
|
||||
match unsafe { fork() }.unwrap() {
|
||||
ForkResult::Parent { child, .. } => {
|
||||
let mut status: c_int = 0;
|
||||
|
||||
let waited_pid = unsafe { wait(&mut status as *mut _) };
|
||||
if waited_pid != child.as_raw() {
|
||||
panic!(
|
||||
"wait(): unexpected child process; got {}, expected {}",
|
||||
waited_pid, child
|
||||
);
|
||||
}
|
||||
|
||||
if WIFEXITED(status) {
|
||||
let exit_status = WEXITSTATUS(status);
|
||||
process::exit(exit_status);
|
||||
} else if WIFSIGNALED(status) {
|
||||
let signal = WTERMSIG(status);
|
||||
process::exit(128 + signal);
|
||||
} else {
|
||||
process::exit(EXIT_FAILURE);
|
||||
}
|
||||
}
|
||||
ForkResult::Child => {
|
||||
setsid().expect("setsid():");
|
||||
|
||||
let err = process::Command::new(&args[0]).args(&args[1..]).exec();
|
||||
|
||||
panic!("exec(): {}", err);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,3 +1,5 @@
|
||||
use crate::prelude::*;
|
||||
|
||||
#[cfg(any(test, feature = "tokio"))]
|
||||
pub mod tokio;
|
||||
|
||||
@ -21,5 +23,32 @@ pub trait GitExecutor {
|
||||
///
|
||||
/// `Err` is returned if the command could not be executed,
|
||||
/// **not** if the command returned a non-zero exit code.
|
||||
async fn execute(&self, args: &[&str]) -> Result<(usize, String, String), Self::Error>;
|
||||
async fn execute_raw(
|
||||
&self,
|
||||
args: &[&str],
|
||||
envs: Option<BTreeMap<String, String>>,
|
||||
) -> Result<(usize, String, String), Self::Error>;
|
||||
|
||||
/// Executes the given Git command with sane defaults.
|
||||
/// `git` is never passed as the first argument (arg 0).
|
||||
///
|
||||
/// Implementers should use this method over [`Self::execute_raw`]
|
||||
/// when possible.
|
||||
async fn execute(
|
||||
&self,
|
||||
args: &[&str],
|
||||
envs: Option<BTreeMap<String, String>>,
|
||||
) -> Result<(usize, String, String), Self::Error> {
|
||||
let mut args = args.as_ref().to_vec();
|
||||
|
||||
args.insert(0, "--no-pager");
|
||||
// TODO(qix-): Test the performance impact of this.
|
||||
args.insert(0, "--no-optional-locks");
|
||||
|
||||
let mut envs = envs.unwrap_or_default();
|
||||
envs.insert("GIT_TERMINAL_PROMPT".into(), "0".into());
|
||||
envs.insert("LC_ALL".into(), "C".into()); // Force English. We need this for parsing output.
|
||||
|
||||
self.execute_raw(&args, Some(envs)).await
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
//! A [Tokio](https://tokio.rs)-based [`GitExecutor`] implementation.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use crate::prelude::*;
|
||||
use tokio::process::Command;
|
||||
|
||||
/// A [`GitExecutor`] implementation using the `git` command-line tool
|
||||
@ -10,9 +10,16 @@ pub struct TokioExecutor;
|
||||
impl super::GitExecutor for TokioExecutor {
|
||||
type Error = std::io::Error;
|
||||
|
||||
async fn execute(&self, args: &[&str]) -> Result<(usize, String, String), Self::Error> {
|
||||
async fn execute_raw(
|
||||
&self,
|
||||
args: &[&str],
|
||||
envs: Option<BTreeMap<String, String>>,
|
||||
) -> Result<(usize, String, String), Self::Error> {
|
||||
let mut cmd = Command::new("git");
|
||||
cmd.args(args);
|
||||
if let Some(envs) = envs {
|
||||
cmd.envs(envs);
|
||||
}
|
||||
|
||||
let output = cmd.output().await?;
|
||||
|
||||
@ -23,82 +30,3 @@ impl super::GitExecutor for TokioExecutor {
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// A [`GitExecutor`] implementation using the `git` command-line tool
|
||||
/// via [`tokio::process::Command`], with the given environment variables.
|
||||
pub struct TokioExecutorEnv {
|
||||
env: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl super::GitExecutor for TokioExecutorEnv {
|
||||
type Error = std::io::Error;
|
||||
|
||||
async fn execute(&self, args: &[&str]) -> Result<(usize, String, String), Self::Error> {
|
||||
let mut cmd = Command::new("git");
|
||||
cmd.args(args);
|
||||
cmd.envs(&self.env);
|
||||
|
||||
let output = cmd.output().await?;
|
||||
|
||||
Ok((
|
||||
output.status.code().unwrap_or(127) as usize,
|
||||
String::from_utf8_lossy(&output.stdout).trim().into(),
|
||||
String::from_utf8_lossy(&output.stderr).trim().into(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Allows executors to create (or modify) a [`TokioExecutorEnv`],
|
||||
/// with added/modified environment variables, set for each execution
|
||||
/// of `git`.
|
||||
pub trait WithEnv: Sized {
|
||||
/// Sets the given environment variable.
|
||||
fn with_env<K: AsRef<str>, V: AsRef<str>>(self, key: K, value: V) -> TokioExecutorEnv;
|
||||
|
||||
/// Creates a new [`TokioExecutorEnv`] with the given additional environment variables.
|
||||
fn with_envs<K: AsRef<str>, V: AsRef<str>, I: IntoIterator<Item = (K, V)>>(
|
||||
self,
|
||||
envs: I,
|
||||
) -> TokioExecutorEnv;
|
||||
}
|
||||
|
||||
impl WithEnv for TokioExecutor {
|
||||
fn with_env<K: AsRef<str>, V: AsRef<str>>(self, key: K, value: V) -> TokioExecutorEnv {
|
||||
TokioExecutorEnv {
|
||||
env: [(key.as_ref().into(), value.as_ref().into())]
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn with_envs<K: AsRef<str>, V: AsRef<str>, I: IntoIterator<Item = (K, V)>>(
|
||||
self,
|
||||
envs: I,
|
||||
) -> TokioExecutorEnv {
|
||||
TokioExecutorEnv {
|
||||
env: envs
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k.as_ref().into(), v.as_ref().into()))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WithEnv for TokioExecutorEnv {
|
||||
fn with_env<K: AsRef<str>, V: AsRef<str>>(mut self, key: K, value: V) -> TokioExecutorEnv {
|
||||
self.env.insert(key.as_ref().into(), value.as_ref().into());
|
||||
self
|
||||
}
|
||||
|
||||
fn with_envs<K: AsRef<str>, V: AsRef<str>, I: IntoIterator<Item = (K, V)>>(
|
||||
mut self,
|
||||
envs: I,
|
||||
) -> TokioExecutorEnv {
|
||||
self.env.extend(
|
||||
envs.into_iter()
|
||||
.map(|(k, v)| (k.as_ref().into(), v.as_ref().into())),
|
||||
);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,10 @@
|
||||
//! NOTE: Doesn't support `no_std` yet.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use super::executor::GitExecutor;
|
||||
use crate::ConfigScope;
|
||||
use crate::{prelude::*, ConfigScope};
|
||||
use rand::Rng;
|
||||
|
||||
/// The number of characters in the secret used for checking
|
||||
/// askpass invocations by ssh/git when connecting to our process.
|
||||
const ASKPASS_SECRET_LENGTH: usize = 24;
|
||||
|
||||
/// Higher level errors that can occur when interacting with the CLI.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
@ -14,6 +15,8 @@ pub enum Error<E: core::error::Error + core::fmt::Debug + Send + Sync + 'static>
|
||||
"git command exited with non-zero exit code {0}: {1:?}\n\nSTDOUT:\n{2}\n\nSTDERR:\n{3}"
|
||||
)]
|
||||
Failed(usize, Vec<String>, String, String),
|
||||
#[error("failed to determine path to this executable: {0}")]
|
||||
NoSelfExe(std::io::Error),
|
||||
}
|
||||
|
||||
/// A [`crate::Repository`] implementation using the `git` CLI
|
||||
@ -29,20 +32,20 @@ impl<E: GitExecutor> Repository<E> {
|
||||
/// Note that this **does not** check if the repository exists,
|
||||
/// but assumes it does.
|
||||
#[inline]
|
||||
pub fn open_unchecked<P: AsRef<Path>>(exec: E, path: P) -> Self {
|
||||
pub fn open_unchecked<P: AsRef<str>>(exec: E, path: P) -> Self {
|
||||
Self {
|
||||
exec,
|
||||
path: path.as_ref().to_str().unwrap().to_string(),
|
||||
path: path.as_ref().to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
/// (Re-)initializes a repository at the given path
|
||||
/// using the given [`GitExecutor`].
|
||||
pub async fn open_or_init<P: AsRef<Path>>(exec: E, path: P) -> Result<Self, Error<E::Error>> {
|
||||
let path = path.as_ref().to_str().unwrap().to_string();
|
||||
pub async fn open_or_init<P: AsRef<str>>(exec: E, path: P) -> Result<Self, Error<E::Error>> {
|
||||
let path = path.as_ref().to_owned();
|
||||
let args = vec!["init", "--quiet", &path];
|
||||
|
||||
let (exit_code, stdout, stderr) = exec.execute(&args).await.map_err(Error::Exec)?;
|
||||
let (exit_code, stdout, stderr) = exec.execute(&args, None).await.map_err(Error::Exec)?;
|
||||
|
||||
if exit_code == 0 {
|
||||
Ok(Self { exec, path })
|
||||
@ -55,6 +58,65 @@ impl<E: GitExecutor> Repository<E> {
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
async fn execute_with_auth_harness(
|
||||
&self,
|
||||
args: &[&str],
|
||||
envs: Option<BTreeMap<String, String>>,
|
||||
) -> Result<(usize, String, String), Error<E::Error>> {
|
||||
let path = std::env::current_exe().map_err(|e| Error::NoSelfExe(e))?;
|
||||
let our_pid = std::process::id();
|
||||
|
||||
let askpath_path = path.with_file_name("gitbutler-git-askpass");
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let setsid_path = path.with_file_name("gitbutler-git-setsid");
|
||||
|
||||
let sock_path = std::env::temp_dir().join(format!("gitbutler-git-{our_pid}.sock"));
|
||||
|
||||
// FIXME(qix-): This is probably not cryptographically secure, did this in a bit
|
||||
// FIXME(qix-): of a hurry. We should probably use a proper CSPRNG here, but this
|
||||
// FIXME(qix-): is probably fine for now (as this security mechanism is probably
|
||||
// FIXME(qix-): overkill to begin with).
|
||||
let secret = rand::thread_rng()
|
||||
.sample_iter(&rand::distributions::Alphanumeric)
|
||||
.take(ASKPASS_SECRET_LENGTH)
|
||||
.map(char::from)
|
||||
.collect::<String>();
|
||||
|
||||
let mut envs = envs.unwrap_or_default();
|
||||
envs.insert(
|
||||
"GITBUTLER_ASKPASS_PIPE".into(),
|
||||
sock_path.to_string_lossy().into_owned(),
|
||||
);
|
||||
envs.insert("GITBUTLER_ASKPASS_SECRET".into(), secret.clone());
|
||||
envs.insert(
|
||||
"SSH_ASKPASS".into(),
|
||||
askpath_path.to_string_lossy().into_owned(),
|
||||
);
|
||||
|
||||
// DISPLAY is required by SSH to check SSH_ASKPASS.
|
||||
// Please don't ask us why, it's unclear.
|
||||
if !std::env::var("DISPLAY").map(|v| v != "").unwrap_or(false) {
|
||||
envs.insert("DISPLAY".into(), ":".into());
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
envs.insert(
|
||||
"GIT_SSH_COMMAND".into(),
|
||||
format!(
|
||||
"{} {}",
|
||||
setsid_path.to_string_lossy(),
|
||||
envs.get("GIT_SSH_COMMAND").unwrap_or(&"ssh".into())
|
||||
),
|
||||
);
|
||||
|
||||
// TODO(qix-): implement the actual socket server code (right now this won't work)
|
||||
|
||||
self.exec
|
||||
.execute(args, Some(envs))
|
||||
.await
|
||||
.map_err(Error::Exec)
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: GitExecutor + 'static> crate::Repository for Repository<E> {
|
||||
@ -81,7 +143,8 @@ impl<E: GitExecutor + 'static> crate::Repository for Repository<E> {
|
||||
|
||||
args.push(key);
|
||||
|
||||
let (exit_code, stdout, stderr) = self.exec.execute(&args).await.map_err(Error::Exec)?;
|
||||
let (exit_code, stdout, stderr) =
|
||||
self.exec.execute(&args, None).await.map_err(Error::Exec)?;
|
||||
|
||||
if exit_code == 0 {
|
||||
Ok(Some(stdout))
|
||||
@ -120,7 +183,8 @@ impl<E: GitExecutor + 'static> crate::Repository for Repository<E> {
|
||||
args.push(key);
|
||||
args.push(value);
|
||||
|
||||
let (exit_code, stdout, stderr) = self.exec.execute(&args).await.map_err(Error::Exec)?;
|
||||
let (exit_code, stdout, stderr) =
|
||||
self.exec.execute(&args, None).await.map_err(Error::Exec)?;
|
||||
|
||||
if exit_code == 0 {
|
||||
Ok(())
|
||||
|
@ -3,6 +3,26 @@
|
||||
//! This library houses a number of Git implementations,
|
||||
//! over which we abstract a common interface and provide
|
||||
//! higher-level operations that are implementation-agnostic.
|
||||
//!
|
||||
//! # Libgit2 Support
|
||||
//! This library supports libgit2 via the `git2` feature.
|
||||
//! Not much in the way of assumptions are made about the environment;
|
||||
//! it's a fairly clean and safe Git backend.
|
||||
//!
|
||||
//! # Fork/Exec (CLI) Support
|
||||
//! This library supports the Git CLI via the `cli` feature.
|
||||
//! Note that this is a fairly experimental implementation that
|
||||
//! uses some (ideally portable) hacks for authentication,
|
||||
//! including a custom executable (or two, in the case of
|
||||
//! *nix systems) for handling automatic authentication
|
||||
//! via the API.
|
||||
//!
|
||||
//! This means those executables must be situated next to
|
||||
//! the executable that is running them (as sibling files),
|
||||
//! for security purposes. They may not be symlinked.
|
||||
//!
|
||||
//! This hampers certain use cases, such as implementing
|
||||
//! [`cli::GitExecutor`] for e.g. remote connections.
|
||||
|
||||
#![cfg_attr(not(feature = "std"), no_std)] // must be first
|
||||
#![feature(error_in_core)]
|
||||
|
@ -5,3 +5,7 @@ pub use alloc::{
|
||||
vec,
|
||||
vec::Vec,
|
||||
};
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
#[allow(unused_imports)]
|
||||
pub use std::collections::BTreeMap;
|
||||
|
@ -1,7 +1,8 @@
|
||||
use core::fmt;
|
||||
|
||||
/// An error that can occur while parsing a refspec from a string.
|
||||
#[derive(Debug, thiserror::Error, PartialEq, Eq, Clone)]
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
#[cfg_attr(feature = "std", derive(thiserror::Error))]
|
||||
pub enum Error {
|
||||
/// Encountered an unexpected character when parsing a [`RefSpec`] from a string.
|
||||
#[error("unexpected character {0:?} (offset {1})")]
|
||||
|
9
gitbutler-git/src/remote.rs
Normal file
9
gitbutler-git/src/remote.rs
Normal file
@ -0,0 +1,9 @@
|
||||
pub enum Authorization {
|
||||
Basic {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
},
|
||||
PublicKey {
|
||||
pub path: PathBuf
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user