diff --git a/Cargo.lock b/Cargo.lock index fe22125aa..fd3441b04 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2367,6 +2367,7 @@ dependencies = [ "gitbutler-reference", "gitbutler-testsupport", "gitbutler-time", + "gitbutler-url", "gitbutler-user", "log", "resolve-path", @@ -2421,6 +2422,7 @@ dependencies = [ "gitbutler-oplog", "gitbutler-project", "gitbutler-reference", + "gitbutler-url", "gitbutler-user", "itertools 0.13.0", "tracing", @@ -2498,6 +2500,7 @@ dependencies = [ "gitbutler-reference", "gitbutler-repo", "gitbutler-storage", + "gitbutler-url", "gitbutler-user", "gitbutler-virtual", "keyring", @@ -2511,6 +2514,15 @@ dependencies = [ name = "gitbutler-time" version = "0.0.0" +[[package]] +name = "gitbutler-url" +version = "0.0.0" +dependencies = [ + "bstr", + "thiserror", + "url", +] + [[package]] name = "gitbutler-user" version = "0.0.0" @@ -2551,6 +2563,7 @@ dependencies = [ "gitbutler-serde", "gitbutler-testsupport", "gitbutler-time", + "gitbutler-url", "gitbutler-user", "glob", "hex", diff --git a/Cargo.toml b/Cargo.toml index 2e274eca3..0f35e3987 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,8 @@ members = [ "crates/gitbutler-fs", "crates/gitbutler-time", "crates/gitbutler-commit", - "crates/gitbutler-tagged-string", + "crates/gitbutler-tagged-string", + "crates/gitbutler-url", ] resolver = "2" @@ -67,6 +68,7 @@ gitbutler-fs = { path = "crates/gitbutler-fs" } gitbutler-time = { path = "crates/gitbutler-time" } gitbutler-commit = { path = "crates/gitbutler-commit" } gitbutler-tagged-string = { path = "crates/gitbutler-tagged-string" } +gitbutler-url = { path = "crates/gitbutler-url" } [profile.release] codegen-units = 1 # Compile crates one after another so the compiler can optimize better diff --git a/crates/gitbutler-core/src/git/mod.rs b/crates/gitbutler-core/src/git/mod.rs deleted file mode 100644 index f93696f54..000000000 --- a/crates/gitbutler-core/src/git/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -mod url; -pub use self::url::*; diff --git a/crates/gitbutler-core/src/lib.rs b/crates/gitbutler-core/src/lib.rs index 1d8b87b9b..8dd6eeb9e 100644 --- a/crates/gitbutler-core/src/lib.rs +++ b/crates/gitbutler-core/src/lib.rs @@ -13,6 +13,5 @@ clippy::too_many_lines )] -pub mod git; #[cfg(target_os = "windows")] pub mod windows; diff --git a/crates/gitbutler-repo/Cargo.toml b/crates/gitbutler-repo/Cargo.toml index 2a598378c..6491ffd5e 100644 --- a/crates/gitbutler-repo/Cargo.toml +++ b/crates/gitbutler-repo/Cargo.toml @@ -28,6 +28,7 @@ gitbutler-error.workspace = true gitbutler-id.workspace = true gitbutler-time.workspace = true gitbutler-commit.workspace = true +gitbutler-url.workspace = true [[test]] name="repo" diff --git a/crates/gitbutler-repo/src/credentials.rs b/crates/gitbutler-repo/src/credentials.rs index e5139eccb..ffa4f0fe8 100644 --- a/crates/gitbutler-repo/src/credentials.rs +++ b/crates/gitbutler-repo/src/credentials.rs @@ -5,9 +5,10 @@ use anyhow::Context; use gitbutler_command_context::ProjectRepository; -use gitbutler_core::git::Url; use gitbutler_project::AuthKey; +use gitbutler_url::{ConvertError, Scheme, Url}; + #[derive(Debug, Clone, PartialEq, Eq)] pub enum SshCredential { Keyfile { @@ -74,7 +75,7 @@ pub enum HelpError { #[error("no url set for remote")] NoUrlSet, #[error("failed to convert url: {0}")] - UrlConvertError(#[from] gitbutler_core::git::ConvertError), + UrlConvertError(#[from] ConvertError), #[error(transparent)] Git(#[from] git2::Error), #[error(transparent)] @@ -92,13 +93,13 @@ impl Helper { .context("failed to parse remote url")?; // if file, no auth needed. - if remote_url.scheme == gitbutler_core::git::Scheme::File { + if remote_url.scheme == Scheme::File { return Ok(vec![(remote, vec![Credential::Noop])]); } match &project_repository.project().preferred_key { AuthKey::Local { private_key_path } => { - let ssh_remote = if remote_url.scheme == gitbutler_core::git::Scheme::Ssh { + let ssh_remote = if remote_url.scheme == Scheme::Ssh { Ok(remote) } else { let ssh_url = remote_url.as_ssh()?; @@ -116,7 +117,7 @@ impl Helper { )]) } AuthKey::GitCredentialsHelper => { - let https_remote = if remote_url.scheme == gitbutler_core::git::Scheme::Https { + let https_remote = if remote_url.scheme == Scheme::Https { Ok(remote) } else { let url = remote_url.as_https()?; @@ -137,7 +138,7 @@ impl Helper { fn https_flow( project_repository: &ProjectRepository, - remote_url: &gitbutler_core::git::Url, + remote_url: &Url, ) -> Result, HelpError> { let mut flow = vec![]; diff --git a/crates/gitbutler-core/src/git/url/convert.rs b/crates/gitbutler-repo/src/giturl/convert.rs similarity index 100% rename from crates/gitbutler-core/src/git/url/convert.rs rename to crates/gitbutler-repo/src/giturl/convert.rs diff --git a/crates/gitbutler-core/src/git/url/mod.rs b/crates/gitbutler-repo/src/giturl/mod.rs similarity index 98% rename from crates/gitbutler-core/src/git/url/mod.rs rename to crates/gitbutler-repo/src/giturl/mod.rs index e11b7f81a..4d8a25fb7 100644 --- a/crates/gitbutler-core/src/git/url/mod.rs +++ b/crates/gitbutler-repo/src/giturl/mod.rs @@ -6,7 +6,7 @@ use std::str::FromStr; use bstr::ByteSlice; pub use convert::ConvertError; -pub use parse::Error as ParseError; +// pub use parse::Error as ParseError; pub use scheme::Scheme; #[derive(Default, Clone, Hash, PartialEq, Eq, Debug, thiserror::Error)] diff --git a/crates/gitbutler-core/src/git/url/parse.rs b/crates/gitbutler-repo/src/giturl/parse.rs similarity index 100% rename from crates/gitbutler-core/src/git/url/parse.rs rename to crates/gitbutler-repo/src/giturl/parse.rs diff --git a/crates/gitbutler-core/src/git/url/scheme.rs b/crates/gitbutler-repo/src/giturl/scheme.rs similarity index 100% rename from crates/gitbutler-core/src/git/url/scheme.rs rename to crates/gitbutler-repo/src/giturl/scheme.rs diff --git a/crates/gitbutler-repo/src/repository.rs b/crates/gitbutler-repo/src/repository.rs index 3696afe3b..2aa5e7a61 100644 --- a/crates/gitbutler-repo/src/repository.rs +++ b/crates/gitbutler-repo/src/repository.rs @@ -5,7 +5,6 @@ use anyhow::{anyhow, Context, Result}; use gitbutler_branch::branch::{Branch, BranchId}; use gitbutler_command_context::ProjectRepository; use gitbutler_commit::commit_headers::CommitHeadersV2; -use gitbutler_core::git; use gitbutler_error::error::Code; use gitbutler_reference::{Refname, RemoteRefname}; @@ -291,7 +290,7 @@ impl RepoActions for ProjectRepository { for (mut remote, callbacks) in auth_flows { if let Some(url) = remote.url() { if !self.project().omit_certificate_check.unwrap_or(false) { - let git_url = git::Url::from_str(url)?; + let git_url = gitbutler_url::Url::from_str(url)?; ssh::check_known_host(&git_url).context("failed to check known host")?; } } @@ -386,7 +385,7 @@ impl RepoActions for ProjectRepository { for (mut remote, callbacks) in auth_flows { if let Some(url) = remote.url() { if !self.project().omit_certificate_check.unwrap_or(false) { - let git_url = git::Url::from_str(url)?; + let git_url = gitbutler_url::Url::from_str(url)?; ssh::check_known_host(&git_url).context("failed to check known host")?; } } diff --git a/crates/gitbutler-repo/src/ssh.rs b/crates/gitbutler-repo/src/ssh.rs index 26761f2db..1247434e7 100644 --- a/crates/gitbutler-repo/src/ssh.rs +++ b/crates/gitbutler-repo/src/ssh.rs @@ -2,8 +2,6 @@ use std::{env, fs, path::Path}; use ssh2::{CheckResult, KnownHostFileKind}; -use gitbutler_core::git; - #[derive(Debug, thiserror::Error)] pub enum Error { #[error(transparent)] @@ -16,8 +14,8 @@ pub enum Error { Failure, } -pub fn check_known_host(remote_url: &git::Url) -> Result<(), Error> { - if remote_url.scheme != git::Scheme::Ssh { +pub fn check_known_host(remote_url: &gitbutler_url::Url) -> Result<(), Error> { + if remote_url.scheme != gitbutler_url::Scheme::Ssh { return Ok(()); } diff --git a/crates/gitbutler-sync/Cargo.toml b/crates/gitbutler-sync/Cargo.toml index 95cc7edb3..d44ecf14f 100644 --- a/crates/gitbutler-sync/Cargo.toml +++ b/crates/gitbutler-sync/Cargo.toml @@ -20,3 +20,4 @@ gitbutler-branch.workspace = true gitbutler-reference.workspace = true gitbutler-error.workspace = true gitbutler-id.workspace = true +gitbutler-url.workspace = true diff --git a/crates/gitbutler-sync/src/cloud.rs b/crates/gitbutler-sync/src/cloud.rs index b9dd959c8..cf607a6fe 100644 --- a/crates/gitbutler-sync/src/cloud.rs +++ b/crates/gitbutler-sync/src/cloud.rs @@ -6,13 +6,13 @@ use anyhow::{anyhow, Context, Result}; use gitbutler_branch::target::Target; use gitbutler_branchstate::VirtualBranchesAccess; use gitbutler_command_context::ProjectRepository; -use gitbutler_core::git::Url; use gitbutler_error::error::Code; use gitbutler_id::id::Id; use gitbutler_oplog::oplog::Oplog; use gitbutler_project as projects; use gitbutler_project::{CodePushState, Project}; use gitbutler_reference::Refname; +use gitbutler_url::Url; use gitbutler_user as users; use itertools::Itertools; diff --git a/crates/gitbutler-testsupport/Cargo.toml b/crates/gitbutler-testsupport/Cargo.toml index 7b04428ed..05e59a94f 100644 --- a/crates/gitbutler-testsupport/Cargo.toml +++ b/crates/gitbutler-testsupport/Cargo.toml @@ -27,3 +27,4 @@ gitbutler-user.workspace = true gitbutler-branch.workspace = true gitbutler-reference.workspace = true gitbutler-storage.workspace = true +gitbutler-url.workspace = true diff --git a/crates/gitbutler-testsupport/src/test_project.rs b/crates/gitbutler-testsupport/src/test_project.rs index c4c7b6e99..64f2365c7 100644 --- a/crates/gitbutler-testsupport/src/test_project.rs +++ b/crates/gitbutler-testsupport/src/test_project.rs @@ -1,7 +1,6 @@ use std::path::PathBuf; use std::{fs, path}; -use gitbutler_core::git; use gitbutler_reference::{LocalRefname, Refname}; use gitbutler_repo::RepositoryExt; use tempfile::TempDir; @@ -371,7 +370,7 @@ impl TestProject { .expect("failed to read references") } - pub fn add_submodule(&self, url: &git::Url, path: &path::Path) { + pub fn add_submodule(&self, url: &gitbutler_url::Url, path: &path::Path) { let mut submodule = self .local_repository .submodule(&url.to_string(), path.as_ref(), false) diff --git a/crates/gitbutler-url/Cargo.toml b/crates/gitbutler-url/Cargo.toml new file mode 100644 index 000000000..02dd24aad --- /dev/null +++ b/crates/gitbutler-url/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "gitbutler-url" +version = "0.0.0" +edition = "2021" +authors = ["GitButler "] +publish = false + +[dependencies] +url = { version = "2.5.2", features = ["serde"] } +thiserror.workspace = true +bstr = "1.9.1" diff --git a/crates/gitbutler-url/src/convert.rs b/crates/gitbutler-url/src/convert.rs new file mode 100644 index 000000000..19c31ffd7 --- /dev/null +++ b/crates/gitbutler-url/src/convert.rs @@ -0,0 +1,128 @@ +use bstr::ByteSlice; + +use super::{Scheme, Url}; + +#[derive(Debug, PartialEq, thiserror::Error)] +pub enum ConvertError { + #[error("Could not convert {from} to {to}")] + UnsupportedPair { from: Scheme, to: Scheme }, +} + +pub(crate) fn to_https_url(url: &Url) -> Result { + match url.scheme { + Scheme::Https => Ok(url.clone()), + Scheme::Http => Ok(Url { + scheme: Scheme::Https, + ..url.clone() + }), + Scheme::Ssh => Ok(Url { + scheme: Scheme::Https, + user: None, + serialize_alternative_form: true, + path: if url.path.starts_with(&[b'/']) { + url.path.clone() + } else { + format!("/{}", url.path.to_str().unwrap()).into() + }, + ..url.clone() + }), + _ => Err(ConvertError::UnsupportedPair { + from: url.scheme.clone(), + to: Scheme::Ssh, + }), + } +} + +pub(crate) fn to_ssh_url(url: &Url) -> Result { + match url.scheme { + Scheme::Ssh => Ok(url.clone()), + Scheme::Http | Scheme::Https => Ok(Url { + scheme: Scheme::Ssh, + user: Some("git".to_string()), + serialize_alternative_form: true, + path: if url.path.starts_with(&[b'/']) { + url.path.trim_start_with(|c| c == '/').into() + } else { + url.path.clone() + }, + ..url.clone() + }), + _ => Err(ConvertError::UnsupportedPair { + from: url.scheme.clone(), + to: Scheme::Ssh, + }), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn to_https_url_test() { + for (input, expected) in [ + ( + "https://github.com/gitbutlerapp/gitbutler.git", + "https://github.com/gitbutlerapp/gitbutler.git", + ), + ( + "http://github.com/gitbutlerapp/gitbutler.git", + "https://github.com/gitbutlerapp/gitbutler.git", + ), + ( + "git@github.com:gitbutlerapp/gitbutler.git", + "https://github.com/gitbutlerapp/gitbutler.git", + ), + ( + "ssh://git@github.com/gitbutlerapp/gitbutler.git", + "https://github.com/gitbutlerapp/gitbutler.git", + ), + ( + "git@bitbucket.org:gitbutler-nikita/test.git", + "https://bitbucket.org/gitbutler-nikita/test.git", + ), + ( + "https://bitbucket.org/gitbutler-nikita/test.git", + "https://bitbucket.org/gitbutler-nikita/test.git", + ), + ] { + let url = input.parse().unwrap(); + let https_url = to_https_url(&url).unwrap(); + assert_eq!(https_url.to_string(), expected, "test case {}", url); + } + } + + #[test] + fn to_ssh_url_test() { + for (input, expected) in [ + ( + "git@github.com:gitbutlerapp/gitbutler.git", + "git@github.com:gitbutlerapp/gitbutler.git", + ), + ( + "https://github.com/gitbutlerapp/gitbutler.git", + "git@github.com:gitbutlerapp/gitbutler.git", + ), + ( + "https://github.com/gitbutlerapp/gitbutler.git", + "git@github.com:gitbutlerapp/gitbutler.git", + ), + ( + "ssh://git@github.com/gitbutlerapp/gitbutler.git", + "ssh://git@github.com/gitbutlerapp/gitbutler.git", + ), + ( + "https://bitbucket.org/gitbutler-nikita/test.git", + "git@bitbucket.org:gitbutler-nikita/test.git", + ), + ( + "git@bitbucket.org:gitbutler-nikita/test.git", + "git@bitbucket.org:gitbutler-nikita/test.git", + ), + ] { + let url = input.parse().unwrap(); + let ssh_url = to_ssh_url(&url).unwrap(); + assert_eq!(ssh_url.to_string(), expected, "test case {}", url); + } + } +} diff --git a/crates/gitbutler-url/src/lib.rs b/crates/gitbutler-url/src/lib.rs new file mode 100644 index 000000000..4d8a25fb7 --- /dev/null +++ b/crates/gitbutler-url/src/lib.rs @@ -0,0 +1,91 @@ +mod convert; +mod parse; +mod scheme; + +use std::str::FromStr; + +use bstr::ByteSlice; +pub use convert::ConvertError; +// pub use parse::Error as ParseError; +pub use scheme::Scheme; + +#[derive(Default, Clone, Hash, PartialEq, Eq, Debug, thiserror::Error)] +pub struct Url { + /// The URL scheme. + pub scheme: Scheme, + /// The user to impersonate on the remote. + user: Option, + /// The password associated with a user. + password: Option, + /// The host to which to connect. Localhost is implied if `None`. + pub host: Option, + /// When serializing, use the alternative forms as it was parsed as such. + serialize_alternative_form: bool, + /// The port to use when connecting to a host. If `None`, standard ports depending on `scheme` will be used. + pub port: Option, + /// The path portion of the URL, usually the location of the git repository. + pub path: bstr::BString, +} + +impl Url { + pub fn is_github(&self) -> bool { + self.host + .as_ref() + .map_or(false, |host| host.contains("github.com")) + } +} + +impl std::fmt::Display for Url { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if !(self.serialize_alternative_form + && (self.scheme == Scheme::File || self.scheme == Scheme::Ssh)) + { + f.write_str(self.scheme.as_str())?; + f.write_str("://")?; + } + match (&self.user, &self.host) { + (Some(user), Some(host)) => { + f.write_str(user)?; + if let Some(password) = &self.password { + f.write_str(":")?; + f.write_str(password)?; + } + f.write_str("@")?; + f.write_str(host)?; + } + (None, Some(host)) => { + f.write_str(host)?; + } + (None, None) => {} + (Some(_user), None) => { + unreachable!("BUG: should not be possible to have a user but no host") + } + }; + if let Some(port) = &self.port { + f.write_str(&format!(":{}", port))?; + } + if self.serialize_alternative_form && self.scheme == Scheme::Ssh { + f.write_str(":")?; + } + f.write_str(self.path.to_str().unwrap())?; + Ok(()) + } +} + +impl Url { + pub fn as_ssh(&self) -> Result { + convert::to_ssh_url(self) + } + + pub fn as_https(&self) -> Result { + convert::to_https_url(self) + } +} + +impl FromStr for Url { + type Err = parse::Error; + + fn from_str(s: &str) -> Result { + parse::parse(s.as_bytes().into()) + } +} diff --git a/crates/gitbutler-url/src/parse.rs b/crates/gitbutler-url/src/parse.rs new file mode 100644 index 000000000..676f9d6d8 --- /dev/null +++ b/crates/gitbutler-url/src/parse.rs @@ -0,0 +1,146 @@ +use std::borrow::Cow; + +use bstr::{BStr, BString, ByteSlice}; + +use super::{Scheme, Url}; + +/// The Error returned by [`parse()`] +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("Could not decode URL as UTF8")] + Utf8(#[from] std::str::Utf8Error), + #[error(transparent)] + Url(#[from] url::ParseError), + #[error("URLs need to specify the path to the repository")] + MissingResourceLocation, + #[error("file URLs require an absolute or relative path to the repository")] + MissingRepositoryPath, + #[error("\"{url}\" is not a valid local path")] + NotALocalFile { url: BString }, + #[error("Relative URLs are not permitted: {url:?}")] + RelativeUrl { url: String }, +} + +fn str_to_protocol(s: &str) -> Scheme { + Scheme::from(s) +} + +fn guess_protocol(url: &[u8]) -> Option<&str> { + match url.find_byte(b':') { + Some(colon_pos) => { + if url[..colon_pos].find_byteset(b"@.").is_some() { + "ssh" + } else { + url.get(colon_pos + 1..).and_then(|from_colon| { + (from_colon.contains(&b'/') || from_colon.contains(&b'\\')).then_some("file") + })? + } + } + None => "file", + } + .into() +} + +/// Extract the path part from an SCP-like URL `[user@]host.xz:path/to/repo.git/` +fn extract_scp_path(url: &str) -> Option<&str> { + url.splitn(2, ':').last() +} + +fn sanitize_for_protocol<'a>(protocol: &str, url: &'a str) -> Cow<'a, str> { + match protocol { + "ssh" => url.replacen(':', "/", 1).into(), + _ => url.into(), + } +} + +fn has_no_explicit_protocol(url: &[u8]) -> bool { + url.find(b"://").is_none() +} + +fn to_owned_url(url: &url::Url) -> Url { + let password = url.password(); + Url { + serialize_alternative_form: false, + scheme: str_to_protocol(url.scheme()), + password: password.map(ToOwned::to_owned), + user: if url.username().is_empty() && password.is_none() { + None + } else { + Some(url.username().into()) + }, + host: url.host_str().map(Into::into), + port: url.port(), + path: url.path().into(), + } +} + +/// Parse the given `bytes` as git url. +/// +/// # Note +/// +/// We cannot and should never have to deal with UTF-16 encoded windows strings, so bytes input is acceptable. +/// For file-paths, we don't expect UTF8 encoding either. +pub fn parse(input: &BStr) -> Result { + let guessed_protocol = + guess_protocol(input).ok_or_else(|| Error::NotALocalFile { url: input.into() })?; + let path_without_file_protocol = input.strip_prefix(b"file://"); + if path_without_file_protocol.is_some() + || (has_no_explicit_protocol(input) && guessed_protocol == "file") + { + let path = + path_without_file_protocol.map_or_else(|| input.into(), |stripped_path| stripped_path); + if path.is_empty() { + return Err(Error::MissingRepositoryPath); + } + let input_starts_with_file_protocol = input.starts_with(b"file://"); + if input_starts_with_file_protocol { + let wanted = &[b'/']; + if !wanted.iter().any(|w| path.contains(w)) { + return Err(Error::MissingRepositoryPath); + } + } + return Ok(Url { + scheme: Scheme::File, + path: path.into(), + serialize_alternative_form: !input_starts_with_file_protocol, + ..Default::default() + }); + } + + let url_str = std::str::from_utf8(input)?; + let (mut url, mut scp_path) = match url::Url::parse(url_str) { + Ok(url) => (url, None), + Err(url::ParseError::RelativeUrlWithoutBase) => { + // happens with bare paths as well as scp like paths. The latter contain a ':' past the host portion, + // which we are trying to detect. + ( + url::Url::parse(&format!( + "{}://{}", + guessed_protocol, + sanitize_for_protocol(guessed_protocol, url_str) + ))?, + extract_scp_path(url_str), + ) + } + Err(err) => return Err(err.into()), + }; + // SCP like URLs without user parse as 'something' with the scheme being the 'host'. Hosts always have dots. + if url.scheme().find('.').is_some() { + // try again with prefixed protocol + url = url::Url::parse(&format!("ssh://{}", sanitize_for_protocol("ssh", url_str)))?; + scp_path = extract_scp_path(url_str); + } + if url.path().is_empty() && ["ssh", "git"].contains(&url.scheme()) { + return Err(Error::MissingResourceLocation); + } + if url.cannot_be_a_base() { + return Err(Error::RelativeUrl { url: url.into() }); + } + + let mut url = to_owned_url(&url); + if let Some(path) = scp_path { + url.path = path.into(); + url.serialize_alternative_form = true; + } + Ok(url) +} diff --git a/crates/gitbutler-url/src/scheme.rs b/crates/gitbutler-url/src/scheme.rs new file mode 100644 index 000000000..31239b5e8 --- /dev/null +++ b/crates/gitbutler-url/src/scheme.rs @@ -0,0 +1,54 @@ +/// A scheme or protocol for use in a [`Url`][super::Url]. +/// +/// It defines how to talk to a given repository. +#[derive(Default, PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] +pub enum Scheme { + /// A local resource that is accessible on the current host. + File, + /// A git daemon, like `File` over TCP/IP. + Git, + /// Launch `git-upload-pack` through an `ssh` tunnel. + #[default] + Ssh, + /// Use the HTTP protocol to talk to git servers. + Http, + /// Use the HTTPS protocol to talk to git servers. + Https, + /// Any other protocol or transport that isn't known at compile time. + /// + /// It's used to support plug-in transports. + Ext(String), +} + +impl<'a> From<&'a str> for Scheme { + fn from(value: &'a str) -> Self { + match value { + "ssh" => Scheme::Ssh, + "file" => Scheme::File, + "git" => Scheme::Git, + "http" => Scheme::Http, + "https" => Scheme::Https, + unknown => Scheme::Ext(unknown.into()), + } + } +} + +impl Scheme { + /// Return ourselves parseable name. + pub fn as_str(&self) -> &str { + match self { + Self::File => "file", + Self::Git => "git", + Self::Ssh => "ssh", + Self::Http => "http", + Self::Https => "https", + Self::Ext(name) => name.as_str(), + } + } +} + +impl std::fmt::Display for Scheme { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.as_str()) + } +} diff --git a/crates/gitbutler-virtual/Cargo.toml b/crates/gitbutler-virtual/Cargo.toml index 6852210aa..79b7f4358 100644 --- a/crates/gitbutler-virtual/Cargo.toml +++ b/crates/gitbutler-virtual/Cargo.toml @@ -22,6 +22,7 @@ gitbutler-serde.workspace = true gitbutler-id.workspace = true gitbutler-time.workspace = true gitbutler-commit.workspace = true +gitbutler-url.workspace = true serde = { workspace = true, features = ["std"]} bstr = "1.9.1" diffy = "0.3.0" diff --git a/crates/gitbutler-virtual/tests/virtual_branches/init.rs b/crates/gitbutler-virtual/tests/virtual_branches/init.rs index 9f5be12b5..5bd05fb4f 100644 --- a/crates/gitbutler-virtual/tests/virtual_branches/init.rs +++ b/crates/gitbutler-virtual/tests/virtual_branches/init.rs @@ -189,7 +189,8 @@ async fn submodule() { } = &Test::default(); let test_project = TestProject::default(); - let submodule_url: git::Url = test_project.path().display().to_string().parse().unwrap(); + let submodule_url: gitbutler_url::Url = + test_project.path().display().to_string().parse().unwrap(); repository.add_submodule(&submodule_url, path::Path::new("submodule")); controller diff --git a/crates/gitbutler-virtual/tests/virtual_branches/mod.rs b/crates/gitbutler-virtual/tests/virtual_branches/mod.rs index 9a43afc79..83a7fffdf 100644 --- a/crates/gitbutler-virtual/tests/virtual_branches/mod.rs +++ b/crates/gitbutler-virtual/tests/virtual_branches/mod.rs @@ -2,7 +2,6 @@ use std::path::PathBuf; use std::{fs, path, str::FromStr}; use gitbutler_branch::branch; -use gitbutler_core::git; use gitbutler_error::error::Marker; use gitbutler_project::{self as projects, Project, ProjectId}; use gitbutler_reference::Refname;