add new Context Menu for file Explorer
fix blocking UI when select the file in Status also select in the file History
This commit is contained in:
+162
-24
@@ -214,6 +214,22 @@ pub fn open_repo_in_explorer(path: String) -> Result<(), String> {
|
||||
open_path_in_file_manager(&repo)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn open_repository_file(path: String, file: String) -> Result<(), String> {
|
||||
let repo = resolve_repo(&path)?;
|
||||
validate_files(std::slice::from_ref(&file))?;
|
||||
let file_path = resolve_repo_child_path(&repo, &file)?;
|
||||
|
||||
if !file_path.exists() {
|
||||
return Err(format!("Datei '{file}' existiert im Working Tree nicht."));
|
||||
}
|
||||
if !file_path.is_file() {
|
||||
return Err(format!("'{file}' ist keine Datei."));
|
||||
}
|
||||
|
||||
reveal_path_in_file_manager(&file_path)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct RepositoryBundle {
|
||||
pub status: GitStatus,
|
||||
@@ -786,15 +802,60 @@ pub fn list_repository_files(path: String) -> Result<Vec<GitRepositoryFile>, Str
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn list_file_history(
|
||||
pub async fn list_file_history(
|
||||
path: String,
|
||||
file: String,
|
||||
limit: Option<u32>,
|
||||
request_id: Option<String>,
|
||||
state: tauri::State<'_, SearchCancellationState>,
|
||||
) -> Result<Vec<GitCommit>, String> {
|
||||
let repo = resolve_repo(&path)?;
|
||||
let state = state.inner().clone();
|
||||
|
||||
tauri::async_runtime::spawn_blocking(move || {
|
||||
let repo = resolve_repo(&path)?;
|
||||
let request_id = request_id
|
||||
.map(|id| id.trim().to_string())
|
||||
.filter(|id| !id.is_empty());
|
||||
let cancellation = request_id.as_ref().map(|request_id| SearchCancellation {
|
||||
state: state.clone(),
|
||||
search_id: request_id.clone(),
|
||||
});
|
||||
|
||||
let result = list_file_history_core(&repo, file, limit, cancellation.as_ref());
|
||||
|
||||
if let Some(request_id) = request_id.as_deref() {
|
||||
let _ = state.clear(request_id);
|
||||
}
|
||||
|
||||
result
|
||||
})
|
||||
.await
|
||||
.map_err(|err| format!("Dateihistorie konnte nicht geladen werden: {err}"))?
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn cancel_file_history(
|
||||
request_id: String,
|
||||
state: tauri::State<'_, SearchCancellationState>,
|
||||
) -> Result<(), String> {
|
||||
let request_id = request_id.trim();
|
||||
if request_id.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
state.cancel(request_id)
|
||||
}
|
||||
|
||||
fn list_file_history_core(
|
||||
repo: &Path,
|
||||
file: String,
|
||||
limit: Option<u32>,
|
||||
cancellation: Option<&SearchCancellation>,
|
||||
) -> Result<Vec<GitCommit>, String> {
|
||||
check_search_cancelled(cancellation)?;
|
||||
validate_files(std::slice::from_ref(&file))?;
|
||||
|
||||
if verify_commit(&repo, "HEAD").is_err() {
|
||||
if verify_commit(repo, "HEAD").is_err() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
@@ -806,13 +867,14 @@ pub fn list_file_history(
|
||||
OsString::from("-n"),
|
||||
OsString::from(limit),
|
||||
];
|
||||
if !is_repository_folder_path(&repo, &file)? {
|
||||
if !is_repository_folder_path(repo, &file)? {
|
||||
args.push(OsString::from("--follow"));
|
||||
}
|
||||
args.extend([OsString::from("--"), OsString::from(file)]);
|
||||
let output = run_git(&repo, args)?;
|
||||
let output = run_git_cancellable(repo, args, cancellation, "Git-Dateihistorie fehlgeschlagen")?;
|
||||
check_search_cancelled(cancellation)?;
|
||||
|
||||
parse_commit_log(&repo, &output)
|
||||
parse_commit_log(repo, &output)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -1416,6 +1478,69 @@ fn open_path_in_file_manager(path: &Path) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn resolve_repo_child_path(repo: &Path, child: &str) -> Result<PathBuf, String> {
|
||||
let child_path = Path::new(child);
|
||||
if child_path.is_absolute()
|
||||
|| child_path.components().any(|component| {
|
||||
matches!(
|
||||
component,
|
||||
std::path::Component::ParentDir
|
||||
| std::path::Component::RootDir
|
||||
| std::path::Component::Prefix(_)
|
||||
)
|
||||
})
|
||||
{
|
||||
return Err("Dateipfad muss innerhalb des Repositorys liegen.".to_string());
|
||||
}
|
||||
|
||||
let candidate = repo.join(child_path);
|
||||
let repo = repo
|
||||
.canonicalize()
|
||||
.map_err(|err| format!("Repository-Pfad konnte nicht aufgeloest werden: {err}"))?;
|
||||
let candidate = candidate
|
||||
.canonicalize()
|
||||
.map_err(|err| format!("Dateipfad konnte nicht aufgeloest werden: {err}"))?;
|
||||
|
||||
if !candidate.starts_with(&repo) {
|
||||
return Err("Dateipfad liegt ausserhalb des Repositorys.".to_string());
|
||||
}
|
||||
|
||||
Ok(candidate)
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn reveal_path_in_file_manager(path: &Path) -> Result<(), String> {
|
||||
let native_path = path.to_string_lossy().replace('/', "\\");
|
||||
let mut command = Command::new("explorer.exe");
|
||||
command.arg(format!("/select,{native_path}"));
|
||||
command.creation_flags(CREATE_NO_WINDOW);
|
||||
command
|
||||
.spawn()
|
||||
.map_err(|err| format!("Explorer konnte nicht gestartet werden: {err}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn reveal_path_in_file_manager(path: &Path) -> Result<(), String> {
|
||||
Command::new("open")
|
||||
.arg("-R")
|
||||
.arg(path)
|
||||
.spawn()
|
||||
.map_err(|err| format!("Finder konnte nicht gestartet werden: {err}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(all(unix, not(target_os = "macos")))]
|
||||
fn reveal_path_in_file_manager(path: &Path) -> Result<(), String> {
|
||||
// No universal "select this file" flag across Linux file managers; open its folder instead.
|
||||
let target = path.parent().unwrap_or(path);
|
||||
Command::new("xdg-open")
|
||||
.arg(target)
|
||||
.spawn()
|
||||
.map_err(|err| format!("Dateimanager konnte nicht gestartet werden: {err}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn verify_commit(repo: &Path, commit: &str) -> Result<String, String> {
|
||||
let commit = commit.trim();
|
||||
if commit.is_empty() {
|
||||
@@ -1518,7 +1643,9 @@ fn detect_worktree_renames(repo: &Path, files: &mut Vec<GitFileStatus>) {
|
||||
|
||||
for (old_path, new_path) in renames {
|
||||
files.retain(|f| {
|
||||
!(f.path == old_path && f.staged.is_none() && f.unstaged == Some(FileStatusKind::Deleted))
|
||||
!(f.path == old_path
|
||||
&& f.staged.is_none()
|
||||
&& f.unstaged == Some(FileStatusKind::Deleted))
|
||||
&& !(f.path == new_path
|
||||
&& f.staged.is_none()
|
||||
&& f.unstaged == Some(FileStatusKind::Untracked))
|
||||
@@ -1573,11 +1700,7 @@ fn worktree_blob_hashes(repo: &Path, paths: &[String]) -> Result<Vec<(String, St
|
||||
.filter(|line| !line.is_empty())
|
||||
.collect();
|
||||
|
||||
Ok(paths
|
||||
.iter()
|
||||
.cloned()
|
||||
.zip(hashes)
|
||||
.collect())
|
||||
Ok(paths.iter().cloned().zip(hashes).collect())
|
||||
}
|
||||
|
||||
fn repository_files(repo: &Path) -> Result<Vec<GitRepositoryFile>, String> {
|
||||
@@ -4052,12 +4175,8 @@ mod tests {
|
||||
run_git_test(&repo.path, ["add", "other.txt"]);
|
||||
run_git_test(&repo.path, ["commit", "-q", "-m", "touch other"]);
|
||||
|
||||
let commits = list_file_history(
|
||||
repo.path.to_string_lossy().to_string(),
|
||||
"old.txt".to_string(),
|
||||
Some(10),
|
||||
)
|
||||
.unwrap();
|
||||
let commits =
|
||||
list_file_history_core(&repo.path, "old.txt".to_string(), Some(10), None).unwrap();
|
||||
|
||||
assert_eq!(commits.len(), 2);
|
||||
assert_eq!(commits[0].summary, "touch selected");
|
||||
@@ -4080,15 +4199,34 @@ mod tests {
|
||||
run_git_test(&repo.path, ["add", "."]);
|
||||
run_git_test(&repo.path, ["commit", "-q", "-m", "src update"]);
|
||||
|
||||
let commits = list_file_history(
|
||||
repo.path.to_string_lossy().to_string(),
|
||||
"src".to_string(),
|
||||
Some(10),
|
||||
)
|
||||
.unwrap();
|
||||
let commits =
|
||||
list_file_history_core(&repo.path, "src".to_string(), Some(10), None).unwrap();
|
||||
|
||||
assert_eq!(commits.len(), 2);
|
||||
assert_eq!(commits[0].summary, "src update");
|
||||
assert_eq!(commits[1].summary, "src initial");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_file_history_can_be_cancelled() {
|
||||
let repo = init_temp_repo("file_history_cancelled");
|
||||
commit_initial_file(&repo.path);
|
||||
|
||||
let state = SearchCancellationState::default();
|
||||
state
|
||||
.cancel("file-history-test")
|
||||
.expect("cancel flag should be set");
|
||||
|
||||
let result = list_file_history_core(
|
||||
&repo.path,
|
||||
"old.txt".to_string(),
|
||||
Some(10),
|
||||
Some(&SearchCancellation {
|
||||
state,
|
||||
search_id: "file-history-test".to_string(),
|
||||
}),
|
||||
);
|
||||
|
||||
assert_eq!(result.unwrap_err(), SEARCH_CANCELLED_MESSAGE);
|
||||
}
|
||||
}
|
||||
|
||||
+10
-7
@@ -3,13 +3,14 @@
|
||||
mod git;
|
||||
|
||||
use git::{
|
||||
apply_file_patch, cancel_code_search, checkout_branch, commit, compare_commits,
|
||||
compare_file_to_head, compare_file_to_parent, create_branch, cred_delete, cred_load, cred_save,
|
||||
delete_branch, diff_file_against_working_tree, get_file_patch, get_remote_url, get_status,
|
||||
list_branches, list_commits, list_file_history, list_repository_files, merge_branch,
|
||||
open_repo_in_explorer, open_repository, open_repository_bundle, pull, push, read_conflict,
|
||||
rename_branch, resolve_conflict, resolve_conflict_side, restore_file_from_commit,
|
||||
restore_files, restore_to_commit, search_code_introductions, stage_files, unstage_files,
|
||||
apply_file_patch, cancel_code_search, cancel_file_history, checkout_branch, commit,
|
||||
compare_commits, compare_file_to_head, compare_file_to_parent, create_branch, cred_delete,
|
||||
cred_load, cred_save, delete_branch, diff_file_against_working_tree, get_file_patch,
|
||||
get_remote_url, get_status, list_branches, list_commits, list_file_history,
|
||||
list_repository_files, merge_branch, open_repo_in_explorer, open_repository,
|
||||
open_repository_bundle, open_repository_file, pull, push, read_conflict, rename_branch,
|
||||
resolve_conflict, resolve_conflict_side, restore_file_from_commit, restore_files,
|
||||
restore_to_commit, search_code_introductions, stage_files, unstage_files,
|
||||
SearchCancellationState,
|
||||
};
|
||||
|
||||
@@ -21,6 +22,7 @@ fn main() {
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
open_repository,
|
||||
open_repo_in_explorer,
|
||||
open_repository_file,
|
||||
get_status,
|
||||
list_branches,
|
||||
checkout_branch,
|
||||
@@ -42,6 +44,7 @@ fn main() {
|
||||
list_repository_files,
|
||||
open_repository_bundle,
|
||||
list_file_history,
|
||||
cancel_file_history,
|
||||
compare_commits,
|
||||
compare_file_to_head,
|
||||
compare_file_to_parent,
|
||||
|
||||
Reference in New Issue
Block a user