Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d0b9de142 | ||
|
|
e503786211 | ||
|
|
d4ac449074 | ||
|
|
a67663f3c5 | ||
|
|
9e48727fce | ||
|
|
a09335b729 | ||
|
|
da05a8461f | ||
|
|
21366cb26b |
@@ -85,7 +85,14 @@
|
|||||||
"Bash(rustc --edition 2021 --crate-type bin -o /dev/null --emit=metadata src/main.rs)",
|
"Bash(rustc --edition 2021 --crate-type bin -o /dev/null --emit=metadata src/main.rs)",
|
||||||
"Bash(grep -B1 \"^error\\\\[E0432\\\\]\\\\|^error$\")",
|
"Bash(grep -B1 \"^error\\\\[E0432\\\\]\\\\|^error$\")",
|
||||||
"Bash(grep -v \"^--$\")",
|
"Bash(grep -v \"^--$\")",
|
||||||
"Bash(grep \"^error$\" -A2)"
|
"Bash(grep \"^error$\" -A2)",
|
||||||
|
"Bash(cargo run *)",
|
||||||
|
"Bash(node_modules/.bin/svelte-check --version)",
|
||||||
|
"Bash(rustfmt --check --edition 2021 src/git.rs src/main.rs)",
|
||||||
|
"Bash(rustfmt --check --edition 2021 src/git.rs)",
|
||||||
|
"Bash(awk *)",
|
||||||
|
"Bash(rustfmt --edition 2021 /tmp/blame_chunk.rs)",
|
||||||
|
"Read(//tmp/**)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "git-lite",
|
"name": "git-lite",
|
||||||
"version": "2026.7.13",
|
"version": "2026.7.14",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "git-lite",
|
"name": "git-lite",
|
||||||
"version": "2026.7.13",
|
"version": "2026.7.14",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@lucide/svelte": "^1.21.0",
|
"@lucide/svelte": "^1.21.0",
|
||||||
"@tailwindcss/vite": "^4.3.1",
|
"@tailwindcss/vite": "^4.3.1",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "git-lite",
|
"name": "git-lite",
|
||||||
"version": "2026.7.13",
|
"version": "2026.7.14",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
Generated
+166
-3
@@ -1128,7 +1128,7 @@ dependencies = [
|
|||||||
"bitflags 2.13.0",
|
"bitflags 2.13.0",
|
||||||
"core-foundation 0.10.1",
|
"core-foundation 0.10.1",
|
||||||
"core-graphics-types",
|
"core-graphics-types",
|
||||||
"foreign-types",
|
"foreign-types 0.5.0",
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2027,6 +2027,15 @@ version = "0.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
|
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "foreign-types"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
|
||||||
|
dependencies = [
|
||||||
|
"foreign-types-shared 0.1.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "foreign-types"
|
name = "foreign-types"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
@@ -2034,7 +2043,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
|
checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"foreign-types-macros",
|
"foreign-types-macros",
|
||||||
"foreign-types-shared",
|
"foreign-types-shared 0.3.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2048,6 +2057,12 @@ dependencies = [
|
|||||||
"syn 2.0.118",
|
"syn 2.0.118",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "foreign-types-shared"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "foreign-types-shared"
|
name = "foreign-types-shared"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
@@ -2526,8 +2541,10 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"tauri",
|
"tauri",
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
|
"tauri-plugin-aptabase",
|
||||||
"tauri-plugin-dialog",
|
"tauri-plugin-dialog",
|
||||||
"tauri-plugin-updater",
|
"tauri-plugin-updater",
|
||||||
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2895,6 +2912,22 @@ dependencies = [
|
|||||||
"webpki-roots 1.0.8",
|
"webpki-roots 1.0.8",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hyper-tls"
|
||||||
|
version = "0.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"http-body-util",
|
||||||
|
"hyper",
|
||||||
|
"hyper-util",
|
||||||
|
"native-tls",
|
||||||
|
"tokio",
|
||||||
|
"tokio-native-tls",
|
||||||
|
"tower-service",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper-util"
|
name = "hyper-util"
|
||||||
version = "0.1.20"
|
version = "0.1.20"
|
||||||
@@ -4007,6 +4040,23 @@ dependencies = [
|
|||||||
"typenum",
|
"typenum",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "native-tls"
|
||||||
|
version = "0.2.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"log",
|
||||||
|
"openssl",
|
||||||
|
"openssl-probe",
|
||||||
|
"openssl-sys",
|
||||||
|
"schannel",
|
||||||
|
"security-framework 3.7.0",
|
||||||
|
"security-framework-sys",
|
||||||
|
"tempfile",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ndk"
|
name = "ndk"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
@@ -4059,6 +4109,18 @@ dependencies = [
|
|||||||
"memoffset",
|
"memoffset",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nix"
|
||||||
|
version = "0.31.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.13.0",
|
||||||
|
"cfg-if",
|
||||||
|
"cfg_aliases",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "no_std_io2"
|
name = "no_std_io2"
|
||||||
version = "0.9.4"
|
version = "0.9.4"
|
||||||
@@ -4523,12 +4585,49 @@ dependencies = [
|
|||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "openssl"
|
||||||
|
version = "0.10.81"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.13.0",
|
||||||
|
"cfg-if",
|
||||||
|
"foreign-types 0.3.2",
|
||||||
|
"libc",
|
||||||
|
"openssl-macros",
|
||||||
|
"openssl-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "openssl-macros"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.118",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl-probe"
|
name = "openssl-probe"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
|
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "openssl-sys"
|
||||||
|
version = "0.9.117"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"libc",
|
||||||
|
"pkg-config",
|
||||||
|
"vcpkg",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "option-ext"
|
name = "option-ext"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -4554,6 +4653,22 @@ dependencies = [
|
|||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "os_info"
|
||||||
|
version = "3.15.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9cf20a545b305cf1da722b236b5155c9bb35f1d5ceb28c048bd96ca842f41b5b"
|
||||||
|
dependencies = [
|
||||||
|
"android_system_properties",
|
||||||
|
"log",
|
||||||
|
"nix 0.31.3",
|
||||||
|
"objc2",
|
||||||
|
"objc2-foundation",
|
||||||
|
"objc2-ui-kit",
|
||||||
|
"serde",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "osakit"
|
name = "osakit"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
@@ -5342,11 +5457,13 @@ dependencies = [
|
|||||||
"http-body-util",
|
"http-body-util",
|
||||||
"hyper",
|
"hyper",
|
||||||
"hyper-rustls",
|
"hyper-rustls",
|
||||||
|
"hyper-tls",
|
||||||
"hyper-util",
|
"hyper-util",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"log",
|
"log",
|
||||||
"mime",
|
"mime",
|
||||||
"mime_guess",
|
"mime_guess",
|
||||||
|
"native-tls",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"quinn",
|
"quinn",
|
||||||
@@ -5357,6 +5474,7 @@ dependencies = [
|
|||||||
"serde_urlencoded",
|
"serde_urlencoded",
|
||||||
"sync_wrapper",
|
"sync_wrapper",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tokio-native-tls",
|
||||||
"tokio-rustls",
|
"tokio-rustls",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
"tower",
|
"tower",
|
||||||
@@ -6618,6 +6736,15 @@ dependencies = [
|
|||||||
"syn 2.0.118",
|
"syn 2.0.118",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sys-locale"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sysctl"
|
name = "sysctl"
|
||||||
version = "0.6.0"
|
version = "0.6.0"
|
||||||
@@ -6877,6 +7004,26 @@ dependencies = [
|
|||||||
"walkdir",
|
"walkdir",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri-plugin-aptabase"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "075ebe083b5f311bdfbf436f8383d316dfb12267be5fd977b57d7701bdb99987"
|
||||||
|
dependencies = [
|
||||||
|
"futures",
|
||||||
|
"log",
|
||||||
|
"os_info",
|
||||||
|
"rand 0.9.4",
|
||||||
|
"reqwest 0.12.28",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"sys-locale",
|
||||||
|
"tauri",
|
||||||
|
"tauri-plugin",
|
||||||
|
"time",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-dialog"
|
name = "tauri-plugin-dialog"
|
||||||
version = "2.7.0"
|
version = "2.7.0"
|
||||||
@@ -7308,6 +7455,16 @@ dependencies = [
|
|||||||
"syn 2.0.118",
|
"syn 2.0.118",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-native-tls"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
|
||||||
|
dependencies = [
|
||||||
|
"native-tls",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-rayon"
|
name = "tokio-rayon"
|
||||||
version = "2.1.0"
|
version = "2.1.0"
|
||||||
@@ -7942,6 +8099,12 @@ dependencies = [
|
|||||||
"uuid 0.8.2",
|
"uuid 0.8.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "vcpkg"
|
||||||
|
version = "0.2.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "version-compare"
|
name = "version-compare"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
@@ -8952,7 +9115,7 @@ dependencies = [
|
|||||||
"futures-sink",
|
"futures-sink",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"hex",
|
"hex",
|
||||||
"nix",
|
"nix 0.29.0",
|
||||||
"ordered-stream",
|
"ordered-stream",
|
||||||
"rand 0.8.6",
|
"rand 0.8.6",
|
||||||
"serde",
|
"serde",
|
||||||
|
|||||||
@@ -17,8 +17,10 @@ serde = { version = "1", features = ["derive"] }
|
|||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
tauri = { version = "2", features = [] }
|
tauri = { version = "2", features = [] }
|
||||||
tauri-plugin-dialog = "=2.7.0"
|
tauri-plugin-dialog = "=2.7.0"
|
||||||
|
tauri-plugin-aptabase = "1.0"
|
||||||
keyring = { version = "3", features = ["apple-native", "windows-native", "async-secret-service", "crypto-rust", "async-io"] }
|
keyring = { version = "3", features = ["apple-native", "windows-native", "async-secret-service", "crypto-rust", "async-io"] }
|
||||||
commit_ai = { path = "crates/commit_ai" }
|
commit_ai = { path = "crates/commit_ai" }
|
||||||
|
tokio = "1.52.3"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
tauri-build = { version = "2", features = [] }
|
tauri-build = { version = "2", features = [] }
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
fn main() {
|
fn main() {
|
||||||
|
println!("cargo:rerun-if-env-changed=APTABASE_KEY");
|
||||||
tauri_build::build();
|
tauri_build::build();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"core:window:allow-close",
|
"core:window:allow-close",
|
||||||
"core:window:allow-is-maximized",
|
"core:window:allow-is-maximized",
|
||||||
"core:window:allow-start-dragging",
|
"core:window:allow-start-dragging",
|
||||||
"dialog:allow-open"
|
"dialog:allow-open",
|
||||||
|
"aptabase:allow-track-event"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -117,6 +117,25 @@ pub struct GitCommitComparison {
|
|||||||
pub patch: String,
|
pub patch: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||||
|
pub struct GitBlameLine {
|
||||||
|
pub line_number: u32,
|
||||||
|
pub content: String,
|
||||||
|
pub commit_hash: String,
|
||||||
|
pub short_hash: String,
|
||||||
|
pub author_name: String,
|
||||||
|
pub author_email: String,
|
||||||
|
pub author_time: i64,
|
||||||
|
pub summary: String,
|
||||||
|
pub is_uncommitted: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||||
|
pub struct GitBlameResult {
|
||||||
|
pub path: String,
|
||||||
|
pub lines: Vec<GitBlameLine>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||||
pub struct ConflictFile {
|
pub struct ConflictFile {
|
||||||
pub path: String,
|
pub path: String,
|
||||||
@@ -1607,6 +1626,107 @@ pub fn cancel_file_history(
|
|||||||
state.cancel(request_id)
|
state.cancel(request_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const UNCOMMITTED_BLAME_HASH: &str = "0000000000000000000000000000000000000000";
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn get_file_blame(path: String, file: String) -> Result<GitBlameResult, String> {
|
||||||
|
let repo = resolve_repo(&path)?;
|
||||||
|
validate_files(std::slice::from_ref(&file))?;
|
||||||
|
|
||||||
|
if verify_commit(&repo, "HEAD").is_err() {
|
||||||
|
return Err("Repository has no commits yet.".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let args = vec![
|
||||||
|
OsString::from("blame"),
|
||||||
|
OsString::from("--line-porcelain"),
|
||||||
|
OsString::from("--"),
|
||||||
|
OsString::from(file.clone()),
|
||||||
|
];
|
||||||
|
let output =
|
||||||
|
run_git(&repo, args).map_err(|err| format!("Could not load blame for '{file}': {err}"))?;
|
||||||
|
|
||||||
|
Ok(GitBlameResult {
|
||||||
|
path: file,
|
||||||
|
lines: parse_blame_porcelain(&output),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_blame_porcelain(output: &[u8]) -> Vec<GitBlameLine> {
|
||||||
|
#[derive(Default, Clone)]
|
||||||
|
struct BlameMeta {
|
||||||
|
author_name: String,
|
||||||
|
author_email: String,
|
||||||
|
author_time: i64,
|
||||||
|
summary: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
let text = String::from_utf8_lossy(output);
|
||||||
|
let mut lines_out: Vec<GitBlameLine> = Vec::new();
|
||||||
|
let mut commit_meta: BTreeMap<String, BlameMeta> = BTreeMap::new();
|
||||||
|
let mut current_hash = String::new();
|
||||||
|
let mut current_final_line: u32 = 0;
|
||||||
|
|
||||||
|
for line in text.split('\n') {
|
||||||
|
if let Some(content) = line.strip_prefix('\t') {
|
||||||
|
let meta = commit_meta.get(¤t_hash).cloned().unwrap_or_default();
|
||||||
|
lines_out.push(GitBlameLine {
|
||||||
|
line_number: current_final_line,
|
||||||
|
content: content.to_string(),
|
||||||
|
commit_hash: current_hash.clone(),
|
||||||
|
short_hash: short_hash(¤t_hash),
|
||||||
|
author_name: meta.author_name,
|
||||||
|
author_email: meta.author_email,
|
||||||
|
author_time: meta.author_time,
|
||||||
|
summary: meta.summary,
|
||||||
|
is_uncommitted: current_hash == UNCOMMITTED_BLAME_HASH,
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut parts = line.splitn(2, ' ');
|
||||||
|
let head = parts.next().unwrap_or("");
|
||||||
|
let tail = parts.next().unwrap_or("");
|
||||||
|
|
||||||
|
if head.len() == 40 && head.bytes().all(|b| b.is_ascii_hexdigit()) {
|
||||||
|
if let Some(final_line) = tail.split_whitespace().nth(1) {
|
||||||
|
current_final_line = final_line.parse().unwrap_or(current_final_line);
|
||||||
|
}
|
||||||
|
current_hash = head.to_string();
|
||||||
|
commit_meta.entry(current_hash.clone()).or_default();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
match head {
|
||||||
|
"author" => {
|
||||||
|
commit_meta
|
||||||
|
.entry(current_hash.clone())
|
||||||
|
.or_default()
|
||||||
|
.author_name = tail.to_string()
|
||||||
|
}
|
||||||
|
"author-mail" => {
|
||||||
|
let email = tail.trim_matches(|c| c == '<' || c == '>').to_string();
|
||||||
|
commit_meta
|
||||||
|
.entry(current_hash.clone())
|
||||||
|
.or_default()
|
||||||
|
.author_email = email;
|
||||||
|
}
|
||||||
|
"author-time" => {
|
||||||
|
commit_meta
|
||||||
|
.entry(current_hash.clone())
|
||||||
|
.or_default()
|
||||||
|
.author_time = tail.parse().unwrap_or(0);
|
||||||
|
}
|
||||||
|
"summary" => {
|
||||||
|
commit_meta.entry(current_hash.clone()).or_default().summary = tail.to_string()
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lines_out
|
||||||
|
}
|
||||||
|
|
||||||
fn list_file_history_core(
|
fn list_file_history_core(
|
||||||
repo: &Path,
|
repo: &Path,
|
||||||
file: String,
|
file: String,
|
||||||
@@ -5419,4 +5539,44 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(result.unwrap_err(), SEARCH_CANCELLED_MESSAGE);
|
assert_eq!(result.unwrap_err(), SEARCH_CANCELLED_MESSAGE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn get_file_blame_attributes_lines_to_the_commits_that_introduced_them() {
|
||||||
|
let repo = init_temp_repo("file_blame");
|
||||||
|
commit_initial_file(&repo.path);
|
||||||
|
fs::write(repo.path.join("old.txt"), "original\ntwo\n").expect("tracked file should change");
|
||||||
|
run_git_test(&repo.path, ["add", "old.txt"]);
|
||||||
|
run_git_test(&repo.path, ["commit", "-q", "-m", "add second line"]);
|
||||||
|
|
||||||
|
let result = get_file_blame(
|
||||||
|
repo.path.to_string_lossy().to_string(),
|
||||||
|
"old.txt".to_string(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(result.path, "old.txt");
|
||||||
|
assert_eq!(result.lines.len(), 2);
|
||||||
|
assert_eq!(result.lines[0].content, "original");
|
||||||
|
assert_eq!(result.lines[0].summary, "init");
|
||||||
|
assert_eq!(result.lines[1].content, "two");
|
||||||
|
assert_eq!(result.lines[1].summary, "add second line");
|
||||||
|
assert!(!result.lines[1].author_name.is_empty());
|
||||||
|
assert!(!result.lines[1].is_uncommitted);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn get_file_blame_marks_uncommitted_working_tree_changes() {
|
||||||
|
let repo = init_temp_repo("file_blame_uncommitted");
|
||||||
|
commit_initial_file(&repo.path);
|
||||||
|
fs::write(repo.path.join("old.txt"), "changed\n").expect("tracked file should change");
|
||||||
|
|
||||||
|
let result = get_file_blame(
|
||||||
|
repo.path.to_string_lossy().to_string(),
|
||||||
|
"old.txt".to_string(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(result.lines.len(), 1);
|
||||||
|
assert!(result.lines[0].is_uncommitted);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-5
@@ -10,19 +10,26 @@ use git::{
|
|||||||
cherry_pick_continue, clone_repository, commit, commit_ai_generate, commit_ai_load,
|
cherry_pick_continue, clone_repository, commit, commit_ai_generate, commit_ai_load,
|
||||||
commit_ai_local_models, commit_ai_status, compare_commits, compare_file_to_head,
|
commit_ai_local_models, commit_ai_status, compare_commits, compare_file_to_head,
|
||||||
compare_file_to_parent, create_branch, create_tag, cred_delete, cred_load, cred_save,
|
compare_file_to_parent, create_branch, create_tag, cred_delete, cred_load, cred_save,
|
||||||
delete_branch, delete_tag, diff_file_against_working_tree, fetch, get_file_patch,
|
delete_branch, delete_tag, diff_file_against_working_tree, fetch, get_file_blame,
|
||||||
get_remote_url, get_status, last_commit_message, list_branches, list_commits,
|
get_file_patch, get_remote_url, get_status, last_commit_message, list_branches, list_commits,
|
||||||
list_file_history, list_repository_files, list_stashes, list_tags, merge_branch,
|
list_file_history, list_repository_files, list_stashes, list_tags, merge_branch,
|
||||||
open_repo_in_explorer, open_repository, open_repository_bundle, open_repository_file, pull,
|
open_repo_in_explorer, open_repository, open_repository_bundle, open_repository_file, pull,
|
||||||
push, push_tag, read_conflict, rebase_abort, rebase_branch, rebase_continue, rename_branch,
|
push, push_tag, read_conflict, rebase_abort, rebase_branch, rebase_continue, rename_branch,
|
||||||
resolve_conflict, resolve_conflict_side, restore_file_from_commit, restore_files,
|
resolve_conflict, resolve_conflict_side, restore_file_from_commit, restore_files,
|
||||||
restore_to_commit, search_code_introductions, stage_files, stash_apply, stash_drop,
|
restore_to_commit, search_code_introductions, stage_files, stash_apply, stash_drop, stash_pop,
|
||||||
stash_pop, stash_push, undo_last_commit, unstage_files,
|
stash_push, undo_last_commit, unstage_files,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn main() {
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||||
|
.plugin(tauri_plugin_aptabase::Builder::new("A-SH-1344793789").with_options(
|
||||||
|
tauri_plugin_aptabase::InitOptions {
|
||||||
|
host: Some("https://aptabase.cbsk-tech.de".to_string()),
|
||||||
|
flush_interval:None
|
||||||
|
}
|
||||||
|
).build())
|
||||||
.manage(SearchCancellationState::default())
|
.manage(SearchCancellationState::default())
|
||||||
.manage(commit_ai::CommitAiEngine::new())
|
.manage(commit_ai::CommitAiEngine::new())
|
||||||
.plugin(tauri_plugin_dialog::init())
|
.plugin(tauri_plugin_dialog::init())
|
||||||
@@ -76,6 +83,7 @@ fn main() {
|
|||||||
open_repository_bundle,
|
open_repository_bundle,
|
||||||
list_file_history,
|
list_file_history,
|
||||||
cancel_file_history,
|
cancel_file_history,
|
||||||
|
get_file_blame,
|
||||||
compare_commits,
|
compare_commits,
|
||||||
compare_file_to_head,
|
compare_file_to_head,
|
||||||
compare_file_to_parent,
|
compare_file_to_parent,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "GitLite",
|
"productName": "GitLite",
|
||||||
"version": "2026.7.13",
|
"version": "2026.7.14",
|
||||||
"identifier": "com.git-lite",
|
"identifier": "com.git-lite",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "npm run dev",
|
"beforeDevCommand": "npm run dev",
|
||||||
|
|||||||
+139
-1
@@ -6,7 +6,10 @@
|
|||||||
|
|
||||||
import TitleBar from "./lib/TitleBar.svelte";
|
import TitleBar from "./lib/TitleBar.svelte";
|
||||||
import AiSettingsDialog from "./lib/components/AiSettingsDialog.svelte";
|
import AiSettingsDialog from "./lib/components/AiSettingsDialog.svelte";
|
||||||
|
import AnalyticsNoticeDialog from "./lib/components/AnalyticsNoticeDialog.svelte";
|
||||||
|
import AppSettingsDialog from "./lib/components/AppSettingsDialog.svelte";
|
||||||
import BranchDeleteConfirmDialog from "./lib/components/BranchDeleteConfirmDialog.svelte";
|
import BranchDeleteConfirmDialog from "./lib/components/BranchDeleteConfirmDialog.svelte";
|
||||||
|
import BlameDialog from "./lib/components/BlameDialog.svelte";
|
||||||
import BranchPanel from "./lib/components/BranchPanel.svelte";
|
import BranchPanel from "./lib/components/BranchPanel.svelte";
|
||||||
import CloneRepositoryDialog from "./lib/components/CloneRepositoryDialog.svelte";
|
import CloneRepositoryDialog from "./lib/components/CloneRepositoryDialog.svelte";
|
||||||
import CommitPanel from "./lib/components/CommitPanel.svelte";
|
import CommitPanel from "./lib/components/CommitPanel.svelte";
|
||||||
@@ -50,6 +53,7 @@
|
|||||||
diffFileAgainstWorkingTree,
|
diffFileAgainstWorkingTree,
|
||||||
compareFileToParent,
|
compareFileToParent,
|
||||||
fetchRemote,
|
fetchRemote,
|
||||||
|
getFileBlame,
|
||||||
getStatus,
|
getStatus,
|
||||||
lastCommitMessage,
|
lastCommitMessage,
|
||||||
listBranches,
|
listBranches,
|
||||||
@@ -93,10 +97,12 @@
|
|||||||
|
|
||||||
import type {
|
import type {
|
||||||
AiSettings,
|
AiSettings,
|
||||||
|
AnalyticsSettings,
|
||||||
CommitAiPhase,
|
CommitAiPhase,
|
||||||
ConflictFile,
|
ConflictFile,
|
||||||
ExplorerNode,
|
ExplorerNode,
|
||||||
ExplorerNodeKind,
|
ExplorerNodeKind,
|
||||||
|
GitBlameLine,
|
||||||
GitBranch as GitBranchInfo,
|
GitBranch as GitBranchInfo,
|
||||||
GitCommit,
|
GitCommit,
|
||||||
GitCommitFile,
|
GitCommitFile,
|
||||||
@@ -120,6 +126,7 @@
|
|||||||
isAuthError,
|
isAuthError,
|
||||||
stripAuthPrefix,
|
stripAuthPrefix,
|
||||||
} from "./lib/credentials";
|
} from "./lib/credentials";
|
||||||
|
import { trackAnalyticsEvent, type AnalyticsEventProperties } from "./lib/analytics";
|
||||||
|
|
||||||
type UpdateToastState = "available" | "downloading" | "installed" | "error";
|
type UpdateToastState = "available" | "downloading" | "installed" | "error";
|
||||||
type AppView = "management" | "repository";
|
type AppView = "management" | "repository";
|
||||||
@@ -148,6 +155,7 @@
|
|||||||
const RECENT_REPOS_KEY = "gitlite.recentRepos.v1";
|
const RECENT_REPOS_KEY = "gitlite.recentRepos.v1";
|
||||||
const REPO_STATUS_CACHE_KEY = "gitlite.repoStatusCache.v1";
|
const REPO_STATUS_CACHE_KEY = "gitlite.repoStatusCache.v1";
|
||||||
const AI_SETTINGS_KEY = "gitlite.aiSettings.v1";
|
const AI_SETTINGS_KEY = "gitlite.aiSettings.v1";
|
||||||
|
const ANALYTICS_SETTINGS_KEY = "gitlite.analyticsSettings.v1";
|
||||||
const COMMIT_PANEL_HEIGHT_KEY = "gitlite.commitPanelHeight.v1";
|
const COMMIT_PANEL_HEIGHT_KEY = "gitlite.commitPanelHeight.v1";
|
||||||
const HISTORY_ASIDE_WIDTH_KEY = "gitlite.historyAsideWidth.v1";
|
const HISTORY_ASIDE_WIDTH_KEY = "gitlite.historyAsideWidth.v1";
|
||||||
const COMMIT_PANEL_DEFAULT_HEIGHT = 220;
|
const COMMIT_PANEL_DEFAULT_HEIGHT = 220;
|
||||||
@@ -197,6 +205,9 @@
|
|||||||
let commitAiPollTimer: ReturnType<typeof setInterval> | undefined;
|
let commitAiPollTimer: ReturnType<typeof setInterval> | undefined;
|
||||||
let aiSettings: AiSettings = defaultAiSettings();
|
let aiSettings: AiSettings = defaultAiSettings();
|
||||||
let aiSettingsOpen = false;
|
let aiSettingsOpen = false;
|
||||||
|
let appSettingsOpen = false;
|
||||||
|
let analyticsNoticeOpen = false;
|
||||||
|
let analyticsSettings: AnalyticsSettings = defaultAnalyticsSettings();
|
||||||
let localModelOptions: LocalModelOption[] = [];
|
let localModelOptions: LocalModelOption[] = [];
|
||||||
let errorMessage = "";
|
let errorMessage = "";
|
||||||
let operation = "";
|
let operation = "";
|
||||||
@@ -218,6 +229,11 @@
|
|||||||
let linePatchText = "";
|
let linePatchText = "";
|
||||||
let linePatchLoading = false;
|
let linePatchLoading = false;
|
||||||
let linePatchError = "";
|
let linePatchError = "";
|
||||||
|
let blameOpen = false;
|
||||||
|
let blameFilePath = "";
|
||||||
|
let blameLines: GitBlameLine[] = [];
|
||||||
|
let blameLoading = false;
|
||||||
|
let blameError = "";
|
||||||
let pendingDiscard: PendingDiscard | null = null;
|
let pendingDiscard: PendingDiscard | null = null;
|
||||||
let globalSearchOpen = false;
|
let globalSearchOpen = false;
|
||||||
let lastSearchQuery = "";
|
let lastSearchQuery = "";
|
||||||
@@ -321,6 +337,7 @@
|
|||||||
// ── Lifecycle ──────────────────────────────────────────────────────────────
|
// ── Lifecycle ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
|
initAnalytics();
|
||||||
loadRepoLists();
|
loadRepoLists();
|
||||||
autoRefreshTimer = setInterval(() => { void autoRefreshTick(); }, AUTO_REFRESH_INTERVAL);
|
autoRefreshTimer = setInterval(() => { void autoRefreshTick(); }, AUTO_REFRESH_INTERVAL);
|
||||||
backgroundRepoStatusTimer = setInterval(() => { void backgroundRepoStatusTick(false); }, BACKGROUND_REPO_STATUS_INTERVAL);
|
backgroundRepoStatusTimer = setInterval(() => { void backgroundRepoStatusTick(false); }, BACKGROUND_REPO_STATUS_INTERVAL);
|
||||||
@@ -546,6 +563,57 @@
|
|||||||
startCommitAiPolling();
|
startCommitAiPolling();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function defaultAnalyticsSettings(): AnalyticsSettings {
|
||||||
|
return {
|
||||||
|
enabled: true,
|
||||||
|
noticeSeen: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadAnalyticsSettings(): AnalyticsSettings {
|
||||||
|
try {
|
||||||
|
const stored = JSON.parse(localStorage.getItem(ANALYTICS_SETTINGS_KEY) ?? "null") as unknown;
|
||||||
|
if (stored && typeof stored === "object") {
|
||||||
|
return { ...defaultAnalyticsSettings(), ...(stored as Partial<AnalyticsSettings>) };
|
||||||
|
}
|
||||||
|
} catch { /* ignore malformed settings */ }
|
||||||
|
return defaultAnalyticsSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
function persistAnalyticsSettings(next: AnalyticsSettings) {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(ANALYTICS_SETTINGS_KEY, JSON.stringify(next));
|
||||||
|
} catch { /* ignore storage quota/private-mode errors */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
function initAnalytics() {
|
||||||
|
analyticsSettings = loadAnalyticsSettings();
|
||||||
|
if (!analyticsSettings.noticeSeen) {
|
||||||
|
analyticsNoticeOpen = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
trackEvent("app_started");
|
||||||
|
}
|
||||||
|
|
||||||
|
function trackEvent(name: string, props?: AnalyticsEventProperties) {
|
||||||
|
if (!analyticsSettings.enabled) return;
|
||||||
|
trackAnalyticsEvent(name, props);
|
||||||
|
}
|
||||||
|
|
||||||
|
function acceptAnalyticsNotice(enabled: boolean) {
|
||||||
|
analyticsSettings = { enabled, noticeSeen: true };
|
||||||
|
persistAnalyticsSettings(analyticsSettings);
|
||||||
|
analyticsNoticeOpen = false;
|
||||||
|
trackEvent("app_started", { first_run: 1 });
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveAppSettings(next: AnalyticsSettings) {
|
||||||
|
analyticsSettings = next;
|
||||||
|
persistAnalyticsSettings(next);
|
||||||
|
appSettingsOpen = false;
|
||||||
|
if (next.enabled) trackEvent("settings_saved", { analytics_enabled: 1 });
|
||||||
|
}
|
||||||
|
|
||||||
function updateCommitMessage(message: string) {
|
function updateCommitMessage(message: string) {
|
||||||
commitMessage = message;
|
commitMessage = message;
|
||||||
if (message !== lastLocalAiGeneratedMessage) {
|
if (message !== lastLocalAiGeneratedMessage) {
|
||||||
@@ -1202,6 +1270,10 @@
|
|||||||
await refreshCommitHistory(activeRepoPath, bundle.commits);
|
await refreshCommitHistory(activeRepoPath, bundle.commits);
|
||||||
await refreshExplorerFiles(activeRepoPath, bundle.files);
|
await refreshExplorerFiles(activeRepoPath, bundle.files);
|
||||||
lastRepoSwitchAt = Date.now();
|
lastRepoSwitchAt = Date.now();
|
||||||
|
trackEvent("repository_opened", {
|
||||||
|
changed_files: bundle.status.files.length,
|
||||||
|
has_upstream: bundle.status.upstream ? 1 : 0,
|
||||||
|
});
|
||||||
void backgroundFetchRepo(activeRepoPath);
|
void backgroundFetchRepo(activeRepoPath);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1921,6 +1993,34 @@
|
|||||||
linePatchError = "";
|
linePatchError = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function openBlame(node: ExplorerNode) {
|
||||||
|
if (!activeRepoPath || node.kind !== "file") return;
|
||||||
|
blameOpen = true;
|
||||||
|
blameFilePath = node.path;
|
||||||
|
blameLines = [];
|
||||||
|
blameError = "";
|
||||||
|
blameLoading = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await getFileBlame(activeRepoPath, node.path);
|
||||||
|
blameLines = result.lines;
|
||||||
|
trackEvent("blame_opened", { lines: result.lines.length });
|
||||||
|
} catch (error) {
|
||||||
|
blameError = errorToMessage(error);
|
||||||
|
errorMessage = blameError;
|
||||||
|
} finally {
|
||||||
|
blameLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeBlame() {
|
||||||
|
if (isBusy) return;
|
||||||
|
blameOpen = false;
|
||||||
|
blameFilePath = "";
|
||||||
|
blameLines = [];
|
||||||
|
blameError = "";
|
||||||
|
}
|
||||||
|
|
||||||
function patchOperationLabel(action: PatchApplyAction, file: GitFileStatus): string {
|
function patchOperationLabel(action: PatchApplyAction, file: GitFileStatus): string {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case "stage":
|
case "stage":
|
||||||
@@ -2045,10 +2145,12 @@
|
|||||||
await refreshCommitHistory(activeRepoPath);
|
await refreshCommitHistory(activeRepoPath);
|
||||||
await refreshExplorerFiles(activeRepoPath);
|
await refreshExplorerFiles(activeRepoPath);
|
||||||
await refreshFileHistory(activeRepoPath);
|
await refreshFileHistory(activeRepoPath);
|
||||||
|
trackEvent("commit_created", { amend: 1 });
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const trackedStagedCount = stagedCount;
|
||||||
await runOperation("Committing", async () => {
|
await runOperation("Committing", async () => {
|
||||||
applyStatus(await commit(activeRepoPath, message));
|
applyStatus(await commit(activeRepoPath, message));
|
||||||
commitMessage = "";
|
commitMessage = "";
|
||||||
@@ -2057,6 +2159,7 @@
|
|||||||
await refreshCommitHistory(activeRepoPath);
|
await refreshCommitHistory(activeRepoPath);
|
||||||
await refreshExplorerFiles(activeRepoPath);
|
await refreshExplorerFiles(activeRepoPath);
|
||||||
await refreshFileHistory(activeRepoPath);
|
await refreshFileHistory(activeRepoPath);
|
||||||
|
trackEvent("commit_created", { amend: 0, staged_files: trackedStagedCount });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2235,6 +2338,12 @@
|
|||||||
function openCompareSelect() {
|
function openCompareSelect() {
|
||||||
if (!hasRepository) return;
|
if (!hasRepository) return;
|
||||||
compareSelectOpen = true;
|
compareSelectOpen = true;
|
||||||
|
trackEvent("compare_opened");
|
||||||
|
}
|
||||||
|
|
||||||
|
function openGlobalSearchDialog() {
|
||||||
|
globalSearchOpen = true;
|
||||||
|
trackEvent("global_search_opened");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function compareSelectedCommits() {
|
async function compareSelectedCommits() {
|
||||||
@@ -2302,6 +2411,7 @@
|
|||||||
const results = await searchCodeIntroductions(activeRepoPath, query, caseSensitive, limit, searchId);
|
const results = await searchCodeIntroductions(activeRepoPath, query, caseSensitive, limit, searchId);
|
||||||
if (globalSearchId === searchId) {
|
if (globalSearchId === searchId) {
|
||||||
globalSearchResults = results;
|
globalSearchResults = results;
|
||||||
|
trackEvent("global_search_completed", { results: results.length, case_sensitive: caseSensitive ? 1 : 0 });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (globalSearchId === searchId) {
|
if (globalSearchId === searchId) {
|
||||||
@@ -2433,10 +2543,11 @@
|
|||||||
onPull={pullRepo}
|
onPull={pullRepo}
|
||||||
onPush={pushRepo}
|
onPush={pushRepo}
|
||||||
onRefresh={refreshRepo}
|
onRefresh={refreshRepo}
|
||||||
onSearch={() => { globalSearchOpen = true; }}
|
onSearch={openGlobalSearchDialog}
|
||||||
onCompare={openCompareSelect}
|
onCompare={openCompareSelect}
|
||||||
onOpenInExplorer={openActiveRepoInExplorer}
|
onOpenInExplorer={openActiveRepoInExplorer}
|
||||||
onToggleAutoRefresh={toggleAutoRefresh}
|
onToggleAutoRefresh={toggleAutoRefresh}
|
||||||
|
onOpenSettings={() => { appSettingsOpen = true; }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="shell-body">
|
<div class="shell-body">
|
||||||
@@ -2726,6 +2837,7 @@
|
|||||||
onCollapseAllFolders={collapseAllExplorerFolders}
|
onCollapseAllFolders={collapseAllExplorerFolders}
|
||||||
onSelectNode={selectExplorerNode}
|
onSelectNode={selectExplorerNode}
|
||||||
onOpenFile={openFileFromExplorer}
|
onOpenFile={openFileFromExplorer}
|
||||||
|
onBlame={openBlame}
|
||||||
/>
|
/>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
@@ -2872,6 +2984,21 @@
|
|||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if analyticsNoticeOpen}
|
||||||
|
<AnalyticsNoticeDialog
|
||||||
|
enabled={analyticsSettings.enabled}
|
||||||
|
onContinue={acceptAnalyticsNotice}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if appSettingsOpen}
|
||||||
|
<AppSettingsDialog
|
||||||
|
analytics={analyticsSettings}
|
||||||
|
onSave={saveAppSettings}
|
||||||
|
onClose={() => { appSettingsOpen = false; }}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if linePatchOpen && linePatchFile}
|
{#if linePatchOpen && linePatchFile}
|
||||||
<LinePatchDialog
|
<LinePatchDialog
|
||||||
file={linePatchFile}
|
file={linePatchFile}
|
||||||
@@ -2886,6 +3013,17 @@
|
|||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if blameOpen}
|
||||||
|
<BlameDialog
|
||||||
|
filePath={blameFilePath}
|
||||||
|
lines={blameLines}
|
||||||
|
{isBusy}
|
||||||
|
isLoading={blameLoading}
|
||||||
|
error={blameError}
|
||||||
|
onClose={closeBlame}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if pendingDiscard}
|
{#if pendingDiscard}
|
||||||
<DiscardConfirmDialog
|
<DiscardConfirmDialog
|
||||||
file={pendingDiscard.file}
|
file={pendingDiscard.file}
|
||||||
|
|||||||
+269
-22
@@ -234,8 +234,7 @@
|
|||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
background: rgba(4,8,18,0.58);
|
background: #050712;
|
||||||
backdrop-filter: blur(8px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.branch-filter-dialog {
|
.branch-filter-dialog {
|
||||||
@@ -247,8 +246,8 @@
|
|||||||
border: 1px solid rgba(94,110,156,0.24);
|
border: 1px solid rgba(94,110,156,0.24);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
background:
|
background:
|
||||||
linear-gradient(180deg, rgba(255,255,255,0.045), transparent 70%),
|
linear-gradient(180deg, #20253f 0%, #151827 72%),
|
||||||
var(--color-surface);
|
#151827;
|
||||||
box-shadow: 0 24px 80px rgba(0,0,0,0.42), inset 0 1px 0 rgba(255,255,255,0.05);
|
box-shadow: 0 24px 80px rgba(0,0,0,0.42), inset 0 1px 0 rgba(255,255,255,0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -440,7 +439,13 @@
|
|||||||
|
|
||||||
/* --- App shell --- */
|
/* --- App shell --- */
|
||||||
|
|
||||||
.shell { display: flex; flex-direction: column; height: 100%; background: transparent; }
|
.shell {
|
||||||
|
--app-titlebar-height: 42px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
.shell-body {
|
.shell-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -454,10 +459,12 @@
|
|||||||
/* --- Titlebar --- */
|
/* --- Titlebar --- */
|
||||||
|
|
||||||
.titlebar {
|
.titlebar {
|
||||||
|
position: relative;
|
||||||
|
z-index: 500;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: auto 1fr auto;
|
grid-template-columns: auto 1fr auto;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 42px;
|
height: var(--app-titlebar-height);
|
||||||
background:
|
background:
|
||||||
linear-gradient(90deg, rgba(100,108,255,0.16), rgba(65,209,255,0.08), rgba(255,211,67,0.035)),
|
linear-gradient(90deg, rgba(100,108,255,0.16), rgba(65,209,255,0.08), rgba(255,211,67,0.035)),
|
||||||
var(--color-bar);
|
var(--color-bar);
|
||||||
@@ -2416,8 +2423,12 @@
|
|||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
background: rgba(0, 0, 0, 0.65);
|
background-color: #050712;
|
||||||
backdrop-filter: blur(2px);
|
background-image: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-chrome-backdrop {
|
||||||
|
top: var(--app-titlebar-height, 42px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dialog {
|
.dialog {
|
||||||
@@ -2427,7 +2438,8 @@
|
|||||||
height: min(760px, 100%);
|
height: min(760px, 100%);
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
background: var(--color-surface);
|
background-color: #111321;
|
||||||
|
background-image: none;
|
||||||
box-shadow: 0 24px 72px rgba(0, 0, 0, 0.6), 0 2px 12px rgba(0,0,0,0.4);
|
box-shadow: 0 24px 72px rgba(0, 0, 0, 0.6), 0 2px 12px rgba(0,0,0,0.4);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
@@ -2444,6 +2456,11 @@
|
|||||||
width: min(1320px, calc(100vw - 32px));
|
width: min(1320px, calc(100vw - 32px));
|
||||||
height: min(860px, calc(100vh - 32px));
|
height: min(860px, calc(100vh - 32px));
|
||||||
}
|
}
|
||||||
|
.blame-dialog {
|
||||||
|
grid-template-rows: auto minmax(0, 1fr);
|
||||||
|
width: min(1320px, calc(100vw - 32px));
|
||||||
|
height: min(860px, calc(100vh - 32px));
|
||||||
|
}
|
||||||
.compare-select-dialog {
|
.compare-select-dialog {
|
||||||
display: block;
|
display: block;
|
||||||
width: min(720px, calc(100vw - 32px));
|
width: min(720px, calc(100vw - 32px));
|
||||||
@@ -2480,6 +2497,14 @@
|
|||||||
max-height: calc(100vh - 32px);
|
max-height: calc(100vh - 32px);
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
.app-settings-dialog,
|
||||||
|
.analytics-notice-dialog {
|
||||||
|
display: block;
|
||||||
|
width: min(620px, calc(100vw - 32px));
|
||||||
|
height: auto;
|
||||||
|
max-height: calc(100vh - 32px);
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
.clone-repository-dialog {
|
.clone-repository-dialog {
|
||||||
display: block;
|
display: block;
|
||||||
width: min(620px, calc(100vw - 32px));
|
width: min(620px, calc(100vw - 32px));
|
||||||
@@ -2534,6 +2559,101 @@
|
|||||||
gap: 14px;
|
gap: 14px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
|
.app-settings-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
.settings-section {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.settings-section > header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
min-width: 0;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 1px solid var(--color-border-subtle);
|
||||||
|
}
|
||||||
|
.settings-section > header svg {
|
||||||
|
color: var(--color-accent);
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
.settings-section h3 {
|
||||||
|
margin: 1px 0 0;
|
||||||
|
color: var(--color-ink);
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 750;
|
||||||
|
}
|
||||||
|
.settings-toggle-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto minmax(0, 1fr);
|
||||||
|
align-items: start;
|
||||||
|
gap: 10px;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid var(--color-border-subtle);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #151827;
|
||||||
|
color: var(--color-ink-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.settings-toggle-row input {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
margin-top: 2px;
|
||||||
|
accent-color: var(--color-accent);
|
||||||
|
}
|
||||||
|
.settings-toggle-row span {
|
||||||
|
display: grid;
|
||||||
|
gap: 4px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.settings-toggle-row strong {
|
||||||
|
color: var(--color-ink);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.settings-toggle-row small {
|
||||||
|
color: var(--color-ink-dim);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
.analytics-notice-body {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto minmax(0, 1fr);
|
||||||
|
gap: 14px;
|
||||||
|
padding: 18px 16px 16px;
|
||||||
|
}
|
||||||
|
.analytics-notice-icon {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
border: 1px solid rgba(65,209,255,0.28);
|
||||||
|
border-radius: 10px;
|
||||||
|
color: var(--color-accent);
|
||||||
|
background: rgba(65,209,255,0.08);
|
||||||
|
}
|
||||||
|
.analytics-notice-copy {
|
||||||
|
display: grid;
|
||||||
|
gap: 9px;
|
||||||
|
min-width: 0;
|
||||||
|
color: var(--color-ink-muted);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.analytics-notice-copy p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.analytics-notice-toggle {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
.analytics-notice-footer {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
.ai-provider-options {
|
.ai-provider-options {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
@@ -2643,7 +2763,7 @@
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dialog-header { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 14px 16px; border-bottom: 1px solid var(--color-border-subtle); background: var(--color-surface-dim); }
|
.dialog-header { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 14px 16px; border-bottom: 1px solid var(--color-border-subtle); background: #171a2b; }
|
||||||
.dialog-header > div:first-child { min-width: 0; }
|
.dialog-header > div:first-child { min-width: 0; }
|
||||||
.dialog-header-actions { display: flex; align-items: center; justify-content: flex-end; gap: 8px; flex: 0 0 auto; min-width: 0; }
|
.dialog-header-actions { display: flex; align-items: center; justify-content: flex-end; gap: 8px; flex: 0 0 auto; min-width: 0; }
|
||||||
.compare-restore { max-width: 170px; min-width: 0; }
|
.compare-restore { max-width: 170px; min-width: 0; }
|
||||||
@@ -2657,7 +2777,7 @@
|
|||||||
.dialog-body { display: grid; grid-template-columns: 280px minmax(0, 1fr); min-height: 0; }
|
.dialog-body { display: grid; grid-template-columns: 280px minmax(0, 1fr); min-height: 0; }
|
||||||
.compare-dialog .dialog-body { grid-template-columns: minmax(260px, 320px) minmax(0, 1fr); }
|
.compare-dialog .dialog-body { grid-template-columns: minmax(260px, 320px) minmax(0, 1fr); }
|
||||||
|
|
||||||
.dialog-files { display: grid; align-content: start; gap: 4px; padding: 8px; overflow: auto; border-right: 1px solid var(--color-border-subtle); background: var(--color-surface-dim); }
|
.dialog-files { display: grid; align-content: start; gap: 4px; padding: 8px; overflow: auto; border-right: 1px solid var(--color-border-subtle); background: #171a2b; }
|
||||||
|
|
||||||
.dialog-file-row {
|
.dialog-file-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -2687,7 +2807,7 @@
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 7px 12px;
|
padding: 7px 12px;
|
||||||
border-bottom: 1px solid var(--color-border-subtle);
|
border-bottom: 1px solid var(--color-border-subtle);
|
||||||
background: var(--color-surface-dim);
|
background: #171a2b;
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--color-ink-muted);
|
color: var(--color-ink-muted);
|
||||||
@@ -2709,7 +2829,7 @@
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
tab-size: 2;
|
tab-size: 2;
|
||||||
background: var(--color-surface-raised);
|
background: #111321;
|
||||||
}
|
}
|
||||||
|
|
||||||
.split-pane {
|
.split-pane {
|
||||||
@@ -2733,7 +2853,7 @@
|
|||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
.split-span.split-meta { color: var(--color-ink-faint); background: rgba(0,0,0,0.18); font-size: 11px; }
|
.split-span.split-meta { color: var(--color-ink-faint); background: #0c0e18; font-size: 11px; }
|
||||||
.split-span.split-hunk { color: #7aacff; background: rgba(122,172,255,0.08); padding: 3px 10px; }
|
.split-span.split-hunk { color: #7aacff; background: rgba(122,172,255,0.08); padding: 3px 10px; }
|
||||||
|
|
||||||
.split-num {
|
.split-num {
|
||||||
@@ -2743,11 +2863,11 @@
|
|||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
border-right: 1px solid var(--color-border-subtle);
|
border-right: 1px solid var(--color-border-subtle);
|
||||||
background: rgba(0,0,0,0.14);
|
background: #0d101a;
|
||||||
}
|
}
|
||||||
.split-num.del { background: rgba(232,96,96,0.12); color: rgba(232,96,96,0.6); border-right-color: rgba(232,96,96,0.2); }
|
.split-num.del { background: rgba(232,96,96,0.12); color: rgba(232,96,96,0.6); border-right-color: rgba(232,96,96,0.2); }
|
||||||
.split-num.add { background: rgba(78,202,118,0.1); color: rgba(78,202,118,0.6); border-right-color: rgba(78,202,118,0.2); }
|
.split-num.add { background: rgba(78,202,118,0.1); color: rgba(78,202,118,0.6); border-right-color: rgba(78,202,118,0.2); }
|
||||||
.split-num.empty { background: rgba(0,0,0,0.08); }
|
.split-num.empty { background: #10131e; }
|
||||||
|
|
||||||
.split-cell {
|
.split-cell {
|
||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
@@ -2758,7 +2878,7 @@
|
|||||||
}
|
}
|
||||||
.split-cell.del { background: rgba(232,96,96,0.1); color: #ef8080; }
|
.split-cell.del { background: rgba(232,96,96,0.1); color: #ef8080; }
|
||||||
.split-cell.add { background: rgba(78,202,118,0.09); color: #5dd88a; }
|
.split-cell.add { background: rgba(78,202,118,0.09); color: #5dd88a; }
|
||||||
.split-cell.empty { background: rgba(0,0,0,0.06); }
|
.split-cell.empty { background: #10131e; }
|
||||||
|
|
||||||
/* Search-hit highlight: amber, distinct from add (green) / del (red).
|
/* Search-hit highlight: amber, distinct from add (green) / del (red).
|
||||||
Higher specificity so it overrides the add/del backgrounds on a matched line. */
|
Higher specificity so it overrides the add/del backgrounds on a matched line. */
|
||||||
@@ -2789,7 +2909,7 @@
|
|||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.05em;
|
letter-spacing: 0.05em;
|
||||||
color: var(--color-ink-faint);
|
color: var(--color-ink-faint);
|
||||||
background: rgba(0,0,0,0.1);
|
background: #0d101a;
|
||||||
}
|
}
|
||||||
.split-col-label + .split-col-label { border-left: 1px solid var(--color-border-subtle); }
|
.split-col-label + .split-col-label { border-left: 1px solid var(--color-border-subtle); }
|
||||||
.split-col-hash {
|
.split-col-hash {
|
||||||
@@ -2805,7 +2925,7 @@
|
|||||||
letter-spacing: 0;
|
letter-spacing: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dialog-footer { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 16px; border-top: 1px solid var(--color-border-subtle); background: var(--color-surface-dim); }
|
.dialog-footer { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 16px; border-top: 1px solid var(--color-border-subtle); background: #171a2b; }
|
||||||
.dialog-footer-info { color: var(--color-ink-dim); font-size: 13px; font-weight: 700; }
|
.dialog-footer-info { color: var(--color-ink-dim); font-size: 13px; font-weight: 700; }
|
||||||
|
|
||||||
.prepared-tag { display: inline-flex; align-items: center; gap: 4px; margin-right: auto; color: #4eca76; font-size: 12px; font-weight: 700; }
|
.prepared-tag { display: inline-flex; align-items: center; gap: 4px; margin-right: auto; color: #4eca76; font-size: 12px; font-weight: 700; }
|
||||||
@@ -2980,6 +3100,134 @@
|
|||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.blame-body {
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #111321;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blame-code-header strong {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
margin-left: auto;
|
||||||
|
color: var(--color-ink-faint);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blame-column-headers {
|
||||||
|
grid-template-columns: minmax(300px, 340px) minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
.blame-commit-col-label {
|
||||||
|
padding-left: 12px;
|
||||||
|
}
|
||||||
|
.blame-code-col-label {
|
||||||
|
padding-left: calc(3.2rem + 10px);
|
||||||
|
}
|
||||||
|
.blame-commit-col-label + .blame-code-col-label {
|
||||||
|
border-left: 1px solid var(--color-border-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blame-diff {
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blame-scroll {
|
||||||
|
min-height: 0;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blame-code-table {
|
||||||
|
width: max-content;
|
||||||
|
min-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blame-group {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(300px, 340px) minmax(max-content, 1fr);
|
||||||
|
align-items: stretch;
|
||||||
|
min-width: max-content;
|
||||||
|
border-bottom: 1px solid var(--color-border-subtle);
|
||||||
|
}
|
||||||
|
.blame-group.uncommitted {
|
||||||
|
background: #171725;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blame-meta {
|
||||||
|
position: sticky;
|
||||||
|
left: 0;
|
||||||
|
z-index: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 3px;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-right: 1px solid var(--color-border-subtle);
|
||||||
|
background: #0d101a;
|
||||||
|
box-shadow: 8px 0 18px rgba(0, 0, 0, 0.18);
|
||||||
|
}
|
||||||
|
.blame-hash {
|
||||||
|
align-self: flex-start;
|
||||||
|
overflow: hidden;
|
||||||
|
max-width: 100%;
|
||||||
|
padding: 2px 7px;
|
||||||
|
border: 1px solid rgba(90,140,248,0.2);
|
||||||
|
border-radius: 5px;
|
||||||
|
background: #141b2d;
|
||||||
|
color: var(--color-accent);
|
||||||
|
font-size: 10.5px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.blame-author {
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--color-ink);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.blame-summary {
|
||||||
|
overflow: hidden;
|
||||||
|
color: #aeb6d8;
|
||||||
|
font-size: 11px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.blame-date {
|
||||||
|
color: var(--color-ink-faint);
|
||||||
|
font-size: 10.5px;
|
||||||
|
}
|
||||||
|
.blame-group:hover .blame-meta {
|
||||||
|
background: #111728;
|
||||||
|
}
|
||||||
|
.blame-group.uncommitted .blame-hash,
|
||||||
|
.blame-group.uncommitted .blame-author {
|
||||||
|
color: #e8b45a;
|
||||||
|
}
|
||||||
|
.blame-group.uncommitted .blame-hash {
|
||||||
|
border-color: rgba(232, 180, 90, 0.26);
|
||||||
|
background: #271f14;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blame-lines {
|
||||||
|
min-width: max-content;
|
||||||
|
grid-template-columns: 4rem minmax(max-content, 1fr);
|
||||||
|
}
|
||||||
|
.blame-line-number {
|
||||||
|
min-height: 20px;
|
||||||
|
padding-right: 8px;
|
||||||
|
}
|
||||||
|
.blame-line-code {
|
||||||
|
min-height: 20px;
|
||||||
|
}
|
||||||
|
.blame-group:hover .blame-line-number {
|
||||||
|
background: #111728;
|
||||||
|
}
|
||||||
|
.blame-group:hover .blame-line-code {
|
||||||
|
background: #151a2b;
|
||||||
|
}
|
||||||
|
|
||||||
.global-search-body {
|
.global-search-body {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: auto auto minmax(0, 1fr);
|
grid-template-rows: auto auto minmax(0, 1fr);
|
||||||
@@ -3370,11 +3618,10 @@
|
|||||||
border: 1px solid rgba(100, 108, 255, 0.36);
|
border: 1px solid rgba(100, 108, 255, 0.36);
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
background:
|
background:
|
||||||
linear-gradient(180deg, rgba(255,255,255,0.06), transparent 42%),
|
linear-gradient(180deg, #222743 0%, #111321 42%),
|
||||||
rgba(15, 16, 28, 0.96);
|
#111321;
|
||||||
box-shadow: 0 32px 90px rgba(0, 0, 0, 0.68), 0 0 0 1px rgba(255,255,255,0.04) inset;
|
box-shadow: 0 32px 90px rgba(0, 0, 0, 0.68), 0 0 0 1px rgba(255,255,255,0.04) inset;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
backdrop-filter: blur(18px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.cred-hero {
|
.cred-hero {
|
||||||
|
|||||||
+12
-1
@@ -2,7 +2,7 @@
|
|||||||
import { onDestroy, onMount } from "svelte";
|
import { onDestroy, onMount } from "svelte";
|
||||||
import { getVersion } from "@tauri-apps/api/app";
|
import { getVersion } from "@tauri-apps/api/app";
|
||||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||||
import { CloudDownload, Download, FolderOpen, GitBranch, GitCompare, LoaderCircle, Minus, RefreshCw, Search, Upload, X } from "@lucide/svelte";
|
import { CloudDownload, Download, FolderOpen, GitBranch, GitCompare, LoaderCircle, Minus, RefreshCw, Search, Settings, Upload, X } from "@lucide/svelte";
|
||||||
|
|
||||||
export let branch: string = "";
|
export let branch: string = "";
|
||||||
export let ahead: number = 0;
|
export let ahead: number = 0;
|
||||||
@@ -21,6 +21,7 @@
|
|||||||
export let onCompare: () => void = () => {};
|
export let onCompare: () => void = () => {};
|
||||||
export let onOpenInExplorer: () => void = () => {};
|
export let onOpenInExplorer: () => void = () => {};
|
||||||
export let onToggleAutoRefresh: () => void = () => {};
|
export let onToggleAutoRefresh: () => void = () => {};
|
||||||
|
export let onOpenSettings: () => void = () => {};
|
||||||
|
|
||||||
const win = getCurrentWindow();
|
const win = getCurrentWindow();
|
||||||
let isMaximized = false;
|
let isMaximized = false;
|
||||||
@@ -198,6 +199,16 @@
|
|||||||
/>
|
/>
|
||||||
<span class="tb-action-label">Auto</span>
|
<span class="tb-action-label">Auto</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="tb-action"
|
||||||
|
onclick={onOpenSettings}
|
||||||
|
title="Settings"
|
||||||
|
aria-label="Settings"
|
||||||
|
>
|
||||||
|
<Settings size={14} aria-hidden="true" />
|
||||||
|
<span class="tb-action-label">Settings</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="titlebar-divider" aria-hidden="true"></div>
|
<div class="titlebar-divider" aria-hidden="true"></div>
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
|
||||||
|
export type AnalyticsEventProperties = Record<string, string | number>;
|
||||||
|
|
||||||
|
export function trackAnalyticsEvent(name: string, props?: AnalyticsEventProperties) {
|
||||||
|
void invoke("plugin:aptabase|track_event", {
|
||||||
|
name,
|
||||||
|
props: props && Object.keys(props).length > 0 ? props : undefined,
|
||||||
|
}).catch(() => {
|
||||||
|
// Analytics must never interrupt the Git workflow.
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { BarChart3, Check, ShieldCheck } from "@lucide/svelte";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
enabled: boolean;
|
||||||
|
onContinue: (enabled: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { enabled = true, onContinue = () => {} }: Props = $props();
|
||||||
|
let allowAnalytics = $state(true);
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
allowAnalytics = enabled;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="dialog-backdrop app-chrome-backdrop" role="presentation">
|
||||||
|
<div class="dialog analytics-notice-dialog" role="dialog" aria-modal="true" aria-label="Analytics notice" tabindex="-1">
|
||||||
|
<header class="dialog-header">
|
||||||
|
<div>
|
||||||
|
<span class="eyebrow">Privacy</span>
|
||||||
|
<h2 class="dialog-title">Anonymous usage analytics</h2>
|
||||||
|
</div>
|
||||||
|
<ShieldCheck size={20} aria-hidden="true" />
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="analytics-notice-body">
|
||||||
|
<div class="analytics-notice-icon">
|
||||||
|
<BarChart3 size={24} aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
<div class="analytics-notice-copy">
|
||||||
|
<p>
|
||||||
|
GitLite can send anonymous usage events to Aptabase so crashes, rough edges, and commonly used workflows are easier to improve.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Events do not include repository paths, remote URLs, branch names, commit messages, diffs, file names, credentials, or source code.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label class="settings-toggle-row analytics-notice-toggle">
|
||||||
|
<input type="checkbox" bind:checked={allowAnalytics} />
|
||||||
|
<span>
|
||||||
|
<strong>Allow anonymous analytics</strong>
|
||||||
|
<small>You can change this later in Settings.</small>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer class="dialog-footer analytics-notice-footer">
|
||||||
|
<span class="dialog-footer-info">Privacy-friendly, optional, and limited to product usage events.</span>
|
||||||
|
<button class="btn-primary" type="button" onclick={() => onContinue(allowAnalytics)}>
|
||||||
|
<Check size={16} aria-hidden="true" />
|
||||||
|
Continue
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Check, Settings, X } from "@lucide/svelte";
|
||||||
|
import type { AnalyticsSettings } from "../types";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
analytics: AnalyticsSettings;
|
||||||
|
onSave: (settings: AnalyticsSettings) => void;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { analytics, onSave = () => {}, onClose = () => {} }: Props = $props();
|
||||||
|
|
||||||
|
let analyticsEnabled = $state(true);
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
analyticsEnabled = analytics.enabled;
|
||||||
|
});
|
||||||
|
|
||||||
|
function save() {
|
||||||
|
onSave({
|
||||||
|
...analytics,
|
||||||
|
enabled: analyticsEnabled,
|
||||||
|
noticeSeen: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="dialog-backdrop" role="presentation">
|
||||||
|
<div class="dialog app-settings-dialog" role="dialog" aria-modal="true" aria-label="Settings" tabindex="-1">
|
||||||
|
<header class="dialog-header">
|
||||||
|
<div>
|
||||||
|
<span class="eyebrow">GitLite</span>
|
||||||
|
<h2 class="dialog-title">Settings</h2>
|
||||||
|
</div>
|
||||||
|
<button class="dialog-close" type="button" onclick={onClose} title="Close">
|
||||||
|
<X size={18} aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<form class="app-settings-form" onsubmit={(event) => { event.preventDefault(); save(); }}>
|
||||||
|
<section class="settings-section">
|
||||||
|
<header>
|
||||||
|
<Settings size={16} aria-hidden="true" />
|
||||||
|
<div>
|
||||||
|
<span class="eyebrow">Analytics</span>
|
||||||
|
<h3>Anonymous usage analytics</h3>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<label class="settings-toggle-row">
|
||||||
|
<input type="checkbox" bind:checked={analyticsEnabled} />
|
||||||
|
<span>
|
||||||
|
<strong>Allow anonymous Aptabase events</strong>
|
||||||
|
<small>No repository paths, remotes, branches, commit messages, file names, diffs, credentials, or code are sent.</small>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="new-branch-actions">
|
||||||
|
<button class="btn-secondary" type="button" onclick={onClose}>Cancel</button>
|
||||||
|
<button class="btn-primary" type="submit">
|
||||||
|
<Check size={16} aria-hidden="true" />
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { FileCode, LoaderCircle, X } from "@lucide/svelte";
|
||||||
|
import type { GitBlameLine } from "../types";
|
||||||
|
|
||||||
|
interface BlameGroup {
|
||||||
|
id: string;
|
||||||
|
hash: string;
|
||||||
|
shortHash: string;
|
||||||
|
authorName: string;
|
||||||
|
authorEmail: string;
|
||||||
|
summary: string;
|
||||||
|
authorTime: number;
|
||||||
|
isUncommitted: boolean;
|
||||||
|
lines: GitBlameLine[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
filePath: string;
|
||||||
|
lines: GitBlameLine[];
|
||||||
|
isBusy: boolean;
|
||||||
|
isLoading: boolean;
|
||||||
|
error: string;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
filePath = "",
|
||||||
|
lines = [],
|
||||||
|
isBusy = false,
|
||||||
|
isLoading = false,
|
||||||
|
error = "",
|
||||||
|
onClose = () => {},
|
||||||
|
}: Props = $props();
|
||||||
|
|
||||||
|
function groupBlameLines(source: GitBlameLine[]): BlameGroup[] {
|
||||||
|
const groups: BlameGroup[] = [];
|
||||||
|
for (const line of source) {
|
||||||
|
const last = groups[groups.length - 1];
|
||||||
|
if (last && last.hash === line.commit_hash) {
|
||||||
|
last.lines.push(line);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
groups.push({
|
||||||
|
id: `${line.commit_hash}-${line.line_number}`,
|
||||||
|
hash: line.commit_hash,
|
||||||
|
shortHash: line.short_hash,
|
||||||
|
authorName: line.author_name,
|
||||||
|
authorEmail: line.author_email,
|
||||||
|
summary: line.summary,
|
||||||
|
authorTime: line.author_time,
|
||||||
|
isUncommitted: line.is_uncommitted,
|
||||||
|
lines: [line],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatBlameDate(seconds: number): string {
|
||||||
|
if (!seconds) return "";
|
||||||
|
const date = new Date(seconds * 1000);
|
||||||
|
if (Number.isNaN(date.getTime())) return "";
|
||||||
|
return new Intl.DateTimeFormat(undefined, { dateStyle: "medium", timeStyle: "short" }).format(date);
|
||||||
|
}
|
||||||
|
|
||||||
|
function groupTooltip(group: BlameGroup): string {
|
||||||
|
if (group.isUncommitted) return "Not committed yet";
|
||||||
|
return `${group.authorName} <${group.authorEmail}>\n${group.summary}\n${group.hash}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let groups = $derived(groupBlameLines(lines));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="dialog-backdrop" role="presentation">
|
||||||
|
<div class="dialog blame-dialog" role="dialog" aria-modal="true" aria-label="File blame">
|
||||||
|
<header class="dialog-header">
|
||||||
|
<div>
|
||||||
|
<span class="eyebrow">Blame</span>
|
||||||
|
<p class="dialog-title" title={filePath}>{filePath}</p>
|
||||||
|
</div>
|
||||||
|
<div class="dialog-header-actions">
|
||||||
|
<span class="pill pill-count">{lines.length}</span>
|
||||||
|
<button class="btn-sm dialog-close" type="button" onclick={onClose} disabled={isBusy} aria-label="Close">
|
||||||
|
<X size={16} aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="dialog-diff blame-body">
|
||||||
|
{#if isLoading}
|
||||||
|
<div class="blank-state">
|
||||||
|
<LoaderCircle class="spin" size={18} aria-hidden="true" />
|
||||||
|
Loading blame...
|
||||||
|
</div>
|
||||||
|
{:else if error}
|
||||||
|
<div class="blank-state">{error}</div>
|
||||||
|
{:else if groups.length === 0}
|
||||||
|
<div class="blank-state">No blame information available for this file.</div>
|
||||||
|
{:else}
|
||||||
|
<div class="diff-header blame-code-header">
|
||||||
|
<FileCode size={13} aria-hidden="true" />
|
||||||
|
<span title={filePath}>{filePath}</span>
|
||||||
|
<strong>{lines.length} lines</strong>
|
||||||
|
</div>
|
||||||
|
<div class="split-col-headers blame-column-headers">
|
||||||
|
<div class="split-col-label blame-commit-col-label">Commit</div>
|
||||||
|
<div class="split-col-label blame-code-col-label">Code</div>
|
||||||
|
</div>
|
||||||
|
<div class="split-diff blame-diff" role="table" aria-label="File blame">
|
||||||
|
<div class="split-pane blame-scroll">
|
||||||
|
<div class="blame-code-table">
|
||||||
|
{#each groups as group (group.id)}
|
||||||
|
<div class="blame-group" class:uncommitted={group.isUncommitted} title={groupTooltip(group)}>
|
||||||
|
<div class="blame-meta">
|
||||||
|
<span class="blame-hash">{group.isUncommitted ? "Uncommitted" : group.shortHash}</span>
|
||||||
|
<span class="blame-author">{group.isUncommitted ? "Not committed yet" : group.authorName}</span>
|
||||||
|
<span class="blame-summary">{group.summary || "No commit message"}</span>
|
||||||
|
{#if !group.isUncommitted}
|
||||||
|
<span class="blame-date">{formatBlameDate(group.authorTime)}</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="split-pane-grid blame-lines">
|
||||||
|
{#each group.lines as line (line.line_number)}
|
||||||
|
<span class="split-num blame-line-number">{line.line_number}</span>
|
||||||
|
<code class="split-cell blame-line-code">{line.content}</code>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -18,6 +18,7 @@
|
|||||||
Folder,
|
Folder,
|
||||||
FolderOpen,
|
FolderOpen,
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
|
History,
|
||||||
Terminal,
|
Terminal,
|
||||||
} from "@lucide/svelte";
|
} from "@lucide/svelte";
|
||||||
import { languageIconForPath } from "../languageIcons";
|
import { languageIconForPath } from "../languageIcons";
|
||||||
@@ -36,6 +37,7 @@
|
|||||||
onCollapseAllFolders: () => void;
|
onCollapseAllFolders: () => void;
|
||||||
onSelectNode: (node: ExplorerNode) => void;
|
onSelectNode: (node: ExplorerNode) => void;
|
||||||
onOpenFile: (node: ExplorerNode) => void;
|
onOpenFile: (node: ExplorerNode) => void;
|
||||||
|
onBlame: (node: ExplorerNode) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
@@ -50,6 +52,7 @@
|
|||||||
onCollapseAllFolders = () => {},
|
onCollapseAllFolders = () => {},
|
||||||
onSelectNode = () => {},
|
onSelectNode = () => {},
|
||||||
onOpenFile = () => {},
|
onOpenFile = () => {},
|
||||||
|
onBlame = () => {},
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
let contextNode = $state<ExplorerNode | null>(null);
|
let contextNode = $state<ExplorerNode | null>(null);
|
||||||
@@ -182,6 +185,13 @@
|
|||||||
onOpenFile(node);
|
onOpenFile(node);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openContextBlame() {
|
||||||
|
const node = contextNode;
|
||||||
|
if (!node || node.kind !== "file") return;
|
||||||
|
closeFileContextMenu();
|
||||||
|
onBlame(node);
|
||||||
|
}
|
||||||
|
|
||||||
function handleWindowKeydown(event: KeyboardEvent) {
|
function handleWindowKeydown(event: KeyboardEvent) {
|
||||||
if (event.key === "Escape") closeFileContextMenu();
|
if (event.key === "Escape") closeFileContextMenu();
|
||||||
}
|
}
|
||||||
@@ -335,5 +345,15 @@
|
|||||||
<ExternalLink size={14} aria-hidden="true" />
|
<ExternalLink size={14} aria-hidden="true" />
|
||||||
Open in Explorer
|
Open in Explorer
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="menuitem"
|
||||||
|
onclick={openContextBlame}
|
||||||
|
disabled={!contextNode.tracked || contextNode.status === "deleted"}
|
||||||
|
title={!contextNode.tracked || contextNode.status === "deleted" ? "Blame is only available for tracked files" : "Show blame for this file"}
|
||||||
|
>
|
||||||
|
<History size={14} aria-hidden="true" />
|
||||||
|
Blame
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -43,12 +43,14 @@
|
|||||||
<style>
|
<style>
|
||||||
.repo-loading {
|
.repo-loading {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
top: var(--app-titlebar-height, 42px);
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
z-index: 400;
|
z-index: 400;
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
background: rgba(6, 6, 14, 0.72);
|
background: #050712;
|
||||||
backdrop-filter: blur(8px);
|
|
||||||
animation: overlay-in 180ms ease;
|
animation: overlay-in 180ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type {
|
|||||||
CommitAiProvider,
|
CommitAiProvider,
|
||||||
CommitAiStatus,
|
CommitAiStatus,
|
||||||
ConflictFile,
|
ConflictFile,
|
||||||
|
GitBlameResult,
|
||||||
GitBranch,
|
GitBranch,
|
||||||
GitCommit,
|
GitCommit,
|
||||||
GitCommitComparison,
|
GitCommitComparison,
|
||||||
@@ -322,6 +323,10 @@ export function cancelFileHistory(requestId: string): Promise<void> {
|
|||||||
return invoke<void>("cancel_file_history", { requestId });
|
return invoke<void>("cancel_file_history", { requestId });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getFileBlame(path: string, file: string): Promise<GitBlameResult> {
|
||||||
|
return invoke<GitBlameResult>("get_file_blame", { path, file });
|
||||||
|
}
|
||||||
|
|
||||||
export function compareCommits(
|
export function compareCommits(
|
||||||
path: string,
|
path: string,
|
||||||
from: string,
|
from: string,
|
||||||
|
|||||||
@@ -33,6 +33,11 @@ export interface AiSettings {
|
|||||||
customModel: string;
|
customModel: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AnalyticsSettings {
|
||||||
|
enabled: boolean;
|
||||||
|
noticeSeen: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface GitStatus {
|
export interface GitStatus {
|
||||||
repo_path: string;
|
repo_path: string;
|
||||||
current_branch: string | null;
|
current_branch: string | null;
|
||||||
@@ -175,6 +180,23 @@ export interface ConflictFile {
|
|||||||
theirs_size: number | null;
|
theirs_size: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface GitBlameLine {
|
||||||
|
line_number: number;
|
||||||
|
content: string;
|
||||||
|
commit_hash: string;
|
||||||
|
short_hash: string;
|
||||||
|
author_name: string;
|
||||||
|
author_email: string;
|
||||||
|
author_time: number;
|
||||||
|
summary: string;
|
||||||
|
is_uncommitted: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GitBlameResult {
|
||||||
|
path: string;
|
||||||
|
lines: GitBlameLine[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface StoredCredential {
|
export interface StoredCredential {
|
||||||
username: string;
|
username: string;
|
||||||
password: string;
|
password: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user