Merge pull request #3373 from gitbutlerapp/lib-in-toplevel

extract general library from `app` crate
This commit is contained in:
Kiril Videlov 2024-03-30 17:29:45 +01:00 committed by GitHub
commit 86b69064c2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
211 changed files with 2680 additions and 1965 deletions

View File

@ -14,6 +14,7 @@ jobs:
outputs:
node: ${{ steps.filter.outputs.node }}
rust: ${{ steps.filter.outputs.rust }}
gitbutler: ${{ steps.filter.outputs.gitbutler }}
gitbutler-app: ${{ steps.filter.outputs.gitbutler-app }}
gitbutler-changeset: ${{ steps.filter.outputs.gitbutler-changeset }}
gitbutler-git: ${{ steps.filter.outputs.gitbutler-git }}
@ -36,6 +37,10 @@ jobs:
- 'gitbutler-!(ui)/**'
gitbutler-app:
- *any-rust
gitbutler:
- *rust
- 'src/**'
- 'tests/**'
gitbutler-changeset:
- *rust
- 'gitbutler-changeset/**'
@ -97,6 +102,28 @@ jobs:
env:
RUSTDOCFLAGS: -Dwarnings
check-gitbutler:
needs: [changes, rust-init]
if: ${{ needs.changes.outputs.gitbutler == 'true' }}
runs-on: ubuntu-latest
container:
image: ghcr.io/gitbutlerapp/ci-base-image:latest
strategy:
matrix:
action:
- test
- check
- check-tests
features:
- ''
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/check-crate
with:
crate: gitbutler
features: ${{ toJson(matrix.features) }}
action: ${{ matrix.action }}
check-gitbutler-app:
needs: [changes, rust-init]
if: ${{ needs.changes.outputs.gitbutler-app == 'true' }}

132
Cargo.lock generated
View File

