mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-12-19 15:41:31 +03:00
Merge branch 'master' into pr/2936
This commit is contained in:
commit
a746e2d5c5
4
.github/actions/init-env-rust/action.yaml
vendored
4
.github/actions/init-env-rust/action.yaml
vendored
@ -10,10 +10,6 @@ runs:
|
||||
prefix-key: gitbutler-client
|
||||
shared-key: rust
|
||||
|
||||
- name: Placeholder for ui assets
|
||||
shell: bash
|
||||
run: mkdir gitbutler-ui/build
|
||||
|
||||
- name: Check versions
|
||||
shell: bash
|
||||
run: |
|
||||
|
51
.github/workflows/push.yaml
vendored
51
.github/workflows/push.yaml
vendored
@ -14,8 +14,7 @@ jobs:
|
||||
rust: ${{ steps.filter.outputs.rust }}
|
||||
gitbutler-app: ${{ steps.filter.outputs.gitbutler-app }}
|
||||
gitbutler-core: ${{ steps.filter.outputs.gitbutler-core }}
|
||||
gitbutler-git: ${{ steps.filter.outputs.gitbutler-git }}
|
||||
gitbutler-diff: ${{ steps.filter.outputs.gitbutler-diff }}
|
||||
gitbutler-changeset: ${{ steps.filter.outputs.gitbutler-changeset }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dorny/paths-filter@v3
|
||||
@ -38,12 +37,9 @@ jobs:
|
||||
gitbutler-core:
|
||||
- *rust
|
||||
- 'gitbutler-core/**'
|
||||
gitbutler-git:
|
||||
gitbutler-changeset:
|
||||
- *rust
|
||||
- 'gitbutler-git/**'
|
||||
gitbutler-diff:
|
||||
- *rust
|
||||
- 'gitbutler-diff/**'
|
||||
- 'gitbutler-changeset/**'
|
||||
|
||||
lint-node:
|
||||
needs: changes
|
||||
@ -95,7 +91,7 @@ jobs:
|
||||
- uses: ./.github/actions/init-env-rust
|
||||
# TODO(qix-): we have to exclude the app here for now because for some
|
||||
# TODO(qix-): reason it doesn't build with the docs feature enabled.
|
||||
- run: cargo doc --no-deps --all-features --document-private-items -p gitbutler-core -p gitbutler-git -p gitbutler-diff
|
||||
- run: cargo doc --no-deps --all-features --document-private-items -p gitbutler-core -p gitbutler-changeset
|
||||
env:
|
||||
RUSTDOCFLAGS: -Dwarnings
|
||||
|
||||
@ -121,39 +117,9 @@ jobs:
|
||||
features: ${{ toJson(matrix.features) }}
|
||||
action: ${{ matrix.action }}
|
||||
|
||||
check-gitbutler-git:
|
||||
check-gitbutler-changeset:
|
||||
needs: [changes, rust-init]
|
||||
if: ${{ needs.changes.outputs.gitbutler-git == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ghcr.io/gitbutlerapp/ci-base-image:latest
|
||||
strategy:
|
||||
matrix:
|
||||
action:
|
||||
- test
|
||||
- check
|
||||
- check-tests
|
||||
features:
|
||||
- ''
|
||||
- '*'
|
||||
- []
|
||||
- [cli]
|
||||
- [cli, tokio]
|
||||
- [serde]
|
||||
- [git2]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
# FIXME(qix-): figure out a way to make these build automatically with tests
|
||||
- run: cargo build --locked -p gitbutler-git --bin gitbutler-git-askpass --bin gitbutler-git-setsid
|
||||
- uses: ./.github/actions/check-crate
|
||||
with:
|
||||
crate: gitbutler-git
|
||||
features: ${{ toJson(matrix.features) }}
|
||||
action: ${{ matrix.action }}
|
||||
|
||||
check-gitbutler-diff:
|
||||
needs: [changes, rust-init]
|
||||
if: ${{ needs.changes.outputs.gitbutler-diff == 'true' }}
|
||||
if: ${{ needs.changes.outputs.gitbutler-changeset == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ghcr.io/gitbutlerapp/ci-base-image:latest
|
||||
@ -173,7 +139,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/check-crate
|
||||
with:
|
||||
crate: gitbutler-diff
|
||||
crate: gitbutler-changeset
|
||||
features: ${{ toJson(matrix.features) }}
|
||||
action: ${{ matrix.action }}
|
||||
|
||||
@ -208,8 +174,7 @@ jobs:
|
||||
needs:
|
||||
- changes
|
||||
- check-gitbutler-app
|
||||
- check-gitbutler-git
|
||||
- check-gitbutler-diff
|
||||
- check-gitbutler-changeset
|
||||
- check-gitbutler-core
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ needs.changes.outputs.rust == 'true' }}
|
||||
|
10
.vscode/extensions.json
vendored
10
.vscode/extensions.json
vendored
@ -1,10 +0,0 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"svelte.svelte-vscode",
|
||||
"tauri-apps.tauri-vscode",
|
||||
"rust-lang.rust-analyzer",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode",
|
||||
"EditorConfig.EditorConfig"
|
||||
]
|
||||
}
|
17
.vscode/launch.json
vendored
17
.vscode/launch.json
vendored
@ -1,17 +0,0 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "GitButler Dev",
|
||||
"program": "${workspaceFolder}/target/debug/GitButler Dev",
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}",
|
||||
"preLaunchTask": "ui:dev",
|
||||
}
|
||||
]
|
||||
}
|
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@ -1,5 +0,0 @@
|
||||
{
|
||||
"prettier.configPath": "./gitbutler-ui/.prettierrc",
|
||||
"prettier.prettierPath": "./node_modules/prettier/index.cjs",
|
||||
"prettier.useEditorConfig": false
|
||||
}
|
14
.vscode/tasks.json
vendored
14
.vscode/tasks.json
vendored
@ -1,14 +0,0 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "ui:dev",
|
||||
"type": "shell",
|
||||
"isBackground": true,
|
||||
"command": "pnpm",
|
||||
"args": [
|
||||
"dev"
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
664
Cargo.lock
generated
664
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -2,14 +2,12 @@
|
||||
members = [
|
||||
"gitbutler-app",
|
||||
"gitbutler-core",
|
||||
"gitbutler-diff",
|
||||
"gitbutler-git",
|
||||
"gitbutler-changeset",
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.dependencies]
|
||||
gitbutler-core = { path = "gitbutler-core" }
|
||||
gitbutler-git = { path = "gitbutler-git" }
|
||||
git2 = { version = "0.18.2", features = ["vendored-openssl", "vendored-libgit2"] }
|
||||
uuid = "1.7.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
|
@ -30,7 +30,7 @@
|
||||
[s2]: https://img.shields.io/discord/1060193121130000425?label=Discord&color=5865F2
|
||||
[l2]: https://discord.gg/MmFkmaJ42D
|
||||
[s3]: https://img.shields.io/badge/Instagram-E4405F?logo=instagram&logoColor=white
|
||||
[l3]: https://instagram.com/gitbutlerapp
|
||||
[l3]: https://www.instagram.com/gitbutler/
|
||||
[s5]: https://img.shields.io/youtube/channel/subscribers/UCEwkZIHGqsTGYvX8wgD0LoQ
|
||||
[l5]: https://www.youtube.com/@gitbutlerapp
|
||||
[s6]: https://img.shields.io/badge/GitButler-%23B9F4F2?logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB3aWR0aD0iMzkiIGhlaWdodD0iMjgiIHZpZXdCb3g9IjAgMCAzOSAyOCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTI1LjIxNDUgMTIuMTk5N0wyLjg3MTA3IDEuMzg5MTJDMS41NDI5NSAwLjc0NjUzMiAwIDEuNzE0MDYgMCAzLjE4OTQ3VjI0LjgxMDVDMCAyNi4yODU5IDEuNTQyOTUgMjcuMjUzNSAyLjg3MTA3IDI2LjYxMDlMMjUuMjE0NSAxNS44MDAzQzI2LjcxOTcgMTUuMDcyMSAyNi43MTk3IDEyLjkyNzkgMjUuMjE0NSAxMi4xOTk3WiIgZmlsbD0iYmxhY2siLz4KPHBhdGggZD0iTTEzLjc4NTUgMTIuMTk5N0wzNi4xMjg5IDEuMzg5MTJDMzcuNDU3MSAwLjc0NjUzMiAzOSAxLjcxNDA2IDM5IDMuMTg5NDdWMjQuODEwNUMzOSAyNi4yODU5IDM3LjQ1NzEgMjcuMjUzNSAzNi4xMjg5IDI2LjYxMDlMMTMuNzg1NSAxNS44MDAzQzEyLjI4MDMgMTUuMDcyMSAxMi4yODAzIDEyLjkyNzkgMTMuNzg1NSAxMi4xOTk3WiIgZmlsbD0idXJsKCNwYWludDBfcmFkaWFsXzMxMF8xMjkpIi8%2BCjxkZWZzPgo8cmFkaWFsR3JhZGllbnQgaWQ9InBhaW50MF9yYWRpYWxfMzEwXzEyOSIgY3g9IjAiIGN5PSIwIiByPSIxIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgZ3JhZGllbnRUcmFuc2Zvcm09InRyYW5zbGF0ZSgxNi41NzAxIDE0KSBzY2FsZSgxOS44NjQxIDE5LjgzODMpIj4KPHN0b3Agb2Zmc2V0PSIwLjMwMTA1NiIgc3RvcC1vcGFjaXR5PSIwIi8%2BCjxzdG9wIG9mZnNldD0iMSIvPgo8L3JhZGlhbEdyYWRpZW50Pgo8L2RlZnM%2BCjwvc3ZnPgo%3D
|
||||
|
@ -22,7 +22,6 @@ pretty_assertions = "1.4"
|
||||
tempfile = "3.10"
|
||||
|
||||
[dependencies]
|
||||
gitbutler-git.workspace = true
|
||||
anyhow = "1.0.79"
|
||||
async-trait = "0.1.77"
|
||||
backoff = "0.4.0"
|
||||
@ -65,11 +64,13 @@ similar = { version = "2.4.0", features = ["unicode"] }
|
||||
slug = "0.1.5"
|
||||
ssh-key = { version = "0.6.4", features = [ "alloc", "ed25519" ] }
|
||||
ssh2 = { version = "0.9.4", features = ["vendored-openssl"] }
|
||||
tauri = { version = "1.5.4", features = ["dialog-open", "fs-read-file", "path-all", "process-relaunch", "protocol-asset", "shell-open", "window-maximize", "window-start-dragging", "window-unmaximize"] }
|
||||
tauri = { version = "1.5.4", features = [ "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" }
|
||||
tauri-plugin-window-state = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
|
||||
tauri-plugin-store = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
|
||||
tauri-plugin-log = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
|
||||
log = "^0.4"
|
||||
thiserror.workspace = true
|
||||
tokio = { workspace = true, features = [ "full", "sync", "tracing" ] }
|
||||
tokio-util = "0.7.10"
|
||||
|
@ -1,3 +1,23 @@
|
||||
fn main() {
|
||||
// Make the UI build directory if it doesn't already exist.
|
||||
// We do this here because the tauri context macro expects it to
|
||||
// exist at build time, and it's otherwise manually required to create
|
||||
// it before building.
|
||||
let manifest_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
assert_eq!(manifest_dir.file_name().unwrap(), "gitbutler-app");
|
||||
let build_dir = manifest_dir.parent().unwrap().join("gitbutler-ui/build");
|
||||
if !build_dir.exists() {
|
||||
// NOTE(qix-): Do not use `create_dir_all` here - the parent directory
|
||||
// NOTE(qix-): already exists, and we want to fail if not (for some reason).
|
||||
#[allow(clippy::expect_fun_call, clippy::create_dir)]
|
||||
std::fs::create_dir(&build_dir).expect(
|
||||
format!(
|
||||
"failed to create gitbutler-ui build directory: {:?}",
|
||||
build_dir
|
||||
)
|
||||
.as_str(),
|
||||
);
|
||||
}
|
||||
|
||||
tauri_build::build();
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ use gblib::{
|
||||
analytics, app, assets, commands, database, deltas, github, keys, logs, menu, projects, sentry,
|
||||
sessions, storage, users, virtual_branches, watcher, zip,
|
||||
};
|
||||
use tauri_plugin_log::LogTarget;
|
||||
use tauri_plugin_store::{with_store, JsonValue, StoreCollection};
|
||||
|
||||
fn main() {
|
||||
@ -21,6 +22,11 @@ fn main() {
|
||||
.block_on(async {
|
||||
tauri::async_runtime::set(tokio::runtime::Handle::current());
|
||||
|
||||
let log = tauri_plugin_log::Builder::default()
|
||||
.log_name("ui-logs")
|
||||
.target(LogTarget::LogDir)
|
||||
.level(log::LevelFilter::Error);
|
||||
|
||||
tauri::Builder::default()
|
||||
.on_window_event(|event| {
|
||||
#[cfg(target_os = "macos")]
|
||||
@ -137,6 +143,7 @@ fn main() {
|
||||
.plugin(tauri_plugin_single_instance::init(|_, _, _| {}))
|
||||
.plugin(tauri_plugin_context_menu::init())
|
||||
.plugin(tauri_plugin_store::Builder::default().build())
|
||||
.plugin(log.build())
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
commands::list_session_files,
|
||||
commands::git_remote_branches,
|
||||
|
@ -1,7 +1,6 @@
|
||||
use std::{collections::HashMap, path};
|
||||
|
||||
use anyhow::Context;
|
||||
use git2::ErrorCode;
|
||||
use tauri::Manager;
|
||||
use tracing::instrument;
|
||||
|
||||
@ -66,42 +65,13 @@ pub async fn git_remote_branches(
|
||||
#[tauri::command(async)]
|
||||
#[instrument(skip(handle))]
|
||||
pub async fn git_head(handle: tauri::AppHandle, project_id: &str) -> Result<String, Error> {
|
||||
use gitbutler_git::Repository;
|
||||
let app = handle.state::<app::App>();
|
||||
let project_id = project_id.parse().map_err(|_| Error::UserError {
|
||||
code: Code::Validation,
|
||||
message: "Malformed project id".to_string(),
|
||||
})?;
|
||||
let project = handle.state::<projects::Controller>().get(&project_id)?;
|
||||
let repo =
|
||||
gitbutler_git::git2::Repository::<gitbutler_git::git2::tokio::TokioThreadedResource>::open(
|
||||
&project.path,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| Error::UserError {
|
||||
code: Code::Projects,
|
||||
message: format!("could not open repository: {e}"),
|
||||
})?;
|
||||
|
||||
repo.symbolic_head().await.map_err(|e| match &e {
|
||||
gitbutler_git::Error::Backend(err) => {
|
||||
if err.code() == ErrorCode::UnbornBranch {
|
||||
return Error::UserError {
|
||||
code: Code::ProjectHead,
|
||||
message:
|
||||
"could not get symbolic head: Cannot load a git repository with 0 commits"
|
||||
.to_string(),
|
||||
};
|
||||
}
|
||||
Error::UserError {
|
||||
code: Code::ProjectHead,
|
||||
message: format!("could not get symbolic head: {e}"),
|
||||
}
|
||||
}
|
||||
_ => Error::UserError {
|
||||
code: Code::ProjectHead,
|
||||
message: format!("could not get symbolic head: {e}"),
|
||||
},
|
||||
})
|
||||
let head = app.git_head(&project_id)?;
|
||||
Ok(head)
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
|
@ -4,8 +4,8 @@ pub fn dedup(existing: &[&str], new: &str) -> String {
|
||||
dedup_fmt(existing, new, " ")
|
||||
}
|
||||
|
||||
// dedup makes sure that _new_ is not in _existing_ by adding a number to it.
|
||||
// the number is increased until the name is unique.
|
||||
/// Makes sure that _new_ is not in _existing_ by adding a number to it.
|
||||
/// the number is increased until the name is unique.
|
||||
pub fn dedup_fmt(existing: &[&str], new: &str, separator: &str) -> String {
|
||||
let used_numbers = existing
|
||||
.iter()
|
||||
|
@ -57,8 +57,8 @@ impl Repository {
|
||||
}
|
||||
|
||||
let projects_dir = root.join("projects");
|
||||
|
||||
let path = projects_dir.join(project.id.to_string());
|
||||
|
||||
let lock_path = projects_dir.join(format!("{}.lock", project.id));
|
||||
|
||||
if path.exists() {
|
||||
@ -75,6 +75,8 @@ impl Repository {
|
||||
lock_path,
|
||||
})
|
||||
} else {
|
||||
std::fs::create_dir_all(&path).context("failed to create project directory")?;
|
||||
|
||||
let git_repository = git::Repository::init_opts(
|
||||
&path,
|
||||
git2::RepositoryInitOptions::new()
|
||||
|
@ -12,7 +12,7 @@ use crate::{
|
||||
};
|
||||
|
||||
use super::{
|
||||
branch::{self, BranchId},
|
||||
branch::BranchId,
|
||||
controller::{Controller, ControllerError},
|
||||
BaseBranch, RemoteBranchFile,
|
||||
};
|
||||
|
@ -1,5 +1,5 @@
|
||||
use crate::git::diff;
|
||||
use anyhow::{Context, Result};
|
||||
use anyhow::Result;
|
||||
|
||||
pub fn hunk_with_context(
|
||||
hunk_diff: &str,
|
||||
@ -143,16 +143,16 @@ mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
let expected = r#"@@ -5,7 +5,7 @@
|
||||
|
||||
|
||||
[features]
|
||||
default = ["serde", "rusqlite"]
|
||||
-serde = ["dep:serde", "uuid/serde"]
|
||||
+SERDE = ["dep:serde", "uuid/serde"]
|
||||
rusqlite = ["dep:rusqlite"]
|
||||
|
||||
|
||||
[dependencies]
|
||||
"#;
|
||||
assert_eq!(with_ctx.diff, expected);
|
||||
assert_eq!(with_ctx.diff.replace("\n \n", "\n\n"), expected);
|
||||
assert_eq!(with_ctx.old_start, 5);
|
||||
assert_eq!(with_ctx.old_lines, 7);
|
||||
assert_eq!(with_ctx.new_start, 5);
|
||||
@ -176,14 +176,14 @@ mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
with_ctx.diff,
|
||||
with_ctx.diff.replace("\n \n", "\n\n"),
|
||||
r#"@@ -1,5 +1,5 @@
|
||||
[package]
|
||||
-name = "gitbutler-core"
|
||||
+NAME = "gitbutler-core"
|
||||
version = "0.0.0"
|
||||
edition = "2021"
|
||||
|
||||
|
||||
"#
|
||||
);
|
||||
assert_eq!(with_ctx.old_start, 1);
|
||||
@ -209,7 +209,7 @@ mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
with_ctx.diff,
|
||||
with_ctx.diff.replace("\n \n", "\n\n"),
|
||||
r#"@@ -1,4 +1,4 @@
|
||||
-[package]
|
||||
+[PACKAGE]
|
||||
@ -241,9 +241,9 @@ mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
with_ctx.diff,
|
||||
with_ctx.diff.replace("\n \n", "\n\n"),
|
||||
r#"@@ -10,5 +10,5 @@
|
||||
|
||||
|
||||
[dependencies]
|
||||
rusqlite = { workspace = true, optional = true }
|
||||
-serde = { workspace = true, optional = true }
|
||||
@ -277,9 +277,9 @@ mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
with_ctx.diff,
|
||||
with_ctx.diff.replace("\n \n", "\n\n"),
|
||||
r#"@@ -5,7 +5,10 @@
|
||||
|
||||
|
||||
[features]
|
||||
default = ["serde", "rusqlite"]
|
||||
-serde = ["dep:serde", "uuid/serde"]
|
||||
@ -288,7 +288,7 @@ mod tests {
|
||||
+three
|
||||
+four
|
||||
rusqlite = ["dep:rusqlite"]
|
||||
|
||||
|
||||
[dependencies]
|
||||
"#
|
||||
);
|
||||
@ -317,16 +317,16 @@ mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
with_ctx.diff,
|
||||
with_ctx.diff.replace("\n \n", "\n\n"),
|
||||
r#"@@ -4,9 +4,7 @@
|
||||
edition = "2021"
|
||||
|
||||
|
||||
[features]
|
||||
-default = ["serde", "rusqlite"]
|
||||
-serde = ["dep:serde", "uuid/serde"]
|
||||
-rusqlite = ["dep:rusqlite"]
|
||||
+foo = ["foo"]
|
||||
|
||||
|
||||
[dependencies]
|
||||
rusqlite = { workspace = true, optional = true }
|
||||
"#
|
||||
@ -381,7 +381,7 @@ mod tests {
|
||||
diff::ChangeType::Added,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(with_ctx.diff, hunk_diff);
|
||||
assert_eq!(with_ctx.diff.replace("\n \n", "\n\n"), hunk_diff);
|
||||
assert_eq!(with_ctx.old_start, 1);
|
||||
assert_eq!(with_ctx.old_lines, 14);
|
||||
assert_eq!(with_ctx.new_start, 0);
|
||||
@ -406,7 +406,7 @@ mod tests {
|
||||
diff::ChangeType::Added,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(with_ctx.diff, hunk_diff);
|
||||
assert_eq!(with_ctx.diff.replace("\n \n", "\n\n"), hunk_diff);
|
||||
assert_eq!(with_ctx.old_start, 0);
|
||||
assert_eq!(with_ctx.old_lines, 0);
|
||||
assert_eq!(with_ctx.new_start, 1);
|
||||
@ -438,10 +438,10 @@ mod tests {
|
||||
+two
|
||||
+three
|
||||
rusqlite = ["dep:rusqlite"]
|
||||
|
||||
|
||||
[dependencies]
|
||||
"#;
|
||||
assert_eq!(with_ctx.diff, expected);
|
||||
assert_eq!(with_ctx.diff.replace("\n \n", "\n\n"), expected);
|
||||
assert_eq!(with_ctx.old_start, 6);
|
||||
assert_eq!(with_ctx.old_lines, 6);
|
||||
assert_eq!(with_ctx.new_start, 6);
|
||||
@ -473,10 +473,10 @@ mod tests {
|
||||
+two
|
||||
+three
|
||||
rusqlite = ["dep:rusqlite"]
|
||||
|
||||
|
||||
[dependencies]
|
||||
"#;
|
||||
assert_eq!(with_ctx.diff, expected);
|
||||
assert_eq!(with_ctx.diff.replace("\n \n", "\n\n"), expected);
|
||||
assert_eq!(with_ctx.old_start, 6);
|
||||
assert_eq!(with_ctx.old_lines, 6);
|
||||
assert_eq!(with_ctx.new_start, 10);
|
||||
@ -492,12 +492,12 @@ mod tests {
|
||||
"#;
|
||||
let expected = r#"@@ -4,9 +4,6 @@
|
||||
edition = "2021"
|
||||
|
||||
|
||||
[features]
|
||||
-default = ["serde", "rusqlite"]
|
||||
-serde = ["dep:serde", "uuid/serde"]
|
||||
-rusqlite = ["dep:rusqlite"]
|
||||
|
||||
|
||||
[dependencies]
|
||||
rusqlite = { workspace = true, optional = true }
|
||||
"#;
|
||||
@ -511,7 +511,7 @@ mod tests {
|
||||
diff::ChangeType::Added,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(with_ctx.diff == expected);
|
||||
assert_eq!(with_ctx.diff.replace("\n \n", "\n\n"), expected);
|
||||
assert_eq!(with_ctx.old_start, 4);
|
||||
assert_eq!(with_ctx.old_lines, 9);
|
||||
assert_eq!(with_ctx.new_start, 4);
|
||||
@ -527,12 +527,12 @@ mod tests {
|
||||
"#;
|
||||
let expected = r#"@@ -4,9 +8,6 @@
|
||||
edition = "2021"
|
||||
|
||||
|
||||
[features]
|
||||
-default = ["serde", "rusqlite"]
|
||||
-serde = ["dep:serde", "uuid/serde"]
|
||||
-rusqlite = ["dep:rusqlite"]
|
||||
|
||||
|
||||
[dependencies]
|
||||
rusqlite = { workspace = true, optional = true }
|
||||
"#;
|
||||
@ -546,7 +546,7 @@ mod tests {
|
||||
diff::ChangeType::Added,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(with_ctx.diff, expected);
|
||||
assert_eq!(with_ctx.diff.replace("\n \n", "\n\n"), expected);
|
||||
assert_eq!(with_ctx.old_start, 4);
|
||||
assert_eq!(with_ctx.old_lines, 9);
|
||||
assert_eq!(with_ctx.new_start, 8);
|
||||
@ -576,10 +576,10 @@ mod tests {
|
||||
-
|
||||
- @waiting_users = User.where(approved: false).count
|
||||
end
|
||||
|
||||
|
||||
def invite
|
||||
";
|
||||
assert_eq!(with_ctx.diff, expected);
|
||||
assert_eq!(with_ctx.diff.replace("\n \n", "\n\n"), expected);
|
||||
assert_eq!(with_ctx.old_start, 8);
|
||||
assert_eq!(with_ctx.old_lines, 8);
|
||||
assert_eq!(with_ctx.new_start, 8);
|
||||
|
@ -939,7 +939,7 @@ fn test_add_new_hunk_to_the_end() -> Result<()> {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_merge_vbranch_upstream_clean() -> Result<()> {
|
||||
fn test_merge_vbranch_upstream_clean_rebase() -> Result<()> {
|
||||
let suite = Suite::default();
|
||||
let Case {
|
||||
project_repository,
|
||||
@ -1056,15 +1056,10 @@ fn test_merge_vbranch_upstream_clean() -> Result<()> {
|
||||
);
|
||||
let contents = std::fs::read(std::path::Path::new(&project.path).join(file_path2))?;
|
||||
assert_eq!("file2\n", String::from_utf8(contents)?);
|
||||
assert_eq!(branch1.files.len(), 0);
|
||||
assert_eq!(branch1.commits.len(), 3);
|
||||
assert_eq!(branch1.files.len(), 1);
|
||||
assert_eq!(branch1.commits.len(), 2);
|
||||
// assert_eq!(branch1.upstream.as_ref().unwrap().commits.len(), 0);
|
||||
|
||||
// make sure the last commit was signed
|
||||
let last_id = &branch1.commits[0].id;
|
||||
let last_commit = project_repository.git_repository.find_commit(*last_id)?;
|
||||
assert!(last_commit.raw_header().unwrap().contains("SSH SIGNATURE"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -1633,7 +1628,7 @@ fn test_detect_mergeable_branch() -> Result<()> {
|
||||
|
||||
let remotes =
|
||||
list_remote_branches(&gb_repository, &project_repository).expect("failed to list remotes");
|
||||
let remote1 = &remotes
|
||||
let _remote1 = &remotes
|
||||
.iter()
|
||||
.find(|b| b.name.to_string() == "refs/remotes/origin/remote_branch")
|
||||
.unwrap();
|
||||
@ -1645,7 +1640,7 @@ fn test_detect_mergeable_branch() -> Result<()> {
|
||||
.unwrap());
|
||||
// assert_eq!(remote1.commits.len(), 1);
|
||||
|
||||
let remote2 = &remotes
|
||||
let _remote2 = &remotes
|
||||
.iter()
|
||||
.find(|b| b.name.to_string() == "refs/remotes/origin/remote_branch2")
|
||||
.unwrap();
|
||||
|
@ -1442,20 +1442,88 @@ pub fn merge_virtual_branch_upstream(
|
||||
Some(upstream_commit.id()),
|
||||
)?;
|
||||
} else {
|
||||
// get the merge tree oid from writing the index out
|
||||
let merge_tree_oid = merge_index
|
||||
.write_tree_to(repo)
|
||||
.context("failed to write tree")?;
|
||||
let merge_tree = repo
|
||||
.find_tree(merge_tree_oid)
|
||||
.context("failed to find merge tree")?;
|
||||
let branch_writer =
|
||||
branch::Writer::new(gb_repository).context("failed to create writer")?;
|
||||
|
||||
if *project_repository.project().ok_with_force_push {
|
||||
// attempt a rebase
|
||||
let (_, committer) = project_repository.git_signatures(user)?;
|
||||
let mut rebase_options = git2::RebaseOptions::new();
|
||||
rebase_options.quiet(true);
|
||||
rebase_options.inmemory(true);
|
||||
let mut rebase = repo
|
||||
.rebase(
|
||||
Some(branch.head),
|
||||
Some(upstream_commit.id()),
|
||||
None,
|
||||
Some(&mut rebase_options),
|
||||
)
|
||||
.context("failed to rebase")?;
|
||||
|
||||
let mut rebase_success = true;
|
||||
// check to see if these commits have already been pushed
|
||||
let mut last_rebase_head = upstream_commit.id();
|
||||
while rebase.next().is_some() {
|
||||
let index = rebase
|
||||
.inmemory_index()
|
||||
.context("failed to get inmemory index")?;
|
||||
if index.has_conflicts() {
|
||||
rebase_success = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if let Ok(commit_id) = rebase.commit(None, &committer.clone().into(), None) {
|
||||
last_rebase_head = commit_id.into();
|
||||
} else {
|
||||
rebase_success = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if rebase_success {
|
||||
// rebase worked out, rewrite the branch head
|
||||
rebase.finish(None).context("failed to finish rebase")?;
|
||||
|
||||
project_repository
|
||||
.git_repository
|
||||
.checkout_tree(&merge_tree)
|
||||
.force()
|
||||
.checkout()
|
||||
.context("failed to checkout tree")?;
|
||||
|
||||
branch.head = last_rebase_head;
|
||||
branch.tree = merge_tree_oid;
|
||||
branch_writer.write(&mut branch)?;
|
||||
super::integration::update_gitbutler_integration(
|
||||
gb_repository,
|
||||
project_repository,
|
||||
)?;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
rebase.abort().context("failed to abort rebase")?;
|
||||
}
|
||||
|
||||
let head_commit = repo
|
||||
.find_commit(branch.head)
|
||||
.context("failed to find head commit")?;
|
||||
let merge_tree = repo
|
||||
.find_tree(merge_tree_oid)
|
||||
.context("failed to find merge tree")?;
|
||||
|
||||
let new_branch_head = project_repository.commit(
|
||||
user,
|
||||
"merged from upstream",
|
||||
format!(
|
||||
"Merged {}/{} into {}",
|
||||
upstream_branch.remote(),
|
||||
upstream_branch.branch(),
|
||||
branch.name
|
||||
)
|
||||
.as_str(),
|
||||
&merge_tree,
|
||||
&[&head_commit, &upstream_commit],
|
||||
signing_key,
|
||||
@ -1468,8 +1536,6 @@ pub fn merge_virtual_branch_upstream(
|
||||
.context("failed to checkout tree")?;
|
||||
|
||||
// write the branch data
|
||||
let branch_writer =
|
||||
branch::Writer::new(gb_repository).context("failed to create writer")?;
|
||||
branch.head = new_branch_head;
|
||||
branch.tree = merge_tree_oid;
|
||||
branch_writer.write(&mut branch)?;
|
||||
|
@ -21,6 +21,9 @@
|
||||
"dialog": {
|
||||
"open": true
|
||||
},
|
||||
"os": {
|
||||
"all": true
|
||||
},
|
||||
"protocol": {
|
||||
"asset": true,
|
||||
"assetScope": ["$APPCACHE/images/*"]
|
||||
|
@ -1,5 +1,5 @@
|
||||
[package]
|
||||
name = "gitbutler-diff"
|
||||
name = "gitbutler-changeset"
|
||||
version = "0.0.0"
|
||||
edition = "2021"
|
||||
|
@ -14,15 +14,11 @@
|
||||
//! Otherwise, neither the length prefix imposed by `(de)serialize_bytes()` nor the
|
||||
//! terrible compaction and optimization of `(de)serialize_tuple()` are acceptable.
|
||||
|
||||
// FIXME(qix-): There are a ton of identifiers in here that make no sense and
|
||||
// FIXME(qix-): were copied over from the exploratory data-science-ey code that
|
||||
// FIXME(qix-): need to be cleaned up. PR welcome!
|
||||
|
||||
const BITS: usize = 3;
|
||||
const SHIFT: usize = 8 - BITS;
|
||||
const SIG_ENTRIES: usize = (1 << BITS) * (1 << BITS);
|
||||
const SIG_BYTES: usize = SIG_ENTRIES * ::core::mem::size_of::<SigBucket>();
|
||||
const TOTAL_BYTES: usize = SIG_BYTES + 4 + 1; // we encode a 4-byte length at the beginning, along with a version byte
|
||||
const FINGERPRINT_ENTRIES: usize = (1 << BITS) * (1 << BITS);
|
||||
const FINGERPRINT_BYTES: usize = FINGERPRINT_ENTRIES * ::core::mem::size_of::<SigBucket>();
|
||||
const TOTAL_BYTES: usize = 1 + 4 + FINGERPRINT_BYTES; // we encode a version byte and a 4-byte length at the beginning
|
||||
|
||||
// NOTE: This is not efficient if `SigBucket` is 1 byte (u8).
|
||||
// NOTE: If `SigBucket` is changed to a u8, then the implementation
|
||||
@ -80,42 +76,44 @@ impl Signature {
|
||||
/// about the signature or the original file contents.
|
||||
///
|
||||
/// Do not use for any security-related purposes.
|
||||
pub fn score_str<S: AsRef<str>>(&self, s: S) -> f64 {
|
||||
pub fn score_str<S: AsRef<str>>(&self, other: S) -> f64 {
|
||||
if self.0[0] != 0 {
|
||||
panic!("unsupported signature version");
|
||||
}
|
||||
|
||||
let original_length = u32::from_le_bytes(self.0[1..5].try_into().unwrap());
|
||||
|
||||
let s = s.as_ref();
|
||||
|
||||
let s_s: String = s.chars().filter(|&x| !char::is_whitespace(x)).collect();
|
||||
let s = s_s.as_bytes();
|
||||
|
||||
if original_length < 2 || s.len() < 2 {
|
||||
let original_length = u32::from_le_bytes(self.0[1..5].try_into().expect("invalid length"));
|
||||
if original_length < 2 {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
let mut intersection_size = 0usize;
|
||||
let other = other.as_ref();
|
||||
let other_string: String = other.chars().filter(|&x| !char::is_whitespace(x)).collect();
|
||||
let other = other_string.as_bytes();
|
||||
|
||||
let mut wb = self.bucket_iter().collect::<Vec<_>>();
|
||||
if other.len() < 2 {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
for (b1, b2) in bigrams(s) {
|
||||
let b1 = b1 >> SHIFT;
|
||||
let b2 = b2 >> SHIFT;
|
||||
let ix = ((b1 as usize) << BITS) | (b2 as usize);
|
||||
if wb[ix] > 0 {
|
||||
wb[ix] = wb[ix].saturating_sub(1);
|
||||
intersection_size += 1;
|
||||
let mut matching_bigrams: usize = 0;
|
||||
|
||||
let mut self_buckets = self.bucket_iter().collect::<Vec<_>>();
|
||||
|
||||
for (left, right) in bigrams(other) {
|
||||
let left = left >> SHIFT;
|
||||
let right = right >> SHIFT;
|
||||
let index = ((left as usize) << BITS) | (right as usize);
|
||||
if self_buckets[index] > 0 {
|
||||
self_buckets[index] -= 1;
|
||||
matching_bigrams += 1;
|
||||
}
|
||||
}
|
||||
|
||||
(2 * intersection_size) as f64 / (original_length as usize + s.len() - 2) as f64
|
||||
(2 * matching_bigrams) as f64 / (original_length as usize + other.len() - 2) as f64
|
||||
}
|
||||
|
||||
fn bucket_iter(&self) -> impl Iterator<Item = SigBucket> + '_ {
|
||||
unsafe {
|
||||
self.0[(TOTAL_BYTES - SIG_BYTES)..]
|
||||
self.0[(TOTAL_BYTES - FINGERPRINT_BYTES)..]
|
||||
.as_chunks_unchecked::<{ ::core::mem::size_of::<SigBucket>() }>()
|
||||
.iter()
|
||||
.map(|ch: &[u8; ::core::mem::size_of::<SigBucket>()]| SigBucket::from_le_bytes(*ch))
|
||||
@ -125,45 +123,50 @@ impl Signature {
|
||||
|
||||
impl<S: AsRef<str>> From<S> for Signature {
|
||||
#[inline]
|
||||
fn from(s: S) -> Self {
|
||||
let s = s.as_ref();
|
||||
fn from(source: S) -> Self {
|
||||
let source = source.as_ref();
|
||||
let source_string: String = source
|
||||
.chars()
|
||||
.filter(|&x| !char::is_whitespace(x))
|
||||
.collect();
|
||||
let source = source_string.as_bytes();
|
||||
|
||||
let a_s: String = s.chars().filter(|&x| !char::is_whitespace(x)).collect();
|
||||
let a = a_s.as_bytes();
|
||||
|
||||
let a_len: u32 = a
|
||||
let source_len: u32 = source
|
||||
.len()
|
||||
.try_into()
|
||||
.expect("strings with a byte-length above u32::MAX are not supported");
|
||||
|
||||
let mut a_res = [0; TOTAL_BYTES];
|
||||
a_res[0] = 0; // version byte
|
||||
a_res[1..5].copy_from_slice(&a_len.to_le_bytes()); // length
|
||||
let mut bytes = [0; TOTAL_BYTES];
|
||||
bytes[0] = 0; // version byte (0)
|
||||
bytes[1..5].copy_from_slice(&source_len.to_le_bytes()); // next 4 bytes are the length
|
||||
|
||||
if a_len >= 2 {
|
||||
let mut a_bigrams = [0 as SigBucket; SIG_ENTRIES];
|
||||
if source_len >= 2 {
|
||||
let mut buckets = [0 as SigBucket; FINGERPRINT_ENTRIES];
|
||||
|
||||
for (b1, b2) in bigrams(a) {
|
||||
let b1 = b1 >> SHIFT;
|
||||
let b2 = b2 >> SHIFT;
|
||||
let encoded_bigram = ((b1 as usize) << BITS) | (b2 as usize);
|
||||
a_bigrams[encoded_bigram] = a_bigrams[encoded_bigram].saturating_add(1);
|
||||
for (left, right) in bigrams(source) {
|
||||
let left = left >> SHIFT;
|
||||
let right = right >> SHIFT;
|
||||
let index = ((left as usize) << BITS) | (right as usize);
|
||||
buckets[index] = buckets[index].saturating_add(1);
|
||||
}
|
||||
|
||||
// NOTE: This is not efficient if `SigBucket` is 1 byte (u8).
|
||||
let mut offset = TOTAL_BYTES - SIG_BYTES;
|
||||
for bucket in a_bigrams {
|
||||
let mut offset = TOTAL_BYTES - FINGERPRINT_BYTES;
|
||||
for bucket in buckets {
|
||||
let start = offset;
|
||||
let end = start + ::core::mem::size_of::<SigBucket>();
|
||||
a_res[start..end].copy_from_slice(&bucket.to_le_bytes());
|
||||
bytes[start..end].copy_from_slice(&bucket.to_le_bytes());
|
||||
offset = end;
|
||||
}
|
||||
}
|
||||
|
||||
Self(a_res)
|
||||
Self(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
/// Copies the passed bytes twice and zips them together with a one-byte offset.
|
||||
/// This produces an iterator of the bigrams (pairs of consecutive bytes) in the input.
|
||||
/// For example, the bigrams of 1, 2, 3, 4, 5 would be (1, 2), (2, 3), (3, 4), (4, 5).
|
||||
#[inline]
|
||||
fn bigrams(s: &[u8]) -> impl Iterator<Item = (u8, u8)> + '_ {
|
||||
s.iter().copied().zip(s.iter().skip(1).copied())
|
||||
@ -173,61 +176,47 @@ fn bigrams(s: &[u8]) -> impl Iterator<Item = (u8, u8)> + '_ {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
macro_rules! assert_score {
|
||||
($sig:ident, $s:expr, $e:expr) => {
|
||||
let score = $sig.score_str($s);
|
||||
if (score - $e).abs() >= 0.1 {
|
||||
panic!(
|
||||
"expected score of {} for string {:?}, got {}",
|
||||
$e, $s, score
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn score_signature() {
|
||||
let sig = Signature::from("hello world");
|
||||
|
||||
macro_rules! assert_score {
|
||||
($s:expr, $e:expr) => {
|
||||
if (sig.score_str($s) - $e).abs() >= 0.1 {
|
||||
panic!(
|
||||
"expected score of {} for string {:?}, got {}",
|
||||
$e,
|
||||
$s,
|
||||
sig.score_str($s)
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// NOTE: The scores here are not exact, but are close enough
|
||||
// to be useful for testing purposes, hence why some have the same
|
||||
// "score" but different strings.
|
||||
assert_score!("hello world", 1.0);
|
||||
assert_score!("hello world!", 0.95);
|
||||
assert_score!("hello world!!", 0.9);
|
||||
assert_score!("hello world!!!", 0.85);
|
||||
assert_score!("hello world!!!!", 0.8);
|
||||
assert_score!("hello world!!!!!", 0.75);
|
||||
assert_score!("hello world!!!!!!", 0.7);
|
||||
assert_score!("hello world!!!!!!!", 0.65);
|
||||
assert_score!("hello world!!!!!!!!", 0.62);
|
||||
assert_score!("hello world!!!!!!!!!", 0.6);
|
||||
assert_score!("hello world!!!!!!!!!!", 0.55);
|
||||
assert_score!(sig, "hello world", 1.0);
|
||||
assert_score!(sig, "hello world!", 0.95);
|
||||
assert_score!(sig, "hello world!!", 0.9);
|
||||
assert_score!(sig, "hello world!!!", 0.85);
|
||||
assert_score!(sig, "hello world!!!!", 0.8);
|
||||
assert_score!(sig, "hello world!!!!!", 0.75);
|
||||
assert_score!(sig, "hello world!!!!!!", 0.7);
|
||||
assert_score!(sig, "hello world!!!!!!!", 0.65);
|
||||
assert_score!(sig, "hello world!!!!!!!!", 0.62);
|
||||
assert_score!(sig, "hello world!!!!!!!!!", 0.6);
|
||||
assert_score!(sig, "hello world!!!!!!!!!!", 0.55);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn score_ignores_whitespace() {
|
||||
let sig = Signature::from("hello world");
|
||||
|
||||
macro_rules! assert_score {
|
||||
($s:expr, $e:expr) => {
|
||||
if (sig.score_str($s) - $e).abs() >= 0.1 {
|
||||
panic!(
|
||||
"expected score of {} for string {:?}, got {}",
|
||||
$e,
|
||||
$s,
|
||||
sig.score_str($s)
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
assert_score!("hello world", 1.0);
|
||||
assert_score!("hello world ", 1.0);
|
||||
assert_score!("hello\nworld ", 1.0);
|
||||
assert_score!("hello\n\tworld ", 1.0);
|
||||
assert_score!("\t\t hel lo\n\two rld \t\t", 1.0);
|
||||
assert_score!(sig, "hello world", 1.0);
|
||||
assert_score!(sig, "hello world ", 1.0);
|
||||
assert_score!(sig, "hello\nworld ", 1.0);
|
||||
assert_score!(sig, "hello\n\tworld ", 1.0);
|
||||
assert_score!(sig, "\t\t hel lo\n\two rld \t\t", 1.0);
|
||||
}
|
||||
|
||||
const TEXT1: &str = include_str!("../fixture/text1.txt");
|
@ -1,8 +1,8 @@
|
||||
//! A [Tokio](https://tokio.rs)-based [`super::GitExecutor`] implementation.
|
||||
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
use std::{collections::HashMap, fs::Permissions, os::unix::fs::PermissionsExt, time::Duration};
|
||||
use std::os::unix::fs::{MetadataExt, PermissionsExt};
|
||||
use std::{collections::HashMap, fs::Permissions, time::Duration};
|
||||
use tokio::process::Command;
|
||||
|
||||
/// A [`super::GitExecutor`] implementation using the `git` command-line tool
|
||||
|
@ -85,6 +85,7 @@
|
||||
"svelte-resize-observer": "^2.0.0",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"tauri-plugin-context-menu": "^0.7.0",
|
||||
"tauri-plugin-log-api": "github:tauri-apps/tauri-plugin-log#v1",
|
||||
"tauri-plugin-store-api": "github:tauri-apps/tauri-plugin-store#v1",
|
||||
"tinykeys": "^2.1.0",
|
||||
"tslib": "^2.6.2",
|
||||
|
@ -1,7 +1,9 @@
|
||||
import { handleErrorWithSentry } from '@sentry/sveltekit';
|
||||
import { error as logErrorToFile } from 'tauri-plugin-log-api';
|
||||
import type { NavigationEvent } from '@sveltejs/kit';
|
||||
|
||||
function myErrorHandler({ error, event }: { error: any; event: NavigationEvent }) {
|
||||
logErrorToFile(error);
|
||||
console.error('An error occurred on the client side:', error, event);
|
||||
}
|
||||
|
||||
@ -16,6 +18,7 @@ export const handleError = handleErrorWithSentry(myErrorHandler);
|
||||
*/
|
||||
const originalUnhandledHandler = window.onunhandledrejection;
|
||||
window.onunhandledrejection = (event: PromiseRejectionEvent) => {
|
||||
logErrorToFile('Unhandled exception: ' + event?.reason?.message + ' ' + event?.reason?.sourceURL);
|
||||
console.log('Unhandled exception', event.reason);
|
||||
originalUnhandledHandler?.bind(window)(event);
|
||||
};
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { showToast } from '$lib/notifications/toasts';
|
||||
import { relaunch } from '@tauri-apps/api/process';
|
||||
import {
|
||||
checkUpdate,
|
||||
installUpdate,
|
||||
@ -8,19 +9,20 @@ import {
|
||||
} from '@tauri-apps/api/updater';
|
||||
import posthog from 'posthog-js';
|
||||
import {
|
||||
BehaviorSubject,
|
||||
switchMap,
|
||||
Observable,
|
||||
from,
|
||||
map,
|
||||
shareReplay,
|
||||
interval,
|
||||
timeout,
|
||||
catchError,
|
||||
of,
|
||||
tap,
|
||||
map,
|
||||
from,
|
||||
timeout,
|
||||
interval,
|
||||
switchMap,
|
||||
shareReplay,
|
||||
catchError,
|
||||
startWith,
|
||||
combineLatestWith,
|
||||
tap
|
||||
distinctUntilChanged,
|
||||
Observable,
|
||||
BehaviorSubject
|
||||
} from 'rxjs';
|
||||
|
||||
// TOOD: Investigate why 'DOWNLOADED' is not in the type provided by Tauri.
|
||||
@ -44,7 +46,10 @@ export class UpdaterService {
|
||||
constructor() {
|
||||
onUpdaterEvent((status) => {
|
||||
const err = status.error;
|
||||
if (err) showErrorToast(err);
|
||||
if (err) {
|
||||
showErrorToast(err);
|
||||
posthog.capture('App Update Status Error', { error: err });
|
||||
}
|
||||
this.status$.next(status.status);
|
||||
}).then((unlistenFn) => (this.unlistenFn = unlistenFn));
|
||||
|
||||
@ -58,10 +63,12 @@ export class UpdaterService {
|
||||
map((update: UpdateResult | undefined) => {
|
||||
if (update?.shouldUpdate) return update.manifest;
|
||||
}),
|
||||
// We don't need the stream to emit if the result is the same version
|
||||
distinctUntilChanged((prev, curr) => prev?.version == curr?.version),
|
||||
// Hide offline/timeout errors since no app ever notifies you about this
|
||||
catchError((err) => {
|
||||
if (!isOffline(err) && !isTimeoutError(err)) {
|
||||
posthog.capture('Updater Check Error', err);
|
||||
posthog.capture('App Update Check Error', { error: err });
|
||||
showErrorToast(err);
|
||||
console.log(err);
|
||||
}
|
||||
@ -83,15 +90,19 @@ export class UpdaterService {
|
||||
// });
|
||||
}
|
||||
|
||||
async install() {
|
||||
async installUpdate() {
|
||||
try {
|
||||
await installUpdate();
|
||||
posthog.capture('App Update Successful');
|
||||
} catch (e: any) {
|
||||
} catch (err: any) {
|
||||
// We expect toast to be shown by error handling in `onUpdaterEvent`
|
||||
posthog.capture('App Update Failed', e);
|
||||
posthog.capture('App Update Install Error', { error: err });
|
||||
}
|
||||
}
|
||||
|
||||
relaunchApp() {
|
||||
relaunch();
|
||||
}
|
||||
}
|
||||
|
||||
function isOffline(err: any): boolean {
|
||||
@ -118,5 +129,4 @@ function showErrorToast(err: any) {
|
||||
`,
|
||||
style: 'error'
|
||||
});
|
||||
posthog.capture('Updater Status Error', err);
|
||||
}
|
||||
|
@ -5,22 +5,25 @@
|
||||
|
||||
export let user: User | undefined;
|
||||
export let pop = false;
|
||||
export let isNavCollapsed = false;
|
||||
</script>
|
||||
|
||||
<button class="btn" class:pop on:click={() => goto('/settings/')}>
|
||||
<span class="name text-base-13 text-semibold">
|
||||
{#if user}
|
||||
{#if user.name}
|
||||
{user.name}
|
||||
{:else if user.given_name}
|
||||
{user.given_name}
|
||||
{:else if user.email}
|
||||
{user.email}
|
||||
<button class="btn" class:pop on:click={() => goto('/settings/')} class:collapsed={isNavCollapsed}>
|
||||
{#if !isNavCollapsed}
|
||||
<span class="name text-base-13 text-semibold">
|
||||
{#if user}
|
||||
{#if user.name}
|
||||
{user.name}
|
||||
{:else if user.given_name}
|
||||
{user.given_name}
|
||||
{:else if user.email}
|
||||
{user.email}
|
||||
{/if}
|
||||
{:else}
|
||||
Account
|
||||
{/if}
|
||||
{:else}
|
||||
Account
|
||||
{/if}
|
||||
</span>
|
||||
</span>
|
||||
{/if}
|
||||
{#if user?.picture}
|
||||
<img class="profile-picture" src={user.picture} alt="Avatar" />
|
||||
{:else}
|
||||
@ -30,8 +33,6 @@
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
<!-- REMOVE IF IT'S NOT NEEDED -->
|
||||
|
||||
<style lang="postcss">
|
||||
.btn {
|
||||
display: flex;
|
||||
@ -93,4 +94,17 @@
|
||||
background: var(--clr-theme-pop-element);
|
||||
color: var(--clr-theme-pop-on-element);
|
||||
}
|
||||
|
||||
/* MODIFIERS */
|
||||
.btn.collapsed {
|
||||
overflow-x: initial;
|
||||
padding: var(--space-8);
|
||||
height: auto;
|
||||
|
||||
& .anon-icon,
|
||||
.profile-picture {
|
||||
width: var(--space-24);
|
||||
height: var(--space-24);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -2,18 +2,13 @@
|
||||
import Button from './Button.svelte';
|
||||
import IconButton from './IconButton.svelte';
|
||||
import { showToast } from '$lib/notifications/toasts';
|
||||
import { relaunch } from '@tauri-apps/api/process';
|
||||
import { installUpdate } from '@tauri-apps/api/updater';
|
||||
import { distinctUntilChanged, tap } from 'rxjs';
|
||||
import { tap } from 'rxjs';
|
||||
import { fade } from 'svelte/transition';
|
||||
import type { UpdaterService } from '$lib/backend/updater';
|
||||
|
||||
export let updaterService: UpdaterService;
|
||||
|
||||
// Extrend update stream to allow dismissing updater by version
|
||||
$: update$ = updaterService.update$.pipe(
|
||||
// Only run operators after this one once per version
|
||||
distinctUntilChanged((prev, curr) => prev?.version == curr?.version),
|
||||
// Reset dismissed boolean when a new version becomes available
|
||||
tap(() => (dismissed = false))
|
||||
);
|
||||
@ -99,8 +94,10 @@
|
||||
New version available
|
||||
{:else if $update$.status == 'PENDING'}
|
||||
Downloading update...
|
||||
{:else if $update$.status == 'DONE'}
|
||||
{:else if $update$.status == 'DOWNLOADED'}
|
||||
Installing update...
|
||||
{:else if $update$.status == 'DONE'}
|
||||
Install complete
|
||||
{:else if $update$.status == 'ERROR'}
|
||||
Error occurred...
|
||||
{/if}
|
||||
@ -128,11 +125,18 @@
|
||||
|
||||
{#if !$update$.status}
|
||||
<div class="cta-btn" transition:fade={{ duration: 100 }}>
|
||||
<Button wide on:click={() => installUpdate()}>Download {$update$.version}</Button>
|
||||
<Button
|
||||
wide
|
||||
on:click={async () => {
|
||||
await updaterService.installUpdate();
|
||||
}}
|
||||
>
|
||||
Download {$update$.version}
|
||||
</Button>
|
||||
</div>
|
||||
{:else if $update$.status == 'DONE'}
|
||||
<div class="cta-btn" transition:fade={{ duration: 100 }}>
|
||||
<Button wide on:click={() => relaunch()}>Restart to update</Button>
|
||||
<Button wide on:click={() => updaterService.relaunchApp()}>Restart</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
@ -2,6 +2,7 @@
|
||||
import SyncButton from './SyncButton.svelte';
|
||||
import Badge from '$lib/components/Badge.svelte';
|
||||
import Icon from '$lib/components/Icon.svelte';
|
||||
import { tooltip } from '$lib/utils/tooltip';
|
||||
import type { Project } from '$lib/backend/projects';
|
||||
import type { GitHubService } from '$lib/github/service';
|
||||
import type { BaseBranchService } from '$lib/vbranches/branchStoresCache';
|
||||
@ -10,6 +11,7 @@
|
||||
export let project: Project;
|
||||
export let baseBranchService: BaseBranchService;
|
||||
export let githubService: GitHubService;
|
||||
export let isNavCollapsed: boolean;
|
||||
|
||||
$: base$ = baseBranchService.base$;
|
||||
$: selected = $page.url.href.endsWith('/base');
|
||||
@ -17,56 +19,67 @@
|
||||
let baseContents: HTMLElement;
|
||||
</script>
|
||||
|
||||
<a href="/{project.id}/base" class="base-branch-card" class:selected bind:this={baseContents}>
|
||||
<div class="icon">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="16" height="16" rx="4" fill="#FB7D61" />
|
||||
<path d="M8 4L12 8L8 12L4 8L8 4Z" fill="white" />
|
||||
</svg>
|
||||
</div>
|
||||
<a
|
||||
use:tooltip={isNavCollapsed ? 'Trunk' : ''}
|
||||
href="/{project.id}/base"
|
||||
class="base-branch-card"
|
||||
class:selected
|
||||
bind:this={baseContents}
|
||||
>
|
||||
{#if isNavCollapsed}
|
||||
{#if ($base$?.behind || 0) > 0}
|
||||
<div class="small-count-badge">
|
||||
<span class="text-base-9 text-bold">{$base$?.behind || 0}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
<img class="icon" src="/images/domain-icons/trunk.svg" alt="" />
|
||||
|
||||
<div class="content">
|
||||
<div class="row_1">
|
||||
<span class="text-base-14 text-semibold trunk-label">Trunk</span>
|
||||
{#if ($base$?.behind || 0) > 0}
|
||||
<Badge count={$base$?.behind || 0} help="Unmerged upstream commits" />
|
||||
{/if}
|
||||
<SyncButton
|
||||
projectId={project.id}
|
||||
{baseBranchService}
|
||||
{githubService}
|
||||
cloudEnabled={project?.api?.sync || false}
|
||||
/>
|
||||
{#if !isNavCollapsed}
|
||||
<div class="content">
|
||||
<div class="row_1">
|
||||
<span class="text-base-14 text-semibold trunk-label">Trunk</span>
|
||||
{#if ($base$?.behind || 0) > 0}
|
||||
<Badge count={$base$?.behind || 0} help="Unmerged upstream commits" />
|
||||
{/if}
|
||||
<SyncButton
|
||||
projectId={project.id}
|
||||
{baseBranchService}
|
||||
{githubService}
|
||||
cloudEnabled={project?.api?.sync || false}
|
||||
/>
|
||||
</div>
|
||||
<div class="row_2 text-base-12">
|
||||
{#if $base$?.remoteUrl.includes('github.com')}
|
||||
<!-- GitHub logo -->
|
||||
<svg
|
||||
style="width:0.75rem; height: 0.75rem"
|
||||
viewBox="0 0 14 14"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M6.98091 0.599976C3.45242 0.599976 0.599976 3.47344 0.599976 7.02832C0.599976 9.86992 2.42763 12.2753 4.96308 13.1266C5.28007 13.1906 5.39619 12.9883 5.39619 12.8181C5.39619 12.6691 5.38574 12.1582 5.38574 11.626C3.61072 12.0092 3.24109 10.8597 3.24109 10.8597C2.95583 10.1147 2.53317 9.92321 2.53317 9.92321C1.9522 9.52941 2.57549 9.52941 2.57549 9.52941C3.21993 9.57199 3.55808 10.1893 3.55808 10.1893C4.12847 11.1683 5.04758 10.8917 5.41735 10.7214C5.47011 10.3063 5.63926 10.0189 5.81885 9.85934C4.40314 9.71031 2.91364 9.15691 2.91364 6.68768C2.91364 5.98525 3.16703 5.41055 3.56853 4.9636C3.50518 4.80399 3.28327 4.14401 3.63201 3.26068C3.63201 3.26068 4.17078 3.09036 5.38561 3.92053C5.90572 3.77982 6.4421 3.70824 6.98091 3.70763C7.51968 3.70763 8.06891 3.78221 8.57607 3.92053C9.79103 3.09036 10.3298 3.26068 10.3298 3.26068C10.6785 4.14401 10.4565 4.80399 10.3932 4.9636C10.8052 5.41055 11.0482 5.98525 11.0482 6.68768C11.0482 9.15691 9.55867 9.6996 8.13238 9.85934C8.36487 10.0615 8.56549 10.4446 8.56549 11.0513C8.56549 11.9133 8.55504 12.6052 8.55504 12.818C8.55504 12.9883 8.67129 13.1906 8.98815 13.1267C11.5236 12.2751 13.3513 9.86992 13.3513 7.02832C13.3617 3.47344 10.4988 0.599976 6.98091 0.599976Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
{:else}
|
||||
<Icon name="branch" />
|
||||
{/if}
|
||||
{$base$?.branchName}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row_2 text-base-12">
|
||||
{#if $base$?.remoteUrl.includes('github.com')}
|
||||
<!-- GitHub logo -->
|
||||
<svg
|
||||
style="width:0.75rem; height: 0.75rem"
|
||||
viewBox="0 0 14 14"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M6.98091 0.599976C3.45242 0.599976 0.599976 3.47344 0.599976 7.02832C0.599976 9.86992 2.42763 12.2753 4.96308 13.1266C5.28007 13.1906 5.39619 12.9883 5.39619 12.8181C5.39619 12.6691 5.38574 12.1582 5.38574 11.626C3.61072 12.0092 3.24109 10.8597 3.24109 10.8597C2.95583 10.1147 2.53317 9.92321 2.53317 9.92321C1.9522 9.52941 2.57549 9.52941 2.57549 9.52941C3.21993 9.57199 3.55808 10.1893 3.55808 10.1893C4.12847 11.1683 5.04758 10.8917 5.41735 10.7214C5.47011 10.3063 5.63926 10.0189 5.81885 9.85934C4.40314 9.71031 2.91364 9.15691 2.91364 6.68768C2.91364 5.98525 3.16703 5.41055 3.56853 4.9636C3.50518 4.80399 3.28327 4.14401 3.63201 3.26068C3.63201 3.26068 4.17078 3.09036 5.38561 3.92053C5.90572 3.77982 6.4421 3.70824 6.98091 3.70763C7.51968 3.70763 8.06891 3.78221 8.57607 3.92053C9.79103 3.09036 10.3298 3.26068 10.3298 3.26068C10.6785 4.14401 10.4565 4.80399 10.3932 4.9636C10.8052 5.41055 11.0482 5.98525 11.0482 6.68768C11.0482 9.15691 9.55867 9.6996 8.13238 9.85934C8.36487 10.0615 8.56549 10.4446 8.56549 11.0513C8.56549 11.9133 8.55504 12.6052 8.55504 12.818C8.55504 12.9883 8.67129 13.1906 8.98815 13.1267C11.5236 12.2751 13.3513 9.86992 13.3513 7.02832C13.3617 3.47344 10.4988 0.599976 6.98091 0.599976Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
{:else}
|
||||
<Icon name="branch" />
|
||||
{/if}
|
||||
{$base$?.branchName}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</a>
|
||||
|
||||
<style lang="postcss">
|
||||
.base-branch-card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
gap: var(--space-10);
|
||||
padding: var(--space-10) var(--space-8);
|
||||
padding: var(--space-10);
|
||||
border-radius: var(--radius-m);
|
||||
transition: background-color var(--transition-fast);
|
||||
}
|
||||
@ -78,6 +91,9 @@
|
||||
}
|
||||
|
||||
.icon {
|
||||
border-radius: var(--radius-s);
|
||||
height: var(--space-20);
|
||||
width: var(--space-20);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.content {
|
||||
@ -100,4 +116,18 @@
|
||||
gap: var(--space-4);
|
||||
color: var(--clr-theme-scale-ntrl-40);
|
||||
}
|
||||
.small-count-badge {
|
||||
position: absolute;
|
||||
top: 10%;
|
||||
right: 10%;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--space-2);
|
||||
min-width: var(--space-14);
|
||||
background-color: var(--clr-theme-err-element);
|
||||
color: var(--clr-theme-scale-ntrl-100);
|
||||
border-radius: var(--radius-m);
|
||||
}
|
||||
</style>
|
||||
|
@ -350,9 +350,11 @@
|
||||
<Resizer
|
||||
viewport={rsViewport}
|
||||
direction="right"
|
||||
inside={$selectedFiles.length > 0}
|
||||
minWidth={320}
|
||||
sticky
|
||||
defaultLineColor={$selectedFiles.length > 0
|
||||
? 'transparent'
|
||||
: 'var(--clr-theme-container-outline-light)'}
|
||||
on:width={(e) => {
|
||||
laneWidth = e.detail / (16 * $userSettings.zoom);
|
||||
lscache.set(laneWidthKey + branch.id, laneWidth, 7 * 1440); // 7 day ttl
|
||||
@ -386,21 +388,8 @@
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: var(--space-4);
|
||||
height: 100%;
|
||||
transform: translateX(var(--selected-resize-shift));
|
||||
|
||||
&:after {
|
||||
pointer-events: none;
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 1px;
|
||||
height: 100%;
|
||||
opacity: var(--selected-opacity);
|
||||
background-color: var(--clr-theme-container-outline-light);
|
||||
}
|
||||
}
|
||||
|
||||
.branch-card__dropzone-wrapper {
|
||||
|
@ -116,8 +116,8 @@
|
||||
<Resizer
|
||||
viewport={rsViewport}
|
||||
direction="right"
|
||||
inside
|
||||
minWidth={240}
|
||||
defaultLineColor="var(--clr-theme-container-outline-light)"
|
||||
on:width={(e) => {
|
||||
fileWidth = e.detail / (16 * $userSettings.zoom);
|
||||
lscache.set(fileWidthKey + branch.id, fileWidth, 7 * 1440); // 7 day ttl
|
||||
@ -151,7 +151,7 @@
|
||||
}
|
||||
|
||||
.file-selected {
|
||||
--selected-resize-shift: calc(var(--space-6) * -1);
|
||||
--selected-resize-shift: calc((var(--space-6) + 0.0625rem) * -1);
|
||||
--selected-target-branch-right-padding: calc(var(--space-4) * -1);
|
||||
--selected-opacity: 0;
|
||||
}
|
||||
@ -165,7 +165,6 @@
|
||||
align-items: self-start;
|
||||
|
||||
padding: var(--space-12) var(--space-12) var(--space-12) 0;
|
||||
border-right: 1px solid var(--clr-theme-container-outline-light);
|
||||
margin-left: var(--selected-target-branch-right-padding);
|
||||
}
|
||||
</style>
|
||||
|
@ -3,7 +3,6 @@
|
||||
import BranchesHeader from './BranchesHeader.svelte';
|
||||
import FilterPopupMenu from '$lib/components/FilterPopupMenu.svelte';
|
||||
import ImgThemed from '$lib/components/ImgThemed.svelte';
|
||||
import Resizer from '$lib/components/Resizer.svelte';
|
||||
import ScrollableContainer from '$lib/components/ScrollableContainer.svelte';
|
||||
import TextBox from '$lib/components/TextBox.svelte';
|
||||
import { persisted } from '$lib/persisted/persisted';
|
||||
@ -64,7 +63,6 @@
|
||||
|
||||
let resizeGuard: HTMLElement;
|
||||
let viewport: HTMLDivElement;
|
||||
let rsViewport: HTMLElement;
|
||||
let contents: HTMLElement;
|
||||
|
||||
let observer: ResizeObserver;
|
||||
@ -137,21 +135,7 @@
|
||||
</script>
|
||||
|
||||
<div class="resize-guard" bind:this={resizeGuard}>
|
||||
<div
|
||||
class="branch-list"
|
||||
bind:this={rsViewport}
|
||||
style:height={$height ? `${$height}rem` : undefined}
|
||||
style:max-height={maxHeight ? `${maxHeight}rem` : undefined}
|
||||
>
|
||||
<Resizer
|
||||
viewport={rsViewport}
|
||||
direction="up"
|
||||
inside
|
||||
minHeight={90}
|
||||
on:height={(e) => {
|
||||
$height = Math.min(maxHeight, e.detail / (16 * $userSettings.zoom));
|
||||
}}
|
||||
/>
|
||||
<div class="branch-list">
|
||||
<BranchesHeader count={$filteredBranches$?.length ?? 0} filtersActive={$filtersActive}>
|
||||
<FilterPopupMenu
|
||||
slot="context-menu"
|
||||
@ -228,13 +212,16 @@
|
||||
gap: var(--space-12);
|
||||
width: 100%;
|
||||
padding-bottom: var(--space-16);
|
||||
padding-left: var(--space-12);
|
||||
padding-right: var(--space-12);
|
||||
padding-left: var(--space-14);
|
||||
padding-right: var(--space-14);
|
||||
}
|
||||
.branch-list {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-top: 1px solid var(--clr-theme-container-outline-light);
|
||||
}
|
||||
.content {
|
||||
display: flex;
|
||||
|
@ -49,9 +49,8 @@
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding: var(--space-12);
|
||||
padding: var(--space-14) var(--space-14) var(--space-12) var(--space-14);
|
||||
gap: var(--space-8);
|
||||
border-top: 1px solid var(--clr-theme-container-outline-light);
|
||||
border-bottom: 1px solid transparent;
|
||||
transition: border-bottom var(--transition-fast);
|
||||
position: relative;
|
||||
|
@ -1,12 +1,41 @@
|
||||
<script lang="ts">
|
||||
import UpdateBaseButton from './UpdateBaseButton.svelte';
|
||||
import { tooltip } from '$lib/utils/tooltip';
|
||||
import type { BranchController } from '$lib/vbranches/branchController';
|
||||
import type { BaseBranchService } from '$lib/vbranches/branchStoresCache';
|
||||
import { page } from '$app/stores';
|
||||
|
||||
export let href: string;
|
||||
export let domain: string;
|
||||
export let label: string;
|
||||
export let iconSrc: string;
|
||||
export let branchController: BranchController;
|
||||
export let baseBranchService: BaseBranchService;
|
||||
export let isNavCollapsed: boolean;
|
||||
|
||||
$: base$ = baseBranchService.base$;
|
||||
|
||||
$: selected = $page.url.href.includes(href);
|
||||
</script>
|
||||
|
||||
<a {href} class="domain-button text-base-14 text-semibold" class:selected>
|
||||
<slot />
|
||||
<a
|
||||
use:tooltip={isNavCollapsed ? label : ''}
|
||||
{href}
|
||||
class="domain-button text-base-14 text-semibold"
|
||||
class:selected
|
||||
>
|
||||
{#if domain === 'workspace'}
|
||||
<img class="icon" src={iconSrc} alt="" />
|
||||
|
||||
{#if !isNavCollapsed}
|
||||
<span class="text-base-14 text-semibold" class:collapsed-txt={isNavCollapsed}>{label}</span>
|
||||
{#if ($base$?.behind || 0) > 0 && !isNavCollapsed}
|
||||
<UpdateBaseButton {branchController} />
|
||||
{/if}
|
||||
{/if}
|
||||
{:else}
|
||||
<slot />
|
||||
{/if}
|
||||
</a>
|
||||
|
||||
<style lang="postcss">
|
||||
@ -15,12 +44,18 @@
|
||||
align-items: center;
|
||||
gap: var(--space-10);
|
||||
border-radius: var(--radius-m);
|
||||
padding: var(--space-10) var(--space-8);
|
||||
padding: var(--space-10);
|
||||
color: var(--clr-theme-scale-ntrl-0);
|
||||
height: var(--space-36);
|
||||
transition: background-color var(--transition-fast);
|
||||
}
|
||||
|
||||
.icon {
|
||||
border-radius: var(--radius-s);
|
||||
height: var(--space-20);
|
||||
width: var(--space-20);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.domain-button:not(.selected):hover,
|
||||
.domain-button:not(.selected):focus,
|
||||
.selected {
|
||||
|
@ -84,7 +84,7 @@
|
||||
}}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
on:contextmenu={(e) =>
|
||||
on:contextmenu|preventDefault={(e) =>
|
||||
popupMenu.openByMouse(e, {
|
||||
files: $selectedFiles.includes(file) ? $selectedFiles : [file]
|
||||
})}
|
||||
|
@ -7,32 +7,53 @@
|
||||
|
||||
export let user: User | undefined;
|
||||
export let projectId: string | undefined;
|
||||
export let isNavCollapsed: boolean;
|
||||
</script>
|
||||
|
||||
<div class="footer" style:border-color="var(--clr-theme-container-outline-light)">
|
||||
<div class="footer" class:collapsed={isNavCollapsed}>
|
||||
<div class="left-btns">
|
||||
<IconButton
|
||||
icon="mail"
|
||||
help="Send feedback"
|
||||
size={isNavCollapsed ? 'xl' : 'l'}
|
||||
width={isNavCollapsed ? '100%' : undefined}
|
||||
on:click={() => events.emit('openSendIssueModal')}
|
||||
/>
|
||||
<Link href={`/${projectId}/settings`}>
|
||||
<IconButton icon="settings" help="Project settings" />
|
||||
<IconButton
|
||||
icon="settings"
|
||||
help="Project settings"
|
||||
size={isNavCollapsed ? 'xl' : 'l'}
|
||||
width={isNavCollapsed ? '100%' : undefined}
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<AccountLink {user} />
|
||||
<AccountLink {user} {isNavCollapsed} />
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
.footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: var(--space-12);
|
||||
border-top: 1px solid var(--clr-theme-container-outline-light);
|
||||
border-color: var(--clr-theme-container-outline-light);
|
||||
}
|
||||
|
||||
.left-btns {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.footer.collapsed {
|
||||
flex-direction: column;
|
||||
padding: 0 var(--space-14);
|
||||
align-items: flex-start;
|
||||
gap: var(--space-4);
|
||||
border: none;
|
||||
|
||||
& .left-btns {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -4,9 +4,10 @@
|
||||
import type iconsJson from '$lib/icons/icons.json';
|
||||
|
||||
export let icon: keyof typeof iconsJson;
|
||||
export let size: 's' | 'm' | 'l' = 'l';
|
||||
export let size: 's' | 'm' | 'l' | 'xl' = 'l';
|
||||
export let loading = false;
|
||||
export let help = '';
|
||||
export let width: string | undefined = undefined;
|
||||
|
||||
let className = '';
|
||||
let selected = false;
|
||||
@ -20,9 +21,11 @@
|
||||
class:small={size == 's'}
|
||||
class:medium={size == 'm'}
|
||||
class:large={size == 'l'}
|
||||
class:x-large={size == 'xl'}
|
||||
use:tooltip={help}
|
||||
{title}
|
||||
on:click
|
||||
style:width
|
||||
>
|
||||
<Icon name={loading ? 'spinner' : icon} />
|
||||
</button>
|
||||
@ -47,6 +50,11 @@
|
||||
background-color: color-mix(in srgb, transparent, var(--darken-tint-light));
|
||||
cursor: default;
|
||||
}
|
||||
.x-large {
|
||||
height: var(--size-btn-xl);
|
||||
width: var(--size-btn-xl);
|
||||
padding: var(--space-12);
|
||||
}
|
||||
.large {
|
||||
height: var(--size-btn-l);
|
||||
width: var(--size-btn-l);
|
||||
|
@ -1,13 +1,17 @@
|
||||
<script lang="ts">
|
||||
import BaseBranchCard from './BaseBranchCard.svelte';
|
||||
import Branches from './Branches.svelte';
|
||||
import DomainButton from './DomainButton.svelte';
|
||||
import Footer from './Footer.svelte';
|
||||
import ProjectSelector from './ProjectSelector.svelte';
|
||||
import UpdateBaseButton from './UpdateBaseButton.svelte';
|
||||
import BaseBranchCard from '$lib/components/BaseBranchCard.svelte';
|
||||
import Resizer from '$lib/components/Resizer.svelte';
|
||||
import Resizer from './Resizer.svelte';
|
||||
import { persisted } from '$lib/persisted/persisted';
|
||||
import { SETTINGS_CONTEXT, type SettingsStore } from '$lib/settings/userSettings';
|
||||
import * as hotkeys from '$lib/utils/hotkeys';
|
||||
import { unsubscribe } from '$lib/utils/random';
|
||||
import { platform } from '@tauri-apps/api/os';
|
||||
import { from } from 'rxjs';
|
||||
import { onMount } from 'svelte';
|
||||
import { getContext } from 'svelte';
|
||||
import type { User } from '$lib/backend/cloud';
|
||||
import type { Project, ProjectService } from '$lib/backend/projects';
|
||||
@ -24,79 +28,217 @@
|
||||
export let githubService: GitHubService;
|
||||
export let projectService: ProjectService;
|
||||
|
||||
const minResizerWidth = 280;
|
||||
const minResizerRatio = 150;
|
||||
const platformName = from(platform());
|
||||
const userSettings = getContext<SettingsStore>(SETTINGS_CONTEXT);
|
||||
const defaultTrayWidthRem = persisted<number | undefined>(
|
||||
undefined,
|
||||
'defaulTrayWidth_ ' + project.id
|
||||
);
|
||||
|
||||
$: base$ = baseBranchService.base$;
|
||||
|
||||
let viewport: HTMLDivElement;
|
||||
let isResizerDragging = false;
|
||||
|
||||
$: isNavCollapsed = persisted<boolean>(false, 'projectNavCollapsed_' + project.id);
|
||||
|
||||
function toggleNavCollapse() {
|
||||
$isNavCollapsed = !$isNavCollapsed;
|
||||
}
|
||||
|
||||
onMount(() =>
|
||||
unsubscribe(
|
||||
hotkeys.on('Meta+/', () => {
|
||||
toggleNavCollapse();
|
||||
})
|
||||
)
|
||||
);
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="navigation relative flex w-80 shrink-0 flex-col border-r"
|
||||
style:width={$defaultTrayWidthRem ? $defaultTrayWidthRem + 'rem' : null}
|
||||
bind:this={viewport}
|
||||
role="menu"
|
||||
tabindex="0"
|
||||
>
|
||||
<div class="drag-region" data-tauri-drag-region></div>
|
||||
<div class="domains">
|
||||
<ProjectSelector {project} {projectService} />
|
||||
<div class="flex flex-col gap-1">
|
||||
<BaseBranchCard {project} {baseBranchService} {githubService} />
|
||||
<DomainButton href={`/${project.id}/board`}>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M0 6.64C0 4.17295 0 2.93942 0.525474 2.01817C0.880399 1.39592 1.39592 0.880399 2.01817 0.525474C2.93942 0 4.17295 0 6.64 0H9.36C11.8271 0 13.0606 0 13.9818 0.525474C14.6041 0.880399 15.1196 1.39592 15.4745 2.01817C16 2.93942 16 4.17295 16 6.64V9.36C16 11.8271 16 13.0606 15.4745 13.9818C15.1196 14.6041 14.6041 15.1196 13.9818 15.4745C13.0606 16 11.8271 16 9.36 16H6.64C4.17295 16 2.93942 16 2.01817 15.4745C1.39592 15.1196 0.880399 14.6041 0.525474 13.9818C0 13.0606 0 11.8271 0 9.36V6.64Z"
|
||||
fill="#48B0AA"
|
||||
/>
|
||||
<rect x="2" y="3" width="6" height="10" rx="2" fill="#D9F3F2" />
|
||||
<rect opacity="0.7" x="10" y="3" width="4" height="10" rx="2" fill="#D9F3F2" />
|
||||
</svg>
|
||||
<aside class="navigation-wrapper">
|
||||
<div class="resizer-wrapper" class:resizerDragging={isResizerDragging} tabindex="0" role="button">
|
||||
<button
|
||||
class="folding-button"
|
||||
on:click={toggleNavCollapse}
|
||||
class:folding-button_folded={$isNavCollapsed}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 0 8 12"
|
||||
fill="none"
|
||||
><path
|
||||
d="M6,0L0,6l6,6"
|
||||
transform="translate(1 0)"
|
||||
stroke-width="1.5"
|
||||
stroke-linejoin="round"
|
||||
/></svg
|
||||
>
|
||||
</button>
|
||||
<Resizer
|
||||
{viewport}
|
||||
direction="right"
|
||||
minWidth={minResizerWidth}
|
||||
defaultLineColor="var(--clr-theme-container-outline-light)"
|
||||
on:click={() => $isNavCollapsed && toggleNavCollapse()}
|
||||
on:dblclick={() => !$isNavCollapsed && toggleNavCollapse()}
|
||||
on:width={(e) => {
|
||||
$defaultTrayWidthRem = e.detail / (16 * $userSettings.zoom);
|
||||
}}
|
||||
on:resizing={(e) => (isResizerDragging = e.detail)}
|
||||
on:overflowValue={(e) => {
|
||||
const overflowValue = e.detail;
|
||||
|
||||
<span>Workspace</span>
|
||||
{#if ($base$?.behind || 0) > 0}
|
||||
<UpdateBaseButton {branchController} />
|
||||
{/if}
|
||||
</DomainButton>
|
||||
</div>
|
||||
if (!$isNavCollapsed && overflowValue > minResizerRatio) {
|
||||
$isNavCollapsed = true;
|
||||
}
|
||||
|
||||
if ($isNavCollapsed && overflowValue < minResizerRatio) {
|
||||
$isNavCollapsed = false;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Branches projectId={project.id} {branchService} {githubService} />
|
||||
<Footer {user} projectId={project.id} />
|
||||
|
||||
<Resizer
|
||||
{viewport}
|
||||
direction="right"
|
||||
minWidth={320}
|
||||
on:width={(e) => {
|
||||
$defaultTrayWidthRem = e.detail / (16 * $userSettings.zoom);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="navigation"
|
||||
class:collapsed={$isNavCollapsed}
|
||||
style:width={$defaultTrayWidthRem && !$isNavCollapsed ? $defaultTrayWidthRem + 'rem' : null}
|
||||
bind:this={viewport}
|
||||
role="menu"
|
||||
tabindex="0"
|
||||
>
|
||||
<!-- condition prevents split second UI shift -->
|
||||
{#if $platformName}
|
||||
<div class="navigation-top">
|
||||
{#if $platformName == 'darwin'}
|
||||
<div class="drag-region" data-tauri-drag-region />
|
||||
{/if}
|
||||
<ProjectSelector {project} {projectService} isNavCollapsed={$isNavCollapsed} />
|
||||
<div class="domains">
|
||||
<BaseBranchCard
|
||||
{project}
|
||||
{baseBranchService}
|
||||
{githubService}
|
||||
isNavCollapsed={$isNavCollapsed}
|
||||
/>
|
||||
<DomainButton
|
||||
href={`/${project.id}/board`}
|
||||
domain="workspace"
|
||||
label="Workspace"
|
||||
iconSrc="/images/domain-icons/working-branches.svg"
|
||||
{branchController}
|
||||
{baseBranchService}
|
||||
isNavCollapsed={$isNavCollapsed}
|
||||
></DomainButton>
|
||||
</div>
|
||||
</div>
|
||||
{#if !$isNavCollapsed}
|
||||
<Branches projectId={project.id} {branchService} {githubService} />
|
||||
{/if}
|
||||
<Footer {user} projectId={project.id} isNavCollapsed={$isNavCollapsed} />
|
||||
{/if}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<style lang="postcss">
|
||||
.navigation-wrapper {
|
||||
display: flex;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
& .folding-button {
|
||||
opacity: 1;
|
||||
transform: translateY(-50%);
|
||||
right: calc(var(--space-6) * -1);
|
||||
transition-delay: 0.1s;
|
||||
|
||||
& svg {
|
||||
transition-delay: 0.1s;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigation {
|
||||
border-right: 1px solid var(--clr-theme-container-outline-light);
|
||||
width: 17.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
background: var(--clr-theme-container-light);
|
||||
max-height: 100%;
|
||||
user-select: none;
|
||||
}
|
||||
.drag-region {
|
||||
flex-shrink: 0;
|
||||
height: var(--space-24);
|
||||
height: var(--space-20);
|
||||
}
|
||||
.navigation-top {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-bottom: var(--space-24);
|
||||
padding-left: var(--space-14);
|
||||
padding-right: var(--space-14);
|
||||
}
|
||||
.domains {
|
||||
padding-bottom: var(--space-24);
|
||||
padding-left: var(--space-12);
|
||||
padding-right: var(--space-12);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.resizer-wrapper {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
height: 100%;
|
||||
width: var(--space-4);
|
||||
|
||||
&:hover,
|
||||
&.resizerDragging {
|
||||
& .folding-button {
|
||||
background-color: var(--resizer-color);
|
||||
border: 1px solid var(--resizer-color);
|
||||
|
||||
& svg {
|
||||
stroke: var(--clr-theme-scale-ntrl-100);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.folding-button {
|
||||
z-index: 42;
|
||||
position: absolute;
|
||||
right: calc(var(--space-2) * -1);
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: var(--space-16);
|
||||
height: var(--space-36);
|
||||
padding: var(--space-4);
|
||||
background: var(--clr-theme-container-light);
|
||||
border-radius: var(--radius-m);
|
||||
border: 1px solid var(--clr-theme-container-outline-light);
|
||||
opacity: 0;
|
||||
transition:
|
||||
background-color var(--transition-fast),
|
||||
border-color var(--transition-fast),
|
||||
opacity var(--transition-medium),
|
||||
all var(--transition-medium);
|
||||
|
||||
& svg {
|
||||
stroke: var(--clr-theme-scale-ntrl-50);
|
||||
transition: stroke var(--transition-fast);
|
||||
}
|
||||
}
|
||||
|
||||
.folding-button_folded {
|
||||
& svg {
|
||||
transform: rotate(180deg) translateX(-0.0625rem);
|
||||
}
|
||||
}
|
||||
|
||||
.navigation.collapsed {
|
||||
width: auto;
|
||||
justify-content: space-between;
|
||||
padding-bottom: var(--space-16);
|
||||
}
|
||||
</style>
|
||||
|
62
gitbutler-ui/src/lib/components/ProjectAvatar.svelte
Normal file
62
gitbutler-ui/src/lib/components/ProjectAvatar.svelte
Normal file
@ -0,0 +1,62 @@
|
||||
<script lang="ts">
|
||||
export let name: string | undefined;
|
||||
|
||||
const colors = [
|
||||
'#E78D8D',
|
||||
'#62CDCD',
|
||||
'#EC90D2',
|
||||
'#7DC8D8',
|
||||
'#F1BC55',
|
||||
'#6B6B4C',
|
||||
'#9785DE',
|
||||
'#99CE63',
|
||||
'#636ECE',
|
||||
'#5FD2B0'
|
||||
];
|
||||
|
||||
function nameToColor(name: string | undefined) {
|
||||
const trimmed = name?.replace(/\s/g, '');
|
||||
if (!trimmed) {
|
||||
return `linear-gradient(45deg, ${colors[0][0]} 15%, ${colors[0][1]} 90%)`;
|
||||
}
|
||||
|
||||
const startHash = trimmed.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0);
|
||||
return colors[startHash % colors.length];
|
||||
}
|
||||
|
||||
function getFirstLetter(name: string | undefined) {
|
||||
return name ? name[0].toUpperCase() : '';
|
||||
}
|
||||
|
||||
$: firstLetter = getFirstLetter(name);
|
||||
</script>
|
||||
|
||||
<div class="project-avatar" style:background-color={nameToColor(name)}>
|
||||
<svg class="avatar-letter" viewBox="0 0 24 24">
|
||||
<text x="50%" y="54%" text-anchor="middle" alignment-baseline="middle">
|
||||
{firstLetter.toUpperCase()}
|
||||
</text>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.project-avatar {
|
||||
flex-shrink: 0;
|
||||
width: var(--space-20);
|
||||
height: var(--space-20);
|
||||
border-radius: var(--radius-m);
|
||||
}
|
||||
|
||||
.avatar-letter {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.avatar-letter text {
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-weight: 800;
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
fill: var(--clr-core-ntrl-100);
|
||||
}
|
||||
</style>
|
@ -1,53 +1,61 @@
|
||||
<script lang="ts">
|
||||
import ProjectAvatar from './ProjectAvatar.svelte';
|
||||
import ProjectsPopup from './ProjectsPopup.svelte';
|
||||
import { clickOutside } from '$lib/clickOutside';
|
||||
import Icon from '$lib/components/Icon.svelte';
|
||||
import { tooltip } from '$lib/utils/tooltip';
|
||||
import type { Project, ProjectService } from '$lib/backend/projects';
|
||||
|
||||
export let project: Project | undefined;
|
||||
export let projectService: ProjectService;
|
||||
export let isNavCollapsed: boolean;
|
||||
|
||||
let popup: ProjectsPopup;
|
||||
let visible: boolean = false;
|
||||
</script>
|
||||
|
||||
<div class="wrapper">
|
||||
<div
|
||||
class="relative"
|
||||
use:clickOutside={{
|
||||
handler: () => {
|
||||
popup.hide();
|
||||
visible = false;
|
||||
},
|
||||
enabled: visible
|
||||
<div
|
||||
class="wrapper"
|
||||
use:clickOutside={{
|
||||
handler: () => {
|
||||
popup.hide();
|
||||
visible = false;
|
||||
},
|
||||
enabled: visible
|
||||
}}
|
||||
>
|
||||
<button
|
||||
class="button"
|
||||
use:tooltip={isNavCollapsed ? project?.title : ''}
|
||||
on:click={(e) => {
|
||||
visible = popup.toggle();
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
<button
|
||||
class="button"
|
||||
on:click={(e) => {
|
||||
visible = popup.toggle();
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
<ProjectAvatar name={project?.title} />
|
||||
{#if !isNavCollapsed}
|
||||
<span class="button__label text-base-14 text-bold">{project?.title}</span>
|
||||
<div class="button__icon">
|
||||
<Icon name="select-chevron" />
|
||||
</div>
|
||||
</button>
|
||||
<ProjectsPopup bind:this={popup} {projectService} />
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
<ProjectsPopup bind:this={popup} {projectService} {isNavCollapsed} />
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
.wrapper {
|
||||
margin-top: var(--space-10);
|
||||
position: relative;
|
||||
margin-top: var(--space-14);
|
||||
margin-bottom: var(--space-16);
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
.button {
|
||||
display: flex;
|
||||
gap: var(--space-10);
|
||||
width: 100%;
|
||||
padding: var(--space-12);
|
||||
padding: var(--space-10);
|
||||
border-radius: var(--radius-m);
|
||||
|
||||
background-color: var(--clr-theme-container-pale);
|
||||
@ -75,6 +83,9 @@
|
||||
flex-grow: 1;
|
||||
color: var(--clr-theme-scale-ntrl-0);
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.button__icon {
|
||||
|
@ -5,6 +5,7 @@
|
||||
import { page } from '$app/stores';
|
||||
|
||||
export let projectService: ProjectService;
|
||||
export let isNavCollapsed: boolean;
|
||||
|
||||
$: projects$ = projectService.projects$;
|
||||
|
||||
@ -22,7 +23,7 @@
|
||||
</script>
|
||||
|
||||
{#if !hidden}
|
||||
<div class="popup">
|
||||
<div class="popup" class:collapsed={isNavCollapsed}>
|
||||
{#if $projects$.length > 0}
|
||||
<div class="popup__projects">
|
||||
{#each $projects$ as project}
|
||||
@ -81,4 +82,9 @@
|
||||
gap: var(--space-2);
|
||||
padding: var(--space-8);
|
||||
}
|
||||
|
||||
/* MODIFIERS */
|
||||
.popup.collapsed {
|
||||
width: 240px;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { pxToRem } from '$lib/utils/pxToRem';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
// The element that is being resized
|
||||
@ -7,11 +8,17 @@
|
||||
// Sets direction of resizing for viewport
|
||||
export let direction: 'left' | 'right' | 'up' | 'down';
|
||||
|
||||
// Needed when overflow is hidden
|
||||
export let inside = false;
|
||||
// Sets the color of the line
|
||||
export let defaultLineColor: string = 'none';
|
||||
export let defaultLineThickness: number = 1;
|
||||
export let hoverLineThickness: number = 2;
|
||||
|
||||
// Needed when overflow is hidden
|
||||
export let sticky = false;
|
||||
|
||||
// Custom z-index in case of overlapping with other elements
|
||||
export let zIndex = 40;
|
||||
|
||||
//
|
||||
export let minWidth = 0;
|
||||
export let minHeight = 0;
|
||||
@ -25,6 +32,7 @@
|
||||
height: number;
|
||||
width: number;
|
||||
resizing: boolean;
|
||||
overflowValue: number;
|
||||
}>();
|
||||
|
||||
function onMouseDown(e: MouseEvent) {
|
||||
@ -41,23 +49,37 @@
|
||||
dispatch('resizing', true);
|
||||
}
|
||||
|
||||
function onOverflowValue(currentValue: number, minVal: number) {
|
||||
if (currentValue < minVal) {
|
||||
dispatch('overflowValue', minVal - currentValue);
|
||||
}
|
||||
}
|
||||
|
||||
function onMouseMove(e: MouseEvent) {
|
||||
dragging = true;
|
||||
if (direction == 'down') {
|
||||
let height = e.clientY - initial;
|
||||
dispatch('height', Math.max(height, minHeight));
|
||||
|
||||
onOverflowValue(height, minHeight);
|
||||
}
|
||||
if (direction == 'up') {
|
||||
let height = document.body.scrollHeight - e.clientY - initial;
|
||||
dispatch('height', Math.max(height, minHeight));
|
||||
|
||||
onOverflowValue(height, minHeight);
|
||||
}
|
||||
if (direction == 'right') {
|
||||
let width = e.clientX - initial + 2;
|
||||
dispatch('width', Math.max(width, minWidth));
|
||||
|
||||
onOverflowValue(width, minWidth);
|
||||
}
|
||||
if (direction == 'left') {
|
||||
let width = document.body.scrollWidth - e.clientX - initial;
|
||||
dispatch('width', Math.max(width, minWidth));
|
||||
|
||||
onOverflowValue(width, minWidth);
|
||||
}
|
||||
}
|
||||
|
||||
@ -71,11 +93,13 @@
|
||||
|
||||
<div
|
||||
on:mousedown={onMouseDown}
|
||||
class="resizer"
|
||||
on:click|stopPropagation
|
||||
on:dblclick|stopPropagation
|
||||
on:keydown|stopPropagation
|
||||
tabindex="0"
|
||||
role="slider"
|
||||
aria-valuenow={viewport?.clientHeight}
|
||||
class:inside
|
||||
class="resizer"
|
||||
class:dragging
|
||||
class:vertical={orientation == 'vertical'}
|
||||
class:horizontal={orientation == 'horizontal'}
|
||||
@ -84,68 +108,111 @@
|
||||
class:left={direction == 'left'}
|
||||
class:right={direction == 'right'}
|
||||
class:sticky
|
||||
/>
|
||||
style:z-index={zIndex}
|
||||
>
|
||||
<div
|
||||
class="resizer-line"
|
||||
style="--resizer-default-line-color: {defaultLineColor}; --resizer-default-line-thickness: {pxToRem(
|
||||
defaultLineThickness
|
||||
)}; --resizer-hover-line-thickness: {pxToRem(hoverLineThickness)}"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
.resizer {
|
||||
--resizer-frame-thickness: var(--space-4);
|
||||
--resizer-default-line-thickness: var(--space-2);
|
||||
--resizer-hover-line-thickness: var(--space-8);
|
||||
--resizer-default-line-color: none;
|
||||
position: absolute;
|
||||
transition: background-color 0.1s ease-out;
|
||||
/* background-color: var(--clr-theme-container-outline-light); */
|
||||
&:hover {
|
||||
transition-delay: 0.1s;
|
||||
}
|
||||
z-index: 40;
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&.dragging {
|
||||
background-color: var(--clr-theme-container-outline-light);
|
||||
outline: none;
|
||||
|
||||
& .resizer-line {
|
||||
transition-delay: 0.1s;
|
||||
background-color: var(--resizer-color);
|
||||
}
|
||||
|
||||
&:not(.vertical) {
|
||||
& .resizer-line {
|
||||
width: var(--resizer-hover-line-thickness);
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.horizontal) {
|
||||
& .resizer-line {
|
||||
height: var(--resizer-hover-line-thickness);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.resizer-line {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: var(--resizer-default-line-color);
|
||||
pointer-events: none;
|
||||
transition:
|
||||
background-color 0.1s ease-out,
|
||||
width 0.1s ease-out,
|
||||
height 0.1s ease-out;
|
||||
}
|
||||
|
||||
.horizontal {
|
||||
width: var(--space-4);
|
||||
height: 100%;
|
||||
cursor: col-resize;
|
||||
top: 0;
|
||||
&:hover {
|
||||
width: var(--space-4);
|
||||
|
||||
& .resizer-line {
|
||||
width: var(--resizer-default-line-thickness);
|
||||
}
|
||||
}
|
||||
.vertical {
|
||||
height: var(--space-4);
|
||||
width: 100%;
|
||||
cursor: row-resize;
|
||||
&:hover {
|
||||
height: var(--space-4);
|
||||
left: 0;
|
||||
|
||||
& .resizer-line {
|
||||
height: var(--resizer-default-line-thickness);
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
right: calc(-1 * var(--space-2));
|
||||
&.inside {
|
||||
right: 0;
|
||||
right: 0;
|
||||
|
||||
& .resizer-line {
|
||||
left: auto;
|
||||
}
|
||||
}
|
||||
.left {
|
||||
left: 0;
|
||||
&:hover {
|
||||
width: var(--space-4);
|
||||
|
||||
& .resizer-line {
|
||||
right: auto;
|
||||
}
|
||||
}
|
||||
.up {
|
||||
top: 0;
|
||||
&:hover {
|
||||
height: var(--space-4);
|
||||
|
||||
& .resizer-line {
|
||||
bottom: auto;
|
||||
}
|
||||
}
|
||||
.down {
|
||||
bottom: calc(-1 * var(--space-2));
|
||||
&:hover {
|
||||
height: var(--space-4);
|
||||
}
|
||||
&.inside {
|
||||
bottom: 0;
|
||||
bottom: 0;
|
||||
|
||||
& .resizer-line {
|
||||
top: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.sticky {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
|
@ -67,7 +67,7 @@
|
||||
}}
|
||||
on:click
|
||||
on:keydown
|
||||
on:contextmenu={(e) =>
|
||||
on:contextmenu|preventDefault={(e) =>
|
||||
popupMenu.openByMouse(e, {
|
||||
files: $selectedFiles.includes(file) ? $selectedFiles : [file]
|
||||
})}
|
||||
|
@ -81,11 +81,6 @@ export function draggable(node: HTMLElement, opts: Partial<DraggableOptions> | u
|
||||
dragHandle = e.target as HTMLElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* The problem with the ghost element is that it gets clipped after rotation unless we enclose
|
||||
* it within a larger bounding box. This means we have an extra `<div>` in the html that is
|
||||
* only present to support the rotation
|
||||
*/
|
||||
function handleDragStart(e: DragEvent) {
|
||||
let elt: HTMLElement | null = dragHandle;
|
||||
while (elt) {
|
||||
|
@ -71,6 +71,7 @@
|
||||
name: newName,
|
||||
picture: picture
|
||||
});
|
||||
updatedUser.github_access_token = $user$?.github_access_token; // prevent overwriting with null
|
||||
userService.setUser(updatedUser);
|
||||
toasts.success('Profile updated');
|
||||
} catch (e) {
|
||||
|
@ -36,6 +36,7 @@
|
||||
|
||||
/* TODO: add focus color */
|
||||
--focus-color: var(--clr-theme-scale-pop-50);
|
||||
--resizer-color: var(--clr-theme-scale-pop-50);
|
||||
}
|
||||
|
||||
:root.dark {
|
||||
@ -121,12 +122,6 @@ the scrollbar to scale with the font size */
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Accordion uses this */
|
||||
|
||||
.collapsed {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* FOCUS STATE */
|
||||
|
||||
.focus-state {
|
||||
|
@ -172,6 +172,7 @@
|
||||
--size-btn-s: 1.25rem;
|
||||
--size-btn-m: 1.625rem;
|
||||
--size-btn-l: 2rem;
|
||||
--size-btn-xl: 2.25rem;
|
||||
--space-2: 0.125rem; /* 2px / 16px = 0.125rem */
|
||||
--space-4: 0.25rem; /* 4px / 16px = 0.25rem */
|
||||
--space-6: 0.375rem; /* 6px / 16px = 0.375rem */
|
||||
|
@ -4,10 +4,7 @@
|
||||
border: 1px solid var(--clr-core-ntrl-30);
|
||||
color: var(--clr-core-ntrl-60);
|
||||
display: inline-block;
|
||||
padding-left: var(--space-8);
|
||||
padding-right: var(--space-8);
|
||||
padding-top: var(--space-4);
|
||||
padding-bottom: var(--space-4);
|
||||
padding: var(--space-6);
|
||||
z-index: 50;
|
||||
|
||||
max-width: 11.25rem;
|
||||
|
4
gitbutler-ui/static/images/domain-icons/trunk.svg
Normal file
4
gitbutler-ui/static/images/domain-icons/trunk.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="16" height="16" fill="#FB7D61"/>
|
||||
<path d="M8 4L12 8L8 12L4 8L8 4Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 198 B |
@ -0,0 +1,5 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="16" height="16" fill="#48B0AA"/>
|
||||
<rect x="2" y="3" width="6" height="10" rx="2" fill="#D9F3F2"/>
|
||||
<rect opacity="0.7" x="10" y="3" width="4" height="10" rx="2" fill="#D9F3F2"/>
|
||||
</svg>
|
After Width: | Height: | Size: 292 B |
@ -215,6 +215,9 @@ importers:
|
||||
tauri-plugin-context-menu:
|
||||
specifier: ^0.7.0
|
||||
version: 0.7.0
|
||||
tauri-plugin-log-api:
|
||||
specifier: github:tauri-apps/tauri-plugin-log#v1
|
||||
version: github.com/tauri-apps/tauri-plugin-log/19f5dcc0425e9127d2c591780e5047b83e77a7c2
|
||||
tauri-plugin-store-api:
|
||||
specifier: github:tauri-apps/tauri-plugin-store#v1
|
||||
version: github.com/tauri-apps/tauri-plugin-store/7d2632996f290b0f18cc5f8a2b2791046400690e
|
||||
@ -4768,6 +4771,14 @@ packages:
|
||||
engines: {node: '>=12.20'}
|
||||
dev: true
|
||||
|
||||
github.com/tauri-apps/tauri-plugin-log/19f5dcc0425e9127d2c591780e5047b83e77a7c2:
|
||||
resolution: {tarball: https://codeload.github.com/tauri-apps/tauri-plugin-log/tar.gz/19f5dcc0425e9127d2c591780e5047b83e77a7c2}
|
||||
name: tauri-plugin-log-api
|
||||
version: 0.0.0
|
||||
dependencies:
|
||||
'@tauri-apps/api': 1.5.3
|
||||
dev: true
|
||||
|
||||
github.com/tauri-apps/tauri-plugin-store/7d2632996f290b0f18cc5f8a2b2791046400690e:
|
||||
resolution: {tarball: https://codeload.github.com/tauri-apps/tauri-plugin-store/tar.gz/7d2632996f290b0f18cc5f8a2b2791046400690e}
|
||||
name: tauri-plugin-store-api
|
||||
|
Loading…
Reference in New Issue
Block a user