move url module to a separate crate

this code is awful - lets nuke it asap
This commit is contained in:
Kiril Videlov 2024-07-09 15:29:24 +02:00
parent 01f3e0f0c4
commit 99d7b85343
No known key found for this signature in database
GPG Key ID: A4C733025427C471
24 changed files with 466 additions and 23 deletions

13
Cargo.lock generated
View File

@ -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",

View File

@ -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

View File

@ -1,2 +0,0 @@
mod url;
pub use self::url::*;

View File

@ -13,6 +13,5 @@
clippy::too_many_lines
)]
pub mod git;
#[cfg(target_os = "windows")]
pub mod windows;

View File

@ -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"

View File

@ -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<Vec<HttpsCredential>, HelpError> {
let mut flow = vec![];

View File

@ -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)]

View File

@ -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")?;
}
}

View File

@ -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(());
}

View File

@ -20,3 +20,4 @@ gitbutler-branch.workspace = true
gitbutler-reference.workspace = true
gitbutler-error.workspace = true
gitbutler-id.workspace = true
gitbutler-url.workspace = true

View File

@ -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;

View File

@ -27,3 +27,4 @@ gitbutler-user.workspace = true
gitbutler-branch.workspace = true
gitbutler-reference.workspace = true
gitbutler-storage.workspace = true
gitbutler-url.workspace = true

View File

@ -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)

View File

@ -0,0 +1,11 @@
[package]
name = "gitbutler-url"
version = "0.0.0"
edition = "2021"
authors = ["GitButler <gitbutler@gitbutler.com>"]
publish = false
[dependencies]
url = { version = "2.5.2", features = ["serde"] }
thiserror.workspace = true
bstr = "1.9.1"

View File

@ -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<Url, ConvertError> {
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<Url, ConvertError> {
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);
}
}
}

View File

@ -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<String>,
/// The password associated with a user.
password: Option<String>,
/// The host to which to connect. Localhost is implied if `None`.
pub host: Option<String>,
/// 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<u16>,
/// 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<Self, ConvertError> {
convert::to_ssh_url(self)
}
pub fn as_https(&self) -> Result<Self, ConvertError> {
convert::to_https_url(self)
}
}
impl FromStr for Url {
type Err = parse::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
parse::parse(s.as_bytes().into())
}
}

View File

@ -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<Url, Error> {
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)
}

View File

@ -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())
}
}

View File

@ -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"

View File

@ -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

View File

@ -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;