@ -276,7 +276,7 @@ dependencies = [
"glib-sys",
"gobject-sys",
"libc",
"system-deps 6.1.1",
"system-deps 6.2.0",
]
[[package]]
@ -625,7 +625,7 @@ checksum = "3c55d429bef56ac9172d25fecb85dc8068307d17acd74b377866b7a1ef25d3c8"
dependencies = [
"glib-sys",
"libc",
"system-deps 6.1.1",
"system-deps 6.2.0",
]
[[package]]
@ -676,9 +676,9 @@ dependencies = [
[[package]]
name = "cfg-expr"
version = "0.15.4"
version = "0.15.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b40ccee03b5175c18cde8f37e7d2a33bcef6f8ec8f7cc0d81090d1bb380949c9"
checksum = "fa50868b64a9a6fda9d593ce778849ea8715cd2a3d2cc17ffdb4a2f2f2f1961d"
dependencies = [
"smallvec",
"target-lexicon",
@ -1451,9 +1451,9 @@ dependencies = [
[[package]]
name = "fiat-crypto"
version = "0.2.1"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0870c84016d4b481be5c9f323c24f65e31e901ae618f0e80f4308fb00de1d2d"
checksum = "1676f435fc1dadde4d03e43f5d62b259e1ce5f40bd4ffb21db2b42ebe59c1382"
[[package]]
name = "field-offset"
@ -1670,9 +1670,9 @@ checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004"
[[package]]
name = "futures-timer"
version = "3.0.2"
version = "3.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c"
checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24"
[[package]]
name = "futures-util"
@ -1740,7 +1740,7 @@ dependencies = [
"glib-sys",
"gobject-sys",
"libc",
"system-deps 6.1.1",
"system-deps 6.2.0",
]
[[package]]
@ -1757,7 +1757,7 @@ dependencies = [
"libc",
"pango-sys",
"pkg-config",
"system-deps 6.1.1",
"system-deps 6.2.0",
]
[[package]]
@ -1771,7 +1771,7 @@ dependencies = [
"gobject-sys",
"libc",
"pkg-config",
"system-deps 6.1.1",
"system-deps 6.2.0",
]
[[package]]
@ -1783,7 +1783,7 @@ dependencies = [
"gdk-sys",
"glib-sys",
"libc",
"system-deps 6.1.1",
"system-deps 6.2.0",
"x11",
]
@ -1875,7 +1875,7 @@ dependencies = [
"glib-sys",
"gobject-sys",
"libc",
"system-deps 6.1.1",
"system-deps 6.2.0",
"winapi 0.3.9",
]
@ -1907,17 +1907,14 @@ dependencies = [
]
[[package]]
name = "gitbutler-app"
name = "gitbutler"
version = "0.0.0"
dependencies = [
"anyhow",
"async-trait",
"backoff",
"backtrace",
"bstr 1.9.1",
"byteorder",
"chrono",
"console-subscriber",
"diffy",
"filetime",
"fslock",
@ -1925,15 +1922,10 @@ dependencies = [
"git2",
"git2-hooks",
"gitbutler-git",
"governor",
"itertools 0.12.1",
"lazy_static",
"log",
"md5",
"nonzero_ext",
"notify",
"notify-debouncer-full",
"num_cpus",
"once_cell",
"pretty_assertions",
"r2d2",
@ -1944,16 +1936,52 @@ dependencies = [
"reqwest 0.12.2",
"resolve-path",
"rusqlite",
"sentry",
"sentry-tracing",
"serde",
"serde_json",
"sha1",
"sha2",
"similar",
"slug",
"ssh-key",
"ssh2",
"tempfile",
"thiserror",
"tokio",
"toml 0.8.12",
"tracing",
"url",
"urlencoding",
"uuid",
"walkdir",
"zip",
]
[[package]]
name = "gitbutler-app"
version = "0.0.0"
dependencies = [
"anyhow",
"async-trait",
"backoff",
"backtrace",
"chrono",
"console-subscriber",
"futures",
"git2",
"gitbutler",
"governor",
"itertools 0.12.1",
"log",
"nonzero_ext",
"notify",
"notify-debouncer-full",
"once_cell",
"pretty_assertions",
"reqwest 0.12.2",
"sentry",
"sentry-tracing",
"serde",
"serde_json",
"slug",
"tauri",
"tauri-build",
"tauri-plugin-context-menu",
@ -1965,15 +1993,9 @@ dependencies = [
"thiserror",
"tokio",
"tokio-util",
"toml 0.8.12",
"tracing",
"tracing-appender",
"tracing-subscriber",
"url",
"urlencoding",
"uuid",
"walkdir",
"zip",
]
[[package]]
@ -2041,7 +2063,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef4b192f8e65e9cf76cbf4ea71fa8e3be4a0e18ffe3d68b8da6836974cc5bad4"
dependencies = [
"libc",
"system-deps 6.1.1",
"system-deps 6.2.0",
]
[[package]]
@ -2071,7 +2093,7 @@ checksum = "0d57ce44246becd17153bd035ab4d32cfee096a657fc01f2231c9278378d1e0a"
dependencies = [
"glib-sys",
"libc",
"system-deps 6.1.1",
"system-deps 6.2.0",
]
[[package]]
@ -2143,7 +2165,7 @@ dependencies = [
"gobject-sys",
"libc",
"pango-sys",
"system-deps 6.1.1",
"system-deps 6.2.0",
]
[[package]]
@ -2237,11 +2259,11 @@ dependencies = [
[[package]]
name = "hdrhistogram"
version = "7.5.2"
version = "7.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f19b9f54f7c7f55e31401bb647626ce0cf0f67b0004982ce815b3ee72a02aa8"
checksum = "765c9198f173dd59ce26ff9f95ef0aafd0a0fe01fb9d72841bc5066a4c06511d"
dependencies = [
"base64 0.13.1",
"base64 0.21.3",
"byteorder",
"flate2",
"nom",
@ -3005,9 +3027,9 @@ checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
[[package]]
name = "matchit"
version = "0.7.2"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed1202b2a6f884ae56f04cff409ab315c5ce26b5e58d7412e484f01fd52f52ef"
checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
[[package]]
name = "md5"
@ -3395,9 +3417,9 @@ dependencies = [
[[package]]
name = "openssl"
version = "0.10.64"
version = "0.10.62"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f"
checksum = "8cde4d2d9200ad5909f8dac647e29482e07c3a35de8a13fce7c9c7747ad9f671"
dependencies = [
"bitflags 2.4.0",
"cfg-if",
@ -3540,7 +3562,7 @@ dependencies = [
"glib-sys",
"gobject-sys",
"libc",
"system-deps 6.1.1",
"system-deps 6.2.0",
]
[[package]]
@ -3997,9 +4019,9 @@ dependencies = [
[[package]]
name = "prost"
version = "0.12.1"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4fdd22f3b9c31b53c060df4a0613a1c7f062d4115a2b984dd15b1858f7e340d"
checksum = "146c289cda302b98a28d40c8b3b90498d6e526dd24ac2ecea73e4e491685b94a"
dependencies = [
"bytes",
"prost-derive",
@ -4007,9 +4029,9 @@ dependencies = [
[[package]]
name = "prost-derive"
version = "0.12.1"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "265baba7fabd416cf5078179f7d2cbeca4ce7a9041111900675ea7c4cb8a4c32"
checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e"
dependencies = [
"anyhow",
"itertools 0.11.0",
@ -4020,9 +4042,9 @@ dependencies = [
[[package]]
name = "prost-types"
version = "0.12.1"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e081b29f63d83a4bc75cfc9f3fe424f9156cf92d8a4f0c9407cce9a1b67327cf"
checksum = "193898f59edcf43c26227dcd4c8427f00d99d61e95dcde58dabd49fa291d470e"
dependencies = [
"prost",
]
@ -5426,14 +5448,14 @@ dependencies = [
[[package]]
name = "system-deps"
version = "6.1.1"
version = "6.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30c2de8a4d8f4b823d634affc9cd2a74ec98c53a756f317e529a48046cbf71f3"
checksum = "2a2d580ff6a20c55dfb86be5f9c238f67835d0e81cbdea8bf5680e0897320331"
dependencies = [
"cfg-expr 0.15.4",
"cfg-expr 0.15.7",
"heck 0.4.1",
"pkg-config",
"toml 0.7.6",
"toml 0.8.12",
"version-compare 0.1.1",
]
@ -5955,9 +5977,9 @@ dependencies = [
[[package]]
name = "tokio-stream"
version = "0.1.14"
version = "0.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842"
checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af"
dependencies = [
"futures-core",
"pin-project-lite",
@ -6537,7 +6559,7 @@ dependencies = [
"pango-sys",
"pkg-config",
"soup2-sys",
"system-deps 6.1.1",
"system-deps 6.2.0",
]
[[package]]

View File

@ -1,3 +1,72 @@
[package]
name = "gitbutler"
version = "0.0.0"
edition = "2021"
rust-version = "1.57"
authors = ["GitButler <gitbutler@gitbutler.com>"]
publish = false
[lib]
doctest = false
[dev-dependencies]
once_cell = "1.19"
pretty_assertions = "1.4"
[dependencies]
toml = "0.8.12"
anyhow = "1.0.81"
async-trait = "0.1.79"
backtrace = { version = "0.3.71", optional = true }
bstr = "1.9.1"
chrono = { version = "0.4.37", features = ["serde"] }
diffy = "0.3.0"
filetime = "0.2.23"
fslock = "0.2.1"
futures = "0.3"
git2.workspace = true
git2-hooks = "0.3"
itertools = "0.12"
lazy_static = "1.4.0"
md5 = "0.7.0"
r2d2 = "0.8.10"
r2d2_sqlite = "0.22.0"
rand = "0.8.5"
refinery = { version = "0.8", features = [ "rusqlite" ] }
regex = "1.10"
reqwest = { version = "0.12.2", features = ["json"] }
resolve-path = "0.1.0"
rusqlite.workspace = true
serde.workspace = true
serde_json = { version = "1.0", features = [ "std", "arbitrary_precision" ] }
sha2 = "0.10.8"
similar = { version = "2.4.0", features = ["unicode"] }
slug = "0.1.5"
ssh-key = { version = "0.6.5", features = [ "alloc", "ed25519" ] }
ssh2 = { version = "0.9.4", features = ["vendored-openssl"] }
log = "^0.4"
thiserror.workspace = true
tokio = { workspace = true, features = [ "full", "sync" ] }
tracing = "0.1.40"
url = { version = "2.5", features = ["serde"] }
urlencoding = "2.1.3"
uuid.workspace = true
walkdir = "2.5.0"
zip = "0.6.5"
tempfile = "3.10"
gitbutler-git = { path = "gitbutler-git" }
[features]
# by default Tauri runs in production mode
# when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is a URL
default = ["error-context"]
error-context = ["dep:backtrace"]
[lints.clippy]
all = "deny"
perf = "deny"
correctness = "deny"
[workspace]
members = [
"gitbutler-app",
@ -8,7 +77,7 @@ resolver = "2"
[workspace.dependencies]
git2 = { version = "0.18.3", features = ["vendored-openssl", "vendored-libgit2"] }
uuid = "1.8.0"
uuid = { version = "1.8.0", features = ["serde"] }
serde = { version = "1.0", features = ["derive"] }
thiserror = "1.0.58"
rusqlite = { version = "0.29.0", features = [ "bundled", "blob" ] }

View File

@ -18,52 +18,31 @@ test = false
tauri-build = { version = "1.5", features = [] }
[dev-dependencies]
once_cell = "1.19"
#once_cell = "1.19"
pretty_assertions = "1.4"
tempfile = "3.10"
[dependencies]
toml = "0.8.12"
anyhow = "1.0.81"
async-trait = "0.1.79"
backoff = "0.4.0"
backtrace = { version = "0.3.71", optional = true }
bstr = "1.9.1"
byteorder = "1.5.0"
chrono = { version = "0.4.37", features = ["serde"] }
console-subscriber = "0.2.0"
diffy = "0.3.0"
filetime = "0.2.23"
fslock = "0.2.1"
futures = "0.3"
git2.workspace = true
git2-hooks = "0.3"
governor = "0.6.3"
itertools = "0.12"
lazy_static = "1.4.0"
md5 = "0.7.0"
nonzero_ext = "0.3.0"
notify = { version = "6.0.1" }
notify-debouncer-full = "0.3.1"
num_cpus = "1.16.0"
once_cell = "1.19"
r2d2 = "0.8.10"
r2d2_sqlite = "0.22.0"
rand = "0.8.5"
refinery = { version = "0.8", features = [ "rusqlite" ] }
regex = "1.10"
reqwest = { version = "0.12.2", features = ["json"] }
resolve-path = "0.1.0"
rusqlite.workspace = true
sentry = { version = "0.32", optional = true, features = ["backtrace", "contexts", "panic", "transport", "anyhow", "debug-images", "reqwest", "native-tls" ] }
sentry-tracing = "0.32.0"
serde.workspace = true
serde_json = { version = "1.0", features = [ "std", "arbitrary_precision" ] }
sha1 = "0.10.6"
sha2 = "0.10.8"
similar = { version = "2.4.0", features = ["unicode"] }
slug = "0.1.5"
ssh-key = { version = "0.6.5", features = [ "alloc", "ed25519" ] }
ssh2 = { version = "0.9.4", features = ["vendored-openssl"] }
tauri = { version = "1.6.1", features = [ "http-all", "os-all", "dialog-open", "fs-read-file", "path-all", "process-relaunch", "protocol-asset", "shell-open", "window-maximize", "window-start-dragging", "window-unmaximize"] }
tauri-plugin-context-menu = { git = "https://github.com/c2r0b/tauri-plugin-context-menu", branch = "main" }
tauri-plugin-single-instance = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
@ -77,13 +56,7 @@ tokio-util = "0.7.10"
tracing = "0.1.40"
tracing-appender = "0.2.3"
tracing-subscriber = "0.3.17"
url = "2.5"
urlencoding = "2.1.3"
uuid.workspace = true
walkdir = "2.5.0"
zip = "0.6.5"
tempfile = "3.10"
gitbutler-git = { path = "../gitbutler-git" }
gitbutler = { path = "../" }
[lints.clippy]
all = "deny"

View File

@ -2,7 +2,7 @@ use std::{fmt, str, sync::Arc};
use tauri::AppHandle;
use crate::{projects::ProjectId, users::User};
use gitbutler::{projects::ProjectId, users::User};
mod posthog;

View File

@ -2,7 +2,8 @@ use std::{collections::HashMap, path};
use anyhow::{Context, Result};
use crate::{
use crate::watcher;
use gitbutler::{
askpass::AskpassBroker,
gb_repository, git,
project_repository::{self, conflicts},
@ -11,7 +12,6 @@ use crate::{
sessions::{self, SessionId},
users,
virtual_branches::BranchId,
watcher,
};
#[derive(Clone)]

View File

@ -1,70 +1,10 @@
use std::{collections::HashMap, sync::Arc};
use serde::Serialize;
use tauri::{AppHandle, Manager};
use tokio::sync::{oneshot, Mutex};
use crate::id::Id;
pub struct AskpassRequest {
sender: oneshot::Sender<Option<String>>,
}
#[derive(Clone)]
pub struct AskpassBroker {
pending_requests: Arc<Mutex<HashMap<Id<AskpassRequest>, AskpassRequest>>>,
handle: AppHandle,
}
#[derive(Debug, Clone, serde::Serialize)]
struct PromptEvent<C: Serialize + Clone> {
id: Id<AskpassRequest>,
prompt: String,
context: C,
}
impl AskpassBroker {
pub fn init(handle: AppHandle) -> Self {
Self {
pending_requests: Arc::new(Mutex::new(HashMap::new())),
handle,
}
}
pub async fn submit_prompt<C: Serialize + Clone>(
&self,
prompt: String,
context: C,
) -> Option<String> {
let (sender, receiver) = oneshot::channel();
let id = Id::generate();
let request = AskpassRequest { sender };
self.pending_requests.lock().await.insert(id, request);
self.handle
.emit_all(
"git_prompt",
PromptEvent {
id,
prompt,
context,
},
)
.expect("failed to emit askpass event");
receiver.await.unwrap()
}
pub async fn handle_response(&self, id: Id<AskpassRequest>, response: Option<String>) {
let mut pending_requests = self.pending_requests.lock().await;
if let Some(request) = pending_requests.remove(&id) {
let _ = request.sender.send(response);
} else {
log::warn!("received response for unknown askpass request: {}", id);
}
}
}
pub mod commands {
use super::{AppHandle, AskpassBroker, AskpassRequest, Id, Manager};
use gitbutler::{
askpass::{AskpassBroker, AskpassRequest},
id::Id,
};
use tauri::{AppHandle, Manager};
#[tauri::command(async)]
#[tracing::instrument(skip(handle, response))]
pub async fn submit_prompt_response(

View File

@ -4,12 +4,12 @@ use anyhow::Context;
use tauri::Manager;
use tracing::instrument;
use crate::{
app,
use crate::{app, watcher};
use gitbutler::{
error::{Code, Error},
gb_repository, git, project_repository, projects, reader,
sessions::SessionId,
users, watcher,
users,
};
impl From<app::Error> for Error {
@ -71,13 +71,13 @@ pub async fn git_test_push(
branch_name: &str,
) -> Result<(), Error> {
let app = handle.state::<app::App>();
let helper = handle.state::<crate::git::credentials::Helper>();
let helper = handle.state::<gitbutler::git::credentials::Helper>();
let project_id = project_id.parse().map_err(|_| Error::UserError {
code: Code::Validation,
message: "Malformed project id".to_string(),
})?;
let askpass_broker = handle
.state::<crate::askpass::AskpassBroker>()
.state::<gitbutler::askpass::AskpassBroker>()
.inner()
.clone();
app.git_test_push(
@ -102,13 +102,13 @@ pub async fn git_test_fetch(
action: Option<String>,
) -> Result<(), Error> {
let app = handle.state::<app::App>();
let helper = handle.state::<crate::git::credentials::Helper>();
let helper = handle.state::<gitbutler::git::credentials::Helper>();
let project_id = project_id.parse().map_err(|_| Error::UserError {
code: Code::Validation,
message: "Malformed project id".to_string(),
})?;
let askpass_broker = handle
.state::<crate::askpass::AskpassBroker>()
.state::<gitbutler::askpass::AskpassBroker>()
.inner()
.clone();
app.git_test_fetch(

View File

@ -1,16 +1,43 @@
mod controller;
mod delta;
mod document;
mod reader;
mod writer;
pub mod commands {
use std::collections::HashMap;
pub mod commands;
pub mod database;
pub mod operations;
use tauri::{AppHandle, Manager};
use tracing::instrument;
pub use controller::Controller;
pub use database::Database;
pub use delta::Delta;
pub use document::Document;
pub use reader::DeltasReader as Reader;
pub use writer::DeltasWriter as Writer;
use crate::error::{Code, Error};
use gitbutler::deltas::{controller::ListError, Controller, Delta};
impl From<ListError> for Error {
fn from(value: ListError) -> Self {
match value {
ListError::Other(error) => {
tracing::error!(?error);
Error::Unknown
}
}
}
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn list_deltas(
handle: AppHandle,
project_id: &str,
session_id: &str,
paths: Option<Vec<&str>>,
) -> Result<HashMap<String, Vec<Delta>>, Error> {
let session_id = session_id.parse().map_err(|_| Error::UserError {
message: "Malformed session id".to_string(),
code: Code::Validation,
})?;
let project_id = project_id.parse().map_err(|_| Error::UserError {
code: Code::Validation,
message: "Malformed project id".to_string(),
})?;
handle
.state::<Controller>()
.list_by_session_id(&project_id, &session_id, &paths)
.map_err(Into::into)
}
}

View File

@ -1,41 +0,0 @@
use std::collections::HashMap;
use tauri::{AppHandle, Manager};
use tracing::instrument;
use crate::error::{Code, Error};
use super::{controller::ListError, Controller, Delta};
impl From<ListError> for Error {
fn from(value: ListError) -> Self {
match value {
ListError::Other(error) => {
tracing::error!(?error);
Error::Unknown
}
}
}
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn list_deltas(
handle: AppHandle,
project_id: &str,
session_id: &str,
paths: Option<Vec<&str>>,
) -> Result<HashMap<String, Vec<Delta>>, Error> {
let session_id = session_id.parse().map_err(|_| Error::UserError {
message: "Malformed session id".to_string(),
code: Code::Validation,
})?;
let project_id = project_id.parse().map_err(|_| Error::UserError {
code: Code::Validation,
message: "Malformed project id".to_string(),
})?;
handle
.state::<Controller>()
.list_by_session_id(&project_id, &session_id, &paths)
.map_err(Into::into)
}

View File

@ -1,9 +1,9 @@
#[cfg(feature = "sentry")]
mod sentry;
pub use legacy::*;
pub(crate) use legacy::*;
pub mod gb {
pub(crate) mod gb {
#[cfg(feature = "error-context")]
pub use error_context::*;
@ -319,6 +319,7 @@ pub mod gb {
mod legacy {
use core::fmt;
use gitbutler::project_repository;
use serde::{ser::SerializeMap, Serialize};
#[derive(Debug)]
@ -389,4 +390,19 @@ mod legacy {
Error::Unknown
}
}
impl From<project_repository::OpenError> for Error {
fn from(value: project_repository::OpenError) -> Self {
match value {
project_repository::OpenError::NotFound(path) => Error::UserError {
code: Code::Projects,
message: format!("{} not found", path.display()),
},
project_repository::OpenError::Other(error) => {
tracing::error!(?error);
Error::Unknown
}
}
}
}
}

View File

@ -1,7 +1,7 @@
use anyhow::{Context, Result};
use tauri::{AppHandle, Manager};
use crate::{
use gitbutler::{
deltas,
projects::ProjectId,
reader,

View File

@ -1 +1,82 @@
pub mod commands;
pub mod commands {
use std::collections::HashMap;
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use tracing::instrument;
use crate::error::Error;
const GITHUB_CLIENT_ID: &str = "cd51880daa675d9e6452";
#[derive(Debug, Deserialize, Serialize, Clone, Default)]
pub struct Verification {
pub user_code: String,
pub device_code: String,
}
#[tauri::command(async)]
#[instrument]
pub async fn init_device_oauth() -> Result<Verification, Error> {
let mut req_body = HashMap::new();
req_body.insert("client_id", GITHUB_CLIENT_ID);
req_body.insert("scope", "repo");
let mut headers = reqwest::header::HeaderMap::new();
headers.insert(
reqwest::header::ACCEPT,
reqwest::header::HeaderValue::from_static("application/json"),
);
let client = reqwest::Client::new();
let res = client
.post("https://github.com/login/device/code")
.headers(headers)
.json(&req_body)
.send()
.await
.context("Failed to send request")?;
let rsp_body = res.text().await.context("Failed to get response body")?;
serde_json::from_str(&rsp_body)
.context("Failed to parse response body")
.map_err(Into::into)
}
#[tauri::command(async)]
#[instrument]
pub async fn check_auth_status(device_code: &str) -> Result<String, Error> {
#[derive(Debug, Deserialize, Serialize, Clone, Default)]
struct AccessTokenContainer {
access_token: String,
}
let mut req_body = HashMap::new();
req_body.insert("client_id", GITHUB_CLIENT_ID);
req_body.insert("device_code", device_code);
req_body.insert("grant_type", "urn:ietf:params:oauth:grant-type:device_code");
let mut headers = reqwest::header::HeaderMap::new();
headers.insert(
reqwest::header::ACCEPT,
reqwest::header::HeaderValue::from_static("application/json"),
);
let client = reqwest::Client::new();
let res = client
.post("https://github.com/login/oauth/access_token")
.headers(headers)
.json(&req_body)
.send()
.await
.context("Failed to send request")?;
let rsp_body = res.text().await.context("Failed to get response body")?;
serde_json::from_str::<AccessTokenContainer>(&rsp_body)
.map(|rsp_body| rsp_body.access_token)
.context("Failed to parse response body")
.map_err(Into::into)
}
}

View File

@ -1,80 +0,0 @@
use std::collections::HashMap;
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use tracing::instrument;
use crate::error::Error;
const GITHUB_CLIENT_ID: &str = "cd51880daa675d9e6452";
#[derive(Debug, Deserialize, Serialize, Clone, Default)]
pub struct Verification {
pub user_code: String,
pub device_code: String,
}
#[tauri::command(async)]
#[instrument]
pub async fn init_device_oauth() -> Result<Verification, Error> {
let mut req_body = HashMap::new();
req_body.insert("client_id", GITHUB_CLIENT_ID);
req_body.insert("scope", "repo");
let mut headers = reqwest::header::HeaderMap::new();
headers.insert(
reqwest::header::ACCEPT,
reqwest::header::HeaderValue::from_static("application/json"),
);
let client = reqwest::Client::new();
let res = client
.post("https://github.com/login/device/code")
.headers(headers)
.json(&req_body)
.send()
.await
.context("Failed to send request")?;
let rsp_body = res.text().await.context("Failed to get response body")?;
serde_json::from_str(&rsp_body)
.context("Failed to parse response body")
.map_err(Into::into)
}
#[tauri::command(async)]
#[instrument]
pub async fn check_auth_status(device_code: &str) -> Result<String, Error> {
#[derive(Debug, Deserialize, Serialize, Clone, Default)]
struct AccessTokenContainer {
access_token: String,
}
let mut req_body = HashMap::new();
req_body.insert("client_id", GITHUB_CLIENT_ID);
req_body.insert("device_code", device_code);
req_body.insert("grant_type", "urn:ietf:params:oauth:grant-type:device_code");
let mut headers = reqwest::header::HeaderMap::new();
headers.insert(
reqwest::header::ACCEPT,
reqwest::header::HeaderValue::from_static("application/json"),
);
let client = reqwest::Client::new();
let res = client
.post("https://github.com/login/oauth/access_token")
.headers(headers)
.json(&req_body)
.send()
.await
.context("Failed to send request")?;
let rsp_body = res.text().await.context("Failed to get response body")?;
serde_json::from_str::<AccessTokenContainer>(&rsp_body)
.map(|rsp_body| rsp_body.access_token)
.context("Failed to parse response body")
.map_err(Into::into)
}

View File

@ -1,7 +1,29 @@
pub mod commands;
mod controller;
mod key;
pub mod storage;
pub mod commands {
use tauri::Manager;
use tracing::instrument;
pub use controller::*;
pub use key::{PrivateKey, PublicKey, SignError};
use crate::error::Error;
use gitbutler::keys::{controller, PublicKey};
impl From<controller::GetOrCreateError> for Error {
fn from(value: controller::GetOrCreateError) -> Self {
match value {
controller::GetOrCreateError::Other(error) => {
tracing::error!(?error, "failed to get or create key");
Error::Unknown
}
}
}
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn get_public_key(handle: tauri::AppHandle) -> Result<PublicKey, Error> {
handle
.state::<controller::Controller>()
.get_or_create()
.map(|key| key.public_key())
.map_err(Into::into)
}
}

View File

@ -1,27 +0,0 @@
use tauri::Manager;
use tracing::instrument;
use crate::error::Error;
use super::{controller, PublicKey};
impl From<controller::GetOrCreateError> for Error {
fn from(value: controller::GetOrCreateError) -> Self {
match value {
controller::GetOrCreateError::Other(error) => {
tracing::error!(?error, "failed to get or create key");
Error::Unknown
}
}
}
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn get_public_key(handle: tauri::AppHandle) -> Result<PublicKey, Error> {
handle
.state::<controller::Controller>()
.get_or_create()
.map(|key| key.public_key())
.map_err(Into::into)
}

View File

@ -15,36 +15,20 @@
pub mod analytics;
pub mod app;
pub mod askpass;
pub mod assets;
pub mod commands;
pub mod database;
pub mod dedup;
pub mod deltas;
pub mod error;
pub mod events;
pub mod fs;
pub mod gb_repository;
pub mod git;
pub mod github;
pub mod id;
pub mod keys;
pub mod lock;
pub mod logs;
pub mod menu;
pub mod path;
pub mod project_repository;
pub mod watcher;
pub mod askpass;
pub mod deltas;
pub mod error;
pub mod github;
pub mod keys;
pub mod projects;
pub mod reader;
pub mod sentry;
pub mod sessions;
pub mod ssh;
pub mod storage;
pub mod types;
pub mod users;
pub mod virtual_branches;
pub mod watcher;
#[cfg(target_os = "windows")]
pub mod windows;
pub mod writer;
pub mod zip;

View File

@ -13,14 +13,17 @@
clippy::too_many_lines
)]
use gitbutler::assets;
use gitbutler::database;
use gitbutler::git;
use gitbutler::storage;
#[cfg(target_os = "windows")]
use gitbutler::windows;
use gitbutler_app::analytics;
use gitbutler_app::app;
use gitbutler_app::askpass;
use gitbutler_app::assets;
use gitbutler_app::commands;
use gitbutler_app::database;
use gitbutler_app::deltas;
use gitbutler_app::git;
use gitbutler_app::github;
use gitbutler_app::keys;
use gitbutler_app::logs;
@ -28,12 +31,9 @@ use gitbutler_app::menu;
use gitbutler_app::projects;
use gitbutler_app::sentry;
use gitbutler_app::sessions;
use gitbutler_app::storage;
use gitbutler_app::users;
use gitbutler_app::virtual_branches;
use gitbutler_app::watcher;
#[cfg(target_os = "windows")]
use gitbutler_app::windows;
use gitbutler_app::zip;
use std::path::PathBuf;
@ -101,7 +101,12 @@ fn main() {
tracing::info!(version = %app_handle.package_info().version, name = %app_handle.package_info().name, "starting app");
let askpass_broker = askpass::AskpassBroker::init(app_handle.clone());
let askpass_broker = gitbutler::askpass::AskpassBroker::init({
let handle = app_handle.clone();
move |event| {
handle.emit_all("git_prompt", event).expect("tauri event emission doesn't fail in practice")
}
});
app_handle.manage(askpass_broker);
let storage_controller = storage::Storage::new(&app_data_dir);
@ -110,16 +115,16 @@ fn main() {
let watcher_controller = watcher::Watchers::new(app_handle.clone());
app_handle.manage(watcher_controller.clone());
let projects_storage_controller = projects::storage::Storage::new(storage_controller.clone());
let projects_storage_controller = gitbutler::projects::storage::Storage::new(storage_controller.clone());
app_handle.manage(projects_storage_controller.clone());
let users_storage_controller = users::storage::Storage::new(storage_controller.clone());
let users_storage_controller = gitbutler::users::storage::Storage::new(storage_controller.clone());
app_handle.manage(users_storage_controller.clone());
let users_controller = users::Controller::new(users_storage_controller.clone());
let users_controller = gitbutler::users::Controller::new(users_storage_controller.clone());
app_handle.manage(users_controller.clone());
let projects_controller = projects::Controller::new(
let projects_controller = gitbutler::projects::Controller::new(
app_data_dir.clone(),
projects_storage_controller.clone(),
users_controller.clone(),
@ -132,21 +137,21 @@ fn main() {
let database_controller = database::Database::open_in_directory(&app_data_dir).expect("failed to open database");
app_handle.manage(database_controller.clone());
let zipper = zip::Zipper::new(&app_cache_dir);
let zipper = gitbutler::zip::Zipper::new(&app_cache_dir);
app_handle.manage(zipper.clone());
app_handle.manage(zip::Controller::new(app_data_dir.clone(), app_log_dir.clone(), zipper.clone(), projects_controller.clone()));
app_handle.manage(gitbutler::zip::Controller::new(app_data_dir.clone(), app_log_dir.clone(), zipper.clone(), projects_controller.clone()));
let deltas_database_controller = deltas::database::Database::new(database_controller.clone());
let deltas_database_controller = gitbutler::deltas::database::Database::new(database_controller.clone());
app_handle.manage(deltas_database_controller.clone());
let deltas_controller = deltas::Controller::new(deltas_database_controller.clone());
let deltas_controller = gitbutler::deltas::Controller::new(deltas_database_controller.clone());
app_handle.manage(deltas_controller);
let keys_storage_controller = keys::storage::Storage::new(storage_controller.clone());
let keys_storage_controller = gitbutler::keys::storage::Storage::new(storage_controller.clone());
app_handle.manage(keys_storage_controller.clone());
let keys_controller = keys::Controller::new(keys_storage_controller.clone());
let keys_controller = gitbutler::keys::Controller::new(keys_storage_controller.clone());
app_handle.manage(keys_controller.clone());
let git_credentials_controller = git::credentials::Helper::new(
@ -156,7 +161,7 @@ fn main() {
);
app_handle.manage(git_credentials_controller.clone());
app_handle.manage(virtual_branches::controller::Controller::new(
app_handle.manage(gitbutler::virtual_branches::controller::Controller::new(
app_data_dir.clone(),
projects_controller.clone(),
users_controller.clone(),
@ -196,10 +201,10 @@ fn main() {
};
}
let sessions_database_controller = sessions::database::Database::new(database_controller.clone());
let sessions_database_controller = gitbutler::sessions::database::Database::new(database_controller.clone());
app_handle.manage(sessions_database_controller.clone());
app_handle.manage(sessions::Controller::new(
app_handle.manage(gitbutler::sessions::Controller::new(
app_data_dir.clone(),
sessions_database_controller.clone(),
projects_controller.clone(),

View File

@ -1,10 +1,206 @@
pub mod commands;
mod controller;
mod project;
pub mod storage;
pub mod commands {
use std::path;
pub use controller::*;
pub use project::{AuthKey, CodePushState, FetchResult, Project, ProjectId};
pub use storage::UpdateRequest;
use tauri::Manager;
use tracing::instrument;
pub use project::ApiProject;
use crate::error::{Code, Error};
use gitbutler::projects::{
self,
controller::{self, Controller},
};
impl From<controller::UpdateError> for Error {
fn from(value: controller::UpdateError) -> Self {
match value {
controller::UpdateError::Validation(
controller::UpdateValidationError::KeyNotFound(path),
) => Error::UserError {
code: Code::Projects,
message: format!("'{}' not found", path.display()),
},
controller::UpdateError::Validation(
controller::UpdateValidationError::KeyNotFile(path),
) => Error::UserError {
code: Code::Projects,
message: format!("'{}' is not a file", path.display()),
},
controller::UpdateError::NotFound => Error::UserError {
code: Code::Projects,
message: "Project not found".into(),
},
controller::UpdateError::Other(error) => {
tracing::error!(?error, "failed to update project");
Error::Unknown
}
}
}
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn update_project(
handle: tauri::AppHandle,
project: projects::UpdateRequest,
) -> Result<projects::Project, Error> {
handle
.state::<Controller>()
.update(&project)
.await
.map_err(Into::into)
}
impl From<controller::AddError> for Error {
fn from(value: controller::AddError) -> Self {
match value {
controller::AddError::NotAGitRepository => Error::UserError {
code: Code::Projects,
message: "Must be a git directory".to_string(),
},
controller::AddError::AlreadyExists => Error::UserError {
code: Code::Projects,
message: "Project already exists".to_string(),
},
controller::AddError::OpenProjectRepository(error) => error.into(),
controller::AddError::NotADirectory => Error::UserError {
code: Code::Projects,
message: "Not a directory".to_string(),
},
controller::AddError::PathNotFound => Error::UserError {
code: Code::Projects,
message: "Path not found".to_string(),
},
controller::AddError::SubmodulesNotSupported => Error::UserError {
code: Code::Projects,
message: "Repositories with git submodules are not supported".to_string(),
},
controller::AddError::User(error) => error.into(),
controller::AddError::Other(error) => {
tracing::error!(?error, "failed to add project");
Error::Unknown
}
}
}
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn add_project(
handle: tauri::AppHandle,
path: &path::Path,
) -> Result<projects::Project, Error> {
handle.state::<Controller>().add(path).map_err(Into::into)
}
impl From<controller::GetError> for Error {
fn from(value: controller::GetError) -> Self {
match value {
controller::GetError::NotFound => Error::UserError {
code: Code::Projects,
message: "Project not found".into(),
},
controller::GetError::Other(error) => {
tracing::error!(?error, "failed to get project");
Error::Unknown
}
}
}
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn get_project(
handle: tauri::AppHandle,
id: &str,
) -> Result<projects::Project, Error> {
let id = id.parse().map_err(|_| Error::UserError {
code: Code::Validation,
message: "Malformed project id".into(),
})?;
handle.state::<Controller>().get(&id).map_err(Into::into)
}
impl From<controller::ListError> for Error {
fn from(value: controller::ListError) -> Self {
match value {
controller::ListError::Other(error) => {
tracing::error!(?error, "failed to list projects");
Error::Unknown
}
}
}
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn list_projects(handle: tauri::AppHandle) -> Result<Vec<projects::Project>, Error> {
handle.state::<Controller>().list().map_err(Into::into)
}
impl From<controller::DeleteError> for Error {
fn from(value: controller::DeleteError) -> Self {
match value {
controller::DeleteError::Other(error) => {
tracing::error!(?error, "failed to delete project");
Error::Unknown
}
}
}
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn delete_project(handle: tauri::AppHandle, id: &str) -> Result<(), Error> {
let id = id.parse().map_err(|_| Error::UserError {
code: Code::Validation,
message: "Malformed project id".into(),
})?;
handle
.state::<Controller>()
.delete(&id)
.await
.map_err(Into::into)
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn git_get_local_config(
handle: tauri::AppHandle,
id: &str,
key: &str,
) -> Result<Option<String>, Error> {
let id = id.parse().map_err(|_| Error::UserError {
code: Code::Validation,
message: "Malformed project id".into(),
})?;
handle
.state::<Controller>()
.get_local_config(&id, key)
.map_err(|e| Error::UserError {
code: Code::Projects,
message: e.to_string(),
})
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn git_set_local_config(
handle: tauri::AppHandle,
id: &str,
key: &str,
value: &str,
) -> Result<(), Error> {
let id = id.parse().map_err(|_| Error::UserError {
code: Code::Validation,
message: "Malformed project id".into(),
})?;
handle
.state::<Controller>()
.set_local_config(&id, key, value)
.map_err(|e| Error::UserError {
code: Code::Projects,
message: e.to_string(),
})
}
}

View File

@ -1,201 +0,0 @@
use std::path;
use tauri::Manager;
use tracing::instrument;
use crate::{
error::{Code, Error},
projects,
};
use super::controller::{self, Controller};
impl From<controller::UpdateError> for Error {
fn from(value: controller::UpdateError) -> Self {
match value {
controller::UpdateError::Validation(
controller::UpdateValidationError::KeyNotFound(path),
) => Error::UserError {
code: Code::Projects,
message: format!("'{}' not found", path.display()),
},
controller::UpdateError::Validation(controller::UpdateValidationError::KeyNotFile(
path,
)) => Error::UserError {
code: Code::Projects,
message: format!("'{}' is not a file", path.display()),
},
controller::UpdateError::NotFound => Error::UserError {
code: Code::Projects,
message: "Project not found".into(),
},
controller::UpdateError::Other(error) => {
tracing::error!(?error, "failed to update project");
Error::Unknown
}
}
}
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn update_project(
handle: tauri::AppHandle,
project: projects::UpdateRequest,
) -> Result<projects::Project, Error> {
handle
.state::<Controller>()
.update(&project)
.await
.map_err(Into::into)
}
impl From<controller::AddError> for Error {
fn from(value: controller::AddError) -> Self {
match value {
controller::AddError::NotAGitRepository => Error::UserError {
code: Code::Projects,
message: "Must be a git directory".to_string(),
},
controller::AddError::AlreadyExists => Error::UserError {
code: Code::Projects,
message: "Project already exists".to_string(),
},
controller::AddError::OpenProjectRepository(error) => error.into(),
controller::AddError::NotADirectory => Error::UserError {
code: Code::Projects,
message: "Not a directory".to_string(),
},
controller::AddError::PathNotFound => Error::UserError {
code: Code::Projects,
message: "Path not found".to_string(),
},
controller::AddError::SubmodulesNotSupported => Error::UserError {
code: Code::Projects,
message: "Repositories with git submodules are not supported".to_string(),
},
controller::AddError::User(error) => error.into(),
controller::AddError::Other(error) => {
tracing::error!(?error, "failed to add project");
Error::Unknown
}
}
}
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn add_project(
handle: tauri::AppHandle,
path: &path::Path,
) -> Result<projects::Project, Error> {
handle.state::<Controller>().add(path).map_err(Into::into)
}
impl From<controller::GetError> for Error {
fn from(value: controller::GetError) -> Self {
match value {
controller::GetError::NotFound => Error::UserError {
code: Code::Projects,
message: "Project not found".into(),
},
controller::GetError::Other(error) => {
tracing::error!(?error, "failed to get project");
Error::Unknown
}
}
}
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn get_project(handle: tauri::AppHandle, id: &str) -> Result<projects::Project, Error> {
let id = id.parse().map_err(|_| Error::UserError {
code: Code::Validation,
message: "Malformed project id".into(),
})?;
handle.state::<Controller>().get(&id).map_err(Into::into)
}
impl From<controller::ListError> for Error {
fn from(value: controller::ListError) -> Self {
match value {
controller::ListError::Other(error) => {
tracing::error!(?error, "failed to list projects");
Error::Unknown
}
}
}
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn list_projects(handle: tauri::AppHandle) -> Result<Vec<projects::Project>, Error> {
handle.state::<Controller>().list().map_err(Into::into)
}
impl From<controller::DeleteError> for Error {
fn from(value: controller::DeleteError) -> Self {
match value {
controller::DeleteError::Other(error) => {
tracing::error!(?error, "failed to delete project");
Error::Unknown
}
}
}
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn delete_project(handle: tauri::AppHandle, id: &str) -> Result<(), Error> {
let id = id.parse().map_err(|_| Error::UserError {
code: Code::Validation,
message: "Malformed project id".into(),
})?;
handle
.state::<Controller>()
.delete(&id)
.await
.map_err(Into::into)
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn git_get_local_config(
handle: tauri::AppHandle,
id: &str,
key: &str,
) -> Result<Option<String>, Error> {
let id = id.parse().map_err(|_| Error::UserError {
code: Code::Validation,
message: "Malformed project id".into(),
})?;
handle
.state::<Controller>()
.get_local_config(&id, key)
.map_err(|e| Error::UserError {
code: Code::Projects,
message: e.to_string(),
})
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn git_set_local_config(
handle: tauri::AppHandle,
id: &str,
key: &str,
value: &str,
) -> Result<(), Error> {
let id = id.parse().map_err(|_| Error::UserError {
code: Code::Validation,
message: "Malformed project id".into(),
})?;
handle
.state::<Controller>()
.set_local_config(&id, key, value)
.map_err(|e| Error::UserError {
code: Code::Projects,
message: e.to_string(),
})
}

View File

@ -12,7 +12,7 @@ use sentry_tracing::SentryLayer;
use tracing::Subscriber;
use tracing_subscriber::registry::LookupSpan;
use crate::users;
use gitbutler::users;
static SENTRY_QUOTA: Quota = Quota::per_second(nonzero!(1_u32)); // 1 per second at most.
static SENTRY_LIMIT: OnceCell<RateLimiter<NotKeyed, InMemoryState, QuantaClock>> = OnceCell::new();

View File

@ -1,15 +1,42 @@
mod controller;
mod iterator;
mod reader;
pub mod session;
mod writer;
pub mod commands {
use tauri::{AppHandle, Manager};
use tracing::instrument;
pub mod commands;
pub mod database;
use crate::error::{Code, Error};
pub use controller::Controller;
pub use database::Database;
pub use iterator::SessionsIterator;
pub use reader::SessionReader as Reader;
pub use session::{Meta, Session, SessionError, SessionId};
pub use writer::SessionWriter as Writer;
use gitbutler::sessions::{
Session,
{controller::ListError, Controller},
};
impl From<ListError> for Error {
fn from(value: ListError) -> Self {
match value {
ListError::UsersError(error) => Error::from(error),
ListError::ProjectsError(error) => Error::from(error),
ListError::ProjectRepositoryError(error) => Error::from(error),
ListError::Other(error) => {
tracing::error!(?error);
Error::Unknown
}
}
}
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn list_sessions(
handle: AppHandle,
project_id: &str,
earliest_timestamp_ms: Option<u128>,
) -> Result<Vec<Session>, Error> {
let project_id = project_id.parse().map_err(|_| Error::UserError {
code: Code::Validation,
message: "Malformed project id".to_string(),
})?;
handle
.state::<Controller>()
.list(&project_id, earliest_timestamp_ms)
.map_err(Into::into)
}
}

View File

@ -1,40 +1 @@
use tauri::{AppHandle, Manager};
use tracing::instrument;
use crate::error::{Code, Error};
use super::{
controller::{Controller, ListError},
Session,
};
impl From<ListError> for Error {
fn from(value: ListError) -> Self {
match value {
ListError::UsersError(error) => Error::from(error),
ListError::ProjectsError(error) => Error::from(error),
ListError::ProjectRepositoryError(error) => Error::from(error),
ListError::Other(error) => {
tracing::error!(?error);
Error::Unknown
}
}
}
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn list_sessions(
handle: AppHandle,
project_id: &str,
earliest_timestamp_ms: Option<u128>,
) -> Result<Vec<Session>, Error> {
let project_id = project_id.parse().map_err(|_| Error::UserError {
code: Code::Validation,
message: "Malformed project id".to_string(),
})?;
handle
.state::<Controller>()
.list(&project_id, earliest_timestamp_ms)
.map_err(Into::into)
}

View File

@ -1,7 +1,82 @@
pub mod commands;
pub mod controller;
pub mod storage;
mod user;
pub mod commands {
use tauri::{AppHandle, Manager};
use tracing::instrument;
pub use controller::*;
pub use user::User;
use crate::{error::Error, sentry};
use gitbutler::{
assets,
users::controller::{self, Controller, GetError},
users::User,
};
impl From<GetError> for Error {
fn from(value: GetError) -> Self {
match value {
GetError::Other(error) => {
tracing::error!(?error, "failed to get user");
Error::Unknown
}
}
}
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn get_user(handle: AppHandle) -> Result<Option<User>, Error> {
let app = handle.state::<Controller>();
let proxy = handle.state::<assets::Proxy>();
match app.get_user()? {
Some(user) => Ok(Some(proxy.proxy_user(user).await)),
None => Ok(None),
}
}
impl From<controller::SetError> for Error {
fn from(value: controller::SetError) -> Self {
match value {
controller::SetError::Other(error) => {
tracing::error!(?error, "failed to set user");
Error::Unknown
}
}
}
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn set_user(handle: AppHandle, user: User) -> Result<User, Error> {
let app = handle.state::<Controller>();
let proxy = handle.state::<assets::Proxy>();
app.set_user(&user)?;
sentry::configure_scope(Some(&user));
Ok(proxy.proxy_user(user).await)
}
impl From<controller::DeleteError> for Error {
fn from(value: controller::DeleteError) -> Self {
match value {
controller::DeleteError::Other(error) => {
tracing::error!(?error, "failed to delete user");
Error::Unknown
}
}
}
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn delete_user(handle: AppHandle) -> Result<(), Error> {
let app = handle.state::<Controller>();
app.delete_user()?;
sentry::configure_scope(None);
Ok(())
}
}

View File

@ -1,79 +0,0 @@
use tauri::{AppHandle, Manager};
use tracing::instrument;
use crate::{assets, error::Error, sentry};
use super::{
controller::{self, Controller, GetError},
User,
};
impl From<GetError> for Error {
fn from(value: GetError) -> Self {
match value {
GetError::Other(error) => {
tracing::error!(?error, "failed to get user");
Error::Unknown
}
}
}
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn get_user(handle: AppHandle) -> Result<Option<User>, Error> {
let app = handle.state::<Controller>();
let proxy = handle.state::<assets::Proxy>();
match app.get_user()? {
Some(user) => Ok(Some(proxy.proxy_user(user).await)),
None => Ok(None),
}
}
impl From<controller::SetError> for Error {
fn from(value: controller::SetError) -> Self {
match value {
controller::SetError::Other(error) => {
tracing::error!(?error, "failed to set user");
Error::Unknown
}
}
}
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn set_user(handle: AppHandle, user: User) -> Result<User, Error> {
let app = handle.state::<Controller>();
let proxy = handle.state::<assets::Proxy>();
app.set_user(&user)?;
sentry::configure_scope(Some(&user));
Ok(proxy.proxy_user(user).await)
}
impl From<controller::DeleteError> for Error {
fn from(value: controller::DeleteError) -> Self {
match value {
controller::DeleteError::Other(error) => {
tracing::error!(?error, "failed to delete user");
Error::Unknown
}
}
}
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn delete_user(handle: AppHandle) -> Result<(), Error> {
let app = handle.state::<Controller>();
app.delete_user()?;
sentry::configure_scope(None);
Ok(())
}

View File

@ -1,31 +1,540 @@
pub mod branch;
pub use branch::{Branch, BranchId};
pub mod context;
pub mod target;
pub mod commands {
use anyhow::Context;
use tauri::{AppHandle, Manager};
use tracing::instrument;
pub mod errors;
use gitbutler::error::{Code, Error};
mod files;
pub use files::*;
use crate::watcher;
use gitbutler::askpass::AskpassBroker;
use gitbutler::virtual_branches::{RemoteBranch, RemoteBranchData};
use gitbutler::{
assets, git, projects,
projects::ProjectId,
virtual_branches::branch::{self, BranchId, BranchOwnershipClaims},
virtual_branches::controller::{Controller, ControllerError},
virtual_branches::BaseBranch,
virtual_branches::{RemoteBranchFile, VirtualBranches},
};
pub mod integration;
pub use integration::GITBUTLER_INTEGRATION_REFERENCE;
fn into_error<E: Into<Error>>(value: ControllerError<E>) -> Error {
match value {
ControllerError::User(error) => error,
ControllerError::Action(error) => error.into(),
ControllerError::VerifyError(error) => error.into(),
ControllerError::Other(error) => {
tracing::error!(?error, "failed to verify branch");
Error::Unknown
}
}
}
mod base;
pub use base::*;
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn commit_virtual_branch(
handle: AppHandle,
project_id: ProjectId,
branch: BranchId,
message: &str,
ownership: Option<BranchOwnershipClaims>,
run_hooks: bool,
) -> Result<git::Oid, Error> {
let oid = handle
.state::<Controller>()
.create_commit(&project_id, &branch, message, ownership.as_ref(), run_hooks)
.await
.map_err(into_error)?;
emit_vbranches(&handle, &project_id).await;
Ok(oid)
}
pub mod controller;
pub use controller::Controller;
/// This is a test command. It retrieves the virtual branches state from the gitbutler repository (legacy state) and persists it into a flat TOML file
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn save_vbranches_state(
handle: AppHandle,
project_id: ProjectId,
branch_ids: Vec<BranchId>,
) -> Result<(), Error> {
handle
.state::<Controller>()
.save_vbranches_state(&project_id, branch_ids)
.await?;
return Ok(());
}
pub mod commands;
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn list_virtual_branches(
handle: AppHandle,
project_id: ProjectId,
) -> Result<VirtualBranches, Error> {
let (branches, uses_diff_context, skipped_files) = handle
.state::<Controller>()
.list_virtual_branches(&project_id)
.await
.map_err(into_error)?;
mod iterator;
pub use iterator::BranchIterator as Iterator;
// Migration: If use_diff_context is not already set and if there are no vbranches, set use_diff_context to true
let has_active_branches = branches.iter().any(|branch| branch.active);
if !uses_diff_context && !has_active_branches {
let _ = handle
.state::<projects::Controller>()
.update(&projects::UpdateRequest {
id: project_id,
use_diff_context: Some(true),
..Default::default()
})
.await;
}
mod r#virtual;
pub use r#virtual::*;
let proxy = handle.state::<assets::Proxy>();
let branches = proxy.proxy_virtual_branches(branches).await;
Ok(VirtualBranches {
branches,
skipped_files,
})
}
mod remote;
pub use remote::*;
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn create_virtual_branch(
handle: AppHandle,
project_id: ProjectId,
branch: branch::BranchCreateRequest,
) -> Result<BranchId, Error> {
let branch_id = handle
.state::<Controller>()
.create_virtual_branch(&project_id, &branch)
.await
.map_err(into_error)?;
emit_vbranches(&handle, &project_id).await;
Ok(branch_id)
}
mod state;
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn create_virtual_branch_from_branch(
handle: AppHandle,
project_id: ProjectId,
branch: git::Refname,
) -> Result<BranchId, Error> {
let branch_id = handle
.state::<Controller>()
.create_virtual_branch_from_branch(&project_id, &branch)
.await
.map_err(into_error)?;
emit_vbranches(&handle, &project_id).await;
Ok(branch_id)
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn merge_virtual_branch_upstream(
handle: AppHandle,
project_id: ProjectId,
branch: BranchId,
) -> Result<(), Error> {
handle
.state::<Controller>()
.merge_virtual_branch_upstream(&project_id, &branch)
.await
.map_err(into_error)?;
emit_vbranches(&handle, &project_id).await;
Ok(())
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn get_base_branch_data(
handle: AppHandle,
project_id: ProjectId,
) -> Result<Option<BaseBranch>, Error> {
if let Some(base_branch) = handle
.state::<Controller>()
.get_base_branch_data(&project_id)
.await
.map_err(into_error)?
{
let proxy = handle.state::<assets::Proxy>();
let base_branch = proxy.proxy_base_branch(base_branch).await;
Ok(Some(base_branch))
} else {
Ok(None)
}
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn set_base_branch(
handle: AppHandle,
project_id: ProjectId,
branch: &str,
) -> Result<BaseBranch, Error> {
let branch_name = format!("refs/remotes/{}", branch)
.parse()
.context("Invalid branch name")?;
let base_branch = handle
.state::<Controller>()
.set_base_branch(&project_id, &branch_name)
.await
.map_err(into_error)?;
let base_branch = handle
.state::<assets::Proxy>()
.proxy_base_branch(base_branch)
.await;
emit_vbranches(&handle, &project_id).await;
Ok(base_branch)
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn update_base_branch(handle: AppHandle, project_id: ProjectId) -> Result<(), Error> {
handle
.state::<Controller>()
.update_base_branch(&project_id)
.await
.map_err(into_error)?;
emit_vbranches(&handle, &project_id).await;
Ok(())
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn update_virtual_branch(
handle: AppHandle,
project_id: ProjectId,
branch: branch::BranchUpdateRequest,
) -> Result<(), Error> {
handle
.state::<Controller>()
.update_virtual_branch(&project_id, branch)
.await
.map_err(into_error)?;
emit_vbranches(&handle, &project_id).await;
Ok(())
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn delete_virtual_branch(
handle: AppHandle,
project_id: ProjectId,
branch_id: BranchId,
) -> Result<(), Error> {
handle
.state::<Controller>()
.delete_virtual_branch(&project_id, &branch_id)
.await
.map_err(into_error)?;
emit_vbranches(&handle, &project_id).await;
Ok(())
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn apply_branch(
handle: AppHandle,
project_id: ProjectId,
branch: BranchId,
) -> Result<(), Error> {
handle
.state::<Controller>()
.apply_virtual_branch(&project_id, &branch)
.await
.map_err(into_error)?;
emit_vbranches(&handle, &project_id).await;
Ok(())
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn unapply_branch(
handle: AppHandle,
project_id: ProjectId,
branch: BranchId,
) -> Result<(), Error> {
handle
.state::<Controller>()
.unapply_virtual_branch(&project_id, &branch)
.await
.map_err(into_error)?;
emit_vbranches(&handle, &project_id).await;
Ok(())
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn unapply_ownership(
handle: AppHandle,
project_id: ProjectId,
ownership: BranchOwnershipClaims,
) -> Result<(), Error> {
handle
.state::<Controller>()
.unapply_ownership(&project_id, &ownership)
.await
.map_err(into_error)?;
emit_vbranches(&handle, &project_id).await;
Ok(())
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn reset_files(
handle: AppHandle,
project_id: ProjectId,
files: &str,
) -> Result<(), Error> {
// convert files to Vec<String>
let files = files
.split('\n')
.map(std::string::ToString::to_string)
.collect::<Vec<String>>();
handle
.state::<Controller>()
.reset_files(&project_id, &files)
.await
.map_err(into_error)?;
emit_vbranches(&handle, &project_id).await;
Ok(())
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn push_virtual_branch(
handle: AppHandle,
project_id: ProjectId,
branch_id: BranchId,
with_force: bool,
) -> Result<(), Error> {
let askpass_broker = handle.state::<AskpassBroker>();
handle
.state::<Controller>()
.push_virtual_branch(
&project_id,
&branch_id,
with_force,
Some((askpass_broker.inner().clone(), Some(branch_id))),
)
.await
.map_err(|e| Error::UserError {
code: Code::Unknown,
message: e.to_string(),
})?;
emit_vbranches(&handle, &project_id).await;
Ok(())
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn can_apply_virtual_branch(
handle: AppHandle,
project_id: ProjectId,
branch_id: BranchId,
) -> Result<bool, Error> {
handle
.state::<Controller>()
.can_apply_virtual_branch(&project_id, &branch_id)
.await
.map_err(Into::into)
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn can_apply_remote_branch(
handle: AppHandle,
project_id: ProjectId,
branch: git::RemoteRefname,
) -> Result<bool, Error> {
handle
.state::<Controller>()
.can_apply_remote_branch(&project_id, &branch)
.await
.map_err(into_error)
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn list_remote_commit_files(
handle: AppHandle,
project_id: ProjectId,
commit_oid: git::Oid,
) -> Result<Vec<RemoteBranchFile>, Error> {
handle
.state::<Controller>()
.list_remote_commit_files(&project_id, commit_oid)
.await
.map_err(Into::into)
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn reset_virtual_branch(
handle: AppHandle,
project_id: ProjectId,
branch_id: BranchId,
target_commit_oid: git::Oid,
) -> Result<(), Error> {
handle
.state::<Controller>()
.reset_virtual_branch(&project_id, &branch_id, target_commit_oid)
.await
.map_err(into_error)?;
emit_vbranches(&handle, &project_id).await;
Ok(())
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn cherry_pick_onto_virtual_branch(
handle: AppHandle,
project_id: ProjectId,
branch_id: BranchId,
target_commit_oid: git::Oid,
) -> Result<Option<git::Oid>, Error> {
let oid = handle
.state::<Controller>()
.cherry_pick(&project_id, &branch_id, target_commit_oid)
.await
.map_err(into_error)?;
emit_vbranches(&handle, &project_id).await;
Ok(oid)
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn amend_virtual_branch(
handle: AppHandle,
project_id: ProjectId,
branch_id: BranchId,
ownership: BranchOwnershipClaims,
) -> Result<git::Oid, Error> {
let oid = handle
.state::<Controller>()
.amend(&project_id, &branch_id, &ownership)
.await
.map_err(into_error)?;
emit_vbranches(&handle, &project_id).await;
Ok(oid)
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn list_remote_branches(
handle: tauri::AppHandle,
project_id: ProjectId,
) -> Result<Vec<RemoteBranch>, Error> {
let branches = handle
.state::<Controller>()
.list_remote_branches(&project_id)
.await
.map_err(into_error)?;
Ok(branches)
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn get_remote_branch_data(
handle: tauri::AppHandle,
project_id: ProjectId,
refname: git::Refname,
) -> Result<RemoteBranchData, Error> {
let branch_data = handle
.state::<Controller>()
.get_remote_branch_data(&project_id, &refname)
.await
.map_err(into_error)?;
let branch_data = handle
.state::<assets::Proxy>()
.proxy_remote_branch_data(branch_data)
.await;
Ok(branch_data)
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn squash_branch_commit(
handle: tauri::AppHandle,
project_id: ProjectId,
branch_id: BranchId,
target_commit_oid: git::Oid,
) -> Result<(), Error> {
handle
.state::<Controller>()
.squash(&project_id, &branch_id, target_commit_oid)
.await
.map_err(into_error)?;
emit_vbranches(&handle, &project_id).await;
Ok(())
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn fetch_from_target(
handle: tauri::AppHandle,
project_id: ProjectId,
action: Option<String>,
) -> Result<BaseBranch, Error> {
let askpass_broker = handle.state::<AskpassBroker>().inner().clone();
let base_branch = handle
.state::<Controller>()
.fetch_from_target(
&project_id,
Some((
askpass_broker,
action.unwrap_or_else(|| "unknown".to_string()),
)),
)
.await
.map_err(into_error)?;
emit_vbranches(&handle, &project_id).await;
Ok(base_branch)
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn move_commit(
handle: tauri::AppHandle,
project_id: ProjectId,
commit_oid: git::Oid,
target_branch_id: BranchId,
) -> Result<(), Error> {
handle
.state::<Controller>()
.move_commit(&project_id, &target_branch_id, commit_oid)
.await
.map_err(into_error)?;
emit_vbranches(&handle, &project_id).await;
Ok(())
}
// XXX(qix-): Is this command used?
#[allow(dead_code)]
pub async fn update_commit_message(
handle: tauri::AppHandle,
project_id: ProjectId,
branch_id: BranchId,
commit_oid: git::Oid,
message: &str,
) -> Result<(), Error> {
handle
.state::<Controller>()
.update_commit_message(&project_id, &branch_id, commit_oid, message)
.await
.map_err(into_error)?;
emit_vbranches(&handle, &project_id).await;
Ok(())
}
async fn emit_vbranches(handle: &AppHandle, project_id: &projects::ProjectId) {
if let Err(error) = handle
.state::<watcher::Watchers>()
.post(watcher::Event::CalculateVirtualBranches(*project_id))
.await
{
tracing::error!(?error);
}
}
}

View File

@ -1,518 +0,0 @@
use crate::{projects::ProjectId, watcher};
use anyhow::Context;
use tauri::{AppHandle, Manager};
use tracing::instrument;
use crate::{
assets,
error::{Code, Error},
git, projects,
};
use super::{
branch::{BranchId, BranchOwnershipClaims},
controller::{Controller, ControllerError},
BaseBranch, RemoteBranchFile,
};
impl<E: Into<Error>> From<ControllerError<E>> for Error {
fn from(value: ControllerError<E>) -> Self {
match value {
ControllerError::User(error) => error,
ControllerError::Action(error) => error.into(),
ControllerError::VerifyError(error) => error.into(),
ControllerError::Other(error) => {
tracing::error!(?error, "failed to verify branch");
Error::Unknown
}
}
}
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn commit_virtual_branch(
handle: AppHandle,
project_id: ProjectId,
branch: BranchId,
message: &str,
ownership: Option<BranchOwnershipClaims>,
run_hooks: bool,
) -> Result<git::Oid, Error> {
let oid = handle
.state::<Controller>()
.create_commit(&project_id, &branch, message, ownership.as_ref(), run_hooks)
.await?;
emit_vbranches(&handle, &project_id).await;
Ok(oid)
}
/// This is a test command. It retrieves the virtual branches state from the gitbutler repository (legacy state) and persists it into a flat TOML file
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn save_vbranches_state(
handle: AppHandle,
project_id: ProjectId,
branch_ids: Vec<BranchId>,
) -> Result<(), Error> {
handle
.state::<Controller>()
.save_vbranches_state(&project_id, branch_ids)
.await?;
return Ok(());
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn list_virtual_branches(
handle: AppHandle,
project_id: ProjectId,
) -> Result<super::VirtualBranches, Error> {
let (branches, uses_diff_context, skipped_files) = handle
.state::<Controller>()
.list_virtual_branches(&project_id)
.await?;
// Migration: If use_diff_context is not already set and if there are no vbranches, set use_diff_context to true
let has_active_branches = branches.iter().any(|branch| branch.active);
if !uses_diff_context && !has_active_branches {
let _ = handle
.state::<projects::Controller>()
.update(&projects::UpdateRequest {
id: project_id,
use_diff_context: Some(true),
..Default::default()
})
.await;
}
let proxy = handle.state::<assets::Proxy>();
let branches = proxy.proxy_virtual_branches(branches).await;
Ok(super::VirtualBranches {
branches,
skipped_files,
})
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn create_virtual_branch(
handle: AppHandle,
project_id: ProjectId,
branch: super::branch::BranchCreateRequest,
) -> Result<BranchId, Error> {
let branch_id = handle
.state::<Controller>()
.create_virtual_branch(&project_id, &branch)
.await?;
emit_vbranches(&handle, &project_id).await;
Ok(branch_id)
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn create_virtual_branch_from_branch(
handle: AppHandle,
project_id: ProjectId,
branch: git::Refname,
) -> Result<BranchId, Error> {
let branch_id = handle
.state::<Controller>()
.create_virtual_branch_from_branch(&project_id, &branch)
.await?;
emit_vbranches(&handle, &project_id).await;
Ok(branch_id)
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn merge_virtual_branch_upstream(
handle: AppHandle,
project_id: ProjectId,
branch: BranchId,
) -> Result<(), Error> {
handle
.state::<Controller>()
.merge_virtual_branch_upstream(&project_id, &branch)
.await?;
emit_vbranches(&handle, &project_id).await;
Ok(())
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn get_base_branch_data(
handle: AppHandle,
project_id: ProjectId,
) -> Result<Option<super::BaseBranch>, Error> {
if let Some(base_branch) = handle
.state::<Controller>()
.get_base_branch_data(&project_id)
.await?
{
let proxy = handle.state::<assets::Proxy>();
let base_branch = proxy.proxy_base_branch(base_branch).await;
Ok(Some(base_branch))
} else {
Ok(None)
}
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn set_base_branch(
handle: AppHandle,
project_id: ProjectId,
branch: &str,
) -> Result<super::BaseBranch, Error> {
let branch_name = format!("refs/remotes/{}", branch)
.parse()
.context("Invalid branch name")?;
let base_branch = handle
.state::<Controller>()
.set_base_branch(&project_id, &branch_name)
.await?;
let base_branch = handle
.state::<assets::Proxy>()
.proxy_base_branch(base_branch)
.await;
emit_vbranches(&handle, &project_id).await;
Ok(base_branch)
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn update_base_branch(handle: AppHandle, project_id: ProjectId) -> Result<(), Error> {
handle
.state::<Controller>()
.update_base_branch(&project_id)
.await?;
emit_vbranches(&handle, &project_id).await;
Ok(())
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn update_virtual_branch(
handle: AppHandle,
project_id: ProjectId,
branch: super::branch::BranchUpdateRequest,
) -> Result<(), Error> {
handle
.state::<Controller>()
.update_virtual_branch(&project_id, branch)
.await?;
emit_vbranches(&handle, &project_id).await;
Ok(())
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn delete_virtual_branch(
handle: AppHandle,
project_id: ProjectId,
branch_id: BranchId,
) -> Result<(), Error> {
handle
.state::<Controller>()
.delete_virtual_branch(&project_id, &branch_id)
.await?;
emit_vbranches(&handle, &project_id).await;
Ok(())
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn apply_branch(
handle: AppHandle,
project_id: ProjectId,
branch: BranchId,
) -> Result<(), Error> {
handle
.state::<Controller>()
.apply_virtual_branch(&project_id, &branch)
.await?;
emit_vbranches(&handle, &project_id).await;
Ok(())
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn unapply_branch(
handle: AppHandle,
project_id: ProjectId,
branch: BranchId,
) -> Result<(), Error> {
handle
.state::<Controller>()
.unapply_virtual_branch(&project_id, &branch)
.await?;
emit_vbranches(&handle, &project_id).await;
Ok(())
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn unapply_ownership(
handle: AppHandle,
project_id: ProjectId,
ownership: BranchOwnershipClaims,
) -> Result<(), Error> {
handle
.state::<Controller>()
.unapply_ownership(&project_id, &ownership)
.await?;
emit_vbranches(&handle, &project_id).await;
Ok(())
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn reset_files(
handle: AppHandle,
project_id: ProjectId,
files: &str,
) -> Result<(), Error> {
// convert files to Vec<String>
let files = files
.split('\n')
.map(std::string::ToString::to_string)
.collect::<Vec<String>>();
handle
.state::<Controller>()
.reset_files(&project_id, &files)
.await?;
emit_vbranches(&handle, &project_id).await;
Ok(())
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn push_virtual_branch(
handle: AppHandle,
project_id: ProjectId,
branch_id: BranchId,
with_force: bool,
) -> Result<(), Error> {
let askpass_broker = handle.state::<crate::askpass::AskpassBroker>();
handle
.state::<Controller>()
.push_virtual_branch(
&project_id,
&branch_id,
with_force,
Some((askpass_broker.inner().clone(), Some(branch_id))),
)
.await
.map_err(|e| Error::UserError {
code: Code::Unknown,
message: e.to_string(),
})?;
emit_vbranches(&handle, &project_id).await;
Ok(())
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn can_apply_virtual_branch(
handle: AppHandle,
project_id: ProjectId,
branch_id: BranchId,
) -> Result<bool, Error> {
handle
.state::<Controller>()
.can_apply_virtual_branch(&project_id, &branch_id)
.await
.map_err(Into::into)
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn can_apply_remote_branch(
handle: AppHandle,
project_id: ProjectId,
branch: git::RemoteRefname,
) -> Result<bool, Error> {
handle
.state::<Controller>()
.can_apply_remote_branch(&project_id, &branch)
.await
.map_err(Into::into)
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn list_remote_commit_files(
handle: AppHandle,
project_id: ProjectId,
commit_oid: git::Oid,
) -> Result<Vec<RemoteBranchFile>, Error> {
handle
.state::<Controller>()
.list_remote_commit_files(&project_id, commit_oid)
.await
.map_err(Into::into)
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn reset_virtual_branch(
handle: AppHandle,
project_id: ProjectId,
branch_id: BranchId,
target_commit_oid: git::Oid,
) -> Result<(), Error> {
handle
.state::<Controller>()
.reset_virtual_branch(&project_id, &branch_id, target_commit_oid)
.await?;
emit_vbranches(&handle, &project_id).await;
Ok(())
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn cherry_pick_onto_virtual_branch(
handle: AppHandle,
project_id: ProjectId,
branch_id: BranchId,
target_commit_oid: git::Oid,
) -> Result<Option<git::Oid>, Error> {
let oid = handle
.state::<Controller>()
.cherry_pick(&project_id, &branch_id, target_commit_oid)
.await?;
emit_vbranches(&handle, &project_id).await;
Ok(oid)
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn amend_virtual_branch(
handle: AppHandle,
project_id: ProjectId,
branch_id: BranchId,
ownership: BranchOwnershipClaims,
) -> Result<git::Oid, Error> {
let oid = handle
.state::<Controller>()
.amend(&project_id, &branch_id, &ownership)
.await?;
emit_vbranches(&handle, &project_id).await;
Ok(oid)
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn list_remote_branches(
handle: tauri::AppHandle,
project_id: ProjectId,
) -> Result<Vec<super::RemoteBranch>, Error> {
let branches = handle
.state::<Controller>()
.list_remote_branches(&project_id)
.await?;
Ok(branches)
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn get_remote_branch_data(
handle: tauri::AppHandle,
project_id: ProjectId,
refname: git::Refname,
) -> Result<super::RemoteBranchData, Error> {
let branch_data = handle
.state::<Controller>()
.get_remote_branch_data(&project_id, &refname)
.await?;
let branch_data = handle
.state::<assets::Proxy>()
.proxy_remote_branch_data(branch_data)
.await;
Ok(branch_data)
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn squash_branch_commit(
handle: tauri::AppHandle,
project_id: ProjectId,
branch_id: BranchId,
target_commit_oid: git::Oid,
) -> Result<(), Error> {
handle
.state::<Controller>()
.squash(&project_id, &branch_id, target_commit_oid)
.await?;
emit_vbranches(&handle, &project_id).await;
Ok(())
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn fetch_from_target(
handle: tauri::AppHandle,
project_id: ProjectId,
action: Option<String>,
) -> Result<BaseBranch, Error> {
let askpass_broker = handle
.state::<crate::askpass::AskpassBroker>()
.inner()
.clone();
let base_branch = handle
.state::<Controller>()
.fetch_from_target(
&project_id,
Some((
askpass_broker,
action.unwrap_or_else(|| "unknown".to_string()),
)),
)
.await?;
emit_vbranches(&handle, &project_id).await;
Ok(base_branch)
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn move_commit(
handle: tauri::AppHandle,
project_id: ProjectId,
commit_oid: git::Oid,
target_branch_id: BranchId,
) -> Result<(), Error> {
handle
.state::<Controller>()
.move_commit(&project_id, &target_branch_id, commit_oid)
.await?;
emit_vbranches(&handle, &project_id).await;
Ok(())
}
// XXX(qix-): Is this command used?
#[allow(dead_code)]
pub async fn update_commit_message(
handle: tauri::AppHandle,
project_id: ProjectId,
branch_id: BranchId,
commit_oid: git::Oid,
message: &str,
) -> Result<(), Error> {
handle
.state::<Controller>()
.update_commit_message(&project_id, &branch_id, commit_oid, message)
.await?;
emit_vbranches(&handle, &project_id).await;
Ok(())
}
async fn emit_vbranches(handle: &AppHandle, project_id: &projects::ProjectId) {
if let Err(error) = handle
.state::<watcher::Watchers>()
.post(watcher::Event::CalculateVirtualBranches(*project_id))
.await
{
tracing::error!(?error);
}
}

View File

@ -17,7 +17,7 @@ use tokio::{
};
use tokio_util::sync::CancellationToken;
use crate::projects::{self, ProjectId};
use gitbutler::projects::{self, Project, ProjectId};
#[derive(Clone)]
pub struct Watchers {
@ -80,6 +80,25 @@ impl Watchers {
}
}
#[async_trait::async_trait]
impl gitbutler::projects::Watchers for Watchers {
fn watch(&self, project: &Project) -> Result<()> {
Watchers::watch(self, project)
}
async fn stop(&self, id: ProjectId) -> Result<()> {
Watchers::stop(self, &id).await
}
async fn fetch(&self, id: ProjectId) -> Result<()> {
self.post(Event::FetchGitbutlerData(id)).await
}
async fn push(&self, id: ProjectId) -> Result<()> {
self.post(Event::PushGitbutlerData(id)).await
}
}
#[derive(Clone)]
struct Watcher {
inner: Arc<WatcherInner>,

View File

@ -10,7 +10,7 @@ use tokio::{
};
use tokio_util::sync::CancellationToken;
use crate::projects::ProjectId;
use gitbutler::projects::ProjectId;
use super::events;

View File

@ -13,7 +13,8 @@ use tokio::{
task,
};
use crate::{git, projects::ProjectId, watcher::events};
use crate::watcher::events;
use gitbutler::{git, projects::ProjectId};
#[derive(Debug, Clone)]
pub struct Dispatcher {

View File

@ -1,12 +1,14 @@
use std::{fmt::Display, path};
use crate::{
analytics, deltas, events,
use gitbutler::{
deltas,
projects::ProjectId,
reader,
sessions::{self, SessionId},
};
use crate::{analytics, events};
#[derive(Debug, PartialEq, Clone)]
pub enum Event {
Flush(ProjectId, sessions::Session),

View File

@ -1,7 +1,8 @@
use anyhow::{Context, Result};
use tauri::{AppHandle, Manager};
use crate::{analytics, users};
use crate::analytics;
use gitbutler::users;
use super::events;

View File

@ -3,7 +3,7 @@ use std::{path, vec};
use anyhow::{Context, Result};
use tauri::{AppHandle, Manager};
use crate::{
use gitbutler::{
deltas, gb_repository, project_repository,
projects::{self, ProjectId},
reader, sessions, users,

View File

@ -9,8 +9,9 @@ use governor::{
use tauri::{AppHandle, Manager};
use tokio::sync::Mutex;
use crate::{
assets, events as app_events,
use crate::events as app_events;
use gitbutler::{
assets,
projects::ProjectId,
virtual_branches::{self, controller::ControllerError, VirtualBranches},
};

View File

@ -4,7 +4,7 @@ use anyhow::{Context, Result};
use tauri::{AppHandle, Manager};
use tokio::sync::Mutex;
use crate::{gb_repository, project_repository, projects, projects::ProjectId, users};
use gitbutler::{gb_repository, project_repository, projects, projects::ProjectId, users};
use super::events;

View File

@ -10,7 +10,7 @@ use governor::{
use tauri::{AppHandle, Manager};
use tokio::sync::Mutex;
use crate::{
use gitbutler::{
project_repository,
projects::{self, ProjectId},
};

View File

@ -4,7 +4,9 @@ use anyhow::{Context, Result};
use tauri::{AppHandle, Manager};
use tokio::sync::Mutex;
use crate::{gb_repository, project_repository, projects, projects::ProjectId, sessions, users};
use gitbutler::{
gb_repository, project_repository, projects, projects::ProjectId, sessions, users,
};
use super::events;

View File

@ -3,8 +3,9 @@ use std::path;
use anyhow::{Context, Result};
use tauri::{AppHandle, Manager};
use crate::{
analytics, events as app_events, gb_repository, git, project_repository,
use crate::{analytics, events as app_events};
use gitbutler::{
gb_repository, git, project_repository,
projects::{self, ProjectId},
users,
};

View File

@ -3,8 +3,9 @@ use std::path;
use anyhow::{Context, Result};
use tauri::{AppHandle, Manager};
use crate::{
deltas, events as app_events, gb_repository, project_repository,
use crate::events as app_events;
use gitbutler::{
deltas, gb_repository, project_repository,
projects::{self, ProjectId},
sessions::{self, SessionId},
users,

View File

@ -4,9 +4,9 @@ use std::sync::{Arc, Mutex, TryLockError};
use anyhow::{Context, Result};
use tauri::{AppHandle, Manager};
use crate::gb_repository::RemoteError;
use crate::projects::ProjectId;
use crate::{gb_repository, project_repository, projects, users};
use gitbutler::gb_repository::RemoteError;
use gitbutler::projects::ProjectId;
use gitbutler::{gb_repository, project_repository, projects, users};
use super::events;

View File

@ -5,7 +5,7 @@ use itertools::Itertools;
use tauri::{AppHandle, Manager};
use tokio::sync::Mutex;
use crate::{
use gitbutler::{
gb_repository,
git::{self, Oid, Repository},
project_repository,
@ -129,9 +129,9 @@ impl Handler {
async fn push_target(
state: &State,
project_repository: &project_repository::Repository,
default_target: &crate::virtual_branches::target::Target,
default_target: &gitbutler::virtual_branches::target::Target,
gb_code_last_commit: Option<Oid>,
project_id: &crate::id::Id<projects::Project>,
project_id: &gitbutler::id::Id<projects::Project>,
user: &Option<users::User>,
) -> Result<(), project_repository::RemoteError> {
let ids = batch_rev_walk(
@ -181,7 +181,7 @@ impl Handler {
async fn update_project(
state: &State,
project_id: &crate::id::Id<projects::Project>,
project_id: &gitbutler::id::Id<projects::Project>,
id: &Oid,
) -> Result<(), project_repository::RemoteError> {
state
@ -211,7 +211,7 @@ struct State {
fn push_all_refs(
project_repository: &project_repository::Repository,
user: &Option<users::User>,
project_id: &crate::id::Id<projects::Project>,
project_id: &gitbutler::id::Id<projects::Project>,
) -> Result<(), project_repository::RemoteError> {
let gb_references = collect_refs(project_repository)?;

View File

@ -1,165 +1,87 @@
pub mod commands;
mod controller;
pub use controller::Controller;
pub mod commands {
#![allow(clippy::used_underscore_binding)]
use std::path;
use std::{
fs,
io::{self, Read, Write},
path, time,
};
use tauri::{AppHandle, Manager};
use tracing::instrument;
use anyhow::{Context, Result};
use sha2::{Digest, Sha256};
use walkdir::{DirEntry, WalkDir};
use zip::{result::ZipError, write, CompressionMethod, ZipWriter};
use crate::error::{Code, Error};
#[derive(Clone)]
pub struct Zipper {
cache: path::PathBuf,
}
use gitbutler::zip::controller;
impl Zipper {
pub fn new<P: AsRef<path::Path>>(cache_dir: P) -> Self {
let cache = cache_dir.as_ref().to_path_buf().join("archives");
Self { cache }
}
// takes a path to create zip of, returns path of a created archive.
pub fn zip<P: AsRef<path::Path>>(&self, path: P) -> Result<path::PathBuf> {
let path = path.as_ref();
if !path.exists() {
return Err(anyhow::anyhow!("{} does not exist", path.display()));
}
if !path.is_dir() {
return Err(anyhow::anyhow!("{} is not a directory", path.display()));
}
let path_hash = calculate_path_hash(path)?;
fs::create_dir_all(&self.cache).context("failed to create cache dir")?;
let archive_path = self.cache.join(format!("{}.zip", path_hash));
if !archive_path.exists() {
doit(path, &archive_path, CompressionMethod::Bzip2)?;
}
Ok(archive_path)
}
}
fn doit<P: AsRef<path::Path>>(
src_dir: P,
dst_file: P,
method: zip::CompressionMethod,
) -> zip::result::ZipResult<()> {
let src = src_dir.as_ref();
let dst = dst_file.as_ref();
if !src.is_dir() {
return Err(ZipError::FileNotFound);
}
let file = fs::File::create(dst).unwrap();
let walkdir = WalkDir::new(src);
let it = walkdir.into_iter();
zip_dir(&mut it.filter_map(Result::ok), src, file, method)?;
Ok(())
}
fn zip_dir<T>(
it: &mut dyn Iterator<Item = DirEntry>,
prefix: &path::Path,
writer: T,
method: zip::CompressionMethod,
) -> zip::result::ZipResult<()>
where
T: io::Write + io::Seek,
{
let mut zip = ZipWriter::new(writer);
let options = write::FileOptions::default()
.compression_method(method)
.unix_permissions(0o755);
let mut buffer = Vec::new();
for entry in it {
let path = entry.path();
let name = path.strip_prefix(prefix).unwrap();
// Write file or directory explicitly
// Some unzip tools unzip files with directory paths correctly, some do not!
if path.is_file() {
#[allow(deprecated)]
zip.start_file_from_path(name, options)?;
let mut f = fs::File::open(path)?;
f.read_to_end(&mut buffer)?;
zip.write_all(&buffer)?;
buffer.clear();
} else if !name.as_os_str().is_empty() {
// Only if not root! Avoids path spec / warning
// and mapname conversion failed error on unzip
#[allow(deprecated)]
zip.add_directory_from_path(name, options)?;
impl From<controller::ArchiveError> for Error {
fn from(error: controller::ArchiveError) -> Self {
match error {
controller::ArchiveError::GetProject(error) => error.into(),
controller::ArchiveError::Other(error) => {
tracing::error!(?error, "failed to archive project");
Error::Unknown
}
}
}
}
zip.finish()?;
Result::Ok(())
}
// returns hash of a path by calculating metadata hash of all files in it.
fn calculate_path_hash<P: AsRef<path::Path>>(path: P) -> Result<String> {
let path = path.as_ref();
let mut hasher = Sha256::new();
if path.is_dir() {
let entries = fs::read_dir(path)?;
let mut entry_paths: Vec<_> = entries
.filter_map(|entry| entry.ok().map(|e| e.path()))
.collect();
entry_paths.sort();
for entry_path in entry_paths {
file_hash(&mut hasher, &entry_path).with_context(|| {
format!(
"failed to calculate hash of file {}",
entry_path.to_str().unwrap()
)
})?;
}
} else if path.is_file() {
file_hash(&mut hasher, path).with_context(|| {
format!(
"failed to calculate hash of file {}",
path.to_str().unwrap()
)
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn get_project_archive_path(
handle: AppHandle,
project_id: &str,
) -> Result<path::PathBuf, Error> {
let project_id = project_id.parse().map_err(|_| Error::UserError {
code: Code::Validation,
message: "Malformed project id".into(),
})?;
handle
.state::<controller::Controller>()
.archive(&project_id)
.map_err(Into::into)
}
Ok(format!("{:X}", hasher.finalize()))
}
impl From<controller::DataArchiveError> for Error {
fn from(value: controller::DataArchiveError) -> Self {
match value {
controller::DataArchiveError::GetProject(error) => error.into(),
controller::DataArchiveError::Other(error) => {
tracing::error!(?error, "failed to archive project data");
Error::Unknown
}
}
}
}
fn file_hash<P: AsRef<path::Path>>(digest: &mut Sha256, path: P) -> Result<()> {
let path = path.as_ref();
let metadata = fs::metadata(path).context("failed to get metadata")?;
digest.update(path.to_str().unwrap().as_bytes());
digest.update(metadata.len().to_string().as_bytes());
digest.update(
metadata
.modified()
.unwrap_or(time::UNIX_EPOCH)
.duration_since(time::UNIX_EPOCH)
.unwrap()
.as_secs()
.to_string()
.as_bytes(),
);
digest.update(
metadata
.created()
.unwrap_or(time::UNIX_EPOCH)
.duration_since(time::UNIX_EPOCH)
.unwrap()
.as_secs()
.to_string()
.as_bytes(),
);
Ok(())
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn get_project_data_archive_path(
handle: AppHandle,
project_id: &str,
) -> Result<path::PathBuf, Error> {
let project_id = project_id.parse().map_err(|_| Error::UserError {
code: Code::Validation,
message: "Malformed project id".into(),
})?;
handle
.state::<controller::Controller>()
.data_archive(&project_id)
.map_err(Into::into)
}
impl From<controller::LogsArchiveError> for Error {
fn from(error: controller::LogsArchiveError) -> Self {
match error {
controller::LogsArchiveError::Other(error) => {
tracing::error!(?error, "failed to archive logs");
Error::Unknown
}
}
}
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn get_logs_archive_path(handle: AppHandle) -> Result<path::PathBuf, Error> {
handle
.state::<controller::Controller>()
.logs_archive()
.map_err(Into::into)
}
}

View File

@ -1,85 +0,0 @@
#![allow(clippy::used_underscore_binding)]
use std::path;
use tauri::{AppHandle, Manager};
use tracing::instrument;
use crate::error::{Code, Error};
use super::controller;
impl From<controller::ArchiveError> for Error {
fn from(error: controller::ArchiveError) -> Self {
match error {
controller::ArchiveError::GetProject(error) => error.into(),
controller::ArchiveError::Other(error) => {
tracing::error!(?error, "failed to archive project");
Error::Unknown
}
}
}
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn get_project_archive_path(
handle: AppHandle,
project_id: &str,
) -> Result<path::PathBuf, Error> {
let project_id = project_id.parse().map_err(|_| Error::UserError {
code: Code::Validation,
message: "Malformed project id".into(),
})?;
handle
.state::<controller::Controller>()
.archive(&project_id)
.map_err(Into::into)
}
impl From<controller::DataArchiveError> for Error {
fn from(value: controller::DataArchiveError) -> Self {
match value {
controller::DataArchiveError::GetProject(error) => error.into(),
controller::DataArchiveError::Other(error) => {
tracing::error!(?error, "failed to archive project data");
Error::Unknown
}
}
}
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn get_project_data_archive_path(
handle: AppHandle,
project_id: &str,
) -> Result<path::PathBuf, Error> {
let project_id = project_id.parse().map_err(|_| Error::UserError {
code: Code::Validation,
message: "Malformed project id".into(),
})?;
handle
.state::<controller::Controller>()
.data_archive(&project_id)
.map_err(Into::into)
}
impl From<controller::LogsArchiveError> for Error {
fn from(error: controller::LogsArchiveError) -> Self {
match error {
controller::LogsArchiveError::Other(error) => {
tracing::error!(?error, "failed to archive logs");
Error::Unknown
}
}
}
}
#[tauri::command(async)]
#[instrument(skip(handle))]
pub async fn get_logs_archive_path(handle: AppHandle) -> Result<path::PathBuf, Error> {
handle
.state::<controller::Controller>()
.logs_archive()
.map_err(Into::into)
}

View File

@ -1,261 +1,4 @@
const VAR_NO_CLEANUP: &str = "GITBUTLER_TESTS_NO_CLEANUP";
pub(crate) mod common;
mod suite {
mod gb_repository;
mod projects;
mod virtual_branches;
}
mod database;
mod deltas;
mod gb_repository;
mod git;
mod keys;
mod lock;
mod reader;
mod sessions;
mod types;
pub mod virtual_branches;
// TODO(ST): move test code into crate and use that, but wait for `crates/`
#[path = "../../tests/shared/mod.rs"]
pub mod shared;
mod watcher;
mod zip;
use std::path::{Path, PathBuf};
use std::{collections::HashMap, fs};
use tempfile::{tempdir, TempDir};
pub struct Suite {
pub local_app_data: Option<TempDir>,
pub storage: gitbutler_app::storage::Storage,
pub users: gitbutler_app::users::Controller,
pub projects: gitbutler_app::projects::Controller,
pub keys: gitbutler_app::keys::Controller,
}
impl Drop for Suite {
fn drop(&mut self) {
if std::env::var_os(VAR_NO_CLEANUP).is_some() {
let _ = self.local_app_data.take().unwrap().into_path();
}
}
}
impl Default for Suite {
fn default() -> Self {
let local_app_data = temp_dir();
let storage = gitbutler_app::storage::Storage::new(&local_app_data);
let users = gitbutler_app::users::Controller::from_path(&local_app_data);
let projects = gitbutler_app::projects::Controller::from_path(&local_app_data);
let keys = gitbutler_app::keys::Controller::from_path(&local_app_data);
Self {
storage,
local_app_data: Some(local_app_data),
users,
projects,
keys,
}
}
}
impl Suite {
pub fn local_app_data(&self) -> &Path {
self.local_app_data.as_ref().unwrap().path()
}
pub fn sign_in(&self) -> gitbutler_app::users::User {
let user = gitbutler_app::users::User {
name: Some("test".to_string()),
email: "test@email.com".to_string(),
access_token: "token".to_string(),
..Default::default()
};
self.users.set_user(&user).expect("failed to add user");
user
}
fn project(&self, fs: HashMap<PathBuf, &str>) -> (gitbutler_app::projects::Project, TempDir) {
let (repository, tmp) = test_repository();
for (path, contents) in fs {
if let Some(parent) = path.parent() {
fs::create_dir_all(repository.path().parent().unwrap().join(parent))
.expect("failed to create dir");
}
fs::write(
repository.path().parent().unwrap().join(&path),
contents.as_bytes(),
)
.expect("failed to write file");
}
commit_all(&repository);
(
self.projects
.add(repository.path().parent().unwrap())
.expect("failed to add project"),
tmp,
)
}
pub fn new_case_with_files(&self, fs: HashMap<PathBuf, &str>) -> Case {
let (project, project_tmp) = self.project(fs);
Case::new(self, project, project_tmp)
}
pub fn new_case(&self) -> Case {
self.new_case_with_files(HashMap::new())
}
}
pub struct Case<'a> {
suite: &'a Suite,
pub project: gitbutler_app::projects::Project,
pub project_repository: gitbutler_app::project_repository::Repository,
pub gb_repository: gitbutler_app::gb_repository::Repository,
pub credentials: gitbutler_app::git::credentials::Helper,
/// The directory containing the `project_repository`
project_tmp: Option<TempDir>,
}
impl Drop for Case<'_> {
fn drop(&mut self) {
if let Some(tmp) = self
.project_tmp
.take()
.filter(|_| std::env::var_os(VAR_NO_CLEANUP).is_some())
{
let _ = tmp.into_path();
}
}
}
impl<'a> Case<'a> {
fn new(
suite: &'a Suite,
project: gitbutler_app::projects::Project,
project_tmp: TempDir,
) -> Case<'a> {
let project_repository = gitbutler_app::project_repository::Repository::open(&project)
.expect("failed to create project repository");
let gb_repository = gitbutler_app::gb_repository::Repository::open(
suite.local_app_data(),
&project_repository,
None,
)
.expect("failed to open gb repository");
let credentials =
gitbutler_app::git::credentials::Helper::from_path(suite.local_app_data());
Case {
suite,
project,
gb_repository,
project_repository,
project_tmp: Some(project_tmp),
credentials,
}
}
pub fn refresh(mut self) -> Self {
let project = self
.suite
.projects
.get(&self.project.id)
.expect("failed to get project");
let project_repository = gitbutler_app::project_repository::Repository::open(&project)
.expect("failed to create project repository");
let user = self.suite.users.get_user().expect("failed to get user");
let credentials =
gitbutler_app::git::credentials::Helper::from_path(self.suite.local_app_data());
Self {
suite: self.suite,
gb_repository: gitbutler_app::gb_repository::Repository::open(
self.suite.local_app_data(),
&project_repository,
user.as_ref(),
)
.expect("failed to open gb repository"),
credentials,
project_repository,
project,
project_tmp: self.project_tmp.take(),
}
}
}
pub fn test_database() -> (gitbutler_app::database::Database, TempDir) {
let tmp = temp_dir();
let db = gitbutler_app::database::Database::open_in_directory(&tmp).unwrap();
(db, tmp)
}
pub fn temp_dir() -> TempDir {
tempdir().unwrap()
}
pub fn empty_bare_repository() -> (gitbutler_app::git::Repository, TempDir) {
let tmp = temp_dir();
(
gitbutler_app::git::Repository::init_opts(&tmp, &init_opts_bare())
.expect("failed to init repository"),
tmp,
)
}
pub fn test_repository() -> (gitbutler_app::git::Repository, TempDir) {
let tmp = temp_dir();
let repository = gitbutler_app::git::Repository::init_opts(&tmp, &init_opts())
.expect("failed to init repository");
let mut index = repository.index().expect("failed to get index");
let oid = index.write_tree().expect("failed to write tree");
let signature = gitbutler_app::git::Signature::now("test", "test@email.com").unwrap();
repository
.commit(
Some(&"refs/heads/master".parse().unwrap()),
&signature,
&signature,
"Initial commit",
&repository.find_tree(oid).expect("failed to find tree"),
&[],
)
.expect("failed to commit");
(repository, tmp)
}
pub fn commit_all(repository: &gitbutler_app::git::Repository) -> gitbutler_app::git::Oid {
let mut index = repository.index().expect("failed to get index");
index
.add_all(["."], git2::IndexAddOption::DEFAULT, None)
.expect("failed to add all");
index.write().expect("failed to write index");
let oid = index.write_tree().expect("failed to write tree");
let signature = gitbutler_app::git::Signature::now("test", "test@email.com").unwrap();
let head = repository.head().expect("failed to get head");
let commit_oid = repository
.commit(
Some(&head.name().unwrap()),
&signature,
&signature,
"some commit",
&repository.find_tree(oid).expect("failed to find tree"),
&[&repository
.find_commit(
repository
.refname_to_id("HEAD")
.expect("failed to get head"),
)
.expect("failed to find commit")],
)
.expect("failed to commit");
commit_oid
}
fn init_opts() -> git2::RepositoryInitOptions {
let mut opts = git2::RepositoryInitOptions::new();
opts.initial_head("master");
opts
}
pub fn init_opts_bare() -> git2::RepositoryInitOptions {
let mut opts = init_opts();
opts.bare(true);
opts
}

View File

@ -7,13 +7,13 @@ use std::{
use once_cell::sync::Lazy;
use crate::{commit_all, Case, Suite};
use gitbutler_app::watcher::handlers::calculate_deltas_handler::Handler;
use gitbutler_app::{
use crate::shared::{commit_all, Case, Suite};
use gitbutler::{
deltas::{self, operations::Operation},
reader, sessions,
virtual_branches::{self, branch},
};
use gitbutler_app::watcher::handlers::calculate_deltas_handler::Handler;
use self::branch::BranchId;
@ -663,7 +663,7 @@ fn should_persist_branches_targets_state_between_sessions() -> Result<()> {
let branches = virtual_branches::Iterator::new(&session_reader)
.unwrap()
.collect::<Result<Vec<virtual_branches::Branch>, gitbutler_app::reader::Error>>()
.collect::<Result<Vec<virtual_branches::Branch>, gitbutler::reader::Error>>()
.unwrap()
.into_iter()
.collect::<Vec<virtual_branches::Branch>>();
@ -719,7 +719,7 @@ fn should_restore_branches_targets_state_from_head_session() -> Result<()> {
let branches = virtual_branches::Iterator::new(&session_reader)
.unwrap()
.collect::<Result<Vec<virtual_branches::Branch>, gitbutler_app::reader::Error>>()
.collect::<Result<Vec<virtual_branches::Branch>, gitbutler::reader::Error>>()
.unwrap()
.into_iter()
.collect::<Vec<virtual_branches::Branch>>();

View File

@ -1,10 +1,10 @@
use std::time::SystemTime;
use gitbutler_app::projects;
use gitbutler::projects;
use pretty_assertions::assert_eq;
use crate::shared::{Case, Suite};
use crate::watcher::handler::test_remote_repository;
use crate::{Case, Suite};
use gitbutler_app::watcher::handlers::fetch_gitbutler_data::Handler;
#[tokio::test]

View File

@ -1,10 +1,10 @@
use anyhow::Result;
use std::fs;
use gitbutler_app::projects;
use gitbutler::projects;
use pretty_assertions::assert_eq;
use crate::{Case, Suite};
use crate::shared::{Case, Suite};
use gitbutler_app::watcher::handlers::git_file_change::Handler;
use gitbutler_app::watcher::{handlers, Event};

View File

@ -1,4 +1,4 @@
use crate::init_opts_bare;
use crate::shared::init_opts_bare;
use tempfile::TempDir;
fn test_remote_repository() -> anyhow::Result<(git2::Repository, TempDir)> {

View File

@ -1,12 +1,12 @@
use anyhow::Result;
use gitbutler_app::{git, projects};
use gitbutler::{git, projects};
use std::collections::HashMap;
use std::path::PathBuf;
use crate::virtual_branches::set_test_target;
use crate::shared::virtual_branches::set_test_target;
use crate::shared::{Case, Suite};
use crate::watcher::handler::test_remote_repository;
use crate::{Case, Suite};
use gitbutler_app::project_repository::LogUntil;
use gitbutler::project_repository::LogUntil;
use gitbutler_app::watcher::handlers::push_project_to_gitbutler::Handler;
fn log_walk(repo: &git2::Repository, head: git::Oid) -> Vec<git::Oid> {

63
src/askpass.rs Normal file
View File

@ -0,0 +1,63 @@
use std::{collections::HashMap, sync::Arc};
use serde::Serialize;
use tokio::sync::{oneshot, Mutex};
use crate::id::Id;
use crate::virtual_branches::BranchId;
pub struct AskpassRequest {
sender: oneshot::Sender<Option<String>>,
}
#[derive(Debug, Clone, serde::Serialize)]
// This is needed to end up with a struct with either `branch_id` or `action`
#[serde(untagged)]
pub enum Context {
Push { branch_id: Option<BranchId> },
Fetch { action: String },
}
#[derive(Clone)]
pub struct AskpassBroker {
pending_requests: Arc<Mutex<HashMap<Id<AskpassRequest>, AskpassRequest>>>,
submit_prompt_event: Arc<dyn Fn(PromptEvent<Context>) + Send + Sync>,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct PromptEvent<C: Serialize + Clone> {
id: Id<AskpassRequest>,
prompt: String,
context: C,
}
impl AskpassBroker {
pub fn init(submit_prompt: impl Fn(PromptEvent<Context>) + Send + Sync + 'static) -> Self {
Self {
pending_requests: Arc::new(Mutex::new(HashMap::new())),
submit_prompt_event: Arc::new(submit_prompt),
}
}
pub async fn submit_prompt(&self, prompt: String, context: Context) -> Option<String> {
let (sender, receiver) = oneshot::channel();
let id = Id::generate();
let request = AskpassRequest { sender };
self.pending_requests.lock().await.insert(id, request);
(self.submit_prompt_event)(PromptEvent {
id,
prompt,
context,
});
receiver.await.unwrap()
}
pub async fn handle_response(&self, id: Id<AskpassRequest>, response: Option<String>) {
let mut pending_requests = self.pending_requests.lock().await;
if let Some(request) = pending_requests.remove(&id) {
let _ = request.sender.send(response);
} else {
log::warn!("received response for unknown askpass request: {}", id);
}
}
}

15
src/deltas.rs Normal file
View File

@ -0,0 +1,15 @@
pub mod controller;
mod delta;
mod document;
mod reader;
mod writer;
pub mod database;
pub mod operations;
pub use controller::Controller;
pub use database::Database;
pub use delta::Delta;
pub use document::Document;
pub use reader::DeltasReader as Reader;
pub use writer::DeltasWriter as Writer;

414
src/error.rs Normal file
View File

@ -0,0 +1,414 @@
#[cfg(feature = "sentry")]
mod sentry;
pub use legacy::*;
pub mod gb {
#[cfg(feature = "error-context")]
pub use error_context::*;
#[cfg(feature = "error-context")]
mod error_context {
use super::{ErrorKind, Result, WithContext};
use backtrace::Backtrace;
use std::collections::BTreeMap;
#[derive(Debug)]
pub struct Context {
pub backtrace: Backtrace,
pub caused_by: Option<Box<ErrorContext>>,
pub vars: BTreeMap<String, String>,
}
impl Default for Context {
fn default() -> Self {
Self {
backtrace: Backtrace::new_unresolved(),
caused_by: None,
vars: BTreeMap::default(),
}
}
}
#[derive(Debug)]
pub struct ErrorContext {
error: ErrorKind,
context: Context,
}
impl core::fmt::Display for ErrorContext {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
self.error.fmt(f)
}
}
impl std::error::Error for ErrorContext {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.context
.caused_by
.as_ref()
.map(|e| e as &dyn std::error::Error)
}
fn provide<'a>(&'a self, request: &mut std::error::Request<'a>) {
if request.would_be_satisfied_by_ref_of::<Backtrace>() {
request.provide_ref(&self.context.backtrace);
}
}
}
impl ErrorContext {
#[inline]
pub fn error(&self) -> &ErrorKind {
&self.error
}
#[inline]
pub fn context(&self) -> &Context {
&self.context
}
pub fn into_owned(self) -> (ErrorKind, Context) {
(self.error, self.context)
}
}
impl<E: Into<ErrorContext>> WithContext<ErrorContext> for E {
fn add_err_context<K: Into<String>, V: Into<String>>(
self,
name: K,
value: V,
) -> ErrorContext {
let mut e = self.into();
e.context.vars.insert(name.into(), value.into());
e
}
fn wrap_err<K: Into<ErrorKind>>(self, error: K) -> ErrorContext {
let mut new_err = ErrorContext {
error: error.into(),
context: Context::default(),
};
new_err.context.caused_by = Some(Box::new(self.into()));
new_err
}
}
impl<T, E> WithContext<Result<T>> for std::result::Result<T, E>
where
E: Into<ErrorKind>,
{
#[inline]
fn add_err_context<K: Into<String>, V: Into<String>>(
self,
name: K,
value: V,
) -> Result<T> {
self.map_err(|e| {
ErrorContext {
error: e.into(),
context: Context::default(),
}
.add_err_context(name, value)
})
}
#[inline]
fn wrap_err<K: Into<ErrorKind>>(self, error: K) -> Result<T> {
self.map_err(|e| {
ErrorContext {
error: e.into(),
context: Context::default(),
}
.wrap_err(error)
})
}
}
#[cfg(feature = "error-context")]
impl serde::Serialize for ErrorContext {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeSeq;
let mut seq = serializer.serialize_seq(None)?;
let mut current = Some(self);
while let Some(err) = current {
seq.serialize_element(&err.error)?;
current = err.context.caused_by.as_deref();
}
seq.end()
}
}
impl From<ErrorKind> for ErrorContext {
fn from(error: ErrorKind) -> Self {
Self {
error,
context: Context::default(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn error_context() {
fn low_level_io() -> std::result::Result<(), std::io::Error> {
Err(std::io::Error::new(std::io::ErrorKind::Other, "oh no!"))
}
fn app_level_io() -> Result<()> {
low_level_io().add_err_context("foo", "bar")?;
unreachable!();
}
use std::error::Error;
let r = app_level_io();
assert!(r.is_err());
let e = r.unwrap_err();
assert_eq!(e.context().vars.get("foo"), Some(&"bar".to_string()));
assert!(e.source().is_none());
assert!(e.to_string().starts_with("io.other-error:"));
}
}
}
pub trait WithContext<R> {
fn add_err_context<K: Into<String>, V: Into<String>>(self, name: K, value: V) -> R;
fn wrap_err<E: Into<ErrorKind>>(self, error: E) -> R;
}
#[cfg(not(feature = "error-context"))]
pub struct Context;
pub trait ErrorCode {
fn code(&self) -> String;
fn message(&self) -> String;
}
#[derive(Debug, thiserror::Error)]
pub enum ErrorKind {
Io(#[from] ::std::io::Error),
Git(#[from] ::git2::Error),
CommonDirNotAvailable(String),
}
impl ErrorCode for std::io::Error {
fn code(&self) -> String {
slug::slugify(self.kind().to_string())
}
fn message(&self) -> String {
self.to_string()
}
}
impl ErrorCode for git2::Error {
fn code(&self) -> String {
slug::slugify(format!("{:?}", self.class()))
}
fn message(&self) -> String {
self.to_string()
}
}
impl ErrorCode for ErrorKind {
fn code(&self) -> String {
match self {
ErrorKind::Io(e) => format!("io.{}", <std::io::Error as ErrorCode>::code(e)),
ErrorKind::Git(e) => format!("git.{}", <git2::Error as ErrorCode>::code(e)),
ErrorKind::CommonDirNotAvailable(_) => "no-common-dir".to_string(),
}
}
fn message(&self) -> String {
match self {
ErrorKind::Io(e) => <std::io::Error as ErrorCode>::message(e),
ErrorKind::Git(e) => <git2::Error as ErrorCode>::message(e),
ErrorKind::CommonDirNotAvailable(s) => format!("{s} is not available"),
}
}
}
impl core::fmt::Display for ErrorKind {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
format!(
"{}: {}",
<Self as ErrorCode>::code(self),
<Self as ErrorCode>::message(self)
)
.fmt(f)
}
}
#[cfg(not(feature = "error-context"))]
pub type Error = ErrorKind;
#[cfg(feature = "error-context")]
pub type Error = ErrorContext;
pub type Result<T> = ::std::result::Result<T, Error>;
#[cfg(not(feature = "error-context"))]
impl ErrorKind {
#[inline]
pub fn error(&self) -> &Error {
self
}
#[inline]
pub fn context(&self) -> Option<&Context> {
None
}
}
#[cfg(not(feature = "error-context"))]
impl WithContext<ErrorKind> for ErrorKind {
#[inline]
fn add_err_context<K: Into<String>, V: Into<String>>(self, _name: K, _value: V) -> Error {
self
}
#[inline]
fn wrap_err(self, _error: Error) -> Error {
self
}
}
#[cfg(not(feature = "error-context"))]
impl<T, E> WithContext<std::result::Result<T, E>> for std::result::Result<T, E> {
#[inline]
fn add_err_context<K: Into<String>, V: Into<String>>(
self,
_name: K,
_value: V,
) -> std::result::Result<T, E> {
self
}
#[inline]
fn wrap_err(self, _error: Error) -> std::result::Result<T, E> {
self
}
}
#[cfg(feature = "error-context")]
impl serde::Serialize for ErrorKind {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeTuple;
let mut seq = serializer.serialize_tuple(2)?;
seq.serialize_element(&self.code())?;
seq.serialize_element(&self.message())?;
seq.end()
}
}
}
//#[deprecated(
// note = "the types in the error::legacy::* module are deprecated; use error::gb::Error and error::gb::Result instead"
//)]
mod legacy {
use core::fmt;
use crate::{keys, projects, users};
use serde::{ser::SerializeMap, Serialize};
#[derive(Debug)]
pub enum Code {
Unknown,
Validation,
Projects,
Branches,
ProjectGitAuth,
ProjectGitRemote,
ProjectConflict,
ProjectHead,
Menu,
PreCommitHook,
CommitMsgHook,
}
impl fmt::Display for Code {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Code::Menu => write!(f, "errors.menu"),
Code::Unknown => write!(f, "errors.unknown"),
Code::Validation => write!(f, "errors.validation"),
Code::Projects => write!(f, "errors.projects"),
Code::Branches => write!(f, "errors.branches"),
Code::ProjectGitAuth => write!(f, "errors.projects.git.auth"),
Code::ProjectGitRemote => write!(f, "errors.projects.git.remote"),
Code::ProjectHead => write!(f, "errors.projects.head"),
Code::ProjectConflict => write!(f, "errors.projects.conflict"),
//TODO: rename js side to be more precise what kind of hook error this is
Code::PreCommitHook => write!(f, "errors.hook"),
Code::CommitMsgHook => write!(f, "errors.hooks.commit.msg"),
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("[{code}]: {message}")]
UserError { code: Code, message: String },
#[error("[errors.unknown]: Something went wrong")]
Unknown,
}
impl Serialize for Error {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let (code, message) = match self {
Error::UserError { code, message } => (code.to_string(), message.to_string()),
Error::Unknown => (
Code::Unknown.to_string(),
"Something went wrong".to_string(),
),
};
let mut map = serializer.serialize_map(Some(2))?;
map.serialize_entry("code", &code)?;
map.serialize_entry("message", &message)?;
map.end()
}
}
impl From<anyhow::Error> for Error {
fn from(error: anyhow::Error) -> Self {
tracing::error!(?error);
Error::Unknown
}
}
impl From<keys::GetOrCreateError> for Error {
fn from(error: keys::GetOrCreateError) -> Self {
tracing::error!(?error);
Error::Unknown
}
}
impl From<users::GetError> for Error {
fn from(error: users::GetError) -> Self {
tracing::error!(?error);
Error::Unknown
}
}
impl From<projects::controller::GetError> for Error {
fn from(error: projects::controller::GetError) -> Self {
tracing::error!(?error);
Error::Unknown
}
}
}

89
src/error/sentry.rs Normal file
View File

@ -0,0 +1,89 @@
use crate::error::gb::{ErrorCode, ErrorContext};
use sentry::{
protocol::{value::Map, Event, Exception, Value},
types::Uuid,
};
use std::collections::BTreeMap;
pub trait SentrySender {
fn send_to_sentry(self) -> Uuid;
}
impl<E: Into<ErrorContext>> SentrySender for E {
fn send_to_sentry(self) -> Uuid {
let sentry_event = self.into().into();
sentry::capture_event(sentry_event)
}
}
trait PopulateException {
fn populate_exception(
self,
exceptions: &mut Vec<Exception>,
vars: &mut BTreeMap<String, Value>,
);
}
impl PopulateException for ErrorContext {
fn populate_exception(
self,
exceptions: &mut Vec<Exception>,
vars: &mut BTreeMap<String, Value>,
) {
let (error, mut context) = self.into_owned();
let mut exc = Exception {
ty: error.code(),
value: Some(error.message()),
..Exception::default()
};
if let Some(cause) = context.caused_by {
cause.populate_exception(exceptions, vars);
}
// We don't resolve at capture time because it can DRASTICALLY
// slow down the application (can take up to 0.5s to resolve
// a *single* frame). We do it here, only when a Sentry event
// is being created.
context.backtrace.resolve();
exc.stacktrace =
sentry::integrations::backtrace::backtrace_to_stacktrace(&context.backtrace);
::backtrace::clear_symbol_cache();
vars.insert(
error.code(),
Value::Object(
context
.vars
.into_iter()
.map(|(k, v)| (k, v.into()))
.collect(),
),
);
exceptions.push(exc);
}
}
impl From<ErrorContext> for Event<'_> {
fn from(error_context: ErrorContext) -> Self {
let mut sentry_event = Event {
message: Some(format!(
"{}: {}",
error_context.error().code(),
error_context.error().message()
)),
..Event::default()
};
let mut vars = BTreeMap::new();
error_context.populate_exception(&mut sentry_event.exception.values, &mut vars);
sentry_event
.extra
.insert("context_vars".into(), Value::Object(Map::from_iter(vars)));
sentry_event
}
}

Some files were not shown because too many files have changed in this diff Show More