add new style and global search
This commit is contained in:
Generated
+159
-1
@@ -1932,6 +1932,7 @@ checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
|
||||
dependencies = [
|
||||
"bitflags 2.13.0",
|
||||
"block2",
|
||||
"libc",
|
||||
"objc2",
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
@@ -2396,6 +2397,30 @@ dependencies = [
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rfd"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672"
|
||||
dependencies = [
|
||||
"block2",
|
||||
"dispatch2",
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"gtk-sys",
|
||||
"js-sys",
|
||||
"log",
|
||||
"objc2",
|
||||
"objc2-app-kit",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation",
|
||||
"raw-window-handle",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "2.1.2"
|
||||
@@ -3040,6 +3065,64 @@ dependencies = [
|
||||
"tauri-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin"
|
||||
version = "2.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74be5dd4bed9afbd145e5716b5fa2ec28cbc29c34ffa61c258c9273d896c8020"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"glob",
|
||||
"plist",
|
||||
"schemars 0.8.22",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri-utils",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-dialog"
|
||||
version = "2.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1fa4150c95ae391946cc8b8f905ab14797427caba3a8a2f79628e956da91809"
|
||||
dependencies = [
|
||||
"log",
|
||||
"raw-window-handle",
|
||||
"rfd",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"tauri-plugin-fs",
|
||||
"thiserror 2.0.18",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-fs"
|
||||
version = "2.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7ecc274121aca0c036a2b42d1cbe83d368d348f54e0bb8a735c2b1548e8f371"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"dunce",
|
||||
"glob",
|
||||
"log",
|
||||
"objc2-foundation",
|
||||
"percent-encoding",
|
||||
"schemars 0.8.22",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_repr",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"tauri-utils",
|
||||
"thiserror 2.0.18",
|
||||
"toml 1.1.2+spec-1.1.0",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-runtime"
|
||||
version = "2.11.3"
|
||||
@@ -3147,6 +3230,7 @@ dependencies = [
|
||||
"serde",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-dialog",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4046,6 +4130,15 @@ dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.60.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
|
||||
dependencies = [
|
||||
"windows-targets 0.53.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
@@ -4079,13 +4172,30 @@ dependencies = [
|
||||
"windows_aarch64_gnullvm 0.52.6",
|
||||
"windows_aarch64_msvc 0.52.6",
|
||||
"windows_i686_gnu 0.52.6",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_gnullvm 0.52.6",
|
||||
"windows_i686_msvc 0.52.6",
|
||||
"windows_x86_64_gnu 0.52.6",
|
||||
"windows_x86_64_gnullvm 0.52.6",
|
||||
"windows_x86_64_msvc 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.53.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
|
||||
dependencies = [
|
||||
"windows-link 0.2.1",
|
||||
"windows_aarch64_gnullvm 0.53.1",
|
||||
"windows_aarch64_msvc 0.53.1",
|
||||
"windows_i686_gnu 0.53.1",
|
||||
"windows_i686_gnullvm 0.53.1",
|
||||
"windows_i686_msvc 0.53.1",
|
||||
"windows_x86_64_gnu 0.53.1",
|
||||
"windows_x86_64_gnullvm 0.53.1",
|
||||
"windows_x86_64_msvc 0.53.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-threading"
|
||||
version = "0.1.0"
|
||||
@@ -4116,6 +4226,12 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.42.2"
|
||||
@@ -4128,6 +4244,12 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.42.2"
|
||||
@@ -4140,12 +4262,24 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.42.2"
|
||||
@@ -4158,6 +4292,12 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.42.2"
|
||||
@@ -4170,6 +4310,12 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.42.2"
|
||||
@@ -4182,6 +4328,12 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.42.2"
|
||||
@@ -4194,6 +4346,12 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.5.40"
|
||||
|
||||
@@ -9,6 +9,7 @@ build = "build.rs"
|
||||
[dependencies]
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
tauri = { version = "2", features = [] }
|
||||
tauri-plugin-dialog = "=2.7.0"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"core:window:allow-unminimize",
|
||||
"core:window:allow-close",
|
||||
"core:window:allow-is-maximized",
|
||||
"core:window:allow-start-dragging"
|
||||
"core:window:allow-start-dragging",
|
||||
"dialog:allow-open"
|
||||
]
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 70 B After Width: | Height: | Size: 128 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 182 KiB |
+680
-2
@@ -1,9 +1,15 @@
|
||||
use serde::Serialize;
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
ffi::{OsStr, OsString},
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
process::{Command, Stdio},
|
||||
sync::{
|
||||
atomic::{AtomicU64, Ordering},
|
||||
Arc, Mutex,
|
||||
},
|
||||
thread,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
|
||||
@@ -102,6 +108,21 @@ pub struct GitRepositoryFile {
|
||||
pub status: Option<FileStatusKind>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
pub struct GitSearchHit {
|
||||
pub commit_hash: String,
|
||||
pub short_hash: String,
|
||||
pub summary: String,
|
||||
pub author_name: String,
|
||||
pub author_email: String,
|
||||
pub date: String,
|
||||
pub file: String,
|
||||
pub old_file: Option<String>,
|
||||
pub line_number: Option<u32>,
|
||||
pub line: String,
|
||||
pub matches_added: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||
struct BranchInfo {
|
||||
current_branch: Option<String>,
|
||||
@@ -118,6 +139,54 @@ enum CheckoutPlan {
|
||||
}
|
||||
|
||||
const FULL_FILE_DIFF_CONTEXT: &str = "--unified=1000000";
|
||||
const SEARCH_CANCELLED_MESSAGE: &str = "Suche wurde abgebrochen.";
|
||||
static CANCELLABLE_GIT_OUTPUT_COUNTER: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct SearchCancellationState {
|
||||
cancelled: Arc<Mutex<BTreeSet<String>>>,
|
||||
}
|
||||
|
||||
impl SearchCancellationState {
|
||||
fn cancel(&self, search_id: &str) -> Result<(), String> {
|
||||
self.cancelled
|
||||
.lock()
|
||||
.map_err(|_| "Such-Abbruchstatus ist nicht erreichbar.".to_string())?
|
||||
.insert(search_id.to_string());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clear(&self, search_id: &str) -> Result<(), String> {
|
||||
self.cancelled
|
||||
.lock()
|
||||
.map_err(|_| "Such-Abbruchstatus ist nicht erreichbar.".to_string())?
|
||||
.remove(search_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_cancelled(&self, search_id: &str) -> Result<bool, String> {
|
||||
Ok(self
|
||||
.cancelled
|
||||
.lock()
|
||||
.map_err(|_| "Such-Abbruchstatus ist nicht erreichbar.".to_string())?
|
||||
.contains(search_id))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct SearchCancellation {
|
||||
state: SearchCancellationState,
|
||||
search_id: String,
|
||||
}
|
||||
|
||||
fn check_search_cancelled(cancellation: Option<&SearchCancellation>) -> Result<(), String> {
|
||||
if let Some(cancellation) = cancellation {
|
||||
if cancellation.state.is_cancelled(&cancellation.search_id)? {
|
||||
return Err(SEARCH_CANCELLED_MESSAGE.to_string());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn open_repository(path: String) -> Result<GitStatus, String> {
|
||||
@@ -394,6 +463,174 @@ pub fn list_file_history(
|
||||
parse_commit_log(&repo, &output)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn search_code_introductions(
|
||||
path: String,
|
||||
query: String,
|
||||
case_sensitive: Option<bool>,
|
||||
limit: Option<u32>,
|
||||
search_id: Option<String>,
|
||||
state: tauri::State<'_, SearchCancellationState>,
|
||||
) -> Result<Vec<GitSearchHit>, String> {
|
||||
let state = state.inner().clone();
|
||||
|
||||
tauri::async_runtime::spawn_blocking(move || {
|
||||
let repo = resolve_repo(&path)?;
|
||||
let query = normalize_newlines(&query);
|
||||
let query = query.trim_matches('\n').to_string();
|
||||
if query.trim().is_empty() {
|
||||
return Err("Suchtext darf nicht leer sein.".to_string());
|
||||
}
|
||||
|
||||
if verify_commit(&repo, "HEAD").is_err() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let case_sensitive = case_sensitive.unwrap_or(false);
|
||||
let limit = limit.unwrap_or(250).clamp(1, 1000) as usize;
|
||||
let search_id = search_id
|
||||
.map(|id| id.trim().to_string())
|
||||
.filter(|id| !id.is_empty());
|
||||
|
||||
let cancellation = search_id.as_ref().map(|search_id| SearchCancellation {
|
||||
state: state.clone(),
|
||||
search_id: search_id.clone(),
|
||||
});
|
||||
|
||||
let result = search_code_introductions_core(
|
||||
&repo,
|
||||
query,
|
||||
case_sensitive,
|
||||
limit,
|
||||
cancellation.as_ref(),
|
||||
);
|
||||
|
||||
if let Some(search_id) = search_id.as_deref() {
|
||||
let _ = state.clear(search_id);
|
||||
}
|
||||
|
||||
result
|
||||
})
|
||||
.await
|
||||
.map_err(|err| format!("Such-Task konnte nicht abgeschlossen werden: {err}"))?
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn cancel_code_search(
|
||||
search_id: String,
|
||||
state: tauri::State<'_, SearchCancellationState>,
|
||||
) -> Result<(), String> {
|
||||
let search_id = search_id.trim();
|
||||
if search_id.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
state.cancel(search_id)
|
||||
}
|
||||
|
||||
fn search_code_introductions_core(
|
||||
repo: &Path,
|
||||
query: String,
|
||||
case_sensitive: bool,
|
||||
limit: usize,
|
||||
cancellation: Option<&SearchCancellation>,
|
||||
) -> Result<Vec<GitSearchHit>, String> {
|
||||
check_search_cancelled(cancellation)?;
|
||||
let candidates = search_candidate_commits(&repo, &query, case_sensitive, cancellation)?;
|
||||
let mut hits = Vec::new();
|
||||
|
||||
for commit in candidates {
|
||||
check_search_cancelled(cancellation)?;
|
||||
if hits.len() >= limit {
|
||||
break;
|
||||
}
|
||||
|
||||
let files = commit_files(&repo, &commit)?;
|
||||
if files.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let parents = commit_parents(&repo, &commit)?;
|
||||
let mut metadata: Option<GitSearchCommitMetadata> = None;
|
||||
|
||||
for file in files {
|
||||
check_search_cancelled(cancellation)?;
|
||||
if hits.len() >= limit {
|
||||
break;
|
||||
}
|
||||
|
||||
if matches!(file.status, FileStatusKind::Deleted) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(after_content) = read_text_blob(&repo, &commit, &file.path)? else {
|
||||
continue;
|
||||
};
|
||||
let after_count = count_matches(&after_content, &query, case_sensitive);
|
||||
if after_count == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let before_count = max_parent_match_count(
|
||||
&repo,
|
||||
&parents,
|
||||
file.old_path.as_deref().unwrap_or(&file.path),
|
||||
&query,
|
||||
case_sensitive,
|
||||
)?;
|
||||
|
||||
if after_count <= before_count {
|
||||
continue;
|
||||
}
|
||||
|
||||
let match_line = first_added_match_line(
|
||||
&repo,
|
||||
parents.first().map(String::as_str),
|
||||
&commit,
|
||||
&file.path,
|
||||
&query,
|
||||
case_sensitive,
|
||||
cancellation,
|
||||
)?
|
||||
.or_else(|| first_match_line(&after_content, &query, case_sensitive));
|
||||
|
||||
let Some((line_number, line)) = match_line else {
|
||||
continue;
|
||||
};
|
||||
let info = metadata
|
||||
.get_or_insert_with(|| {
|
||||
commit_search_metadata(&repo, &commit).unwrap_or_else(|_| {
|
||||
GitSearchCommitMetadata {
|
||||
commit_hash: commit.clone(),
|
||||
short_hash: short_hash(&commit),
|
||||
author_name: String::new(),
|
||||
author_email: String::new(),
|
||||
date: String::new(),
|
||||
summary: String::new(),
|
||||
}
|
||||
})
|
||||
})
|
||||
.clone();
|
||||
|
||||
hits.push(GitSearchHit {
|
||||
commit_hash: info.commit_hash,
|
||||
short_hash: info.short_hash,
|
||||
summary: info.summary,
|
||||
author_name: info.author_name,
|
||||
author_email: info.author_email,
|
||||
date: info.date,
|
||||
file: file.path,
|
||||
old_file: file.old_path,
|
||||
line_number: Some(line_number),
|
||||
line,
|
||||
matches_added: after_count.saturating_sub(before_count) as u32,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(hits)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn restore_to_commit(path: String, commit: String) -> Result<GitStatus, String> {
|
||||
let repo = resolve_repo(&path)?;
|
||||
@@ -770,6 +1007,265 @@ fn has_unresolved_conflicts(status: &GitStatus) -> bool {
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
struct GitSearchCommitMetadata {
|
||||
commit_hash: String,
|
||||
short_hash: String,
|
||||
author_name: String,
|
||||
author_email: String,
|
||||
date: String,
|
||||
summary: String,
|
||||
}
|
||||
|
||||
fn search_candidate_commits(
|
||||
repo: &Path,
|
||||
query: &str,
|
||||
case_sensitive: bool,
|
||||
cancellation: Option<&SearchCancellation>,
|
||||
) -> Result<Vec<String>, String> {
|
||||
let output = if query.contains('\n') {
|
||||
run_git_cancellable(
|
||||
repo,
|
||||
["rev-list", "--all", "--reverse"],
|
||||
cancellation,
|
||||
"Git-Suche fehlgeschlagen",
|
||||
)?
|
||||
} else {
|
||||
let mut args = vec![
|
||||
OsString::from("log"),
|
||||
OsString::from("--all"),
|
||||
OsString::from("--reverse"),
|
||||
OsString::from("--format=%H"),
|
||||
];
|
||||
if !case_sensitive {
|
||||
args.push(OsString::from("-i"));
|
||||
}
|
||||
args.push(OsString::from(format!("-S{query}")));
|
||||
run_git_cancellable(repo, args, cancellation, "Git-Suche fehlgeschlagen")?
|
||||
};
|
||||
|
||||
Ok(String::from_utf8_lossy(&output)
|
||||
.lines()
|
||||
.map(str::trim)
|
||||
.filter(|line| !line.is_empty())
|
||||
.map(ToString::to_string)
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn commit_parents(repo: &Path, commit: &str) -> Result<Vec<String>, String> {
|
||||
let output = run_git(repo, ["rev-list", "--parents", "-n", "1", commit])?;
|
||||
let text = String::from_utf8_lossy(&output);
|
||||
Ok(text
|
||||
.split_whitespace()
|
||||
.skip(1)
|
||||
.map(ToString::to_string)
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn max_parent_match_count(
|
||||
repo: &Path,
|
||||
parents: &[String],
|
||||
file: &str,
|
||||
query: &str,
|
||||
case_sensitive: bool,
|
||||
) -> Result<usize, String> {
|
||||
let mut max_count = 0;
|
||||
for parent in parents {
|
||||
if let Some(content) = read_text_blob(repo, parent, file)? {
|
||||
max_count = max_count.max(count_matches(&content, query, case_sensitive));
|
||||
}
|
||||
}
|
||||
Ok(max_count)
|
||||
}
|
||||
|
||||
fn read_text_blob(repo: &Path, commit: &str, file: &str) -> Result<Option<String>, String> {
|
||||
let spec = format!("{commit}:{file}");
|
||||
let output = Command::new("git")
|
||||
.arg("-C")
|
||||
.arg(repo)
|
||||
.args(["show", spec.as_str()])
|
||||
.output()
|
||||
.map_err(|err| format!("Git konnte nicht gestartet werden. Ist Git installiert? {err}"))?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
if is_binary_bytes(&output.stdout) {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
Ok(Some(normalize_newlines(&String::from_utf8_lossy(
|
||||
&output.stdout,
|
||||
))))
|
||||
}
|
||||
|
||||
fn normalize_newlines(value: &str) -> String {
|
||||
value.replace("\r\n", "\n").replace('\r', "\n")
|
||||
}
|
||||
|
||||
fn count_matches(content: &str, query: &str, case_sensitive: bool) -> usize {
|
||||
let content = if case_sensitive {
|
||||
content.to_string()
|
||||
} else {
|
||||
content.to_ascii_lowercase()
|
||||
};
|
||||
let query = if case_sensitive {
|
||||
query.to_string()
|
||||
} else {
|
||||
query.to_ascii_lowercase()
|
||||
};
|
||||
|
||||
if query.is_empty() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let mut count = 0;
|
||||
let mut start = 0;
|
||||
while let Some(index) = content[start..].find(&query) {
|
||||
count += 1;
|
||||
start += index + query.len();
|
||||
}
|
||||
count
|
||||
}
|
||||
|
||||
fn first_added_match_line(
|
||||
repo: &Path,
|
||||
parent: Option<&str>,
|
||||
commit: &str,
|
||||
file: &str,
|
||||
query: &str,
|
||||
case_sensitive: bool,
|
||||
cancellation: Option<&SearchCancellation>,
|
||||
) -> Result<Option<(u32, String)>, String> {
|
||||
let Some(parent) = parent else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
check_search_cancelled(cancellation)?;
|
||||
let output = run_git_with_paths_cancellable(
|
||||
repo,
|
||||
&["diff", "--unified=0", parent, commit],
|
||||
&[file.to_string()],
|
||||
cancellation,
|
||||
"Git-Diff fuer Suchtreffer fehlgeschlagen",
|
||||
)?;
|
||||
let patch = String::from_utf8_lossy(&output);
|
||||
let line_query = first_query_line(query);
|
||||
let mut new_line = 0u32;
|
||||
|
||||
for line in patch.lines() {
|
||||
check_search_cancelled(cancellation)?;
|
||||
if line.starts_with("@@") {
|
||||
if let Some(start) = parse_new_hunk_start(line) {
|
||||
new_line = start;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if line.starts_with("+++") || line.starts_with("---") || line.starts_with("diff ") {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(added) = line.strip_prefix('+') {
|
||||
if count_matches(added, line_query, case_sensitive) > 0 {
|
||||
return Ok(Some((new_line.max(1), compact_search_line(added))));
|
||||
}
|
||||
new_line = new_line.saturating_add(1);
|
||||
} else if line.starts_with('-') {
|
||||
continue;
|
||||
} else if line.starts_with(' ') {
|
||||
new_line = new_line.saturating_add(1);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn first_query_line(query: &str) -> &str {
|
||||
query
|
||||
.lines()
|
||||
.map(str::trim)
|
||||
.find(|line| !line.is_empty())
|
||||
.unwrap_or(query)
|
||||
}
|
||||
|
||||
fn parse_new_hunk_start(line: &str) -> Option<u32> {
|
||||
let plus = line.split_whitespace().find(|part| part.starts_with('+'))?;
|
||||
let number = plus
|
||||
.trim_start_matches('+')
|
||||
.split_once(',')
|
||||
.map(|(start, _)| start)
|
||||
.unwrap_or_else(|| plus.trim_start_matches('+'));
|
||||
number.parse().ok()
|
||||
}
|
||||
|
||||
fn first_match_line(content: &str, query: &str, case_sensitive: bool) -> Option<(u32, String)> {
|
||||
let haystack = if case_sensitive {
|
||||
content.to_string()
|
||||
} else {
|
||||
content.to_ascii_lowercase()
|
||||
};
|
||||
let needle = if case_sensitive {
|
||||
query.to_string()
|
||||
} else {
|
||||
query.to_ascii_lowercase()
|
||||
};
|
||||
let index = haystack.find(&needle)?;
|
||||
let line_number = content[..index]
|
||||
.bytes()
|
||||
.filter(|byte| *byte == b'\n')
|
||||
.count() as u32
|
||||
+ 1;
|
||||
let line_start = content[..index].rfind('\n').map(|pos| pos + 1).unwrap_or(0);
|
||||
let line_end = content[index..]
|
||||
.find('\n')
|
||||
.map(|pos| index + pos)
|
||||
.unwrap_or(content.len());
|
||||
Some((
|
||||
line_number,
|
||||
compact_search_line(&content[line_start..line_end]),
|
||||
))
|
||||
}
|
||||
|
||||
fn compact_search_line(line: &str) -> String {
|
||||
const MAX_LEN: usize = 240;
|
||||
let compact = line.trim().replace('\t', " ");
|
||||
if compact.chars().count() <= MAX_LEN {
|
||||
return compact;
|
||||
}
|
||||
|
||||
let mut shortened: String = compact.chars().take(MAX_LEN).collect();
|
||||
shortened.push_str("...");
|
||||
shortened
|
||||
}
|
||||
|
||||
fn commit_search_metadata(repo: &Path, commit: &str) -> Result<GitSearchCommitMetadata, String> {
|
||||
let output = run_git(
|
||||
repo,
|
||||
[
|
||||
"show",
|
||||
"-s",
|
||||
"--format=%H%x1f%h%x1f%an%x1f%ae%x1f%aI%x1f%s",
|
||||
commit,
|
||||
],
|
||||
)?;
|
||||
let text = String::from_utf8_lossy(&output);
|
||||
let fields: Vec<&str> = text.trim_end().splitn(6, '\x1f').collect();
|
||||
if fields.len() != 6 {
|
||||
return Err(format!("Unerwarteter Git-Commit-Eintrag: {text}"));
|
||||
}
|
||||
|
||||
Ok(GitSearchCommitMetadata {
|
||||
commit_hash: fields[0].to_string(),
|
||||
short_hash: fields[1].to_string(),
|
||||
author_name: fields[2].to_string(),
|
||||
author_email: fields[3].to_string(),
|
||||
date: fields[4].to_string(),
|
||||
summary: fields[5].to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_commit_log(repo: &Path, output: &[u8]) -> Result<Vec<GitCommit>, String> {
|
||||
const FIELD_SEPARATOR: char = '\x1f';
|
||||
const RECORD_SEPARATOR: char = '\x1e';
|
||||
@@ -1283,6 +1779,20 @@ fn run_git_with_paths(
|
||||
run_git(repo, args)
|
||||
}
|
||||
|
||||
fn run_git_with_paths_cancellable(
|
||||
repo: &Path,
|
||||
base_args: &[&str],
|
||||
files: &[String],
|
||||
cancellation: Option<&SearchCancellation>,
|
||||
context: &str,
|
||||
) -> Result<Vec<u8>, String> {
|
||||
let mut args = Vec::with_capacity(base_args.len() + files.len() + 1);
|
||||
args.extend(base_args.iter().map(OsString::from));
|
||||
args.push(OsString::from("--"));
|
||||
args.extend(files.iter().map(OsString::from));
|
||||
run_git_cancellable(repo, args, cancellation, context)
|
||||
}
|
||||
|
||||
fn run_git<I, S>(repo: &Path, args: I) -> Result<Vec<u8>, String>
|
||||
where
|
||||
I: IntoIterator<Item = S>,
|
||||
@@ -1291,6 +1801,91 @@ where
|
||||
run_git_at(repo, args, "Git-Befehl fehlgeschlagen")
|
||||
}
|
||||
|
||||
fn run_git_cancellable<I, S>(
|
||||
repo: &Path,
|
||||
args: I,
|
||||
cancellation: Option<&SearchCancellation>,
|
||||
context: &str,
|
||||
) -> Result<Vec<u8>, String>
|
||||
where
|
||||
I: IntoIterator<Item = S>,
|
||||
S: AsRef<OsStr>,
|
||||
{
|
||||
check_search_cancelled(cancellation)?;
|
||||
|
||||
let counter = CANCELLABLE_GIT_OUTPUT_COUNTER.fetch_add(1, Ordering::Relaxed);
|
||||
let temp_dir = std::env::temp_dir();
|
||||
let stdout_path = temp_dir.join(format!(
|
||||
"gitlite_search_{}_{}.out",
|
||||
std::process::id(),
|
||||
counter
|
||||
));
|
||||
let stderr_path = temp_dir.join(format!(
|
||||
"gitlite_search_{}_{}.err",
|
||||
std::process::id(),
|
||||
counter
|
||||
));
|
||||
let stdout_file = std::fs::File::create(&stdout_path)
|
||||
.map_err(|err| format!("Git-Ausgabedatei konnte nicht erstellt werden: {err}"))?;
|
||||
let stderr_file = std::fs::File::create(&stderr_path)
|
||||
.map_err(|err| format!("Git-Fehlerdatei konnte nicht erstellt werden: {err}"))?;
|
||||
|
||||
let mut child = Command::new("git")
|
||||
.arg("-C")
|
||||
.arg(repo)
|
||||
.args(args)
|
||||
.stdout(Stdio::from(stdout_file))
|
||||
.stderr(Stdio::from(stderr_file))
|
||||
.spawn()
|
||||
.map_err(|err| {
|
||||
let _ = std::fs::remove_file(&stdout_path);
|
||||
let _ = std::fs::remove_file(&stderr_path);
|
||||
format!("Git konnte nicht gestartet werden. Ist Git installiert? {err}")
|
||||
})?;
|
||||
|
||||
let status = loop {
|
||||
if let Err(err) = check_search_cancelled(cancellation) {
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
let _ = std::fs::remove_file(&stdout_path);
|
||||
let _ = std::fs::remove_file(&stderr_path);
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
if let Some(status) = child
|
||||
.try_wait()
|
||||
.map_err(|err| format!("Git-Prozess konnte nicht geprueft werden: {err}"))?
|
||||
{
|
||||
break status;
|
||||
}
|
||||
|
||||
thread::sleep(Duration::from_millis(60));
|
||||
};
|
||||
|
||||
let stdout = std::fs::read(&stdout_path)
|
||||
.map_err(|err| format!("Git-Ausgabe konnte nicht gelesen werden: {err}"))?;
|
||||
let stderr = std::fs::read(&stderr_path)
|
||||
.map_err(|err| format!("Git-Fehlerausgabe konnte nicht gelesen werden: {err}"))?;
|
||||
let _ = std::fs::remove_file(&stdout_path);
|
||||
let _ = std::fs::remove_file(&stderr_path);
|
||||
|
||||
if status.success() {
|
||||
return Ok(stdout);
|
||||
}
|
||||
|
||||
let stderr_text = String::from_utf8_lossy(&stderr);
|
||||
let stdout_text = String::from_utf8_lossy(&stdout);
|
||||
let details = if !stderr_text.trim().is_empty() {
|
||||
stderr_text.trim()
|
||||
} else if !stdout_text.trim().is_empty() {
|
||||
stdout_text.trim()
|
||||
} else {
|
||||
"unbekannter Fehler"
|
||||
};
|
||||
|
||||
Err(format!("{context}: {details}"))
|
||||
}
|
||||
|
||||
fn run_git_at<I, S>(path: &Path, args: I, context: &str) -> Result<Vec<u8>, String>
|
||||
where
|
||||
I: IntoIterator<Item = S>,
|
||||
@@ -1565,6 +2160,89 @@ mod tests {
|
||||
run_git_test(repo, ["commit", "-q", "-m", "init"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_code_introductions_finds_added_string() {
|
||||
let repo = init_temp_repo("search_added_string");
|
||||
fs::create_dir_all(repo.path.join("src")).expect("src directory should be created");
|
||||
fs::write(repo.path.join("src/app.ts"), "export const existing = 1;\n")
|
||||
.expect("initial source file should be written");
|
||||
run_git_test(&repo.path, ["add", "src/app.ts"]);
|
||||
run_git_test(&repo.path, ["commit", "-q", "-m", "initial source"]);
|
||||
|
||||
fs::write(
|
||||
repo.path.join("src/app.ts"),
|
||||
"export const existing = 1;\n\nexport function renderWidget() {\n return \"needle-token\";\n}\n",
|
||||
)
|
||||
.expect("updated source file should be written");
|
||||
run_git_test(&repo.path, ["add", "src/app.ts"]);
|
||||
run_git_test(&repo.path, ["commit", "-q", "-m", "add render widget"]);
|
||||
|
||||
let hits =
|
||||
search_code_introductions_core(&repo.path, "renderWidget".to_string(), false, 20, None)
|
||||
.expect("search should succeed");
|
||||
|
||||
assert_eq!(hits.len(), 1);
|
||||
assert_eq!(hits[0].file, "src/app.ts");
|
||||
assert_eq!(hits[0].summary, "add render widget");
|
||||
assert_eq!(hits[0].line_number, Some(3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_code_introductions_finds_multiline_function_block() {
|
||||
let repo = init_temp_repo("search_multiline_function");
|
||||
fs::write(repo.path.join("module.ts"), "export const ready = true;\n")
|
||||
.expect("initial module should be written");
|
||||
run_git_test(&repo.path, ["add", "module.ts"]);
|
||||
run_git_test(&repo.path, ["commit", "-q", "-m", "initial module"]);
|
||||
|
||||
let function_body = "export function parseThing() {\n return \"needle-token\";\n}";
|
||||
fs::write(
|
||||
repo.path.join("module.ts"),
|
||||
format!("export const ready = true;\n\n{function_body}\n"),
|
||||
)
|
||||
.expect("updated module should be written");
|
||||
run_git_test(&repo.path, ["add", "module.ts"]);
|
||||
run_git_test(&repo.path, ["commit", "-q", "-m", "add parser"]);
|
||||
|
||||
let hits =
|
||||
search_code_introductions_core(&repo.path, function_body.to_string(), false, 20, None)
|
||||
.expect("search should succeed");
|
||||
|
||||
assert_eq!(hits.len(), 1);
|
||||
assert_eq!(hits[0].file, "module.ts");
|
||||
assert_eq!(hits[0].summary, "add parser");
|
||||
assert_eq!(hits[0].line_number, Some(3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_code_introductions_can_be_cancelled() {
|
||||
let repo = init_temp_repo("search_cancelled");
|
||||
fs::write(
|
||||
repo.path.join("module.ts"),
|
||||
"export const value = \"needle\";\n",
|
||||
)
|
||||
.expect("module should be written");
|
||||
run_git_test(&repo.path, ["add", "module.ts"]);
|
||||
run_git_test(&repo.path, ["commit", "-q", "-m", "add module"]);
|
||||
|
||||
let state = SearchCancellationState::default();
|
||||
state
|
||||
.cancel("test-search")
|
||||
.expect("cancel flag should be set");
|
||||
let result = search_code_introductions_core(
|
||||
&repo.path,
|
||||
"needle".to_string(),
|
||||
false,
|
||||
20,
|
||||
Some(&SearchCancellation {
|
||||
state: state.clone(),
|
||||
search_id: "test-search".to_string(),
|
||||
}),
|
||||
);
|
||||
|
||||
assert_eq!(result.unwrap_err(), SEARCH_CANCELLED_MESSAGE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_branch_tracking_and_file_states() {
|
||||
let raw = b"## main...origin/main [ahead 2, behind 1]\0 M changed.txt\0D deleted.txt\0?? new.txt\0";
|
||||
|
||||
@@ -3,14 +3,17 @@
|
||||
mod git;
|
||||
|
||||
use git::{
|
||||
checkout_branch, commit, compare_commits, diff_file_against_working_tree, get_status,
|
||||
list_branches, list_commits, list_file_history, list_repository_files, merge_branch,
|
||||
open_repository, pull, push, read_conflict, resolve_conflict, resolve_conflict_side,
|
||||
restore_file_from_commit, restore_files, restore_to_commit, stage_files, unstage_files,
|
||||
cancel_code_search, checkout_branch, commit, compare_commits, diff_file_against_working_tree,
|
||||
get_status, list_branches, list_commits, list_file_history, list_repository_files,
|
||||
merge_branch, open_repository, pull, push, read_conflict, resolve_conflict,
|
||||
resolve_conflict_side, restore_file_from_commit, restore_files, restore_to_commit,
|
||||
search_code_introductions, stage_files, unstage_files, SearchCancellationState,
|
||||
};
|
||||
|
||||
fn main() {
|
||||
tauri::Builder::default()
|
||||
.manage(SearchCancellationState::default())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
open_repository,
|
||||
get_status,
|
||||
@@ -30,6 +33,8 @@ fn main() {
|
||||
list_file_history,
|
||||
compare_commits,
|
||||
diff_file_against_working_tree,
|
||||
search_code_introductions,
|
||||
cancel_code_search,
|
||||
read_conflict,
|
||||
resolve_conflict,
|
||||
resolve_conflict_side
|
||||
|
||||
Reference in New Issue
Block a user