From 256d722a34f2176c6c86df4ada82d8db03163110 Mon Sep 17 00:00:00 2001 From: HMasataka Date: Sat, 7 Mar 2026 16:36:39 +0900 Subject: [PATCH 1/9] =?UTF-8?q?refactor(state):=20AppState=E3=82=92?= =?UTF-8?q?=E3=83=9E=E3=83=AB=E3=83=81=E3=83=AA=E3=83=9D=E3=82=B8=E3=83=88?= =?UTF-8?q?=E3=83=AA=E5=AF=BE=E5=BF=9C=E3=81=AEHashMap=E6=A7=8B=E9=80=A0?= =?UTF-8?q?=E3=81=AB=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 単一リポジトリ前提のMutex>>から HashMapベースのマルチタブ管理に移行。 with_repoヘルパー・タブ管理コマンド・watcherのタブ対応を追加。 Co-Authored-By: Claude Opus 4.6 --- src-tauri/src/commands/mod.rs | 18 +++++++ src-tauri/src/commands/repo.rs | 71 +++++++++++++------------- src-tauri/src/commands/tab.rs | 92 ++++++++++++++++++++++++++++++++++ src-tauri/src/state.rs | 25 ++++++++- src-tauri/src/watcher.rs | 4 +- 5 files changed, 173 insertions(+), 37 deletions(-) create mode 100644 src-tauri/src/commands/tab.rs diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index 45ea82c..d94ae22 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -16,5 +16,23 @@ pub mod revert; pub mod search; pub mod stash; pub mod submodule; +pub mod tab; pub mod tag; pub mod worktree; + +use crate::git::backend::GitBackend; +use crate::state::AppState; + +pub fn with_repo(state: &AppState, tab_id: &str, f: F) -> Result +where + F: FnOnce(&dyn GitBackend) -> Result, +{ + let tabs = state + .tabs + .lock() + .map_err(|e| format!("Lock poisoned: {e}"))?; + let ctx = tabs + .get(tab_id) + .ok_or("No repository opened for this tab")?; + f(ctx.backend.as_ref()) +} diff --git a/src-tauri/src/commands/repo.rs b/src-tauri/src/commands/repo.rs index 90d7ce2..02634f7 100644 --- a/src-tauri/src/commands/repo.rs +++ b/src-tauri/src/commands/repo.rs @@ -1,57 +1,57 @@ use tauri::{AppHandle, Emitter, State}; use crate::config::{self, RecentRepo}; -use crate::git::backend::GitBackend; use crate::git::dispatcher::GitDispatcher; -use crate::state::AppState; +use crate::state::{self, AppState, RepoContext}; use crate::watcher; -fn repo_name_from_path(path: &str) -> String { - std::path::Path::new(path) - .file_name() - .map(|n| n.to_string_lossy().to_string()) - .unwrap_or_else(|| path.to_string()) -} - -fn setup_repo_after_open( - backend: Box, +pub(crate) fn setup_repo_after_open( + backend: Box, app_handle: &AppHandle, state: &State<'_, AppState>, path: &str, + tab_id: &str, ) -> Result<(), String> { let watch_path = backend.workdir().to_path_buf(); + let name = state::repo_name_from_path(path); + + let watcher_box = match watcher::start_watcher(app_handle.clone(), &watch_path, tab_id) { + Ok(w) => Some(w), + Err(e) => { + log::error!("Failed to start watcher: {}", e); + None + } + }; + + let mut cfg = config::load_config().map_err(|e| e.to_string())?; + cfg.last_opened_repo = Some(path.to_string()); + config::add_recent_repo(&mut cfg, path, &name); + + let ctx = RepoContext { + backend, + watcher: watcher_box, + path: path.to_string(), + name, + }; { - let mut repo_lock = state - .repo + let mut tabs = state + .tabs .lock() .map_err(|e| format!("Lock poisoned: {e}"))?; - *repo_lock = Some(backend); + tabs.insert(tab_id.to_string(), ctx); } { - let mut watcher_lock = state - .watcher + let mut active = state + .active_tab .lock() .map_err(|e| format!("Lock poisoned: {e}"))?; - *watcher_lock = None; - - match watcher::start_watcher(app_handle.clone(), &watch_path) { - Ok(w) => { - *watcher_lock = Some(w); - } - Err(e) => { - log::error!("Failed to start watcher: {}", e); - } - } + *active = Some(tab_id.to_string()); } - - let mut cfg = config::load_config().map_err(|e| e.to_string())?; - cfg.last_opened_repo = Some(path.to_string()); - config::add_recent_repo(&mut cfg, path, &repo_name_from_path(path)); config::save_config(&cfg).map_err(|e| e.to_string())?; - let _ = app_handle.emit("repo:changed", ()); + let _ = app_handle.emit("repo:changed", tab_id.to_string()); Ok(()) } @@ -59,17 +59,19 @@ fn setup_repo_after_open( #[tauri::command] pub fn open_repository( path: String, + tab_id: String, app_handle: AppHandle, state: State<'_, AppState>, ) -> Result<(), String> { let backend = GitDispatcher::open_default(&path).map_err(|e| e.to_string())?; - setup_repo_after_open(backend, &app_handle, &state, &path) + setup_repo_after_open(backend, &app_handle, &state, &path, &tab_id) } #[tauri::command] pub fn init_repository( path: String, gitignore_template: Option, + tab_id: String, app_handle: AppHandle, state: State<'_, AppState>, ) -> Result<(), String> { @@ -84,7 +86,7 @@ pub fn init_repository( } } - setup_repo_after_open(backend, &app_handle, &state, &path) + setup_repo_after_open(backend, &app_handle, &state, &path, &tab_id) } #[tauri::command] @@ -105,9 +107,10 @@ pub fn remove_recent_repo(path: String) -> Result<(), String> { pub fn clone_repository( url: String, path: String, + tab_id: String, app_handle: AppHandle, state: State<'_, AppState>, ) -> Result<(), String> { let backend = GitDispatcher::clone_repo(&url, &path).map_err(|e| e.to_string())?; - setup_repo_after_open(backend, &app_handle, &state, &path) + setup_repo_after_open(backend, &app_handle, &state, &path, &tab_id) } diff --git a/src-tauri/src/commands/tab.rs b/src-tauri/src/commands/tab.rs new file mode 100644 index 0000000..dd37f5f --- /dev/null +++ b/src-tauri/src/commands/tab.rs @@ -0,0 +1,92 @@ +use serde::Serialize; +use tauri::{AppHandle, State}; + +use crate::git::dispatcher::GitDispatcher; +use crate::state::{AppState, TabId}; + +#[derive(Debug, Serialize)] +pub struct TabInfo { + pub id: String, + pub name: String, + pub path: String, +} + +#[tauri::command] +pub fn open_tab( + path: String, + tab_id: String, + app_handle: AppHandle, + state: State<'_, AppState>, +) -> Result<(), String> { + let backend = GitDispatcher::open_default(&path).map_err(|e| e.to_string())?; + super::repo::setup_repo_after_open(backend, &app_handle, &state, &path, &tab_id) +} + +#[tauri::command] +pub fn close_tab(tab_id: String, state: State<'_, AppState>) -> Result<(), String> { + let mut tabs = state + .tabs + .lock() + .map_err(|e| format!("Lock poisoned: {e}"))?; + tabs.remove(&tab_id); + + let mut active = state + .active_tab + .lock() + .map_err(|e| format!("Lock poisoned: {e}"))?; + if active.as_deref() == Some(&tab_id) { + *active = tabs.keys().next().cloned(); + } + + Ok(()) +} + +#[tauri::command] +pub fn set_active_tab(tab_id: String, state: State<'_, AppState>) -> Result<(), String> { + let tabs = state + .tabs + .lock() + .map_err(|e| format!("Lock poisoned: {e}"))?; + if !tabs.contains_key(&tab_id) { + return Err("Tab not found".to_string()); + } + drop(tabs); + + let mut active = state + .active_tab + .lock() + .map_err(|e| format!("Lock poisoned: {e}"))?; + *active = Some(tab_id); + + Ok(()) +} + +#[tauri::command] +pub fn list_tabs(state: State<'_, AppState>) -> Result, String> { + let tabs = state + .tabs + .lock() + .map_err(|e| format!("Lock poisoned: {e}"))?; + let mut result: Vec = tabs + .iter() + .map(|(id, ctx)| TabInfo { + id: id.clone(), + name: ctx.name.clone(), + path: ctx.path.clone(), + }) + .collect(); + + // Sort so active tab context is consistent + result.sort_by(|a, b| a.id.cmp(&b.id)); + + Ok(result) +} + +#[tauri::command] +pub fn get_active_tab(state: State<'_, AppState>) -> Result, String> { + let active = state + .active_tab + .lock() + .map_err(|e| format!("Lock poisoned: {e}"))?; + Ok(active.clone()) +} diff --git a/src-tauri/src/state.rs b/src-tauri/src/state.rs index a1f4b95..3ab9062 100644 --- a/src-tauri/src/state.rs +++ b/src-tauri/src/state.rs @@ -1,8 +1,29 @@ +use std::collections::HashMap; +use std::path::Path; use std::sync::Mutex; use crate::git::backend::GitBackend; +pub const DEFAULT_TAB_ID: &str = "default"; + +pub type TabId = String; + +pub struct RepoContext { + pub backend: Box, + pub watcher: Option>, + pub path: String, + pub name: String, +} + pub struct AppState { - pub repo: Mutex>>, - pub watcher: Mutex>>, + pub tabs: Mutex>, + pub active_tab: Mutex>, + pub auto_fetch_handle: Mutex>>, +} + +pub fn repo_name_from_path(path: &str) -> String { + Path::new(path) + .file_name() + .map(|n| n.to_string_lossy().to_string()) + .unwrap_or_else(|| path.to_string()) } diff --git a/src-tauri/src/watcher.rs b/src-tauri/src/watcher.rs index 5a63c5f..eaf154c 100644 --- a/src-tauri/src/watcher.rs +++ b/src-tauri/src/watcher.rs @@ -7,14 +7,16 @@ use tauri::{AppHandle, Emitter}; pub fn start_watcher( app_handle: AppHandle, repo_path: &Path, + tab_id: &str, ) -> Result, Box> { let handle = app_handle.clone(); + let tab_id_owned = tab_id.to_string(); let mut debouncer = new_debouncer( Duration::from_millis(500), move |res: Result, notify::Error>| { if res.is_ok() { - let _ = handle.emit("repo:changed", ()); + let _ = handle.emit("repo:changed", &tab_id_owned); } }, )?; From 455d959ed47f0fb46606d497e9f021f178722e4e Mon Sep 17 00:00:00 2001 From: HMasataka Date: Sat, 7 Mar 2026 16:36:52 +0900 Subject: [PATCH 2/9] =?UTF-8?q?refactor(commands):=20=E5=85=A8Tauri?= =?UTF-8?q?=E3=82=B3=E3=83=9E=E3=83=B3=E3=83=89=E3=81=ABtab=5Fid=E3=83=91?= =?UTF-8?q?=E3=83=A9=E3=83=A1=E3=83=BC=E3=82=BF=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit with_repoヘルパー経由でタブ単位のリポジトリアクセスに統一。 git/branch/remote/history/stash/tag/conflict/rebase等の全コマンドを対応。 lib.rsに新コマンド登録とAppState初期化を更新。 Co-Authored-By: Claude Opus 4.6 --- src-tauri/src/commands/ai.rs | 100 ++++--------- src-tauri/src/commands/branch.rs | 102 +++++++------ src-tauri/src/commands/cherry_pick.rs | 53 +++---- src-tauri/src/commands/conflict.rs | 95 ++++++------ src-tauri/src/commands/git.rs | 207 +++++++++++--------------- src-tauri/src/commands/gitconfig.rs | 67 ++++----- src-tauri/src/commands/history.rs | 72 ++++----- src-tauri/src/commands/hosting.rs | 51 ++++--- src-tauri/src/commands/rebase.rs | 105 ++++++------- src-tauri/src/commands/remote.rs | 96 ++++++------ src-tauri/src/commands/reset.rs | 41 +++-- src-tauri/src/commands/revert.rs | 44 +++--- src-tauri/src/commands/search.rs | 39 +++-- src-tauri/src/commands/stash.rs | 79 +++++----- src-tauri/src/commands/submodule.rs | 76 +++++----- src-tauri/src/commands/tag.rs | 52 +++---- src-tauri/src/commands/worktree.rs | 44 +++--- src-tauri/src/lib.rs | 73 ++++++--- 18 files changed, 648 insertions(+), 748 deletions(-) diff --git a/src-tauri/src/commands/ai.rs b/src-tauri/src/commands/ai.rs index e84d755..67fcadf 100644 --- a/src-tauri/src/commands/ai.rs +++ b/src-tauri/src/commands/ai.rs @@ -4,8 +4,9 @@ use crate::ai::adapter::LlmAdapter; use crate::ai::detector; use crate::ai::types::{ AiConfig, CliAdapterInfo, CommitMessageStyle, ConflictSuggestion, GenerateRequest, - GenerateResult, Language, PrDescription, ReviewResult, + GenerateResult, Language, ReviewResult, }; +use crate::commands::with_repo; use crate::config; use crate::git::types::FileDiff; use crate::state::AppState; @@ -54,6 +55,7 @@ pub fn detect_cli_adapters() -> Result, String> { #[tauri::command] pub fn generate_commit_message( + tab_id: String, format: String, language: String, state: State<'_, AppState>, @@ -63,21 +65,16 @@ pub fn generate_commit_message( let lang: Language = serde_json::from_value(serde_json::Value::String(language)) .map_err(|e| format!("Invalid language: {e}"))?; - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - - let diff_options = crate::git::types::DiffOptions { - staged: true, - ..Default::default() - }; - let diffs = backend - .diff(None, &diff_options) - .map_err(|e| e.to_string())?; - - let diff_text = format_diff_text(&diffs); + let diff_text = with_repo(&state, &tab_id, |backend| { + let diff_options = crate::git::types::DiffOptions { + staged: true, + ..Default::default() + }; + let diffs = backend + .diff(None, &diff_options) + .map_err(|e| e.to_string())?; + Ok(format_diff_text(&diffs)) + })?; if diff_text.trim().is_empty() { return Err("No staged changes to generate a commit message from".to_string()); @@ -95,28 +92,25 @@ pub fn generate_commit_message( } #[tauri::command] -pub fn review_diff(state: State<'_, AppState>) -> Result { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - - let staged_options = crate::git::types::DiffOptions { - staged: true, - ..Default::default() - }; - let unstaged_options = crate::git::types::DiffOptions { - staged: false, - ..Default::default() - }; - - let staged_diffs = backend - .diff(None, &staged_options) - .map_err(|e| e.to_string())?; - let unstaged_diffs = backend - .diff(None, &unstaged_options) - .map_err(|e| e.to_string())?; +pub fn review_diff(tab_id: String, state: State<'_, AppState>) -> Result { + let (staged_diffs, unstaged_diffs) = with_repo(&state, &tab_id, |backend| { + let staged_options = crate::git::types::DiffOptions { + staged: true, + ..Default::default() + }; + let unstaged_options = crate::git::types::DiffOptions { + staged: false, + ..Default::default() + }; + + let staged = backend + .diff(None, &staged_options) + .map_err(|e| e.to_string())?; + let unstaged = backend + .diff(None, &unstaged_options) + .map_err(|e| e.to_string())?; + Ok((staged, unstaged)) + })?; let mut all_diffs = staged_diffs; all_diffs.extend(unstaged_diffs); @@ -148,36 +142,6 @@ pub fn ai_resolve_conflict( crate::ai::conflict::parse_conflict_response(&raw).map_err(|e| e.to_string()) } -#[tauri::command] -pub fn generate_pr_description(state: State<'_, AppState>) -> Result { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - - let branch_name = backend.current_branch().map_err(|e| e.to_string())?; - - let diff_options = crate::git::types::DiffOptions { - staged: true, - ..Default::default() - }; - let diffs = backend - .diff(None, &diff_options) - .map_err(|e| e.to_string())?; - - let diff_text = format_diff_text(&diffs); - - if diff_text.trim().is_empty() { - return Err("No changes to generate a PR description from".to_string()); - } - - let adapter = get_adapter()?; - let prompt = crate::ai::pr::build_pr_description_prompt(&diff_text, &branch_name); - let raw = adapter.execute_prompt(&prompt).map_err(|e| e.to_string())?; - crate::ai::pr::parse_pr_description_response(&raw).map_err(|e| e.to_string()) -} - #[tauri::command] pub fn get_ai_config() -> Result { let cfg = config::load_config().map_err(|e| e.to_string())?; diff --git a/src-tauri/src/commands/branch.rs b/src-tauri/src/commands/branch.rs index 87eb1aa..4b8fb8d 100644 --- a/src-tauri/src/commands/branch.rs +++ b/src-tauri/src/commands/branch.rs @@ -1,92 +1,90 @@ use tauri::State; +use crate::commands::with_repo; use crate::git::types::{BranchInfo, CommitInfo, MergeOption, MergeResult}; use crate::state::AppState; #[tauri::command] -pub fn list_branches(state: State<'_, AppState>) -> Result, String> { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend.list_branches().map_err(|e| e.to_string()) +pub fn list_branches( + tab_id: String, + state: State<'_, AppState>, +) -> Result, String> { + with_repo(&state, &tab_id, |backend| { + backend.list_branches().map_err(|e| e.to_string()) + }) } #[tauri::command] -pub fn create_branch(name: String, state: State<'_, AppState>) -> Result<(), String> { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend.create_branch(&name).map_err(|e| e.to_string()) +pub fn create_branch( + tab_id: String, + name: String, + state: State<'_, AppState>, +) -> Result<(), String> { + with_repo(&state, &tab_id, |backend| { + backend.create_branch(&name).map_err(|e| e.to_string()) + }) } #[tauri::command] -pub fn checkout_branch(name: String, state: State<'_, AppState>) -> Result<(), String> { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend.checkout_branch(&name).map_err(|e| e.to_string()) +pub fn checkout_branch( + tab_id: String, + name: String, + state: State<'_, AppState>, +) -> Result<(), String> { + with_repo(&state, &tab_id, |backend| { + backend.checkout_branch(&name).map_err(|e| e.to_string()) + }) } #[tauri::command] -pub fn delete_branch(name: String, state: State<'_, AppState>) -> Result<(), String> { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend.delete_branch(&name).map_err(|e| e.to_string()) +pub fn delete_branch( + tab_id: String, + name: String, + state: State<'_, AppState>, +) -> Result<(), String> { + with_repo(&state, &tab_id, |backend| { + backend.delete_branch(&name).map_err(|e| e.to_string()) + }) } #[tauri::command] pub fn rename_branch( + tab_id: String, old_name: String, new_name: String, state: State<'_, AppState>, ) -> Result<(), String> { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend - .rename_branch(&old_name, &new_name) - .map_err(|e| e.to_string()) + with_repo(&state, &tab_id, |backend| { + backend + .rename_branch(&old_name, &new_name) + .map_err(|e| e.to_string()) + }) } #[tauri::command] pub fn merge_branch( + tab_id: String, branch_name: String, option: MergeOption, state: State<'_, AppState>, ) -> Result { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend - .merge_branch(&branch_name, option) - .map_err(|e| e.to_string()) + with_repo(&state, &tab_id, |backend| { + backend + .merge_branch(&branch_name, option) + .map_err(|e| e.to_string()) + }) } #[tauri::command] pub fn get_branch_commits( + tab_id: String, branch_name: String, limit: usize, state: State<'_, AppState>, ) -> Result, String> { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend - .get_branch_commits(&branch_name, limit) - .map_err(|e| e.to_string()) + with_repo(&state, &tab_id, |backend| { + backend + .get_branch_commits(&branch_name, limit) + .map_err(|e| e.to_string()) + }) } diff --git a/src-tauri/src/commands/cherry_pick.rs b/src-tauri/src/commands/cherry_pick.rs index 41fbf4d..cb5ce79 100644 --- a/src-tauri/src/commands/cherry_pick.rs +++ b/src-tauri/src/commands/cherry_pick.rs @@ -1,51 +1,44 @@ use tauri::State; +use crate::commands::with_repo; use crate::git::types::{CherryPickMode, CherryPickResult}; use crate::state::AppState; #[tauri::command] pub fn cherry_pick( + tab_id: String, oids: Vec, mode: CherryPickMode, state: State<'_, AppState>, ) -> Result { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - let oid_refs: Vec<&str> = oids.iter().map(|s| s.as_str()).collect(); - backend - .cherry_pick(&oid_refs, mode) - .map_err(|e| e.to_string()) + with_repo(&state, &tab_id, |backend| { + let oid_refs: Vec<&str> = oids.iter().map(|s| s.as_str()).collect(); + backend + .cherry_pick(&oid_refs, mode) + .map_err(|e| e.to_string()) + }) } #[tauri::command] -pub fn is_cherry_picking(state: State<'_, AppState>) -> Result { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend.is_cherry_picking().map_err(|e| e.to_string()) +pub fn is_cherry_picking(tab_id: String, state: State<'_, AppState>) -> Result { + with_repo(&state, &tab_id, |backend| { + backend.is_cherry_picking().map_err(|e| e.to_string()) + }) } #[tauri::command] -pub fn abort_cherry_pick(state: State<'_, AppState>) -> Result<(), String> { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend.abort_cherry_pick().map_err(|e| e.to_string()) +pub fn abort_cherry_pick(tab_id: String, state: State<'_, AppState>) -> Result<(), String> { + with_repo(&state, &tab_id, |backend| { + backend.abort_cherry_pick().map_err(|e| e.to_string()) + }) } #[tauri::command] -pub fn continue_cherry_pick(state: State<'_, AppState>) -> Result { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend.continue_cherry_pick().map_err(|e| e.to_string()) +pub fn continue_cherry_pick( + tab_id: String, + state: State<'_, AppState>, +) -> Result { + with_repo(&state, &tab_id, |backend| { + backend.continue_cherry_pick().map_err(|e| e.to_string()) + }) } diff --git a/src-tauri/src/commands/conflict.rs b/src-tauri/src/commands/conflict.rs index b220fd5..f2b2ec6 100644 --- a/src-tauri/src/commands/conflict.rs +++ b/src-tauri/src/commands/conflict.rs @@ -1,87 +1,80 @@ use tauri::State; +use crate::commands::with_repo; use crate::git::types::{CommitResult, ConflictFile, ConflictResolution}; use crate::state::AppState; #[tauri::command] -pub fn get_conflict_files(state: State<'_, AppState>) -> Result, String> { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend.get_conflict_files().map_err(|e| e.to_string()) +pub fn get_conflict_files( + tab_id: String, + state: State<'_, AppState>, +) -> Result, String> { + with_repo(&state, &tab_id, |backend| { + backend.get_conflict_files().map_err(|e| e.to_string()) + }) } #[tauri::command] pub fn resolve_conflict( + tab_id: String, path: String, resolution: ConflictResolution, state: State<'_, AppState>, ) -> Result<(), String> { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend - .resolve_conflict(&path, resolution) - .map_err(|e| e.to_string()) + with_repo(&state, &tab_id, |backend| { + backend + .resolve_conflict(&path, resolution) + .map_err(|e| e.to_string()) + }) } #[tauri::command] pub fn resolve_conflict_block( + tab_id: String, path: String, block_index: usize, resolution: ConflictResolution, state: State<'_, AppState>, ) -> Result<(), String> { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend - .resolve_conflict_block(&path, block_index, resolution) - .map_err(|e| e.to_string()) + with_repo(&state, &tab_id, |backend| { + backend + .resolve_conflict_block(&path, block_index, resolution) + .map_err(|e| e.to_string()) + }) } #[tauri::command] -pub fn mark_resolved(path: String, state: State<'_, AppState>) -> Result<(), String> { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend.mark_resolved(&path).map_err(|e| e.to_string()) +pub fn mark_resolved( + tab_id: String, + path: String, + state: State<'_, AppState>, +) -> Result<(), String> { + with_repo(&state, &tab_id, |backend| { + backend.mark_resolved(&path).map_err(|e| e.to_string()) + }) } #[tauri::command] -pub fn abort_merge(state: State<'_, AppState>) -> Result<(), String> { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend.abort_merge().map_err(|e| e.to_string()) +pub fn abort_merge(tab_id: String, state: State<'_, AppState>) -> Result<(), String> { + with_repo(&state, &tab_id, |backend| { + backend.abort_merge().map_err(|e| e.to_string()) + }) } #[tauri::command] -pub fn continue_merge(message: String, state: State<'_, AppState>) -> Result { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend.continue_merge(&message).map_err(|e| e.to_string()) +pub fn continue_merge( + tab_id: String, + message: String, + state: State<'_, AppState>, +) -> Result { + with_repo(&state, &tab_id, |backend| { + backend.continue_merge(&message).map_err(|e| e.to_string()) + }) } #[tauri::command] -pub fn is_merging(state: State<'_, AppState>) -> Result { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend.is_merging().map_err(|e| e.to_string()) +pub fn is_merging(tab_id: String, state: State<'_, AppState>) -> Result { + with_repo(&state, &tab_id, |backend| { + backend.is_merging().map_err(|e| e.to_string()) + }) } diff --git a/src-tauri/src/commands/git.rs b/src-tauri/src/commands/git.rs index 43f56e8..bbaba0b 100644 --- a/src-tauri/src/commands/git.rs +++ b/src-tauri/src/commands/git.rs @@ -2,211 +2,182 @@ use std::path::Path; use tauri::State; +use crate::commands::with_repo; use crate::git::types::{ CommitResult, DiffOptions, FileDiff, HunkIdentifier, LineRange, RepoStatus, }; use crate::state::AppState; #[tauri::command] -pub fn get_status(state: State<'_, AppState>) -> Result { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend.status().map_err(|e| e.to_string()) +pub fn get_status(tab_id: String, state: State<'_, AppState>) -> Result { + with_repo(&state, &tab_id, |backend| { + backend.status().map_err(|e| e.to_string()) + }) } #[tauri::command] pub fn get_diff( + tab_id: String, path: Option, staged: bool, state: State<'_, AppState>, ) -> Result, String> { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - let options = DiffOptions { - staged, - ..Default::default() - }; - let path_buf = path.map(std::path::PathBuf::from); - backend - .diff(path_buf.as_deref(), &options) - .map_err(|e| e.to_string()) + with_repo(&state, &tab_id, |backend| { + let options = DiffOptions { + staged, + ..Default::default() + }; + let path_buf = path.map(std::path::PathBuf::from); + backend + .diff(path_buf.as_deref(), &options) + .map_err(|e| e.to_string()) + }) } #[tauri::command] -pub fn stage_file(path: String, state: State<'_, AppState>) -> Result<(), String> { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend.stage(Path::new(&path)).map_err(|e| e.to_string()) +pub fn stage_file(tab_id: String, path: String, state: State<'_, AppState>) -> Result<(), String> { + with_repo(&state, &tab_id, |backend| { + backend.stage(Path::new(&path)).map_err(|e| e.to_string()) + }) } #[tauri::command] -pub fn unstage_file(path: String, state: State<'_, AppState>) -> Result<(), String> { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend.unstage(Path::new(&path)).map_err(|e| e.to_string()) +pub fn unstage_file( + tab_id: String, + path: String, + state: State<'_, AppState>, +) -> Result<(), String> { + with_repo(&state, &tab_id, |backend| { + backend.unstage(Path::new(&path)).map_err(|e| e.to_string()) + }) } #[tauri::command] -pub fn stage_all(state: State<'_, AppState>) -> Result<(), String> { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend.stage_all().map_err(|e| e.to_string()) +pub fn stage_all(tab_id: String, state: State<'_, AppState>) -> Result<(), String> { + with_repo(&state, &tab_id, |backend| { + backend.stage_all().map_err(|e| e.to_string()) + }) } #[tauri::command] -pub fn unstage_all(state: State<'_, AppState>) -> Result<(), String> { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend.unstage_all().map_err(|e| e.to_string()) +pub fn unstage_all(tab_id: String, state: State<'_, AppState>) -> Result<(), String> { + with_repo(&state, &tab_id, |backend| { + backend.unstage_all().map_err(|e| e.to_string()) + }) } #[tauri::command] pub fn commit( + tab_id: String, message: String, amend: bool, sign: bool, state: State<'_, AppState>, ) -> Result { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend - .commit(&message, amend, sign) - .map_err(|e| e.to_string()) + with_repo(&state, &tab_id, |backend| { + backend + .commit(&message, amend, sign) + .map_err(|e| e.to_string()) + }) } #[tauri::command] -pub fn get_current_branch(state: State<'_, AppState>) -> Result { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend.current_branch().map_err(|e| e.to_string()) +pub fn get_current_branch(tab_id: String, state: State<'_, AppState>) -> Result { + with_repo(&state, &tab_id, |backend| { + backend.current_branch().map_err(|e| e.to_string()) + }) } #[tauri::command] pub fn stage_hunk( + tab_id: String, path: String, hunk: HunkIdentifier, state: State<'_, AppState>, ) -> Result<(), String> { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend - .stage_hunk(Path::new(&path), &hunk) - .map_err(|e| e.to_string()) + with_repo(&state, &tab_id, |backend| { + backend + .stage_hunk(Path::new(&path), &hunk) + .map_err(|e| e.to_string()) + }) } #[tauri::command] pub fn unstage_hunk( + tab_id: String, path: String, hunk: HunkIdentifier, state: State<'_, AppState>, ) -> Result<(), String> { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend - .unstage_hunk(Path::new(&path), &hunk) - .map_err(|e| e.to_string()) + with_repo(&state, &tab_id, |backend| { + backend + .unstage_hunk(Path::new(&path), &hunk) + .map_err(|e| e.to_string()) + }) } #[tauri::command] pub fn discard_hunk( + tab_id: String, path: String, hunk: HunkIdentifier, state: State<'_, AppState>, ) -> Result<(), String> { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend - .discard_hunk(Path::new(&path), &hunk) - .map_err(|e| e.to_string()) + with_repo(&state, &tab_id, |backend| { + backend + .discard_hunk(Path::new(&path), &hunk) + .map_err(|e| e.to_string()) + }) } #[tauri::command] pub fn stage_lines( + tab_id: String, path: String, line_range: LineRange, state: State<'_, AppState>, ) -> Result<(), String> { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend - .stage_lines(Path::new(&path), &line_range) - .map_err(|e| e.to_string()) + with_repo(&state, &tab_id, |backend| { + backend + .stage_lines(Path::new(&path), &line_range) + .map_err(|e| e.to_string()) + }) } #[tauri::command] pub fn unstage_lines( + tab_id: String, path: String, line_range: LineRange, state: State<'_, AppState>, ) -> Result<(), String> { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend - .unstage_lines(Path::new(&path), &line_range) - .map_err(|e| e.to_string()) + with_repo(&state, &tab_id, |backend| { + backend + .unstage_lines(Path::new(&path), &line_range) + .map_err(|e| e.to_string()) + }) } #[tauri::command] pub fn discard_lines( + tab_id: String, path: String, line_range: LineRange, state: State<'_, AppState>, ) -> Result<(), String> { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend - .discard_lines(Path::new(&path), &line_range) - .map_err(|e| e.to_string()) + with_repo(&state, &tab_id, |backend| { + backend + .discard_lines(Path::new(&path), &line_range) + .map_err(|e| e.to_string()) + }) } #[tauri::command] -pub fn get_head_commit_message(state: State<'_, AppState>) -> Result { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend.get_head_commit_message().map_err(|e| e.to_string()) +pub fn get_head_commit_message( + tab_id: String, + state: State<'_, AppState>, +) -> Result { + with_repo(&state, &tab_id, |backend| { + backend.get_head_commit_message().map_err(|e| e.to_string()) + }) } diff --git a/src-tauri/src/commands/gitconfig.rs b/src-tauri/src/commands/gitconfig.rs index ff9efdd..2c950a5 100644 --- a/src-tauri/src/commands/gitconfig.rs +++ b/src-tauri/src/commands/gitconfig.rs @@ -1,81 +1,72 @@ use tauri::State; +use crate::commands::with_repo; use crate::git::types::{GitConfigEntry, GitConfigScope}; use crate::state::AppState; #[tauri::command] pub fn get_gitconfig_entries( + tab_id: String, scope: GitConfigScope, state: State<'_, AppState>, ) -> Result, String> { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend - .get_gitconfig_entries(scope) - .map_err(|e| e.to_string()) + with_repo(&state, &tab_id, |backend| { + backend + .get_gitconfig_entries(scope) + .map_err(|e| e.to_string()) + }) } #[tauri::command] pub fn get_gitconfig_value( + tab_id: String, scope: GitConfigScope, key: String, state: State<'_, AppState>, ) -> Result, String> { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend - .get_gitconfig_value(scope, &key) - .map_err(|e| e.to_string()) + with_repo(&state, &tab_id, |backend| { + backend + .get_gitconfig_value(scope, &key) + .map_err(|e| e.to_string()) + }) } #[tauri::command] pub fn set_gitconfig_value( + tab_id: String, scope: GitConfigScope, key: String, value: String, state: State<'_, AppState>, ) -> Result<(), String> { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend - .set_gitconfig_value(scope, &key, &value) - .map_err(|e| e.to_string()) + with_repo(&state, &tab_id, |backend| { + backend + .set_gitconfig_value(scope, &key, &value) + .map_err(|e| e.to_string()) + }) } #[tauri::command] pub fn unset_gitconfig_value( + tab_id: String, scope: GitConfigScope, key: String, state: State<'_, AppState>, ) -> Result<(), String> { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend - .unset_gitconfig_value(scope, &key) - .map_err(|e| e.to_string()) + with_repo(&state, &tab_id, |backend| { + backend + .unset_gitconfig_value(scope, &key) + .map_err(|e| e.to_string()) + }) } #[tauri::command] pub fn get_gitconfig_path( + tab_id: String, scope: GitConfigScope, state: State<'_, AppState>, ) -> Result { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend.get_gitconfig_path(scope).map_err(|e| e.to_string()) + with_repo(&state, &tab_id, |backend| { + backend.get_gitconfig_path(scope).map_err(|e| e.to_string()) + }) } diff --git a/src-tauri/src/commands/history.rs b/src-tauri/src/commands/history.rs index f4296b9..844f6c1 100644 --- a/src-tauri/src/commands/history.rs +++ b/src-tauri/src/commands/history.rs @@ -1,5 +1,6 @@ use tauri::State; +use crate::commands::with_repo; use crate::git::types::{ BlameResult, CommitDetail, CommitInfo, CommitLogResult, FileDiff, LogFilter, }; @@ -7,76 +8,69 @@ use crate::state::AppState; #[tauri::command] pub fn get_commit_log( + tab_id: String, filter: LogFilter, limit: usize, skip: usize, state: State<'_, AppState>, ) -> Result { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend - .get_commit_log(&filter, limit, skip) - .map_err(|e| e.to_string()) + with_repo(&state, &tab_id, |backend| { + backend + .get_commit_log(&filter, limit, skip) + .map_err(|e| e.to_string()) + }) } #[tauri::command] -pub fn get_commit_detail(oid: String, state: State<'_, AppState>) -> Result { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend.get_commit_detail(&oid).map_err(|e| e.to_string()) +pub fn get_commit_detail( + tab_id: String, + oid: String, + state: State<'_, AppState>, +) -> Result { + with_repo(&state, &tab_id, |backend| { + backend.get_commit_detail(&oid).map_err(|e| e.to_string()) + }) } #[tauri::command] pub fn get_commit_file_diff( + tab_id: String, oid: String, path: String, state: State<'_, AppState>, ) -> Result, String> { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend - .get_commit_file_diff(&oid, &path) - .map_err(|e| e.to_string()) + with_repo(&state, &tab_id, |backend| { + backend + .get_commit_file_diff(&oid, &path) + .map_err(|e| e.to_string()) + }) } #[tauri::command] pub fn get_blame( + tab_id: String, path: String, commit_oid: Option, state: State<'_, AppState>, ) -> Result { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend - .get_blame(&path, commit_oid.as_deref()) - .map_err(|e| e.to_string()) + with_repo(&state, &tab_id, |backend| { + backend + .get_blame(&path, commit_oid.as_deref()) + .map_err(|e| e.to_string()) + }) } #[tauri::command] pub fn get_file_history( + tab_id: String, path: String, limit: usize, skip: usize, state: State<'_, AppState>, ) -> Result, String> { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend - .get_file_history(&path, limit, skip) - .map_err(|e| e.to_string()) + with_repo(&state, &tab_id, |backend| { + backend + .get_file_history(&path, limit, skip) + .map_err(|e| e.to_string()) + }) } diff --git a/src-tauri/src/commands/hosting.rs b/src-tauri/src/commands/hosting.rs index 635dea6..72cb443 100644 --- a/src-tauri/src/commands/hosting.rs +++ b/src-tauri/src/commands/hosting.rs @@ -1,66 +1,69 @@ use tauri::State; use tauri_plugin_opener::OpenerExt; +use crate::commands::with_repo; use crate::hosting::detector; use crate::hosting::github; use crate::hosting::types::{HostingInfo, Issue, PrDetail, PullRequest}; use crate::state::AppState; -fn get_repo_path(state: &State<'_, AppState>) -> Result { - let guard = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let repo = guard.as_ref().ok_or("No repository opened")?; - Ok(repo.workdir().to_string_lossy().to_string()) +fn get_repo_path(state: &State<'_, AppState>, tab_id: &str) -> Result { + with_repo(state, tab_id, |backend| { + Ok(backend.workdir().to_string_lossy().to_string()) + }) } #[tauri::command] -pub fn detect_hosting_provider(state: State<'_, AppState>) -> Result { - let guard = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let repo = guard.as_ref().ok_or("No repository opened")?; - let remotes = repo.list_remotes().map_err(|e| e.to_string())?; - let remote = remotes.first().ok_or("No remotes found")?; - Ok(detector::detect_from_remote_url(&remote.url)) +pub fn detect_hosting_provider( + tab_id: String, + state: State<'_, AppState>, +) -> Result { + with_repo(&state, &tab_id, |backend| { + let remotes = backend.list_remotes().map_err(|e| e.to_string())?; + let remote = remotes.first().ok_or("No remotes found")?; + Ok(detector::detect_from_remote_url(&remote.url)) + }) } #[tauri::command] -pub fn list_pull_requests(state: State<'_, AppState>) -> Result, String> { - let repo_path = get_repo_path(&state)?; +pub fn list_pull_requests( + tab_id: String, + state: State<'_, AppState>, +) -> Result, String> { + let repo_path = get_repo_path(&state, &tab_id)?; github::list_pull_requests(&repo_path) } #[tauri::command] pub fn get_pull_request_detail( + tab_id: String, state: State<'_, AppState>, number: u64, ) -> Result { - let repo_path = get_repo_path(&state)?; + let repo_path = get_repo_path(&state, &tab_id)?; github::get_pull_request_detail(&repo_path, number) } #[tauri::command] -pub fn list_issues(state: State<'_, AppState>) -> Result, String> { - let repo_path = get_repo_path(&state)?; +pub fn list_issues(tab_id: String, state: State<'_, AppState>) -> Result, String> { + let repo_path = get_repo_path(&state, &tab_id)?; github::list_issues(&repo_path) } #[tauri::command] -pub fn get_default_branch(state: State<'_, AppState>) -> Result { - let repo_path = get_repo_path(&state)?; +pub fn get_default_branch(tab_id: String, state: State<'_, AppState>) -> Result { + let repo_path = get_repo_path(&state, &tab_id)?; github::get_default_branch(&repo_path) } #[tauri::command] pub fn create_pull_request_url( + tab_id: String, state: State<'_, AppState>, head: String, base: String, ) -> Result { - let repo_path = get_repo_path(&state)?; + let repo_path = get_repo_path(&state, &tab_id)?; github::create_pull_request_url(&repo_path, &head, &base) } diff --git a/src-tauri/src/commands/rebase.rs b/src-tauri/src/commands/rebase.rs index bfac481..9c912bd 100644 --- a/src-tauri/src/commands/rebase.rs +++ b/src-tauri/src/commands/rebase.rs @@ -1,101 +1,88 @@ use tauri::State; +use crate::commands::with_repo; use crate::git::types::{MergeBaseContent, RebaseResult, RebaseState, RebaseTodoEntry}; use crate::state::AppState; #[tauri::command] -pub fn rebase(onto: String, state: State<'_, AppState>) -> Result { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend.rebase(&onto).map_err(|e| e.to_string()) +pub fn rebase( + tab_id: String, + onto: String, + state: State<'_, AppState>, +) -> Result { + with_repo(&state, &tab_id, |backend| { + backend.rebase(&onto).map_err(|e| e.to_string()) + }) } #[tauri::command] pub fn interactive_rebase( + tab_id: String, onto: String, todo: Vec, state: State<'_, AppState>, ) -> Result { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend - .interactive_rebase(&onto, &todo) - .map_err(|e| e.to_string()) + with_repo(&state, &tab_id, |backend| { + backend + .interactive_rebase(&onto, &todo) + .map_err(|e| e.to_string()) + }) } #[tauri::command] -pub fn is_rebasing(state: State<'_, AppState>) -> Result { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend.is_rebasing().map_err(|e| e.to_string()) +pub fn is_rebasing(tab_id: String, state: State<'_, AppState>) -> Result { + with_repo(&state, &tab_id, |backend| { + backend.is_rebasing().map_err(|e| e.to_string()) + }) } #[tauri::command] -pub fn abort_rebase(state: State<'_, AppState>) -> Result<(), String> { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend.abort_rebase().map_err(|e| e.to_string()) +pub fn abort_rebase(tab_id: String, state: State<'_, AppState>) -> Result<(), String> { + with_repo(&state, &tab_id, |backend| { + backend.abort_rebase().map_err(|e| e.to_string()) + }) } #[tauri::command] -pub fn continue_rebase(state: State<'_, AppState>) -> Result { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend.continue_rebase().map_err(|e| e.to_string()) +pub fn continue_rebase(tab_id: String, state: State<'_, AppState>) -> Result { + with_repo(&state, &tab_id, |backend| { + backend.continue_rebase().map_err(|e| e.to_string()) + }) } #[tauri::command] -pub fn get_rebase_state(state: State<'_, AppState>) -> Result, String> { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend.get_rebase_state().map_err(|e| e.to_string()) +pub fn get_rebase_state( + tab_id: String, + state: State<'_, AppState>, +) -> Result, String> { + with_repo(&state, &tab_id, |backend| { + backend.get_rebase_state().map_err(|e| e.to_string()) + }) } #[tauri::command] pub fn get_rebase_todo( + tab_id: String, onto: String, limit: usize, state: State<'_, AppState>, ) -> Result, String> { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend - .get_rebase_todo(&onto, limit) - .map_err(|e| e.to_string()) + with_repo(&state, &tab_id, |backend| { + backend + .get_rebase_todo(&onto, limit) + .map_err(|e| e.to_string()) + }) } #[tauri::command] pub fn get_merge_base_content( + tab_id: String, path: String, state: State<'_, AppState>, ) -> Result { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend - .get_merge_base_content(&path) - .map_err(|e| e.to_string()) + with_repo(&state, &tab_id, |backend| { + backend + .get_merge_base_content(&path) + .map_err(|e| e.to_string()) + }) } diff --git a/src-tauri/src/commands/remote.rs b/src-tauri/src/commands/remote.rs index e6b89fa..d5c4652 100644 --- a/src-tauri/src/commands/remote.rs +++ b/src-tauri/src/commands/remote.rs @@ -1,89 +1,85 @@ use tauri::State; +use crate::commands::with_repo; use crate::git::types::{FetchResult, MergeResult, PullOption, PushResult, RemoteInfo}; use crate::state::AppState; #[tauri::command] pub fn fetch_remote( + tab_id: String, remote_name: String, state: State<'_, AppState>, ) -> Result { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend.fetch(&remote_name).map_err(|e| e.to_string()) + with_repo(&state, &tab_id, |backend| { + backend.fetch(&remote_name).map_err(|e| e.to_string()) + }) } #[tauri::command] pub fn pull_remote( + tab_id: String, remote_name: String, option: PullOption, state: State<'_, AppState>, ) -> Result { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend - .pull(&remote_name, option) - .map_err(|e| e.to_string()) + with_repo(&state, &tab_id, |backend| { + backend + .pull(&remote_name, option) + .map_err(|e| e.to_string()) + }) } #[tauri::command] -pub fn push_remote(remote_name: String, state: State<'_, AppState>) -> Result { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend.push(&remote_name).map_err(|e| e.to_string()) +pub fn push_remote( + tab_id: String, + remote_name: String, + state: State<'_, AppState>, +) -> Result { + with_repo(&state, &tab_id, |backend| { + backend.push(&remote_name).map_err(|e| e.to_string()) + }) } #[tauri::command] -pub fn list_remotes(state: State<'_, AppState>) -> Result, String> { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend.list_remotes().map_err(|e| e.to_string()) +pub fn list_remotes(tab_id: String, state: State<'_, AppState>) -> Result, String> { + with_repo(&state, &tab_id, |backend| { + backend.list_remotes().map_err(|e| e.to_string()) + }) } #[tauri::command] -pub fn add_remote(name: String, url: String, state: State<'_, AppState>) -> Result<(), String> { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend.add_remote(&name, &url).map_err(|e| e.to_string()) +pub fn add_remote( + tab_id: String, + name: String, + url: String, + state: State<'_, AppState>, +) -> Result<(), String> { + with_repo(&state, &tab_id, |backend| { + backend.add_remote(&name, &url).map_err(|e| e.to_string()) + }) } #[tauri::command] -pub fn remove_remote(name: String, state: State<'_, AppState>) -> Result<(), String> { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend.remove_remote(&name).map_err(|e| e.to_string()) +pub fn remove_remote( + tab_id: String, + name: String, + state: State<'_, AppState>, +) -> Result<(), String> { + with_repo(&state, &tab_id, |backend| { + backend.remove_remote(&name).map_err(|e| e.to_string()) + }) } #[tauri::command] pub fn edit_remote( + tab_id: String, name: String, new_url: String, state: State<'_, AppState>, ) -> Result<(), String> { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend - .edit_remote(&name, &new_url) - .map_err(|e| e.to_string()) + with_repo(&state, &tab_id, |backend| { + backend + .edit_remote(&name, &new_url) + .map_err(|e| e.to_string()) + }) } diff --git a/src-tauri/src/commands/reset.rs b/src-tauri/src/commands/reset.rs index 9732853..fe7c757 100644 --- a/src-tauri/src/commands/reset.rs +++ b/src-tauri/src/commands/reset.rs @@ -1,44 +1,43 @@ use tauri::State; +use crate::commands::with_repo; use crate::git::types::{ReflogEntry, ResetMode, ResetResult}; use crate::state::AppState; #[tauri::command] pub fn reset( + tab_id: String, oid: String, mode: ResetMode, state: State<'_, AppState>, ) -> Result { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend.reset(&oid, mode).map_err(|e| e.to_string()) + with_repo(&state, &tab_id, |backend| { + backend.reset(&oid, mode).map_err(|e| e.to_string()) + }) } #[tauri::command] -pub fn reset_file(path: String, oid: String, state: State<'_, AppState>) -> Result<(), String> { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend.reset_file(&path, &oid).map_err(|e| e.to_string()) +pub fn reset_file( + tab_id: String, + path: String, + oid: String, + state: State<'_, AppState>, +) -> Result<(), String> { + with_repo(&state, &tab_id, |backend| { + backend.reset_file(&path, &oid).map_err(|e| e.to_string()) + }) } #[tauri::command] pub fn get_reflog( + tab_id: String, ref_name: String, limit: usize, state: State<'_, AppState>, ) -> Result, String> { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend - .get_reflog(&ref_name, limit) - .map_err(|e| e.to_string()) + with_repo(&state, &tab_id, |backend| { + backend + .get_reflog(&ref_name, limit) + .map_err(|e| e.to_string()) + }) } diff --git a/src-tauri/src/commands/revert.rs b/src-tauri/src/commands/revert.rs index 96fa76c..7829c64 100644 --- a/src-tauri/src/commands/revert.rs +++ b/src-tauri/src/commands/revert.rs @@ -1,48 +1,38 @@ use tauri::State; +use crate::commands::with_repo; use crate::git::types::{RevertMode, RevertResult}; use crate::state::AppState; #[tauri::command] pub fn revert( + tab_id: String, oid: String, mode: RevertMode, state: State<'_, AppState>, ) -> Result { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend.revert(&oid, mode).map_err(|e| e.to_string()) + with_repo(&state, &tab_id, |backend| { + backend.revert(&oid, mode).map_err(|e| e.to_string()) + }) } #[tauri::command] -pub fn is_reverting(state: State<'_, AppState>) -> Result { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend.is_reverting().map_err(|e| e.to_string()) +pub fn is_reverting(tab_id: String, state: State<'_, AppState>) -> Result { + with_repo(&state, &tab_id, |backend| { + backend.is_reverting().map_err(|e| e.to_string()) + }) } #[tauri::command] -pub fn abort_revert(state: State<'_, AppState>) -> Result<(), String> { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend.abort_revert().map_err(|e| e.to_string()) +pub fn abort_revert(tab_id: String, state: State<'_, AppState>) -> Result<(), String> { + with_repo(&state, &tab_id, |backend| { + backend.abort_revert().map_err(|e| e.to_string()) + }) } #[tauri::command] -pub fn continue_revert(state: State<'_, AppState>) -> Result { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend.continue_revert().map_err(|e| e.to_string()) +pub fn continue_revert(tab_id: String, state: State<'_, AppState>) -> Result { + with_repo(&state, &tab_id, |backend| { + backend.continue_revert().map_err(|e| e.to_string()) + }) } diff --git a/src-tauri/src/commands/search.rs b/src-tauri/src/commands/search.rs index e7c7866..cead1ef 100644 --- a/src-tauri/src/commands/search.rs +++ b/src-tauri/src/commands/search.rs @@ -1,49 +1,44 @@ use tauri::State; +use crate::commands::with_repo; use crate::git::search::{CodeSearchResult, CommitSearchResult, FilenameSearchResult}; use crate::state::AppState; #[tauri::command] pub fn search_code( + tab_id: String, query: String, is_regex: bool, state: State<'_, AppState>, ) -> Result, String> { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend - .search_code(&query, is_regex) - .map_err(|e| e.to_string()) + with_repo(&state, &tab_id, |backend| { + backend + .search_code(&query, is_regex) + .map_err(|e| e.to_string()) + }) } #[tauri::command] pub fn search_commits( + tab_id: String, query: String, search_diff: bool, state: State<'_, AppState>, ) -> Result, String> { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend - .search_commits(&query, search_diff) - .map_err(|e| e.to_string()) + with_repo(&state, &tab_id, |backend| { + backend + .search_commits(&query, search_diff) + .map_err(|e| e.to_string()) + }) } #[tauri::command] pub fn search_filenames( + tab_id: String, query: String, state: State<'_, AppState>, ) -> Result, String> { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend.search_filenames(&query).map_err(|e| e.to_string()) + with_repo(&state, &tab_id, |backend| { + backend.search_filenames(&query).map_err(|e| e.to_string()) + }) } diff --git a/src-tauri/src/commands/stash.rs b/src-tauri/src/commands/stash.rs index accf424..8a788cf 100644 --- a/src-tauri/src/commands/stash.rs +++ b/src-tauri/src/commands/stash.rs @@ -1,66 +1,57 @@ use tauri::State; +use crate::commands::with_repo; use crate::git::types::{FileDiff, StashEntry}; use crate::state::AppState; #[tauri::command] -pub fn stash_save(message: Option, state: State<'_, AppState>) -> Result<(), String> { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend - .stash_save(message.as_deref()) - .map_err(|e| e.to_string()) +pub fn stash_save( + tab_id: String, + message: Option, + state: State<'_, AppState>, +) -> Result<(), String> { + with_repo(&state, &tab_id, |backend| { + backend + .stash_save(message.as_deref()) + .map_err(|e| e.to_string()) + }) } #[tauri::command] -pub fn list_stashes(state: State<'_, AppState>) -> Result, String> { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend.stash_list().map_err(|e| e.to_string()) +pub fn list_stashes(tab_id: String, state: State<'_, AppState>) -> Result, String> { + with_repo(&state, &tab_id, |backend| { + backend.stash_list().map_err(|e| e.to_string()) + }) } #[tauri::command] -pub fn apply_stash(index: usize, state: State<'_, AppState>) -> Result<(), String> { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend.stash_apply(index).map_err(|e| e.to_string()) +pub fn apply_stash(tab_id: String, index: usize, state: State<'_, AppState>) -> Result<(), String> { + with_repo(&state, &tab_id, |backend| { + backend.stash_apply(index).map_err(|e| e.to_string()) + }) } #[tauri::command] -pub fn pop_stash(index: usize, state: State<'_, AppState>) -> Result<(), String> { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend.stash_pop(index).map_err(|e| e.to_string()) +pub fn pop_stash(tab_id: String, index: usize, state: State<'_, AppState>) -> Result<(), String> { + with_repo(&state, &tab_id, |backend| { + backend.stash_pop(index).map_err(|e| e.to_string()) + }) } #[tauri::command] -pub fn drop_stash(index: usize, state: State<'_, AppState>) -> Result<(), String> { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend.stash_drop(index).map_err(|e| e.to_string()) +pub fn drop_stash(tab_id: String, index: usize, state: State<'_, AppState>) -> Result<(), String> { + with_repo(&state, &tab_id, |backend| { + backend.stash_drop(index).map_err(|e| e.to_string()) + }) } #[tauri::command] -pub fn get_stash_diff(index: usize, state: State<'_, AppState>) -> Result, String> { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend.stash_diff(index).map_err(|e| e.to_string()) +pub fn get_stash_diff( + tab_id: String, + index: usize, + state: State<'_, AppState>, +) -> Result, String> { + with_repo(&state, &tab_id, |backend| { + backend.stash_diff(index).map_err(|e| e.to_string()) + }) } diff --git a/src-tauri/src/commands/submodule.rs b/src-tauri/src/commands/submodule.rs index ddf26c4..d2c1b8f 100644 --- a/src-tauri/src/commands/submodule.rs +++ b/src-tauri/src/commands/submodule.rs @@ -1,56 +1,58 @@ use tauri::State; +use crate::commands::with_repo; use crate::git::types::SubmoduleInfo; use crate::state::AppState; #[tauri::command] -pub fn list_submodules(state: State<'_, AppState>) -> Result, String> { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend.list_submodules().map_err(|e| e.to_string()) +pub fn list_submodules( + tab_id: String, + state: State<'_, AppState>, +) -> Result, String> { + with_repo(&state, &tab_id, |backend| { + backend.list_submodules().map_err(|e| e.to_string()) + }) } #[tauri::command] -pub fn add_submodule(url: String, path: String, state: State<'_, AppState>) -> Result<(), String> { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend - .add_submodule(&url, &path) - .map_err(|e| e.to_string()) +pub fn add_submodule( + tab_id: String, + url: String, + path: String, + state: State<'_, AppState>, +) -> Result<(), String> { + with_repo(&state, &tab_id, |backend| { + backend + .add_submodule(&url, &path) + .map_err(|e| e.to_string()) + }) } #[tauri::command] -pub fn update_submodule(path: String, state: State<'_, AppState>) -> Result<(), String> { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend.update_submodule(&path).map_err(|e| e.to_string()) +pub fn update_submodule( + tab_id: String, + path: String, + state: State<'_, AppState>, +) -> Result<(), String> { + with_repo(&state, &tab_id, |backend| { + backend.update_submodule(&path).map_err(|e| e.to_string()) + }) } #[tauri::command] -pub fn update_all_submodules(state: State<'_, AppState>) -> Result<(), String> { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend.update_all_submodules().map_err(|e| e.to_string()) +pub fn update_all_submodules(tab_id: String, state: State<'_, AppState>) -> Result<(), String> { + with_repo(&state, &tab_id, |backend| { + backend.update_all_submodules().map_err(|e| e.to_string()) + }) } #[tauri::command] -pub fn remove_submodule(path: String, state: State<'_, AppState>) -> Result<(), String> { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend.remove_submodule(&path).map_err(|e| e.to_string()) +pub fn remove_submodule( + tab_id: String, + path: String, + state: State<'_, AppState>, +) -> Result<(), String> { + with_repo(&state, &tab_id, |backend| { + backend.remove_submodule(&path).map_err(|e| e.to_string()) + }) } diff --git a/src-tauri/src/commands/tag.rs b/src-tauri/src/commands/tag.rs index 3393eb3..e93a226 100644 --- a/src-tauri/src/commands/tag.rs +++ b/src-tauri/src/commands/tag.rs @@ -1,50 +1,44 @@ use tauri::State; +use crate::commands::with_repo; use crate::git::types::TagInfo; use crate::state::AppState; #[tauri::command] -pub fn list_tags(state: State<'_, AppState>) -> Result, String> { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend.list_tags().map_err(|e| e.to_string()) +pub fn list_tags(tab_id: String, state: State<'_, AppState>) -> Result, String> { + with_repo(&state, &tab_id, |backend| { + backend.list_tags().map_err(|e| e.to_string()) + }) } #[tauri::command] pub fn create_tag( + tab_id: String, name: String, message: Option, state: State<'_, AppState>, ) -> Result<(), String> { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend - .create_tag(&name, message.as_deref()) - .map_err(|e| e.to_string()) + with_repo(&state, &tab_id, |backend| { + backend + .create_tag(&name, message.as_deref()) + .map_err(|e| e.to_string()) + }) } #[tauri::command] -pub fn delete_tag(name: String, state: State<'_, AppState>) -> Result<(), String> { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend.delete_tag(&name).map_err(|e| e.to_string()) +pub fn delete_tag(tab_id: String, name: String, state: State<'_, AppState>) -> Result<(), String> { + with_repo(&state, &tab_id, |backend| { + backend.delete_tag(&name).map_err(|e| e.to_string()) + }) } #[tauri::command] -pub fn checkout_tag(name: String, state: State<'_, AppState>) -> Result<(), String> { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend.checkout_tag(&name).map_err(|e| e.to_string()) +pub fn checkout_tag( + tab_id: String, + name: String, + state: State<'_, AppState>, +) -> Result<(), String> { + with_repo(&state, &tab_id, |backend| { + backend.checkout_tag(&name).map_err(|e| e.to_string()) + }) } diff --git a/src-tauri/src/commands/worktree.rs b/src-tauri/src/commands/worktree.rs index db52b04..41aff5b 100644 --- a/src-tauri/src/commands/worktree.rs +++ b/src-tauri/src/commands/worktree.rs @@ -1,40 +1,40 @@ use tauri::State; +use crate::commands::with_repo; use crate::git::types::WorktreeInfo; use crate::state::AppState; #[tauri::command] -pub fn list_worktrees(state: State<'_, AppState>) -> Result, String> { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend.list_worktrees().map_err(|e| e.to_string()) +pub fn list_worktrees( + tab_id: String, + state: State<'_, AppState>, +) -> Result, String> { + with_repo(&state, &tab_id, |backend| { + backend.list_worktrees().map_err(|e| e.to_string()) + }) } #[tauri::command] pub fn add_worktree( + tab_id: String, path: String, branch: String, state: State<'_, AppState>, ) -> Result<(), String> { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend - .add_worktree(&path, &branch) - .map_err(|e| e.to_string()) + with_repo(&state, &tab_id, |backend| { + backend + .add_worktree(&path, &branch) + .map_err(|e| e.to_string()) + }) } #[tauri::command] -pub fn remove_worktree(path: String, state: State<'_, AppState>) -> Result<(), String> { - let repo_lock = state - .repo - .lock() - .map_err(|e| format!("Lock poisoned: {e}"))?; - let backend = repo_lock.as_ref().ok_or("No repository opened")?; - backend.remove_worktree(&path).map_err(|e| e.to_string()) +pub fn remove_worktree( + tab_id: String, + path: String, + state: State<'_, AppState>, +) -> Result<(), String> { + with_repo(&state, &tab_id, |backend| { + backend.remove_worktree(&path).map_err(|e| e.to_string()) + }) } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index ed4a980..bb096a4 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,4 +1,5 @@ pub mod ai; +pub mod auto_fetch; pub mod commands; pub mod config; pub mod git; @@ -6,19 +7,18 @@ pub mod hosting; pub mod state; pub mod watcher; +use std::collections::HashMap; use std::sync::Mutex; -use state::AppState; +use state::{AppState, RepoContext, DEFAULT_TAB_ID}; use tauri::Manager; /// CLI 引数 → last_opened_repo → カレントディレクトリ の優先順位でリポジトリパスを解決する fn resolve_repo_path() -> Option { - // 1. CLI 引数にパスが指定されている場合 if let Some(arg) = std::env::args().nth(1) { return std::fs::canonicalize(&arg).ok(); } - // 2. last_opened_repo が設定されている場合 if let Ok(cfg) = config::load_config() { if let Some(last) = cfg.last_opened_repo { let path = std::path::PathBuf::from(&last); @@ -28,7 +28,6 @@ fn resolve_repo_path() -> Option { } } - // 3. カレントディレクトリ std::env::current_dir().ok() } @@ -39,27 +38,47 @@ pub fn run() { .as_ref() .and_then(|path| git::dispatcher::GitDispatcher::open_default(path).ok()); - // リポジトリを正常に開けた場合、last_opened_repo に保存 - if let (Some(path), Some(_)) = (&repo_path, &repo) { + let default_tab_id = DEFAULT_TAB_ID.to_string(); + let mut tabs = HashMap::new(); + let mut active_tab = None; + + if let (Some(path), Some(backend)) = (&repo_path, repo) { if let Ok(mut cfg) = config::load_config() { cfg.last_opened_repo = Some(path.to_string_lossy().to_string()); let _ = config::save_config(&cfg); } + + let path_str = path.to_string_lossy().to_string(); + let name = state::repo_name_from_path(&path_str); + let ctx = RepoContext { + backend, + watcher: None, + path: path_str, + name, + }; + tabs.insert(default_tab_id.clone(), ctx); + active_tab = Some(default_tab_id.clone()); } - // git2::Repository::discover で実際の workdir を解決する - let watch_path = repo_path.as_ref().and_then(|path| { - git2::Repository::discover(path) - .ok() - .and_then(|repo| repo.workdir().map(|p| p.to_path_buf())) + let watch_info: Option<(std::path::PathBuf, String)> = repo_path.as_ref().and_then(|path| { + git2::Repository::discover(path).ok().and_then(|repo| { + repo.workdir() + .map(|p| (p.to_path_buf(), default_tab_id.clone())) + }) }); + let auto_fetch_interval = config::load_config() + .map(|c| c.tools.auto_fetch_interval) + .unwrap_or(300); + tauri::Builder::default() .plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_dialog::init()) + .plugin(tauri_plugin_notification::init()) .manage(AppState { - repo: Mutex::new(repo), - watcher: Mutex::new(None), + tabs: Mutex::new(tabs), + active_tab: Mutex::new(active_tab), + auto_fetch_handle: Mutex::new(None), }) .setup(move |app| { if cfg!(debug_assertions) { @@ -70,11 +89,14 @@ pub fn run() { )?; } - if let Some(path) = watch_path { - match watcher::start_watcher(app.handle().clone(), &path) { + if let Some((watch_path, tab_id)) = watch_info { + match watcher::start_watcher(app.handle().clone(), &watch_path, &tab_id) { Ok(w) => { let state = app.state::(); - *state.watcher.lock().unwrap() = Some(w); + let mut tabs = state.tabs.lock().unwrap(); + if let Some(ctx) = tabs.get_mut(&tab_id) { + ctx.watcher = Some(w); + } } Err(e) => { log::error!("ファイル監視の起動に失敗: {}", e); @@ -82,9 +104,27 @@ pub fn run() { } } + // auto fetch の起動 + let auto_fetch_enabled = config::load_config() + .map(|c| c.tools.auto_fetch_on_open) + .unwrap_or(true); + + if auto_fetch_enabled && auto_fetch_interval > 0 { + let handle = + auto_fetch::start_auto_fetch(app.handle().clone(), auto_fetch_interval as u64); + let state = app.state::(); + let mut auto_fetch_lock = state.auto_fetch_handle.lock().unwrap(); + *auto_fetch_lock = Some(Box::new(handle)); + } + Ok(()) }) .invoke_handler(tauri::generate_handler![ + commands::tab::open_tab, + commands::tab::close_tab, + commands::tab::set_active_tab, + commands::tab::list_tabs, + commands::tab::get_active_tab, commands::git::get_status, commands::git::get_diff, commands::git::stage_file, @@ -161,7 +201,6 @@ pub fn run() { commands::ai::generate_commit_message, commands::ai::review_diff, commands::ai::ai_resolve_conflict, - commands::ai::generate_pr_description, commands::ai::get_ai_config, commands::ai::save_ai_config, commands::hosting::detect_hosting_provider, From a6bc0e65e82eefaeb2457d0d688e58f8a28f105e Mon Sep 17 00:00:00 2001 From: HMasataka Date: Sat, 7 Mar 2026 16:37:03 +0900 Subject: [PATCH 3/9] =?UTF-8?q?feat(backend):=20=E8=87=AA=E5=8B=95fetch?= =?UTF-8?q?=E3=83=90=E3=83=83=E3=82=AF=E3=82=B0=E3=83=A9=E3=82=A6=E3=83=B3?= =?UTF-8?q?=E3=83=89=E3=82=BF=E3=82=B9=E3=82=AF=E3=81=A8notification?= =?UTF-8?q?=E3=83=97=E3=83=A9=E3=82=B0=E3=82=A4=E3=83=B3=E3=82=92=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit auto_fetch.rsで定期的にgit fetch CLIを実行しauto-fetch:updatedイベントをemit。 Mutex保持中のネットワークI/Oを回避するためgit CLIでロック外実行。 tauri-plugin-notificationを依存追加。未使用のPR生成コードを削除。 Co-Authored-By: Claude Opus 4.6 --- src-tauri/Cargo.lock | 114 +++++++++++++++++++-- src-tauri/Cargo.toml | 1 + src-tauri/capabilities/default.json | 3 +- src-tauri/src/ai/mod.rs | 1 - src-tauri/src/ai/pr.rs | 75 -------------- src-tauri/src/ai/types.rs | 18 ---- src-tauri/src/auto_fetch.rs | 151 ++++++++++++++++++++++++++++ 7 files changed, 259 insertions(+), 104 deletions(-) delete mode 100644 src-tauri/src/ai/pr.rs create mode 100644 src-tauri/src/auto_fetch.rs diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index b6c0c5d..5fd7ce2 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -92,6 +92,7 @@ dependencies = [ "tauri-build", "tauri-plugin-dialog", "tauri-plugin-log", + "tauri-plugin-notification", "tauri-plugin-opener", "tempfile", "thiserror 2.0.18", @@ -2324,6 +2325,18 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" +[[package]] +name = "mac-notification-sys" +version = "0.6.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26053f9919b5b032f327ab94d830f2465c4c88138e9df23c8fcd305060a9b28b" +dependencies = [ + "cc", + "objc2", + "objc2-foundation", + "time", +] + [[package]] name = "markup5ever" version = "0.14.1" @@ -2509,6 +2522,20 @@ dependencies = [ "tempfile", ] +[[package]] +name = "notify-rust" +version = "4.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21af20a1b50be5ac5861f74af1a863da53a11c38684d9818d82f1c42f7fdc6c2" +dependencies = [ + "futures-lite", + "log", + "mac-notification-sys", + "serde", + "tauri-winrt-notification", + "zbus", +] + [[package]] name = "notify-types" version = "1.0.1" @@ -2520,9 +2547,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.2.0" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" [[package]] name = "num-traits" @@ -3097,7 +3124,7 @@ checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" dependencies = [ "base64 0.22.1", "indexmap 2.13.0", - "quick-xml", + "quick-xml 0.38.4", "serde", "time", ] @@ -3257,6 +3284,15 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "quick-xml" +version = "0.37.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" +dependencies = [ + "memchr", +] + [[package]] name = "quick-xml" version = "0.38.4" @@ -3312,6 +3348,16 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + [[package]] name = "rand_chacha" version = "0.2.2" @@ -3332,6 +3378,16 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + [[package]] name = "rand_core" version = "0.5.1" @@ -3350,6 +3406,15 @@ dependencies = [ "getrandom 0.2.17", ] +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + [[package]] name = "rand_hc" version = "0.2.0" @@ -4496,6 +4561,25 @@ dependencies = [ "time", ] +[[package]] +name = "tauri-plugin-notification" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01fc2c5ff41105bd1f7242d8201fdf3efd70749b82fa013a17f2126357d194cc" +dependencies = [ + "log", + "notify-rust", + "rand 0.9.2", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", + "time", + "url", +] + [[package]] name = "tauri-plugin-opener" version = "2.5.3" @@ -4619,6 +4703,18 @@ dependencies = [ "toml 0.9.12+spec-1.1.0", ] +[[package]] +name = "tauri-winrt-notification" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b1e66e07de489fe43a46678dd0b8df65e0c973909df1b60ba33874e297ba9b9" +dependencies = [ + "quick-xml 0.37.5", + "thiserror 2.0.18", + "windows", + "windows-version", +] + [[package]] name = "tempfile" version = "3.25.0" @@ -4685,9 +4781,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.47" +version = "0.3.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +checksum = "f9e442fc33d7fdb45aa9bfeb312c095964abdf596f7567261062b2a7107aaabd" dependencies = [ "deranged", "itoa", @@ -4702,15 +4798,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.8" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" +checksum = "8b36ee98fd31ec7426d599183e8fe26932a8dc1fb76ddb6214d05493377d34ca" [[package]] name = "time-macros" -version = "0.2.27" +version = "0.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +checksum = "71e552d1249bf61ac2a52db88179fd0673def1e1ad8243a00d9ec9ed71fee3dd" dependencies = [ "num-conv", "time-core", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 849d9af..6a1c5ea 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -34,6 +34,7 @@ which = "7" chrono = { version = "0.4", features = ["serde"] } reqwest = { version = "0.12", features = ["blocking", "json"] } tauri-plugin-dialog = "2" +tauri-plugin-notification = "2" [dev-dependencies] tempfile = "3" diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 349a560..86c2f5e 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -12,6 +12,7 @@ "core:window:allow-toggle-maximize", "core:window:allow-start-dragging", "opener:default", - "dialog:default" + "dialog:default", + "notification:default" ] } diff --git a/src-tauri/src/ai/mod.rs b/src-tauri/src/ai/mod.rs index 33bb81a..5b92242 100644 --- a/src-tauri/src/ai/mod.rs +++ b/src-tauri/src/ai/mod.rs @@ -1,7 +1,6 @@ pub mod adapter; pub mod conflict; pub mod detector; -pub mod pr; pub mod prompt; pub mod review; pub mod types; diff --git a/src-tauri/src/ai/pr.rs b/src-tauri/src/ai/pr.rs deleted file mode 100644 index 049acd1..0000000 --- a/src-tauri/src/ai/pr.rs +++ /dev/null @@ -1,75 +0,0 @@ -use crate::ai::types::{AiError, PrDescription}; - -pub fn build_pr_description_prompt(diff: &str, branch_name: &str) -> String { - format!( - "Generate a pull request title and description for the following changes.\n\n\ - Branch: {branch_name}\n\n\ - Output ONLY a JSON object with this exact structure (no markdown fences, no explanation):\n\ - {{\"title\":\"PR title (concise, under 72 chars)\",\"body\":\"PR description in markdown\"}}\n\n\ - Rules:\n\ - - Title should be concise and descriptive\n\ - - Body should include a summary of changes, motivation, and any notable implementation details\n\ - - Use markdown formatting in the body (headers, bullet points, etc.)\n\ - - Focus on the \"what\" and \"why\", not the \"how\"\n\n\ - Diff:\n\ - ```\n\ - {diff}\n\ - ```" - ) -} - -pub fn parse_pr_description_response(raw: &str) -> Result { - let trimmed = raw.trim(); - let json_str = super::strip_code_fences(trimmed); - - serde_json::from_str::(json_str) - .map_err(|e| AiError::ParseFailed(format!("invalid PR description JSON: {e}"))) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn build_prompt_includes_branch_and_diff() { - let prompt = build_pr_description_prompt("diff content", "feat/auth"); - assert!(prompt.contains("feat/auth")); - assert!(prompt.contains("diff content")); - } - - #[test] - fn build_prompt_specifies_json_format() { - let prompt = build_pr_description_prompt("diff", "main"); - assert!(prompt.contains("JSON")); - assert!(prompt.contains("\"title\"")); - assert!(prompt.contains("\"body\"")); - } - - #[test] - fn parse_valid_pr_description() { - let json = "{\"title\":\"Add authentication handler\",\"body\":\"Summary of changes\"}"; - let result = parse_pr_description_response(json).unwrap(); - assert_eq!(result.title, "Add authentication handler"); - assert!(result.body.contains("Summary")); - } - - #[test] - fn parse_response_with_code_fences() { - let raw = "```json\n{\"title\":\"Fix bug\",\"body\":\"Fixed the bug\"}\n```"; - let result = parse_pr_description_response(raw).unwrap(); - assert_eq!(result.title, "Fix bug"); - } - - #[test] - fn parse_invalid_json_returns_error() { - let result = parse_pr_description_response("not json"); - assert!(result.is_err()); - } - - #[test] - fn parse_empty_body_is_valid() { - let json = r#"{"title":"Update deps","body":""}"#; - let result = parse_pr_description_response(json).unwrap(); - assert!(result.body.is_empty()); - } -} diff --git a/src-tauri/src/ai/types.rs b/src-tauri/src/ai/types.rs index f91ef66..41509cd 100644 --- a/src-tauri/src/ai/types.rs +++ b/src-tauri/src/ai/types.rs @@ -94,14 +94,6 @@ pub struct ConflictSuggestion { pub reason: String, } -// === PR description types === - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct PrDescription { - pub title: String, - pub body: String, -} - #[derive(Debug, thiserror::Error)] pub enum AiError { #[error("no adapter available")] @@ -247,16 +239,6 @@ exclude_patterns = ["*.key"] assert_eq!(json["resolved_code"], "merged code"); } - #[test] - fn pr_description_serializes_to_json() { - let pr = PrDescription { - title: "Add auth handler".to_string(), - body: "This PR adds authentication.".to_string(), - }; - let json = serde_json::to_value(&pr).unwrap(); - assert_eq!(json["title"], "Add auth handler"); - } - #[test] fn confidence_level_serializes_as_lowercase() { assert_eq!( diff --git a/src-tauri/src/auto_fetch.rs b/src-tauri/src/auto_fetch.rs new file mode 100644 index 0000000..3ad6e03 --- /dev/null +++ b/src-tauri/src/auto_fetch.rs @@ -0,0 +1,151 @@ +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +use std::time::Duration; + +use serde::Serialize; +use tauri::{AppHandle, Emitter, Manager}; + +use crate::state::AppState; + +#[derive(Debug, Clone, Serialize)] +pub struct AutoFetchUpdate { + pub tab_id: String, + pub remote_name: String, + pub new_commits_count: u32, +} + +pub struct AutoFetchHandle { + stop_flag: Arc, +} + +impl AutoFetchHandle { + pub fn stop(&self) { + self.stop_flag.store(true, Ordering::Relaxed); + } +} + +impl Drop for AutoFetchHandle { + fn drop(&mut self) { + self.stop(); + } +} + +pub fn start_auto_fetch(app_handle: AppHandle, interval_secs: u64) -> AutoFetchHandle { + let stop_flag = Arc::new(AtomicBool::new(false)); + let stop_flag_clone = Arc::clone(&stop_flag); + + std::thread::spawn(move || { + loop { + for _ in 0..interval_secs { + if stop_flag_clone.load(Ordering::Relaxed) { + return; + } + std::thread::sleep(Duration::from_secs(1)); + } + + if stop_flag_clone.load(Ordering::Relaxed) { + return; + } + + let state: tauri::State<'_, AppState> = match app_handle.try_state::() { + Some(s) => s, + None => continue, + }; + + // Collect tab info while holding the lock briefly + let tab_entries: Vec<(String, Vec)> = { + let tabs = match state.tabs.try_lock() { + Ok(t) => t, + Err(_) => continue, + }; + + tabs.iter() + .filter_map(|(tab_id, ctx)| { + let remotes = ctx.backend.list_remotes().ok()?; + let remote_names: Vec = + remotes.iter().map(|r| r.name.clone()).collect(); + if remote_names.is_empty() { + None + } else { + Some((tab_id.clone(), remote_names)) + } + }) + .collect() + }; + + for (tab_id, remote_names) in &tab_entries { + for remote_name in remote_names { + if stop_flag_clone.load(Ordering::Relaxed) { + return; + } + + // Snapshot branches before fetch and extract repo path (short lock) + let (branches_before, repo_path) = { + let tabs = match state.tabs.try_lock() { + Ok(t) => t, + Err(_) => continue, + }; + match tabs.get(tab_id) { + Some(ctx) => ( + ctx.backend.list_branches().unwrap_or_default(), + ctx.backend.workdir().to_path_buf(), + ), + None => continue, + } + }; + + // Execute fetch outside of lock via git CLI to avoid holding + // MutexGuard during network I/O + { + let output = std::process::Command::new("git") + .args(["fetch", remote_name]) + .current_dir(&repo_path) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .status(); + match output { + Ok(status) if status.success() => {} + _ => continue, + } + } + + // Snapshot branches after fetch (short lock) + let branches_after = { + let tabs = match state.tabs.try_lock() { + Ok(t) => t, + Err(_) => continue, + }; + match tabs.get(tab_id) { + Some(ctx) => ctx.backend.list_branches().unwrap_or_default(), + None => continue, + } + }; + + // Compare outside of any lock + let mut new_commits: u32 = 0; + for after_branch in &branches_after { + if let Some(before_branch) = + branches_before.iter().find(|b| b.name == after_branch.name) + { + if after_branch.behind_count > before_branch.behind_count { + new_commits += + after_branch.behind_count - before_branch.behind_count; + } + } + } + + if new_commits > 0 { + let update = AutoFetchUpdate { + tab_id: tab_id.clone(), + remote_name: remote_name.clone(), + new_commits_count: new_commits, + }; + let _ = app_handle.emit("auto-fetch:updated", &update); + } + } + } + } + }); + + AutoFetchHandle { stop_flag } +} From 40776a87933e969a7ea7bd8957c269863fba885e Mon Sep 17 00:00:00 2001 From: HMasataka Date: Sat, 7 Mar 2026 16:37:18 +0900 Subject: [PATCH 4/9] =?UTF-8?q?feat(frontend):=20=E3=82=BF=E3=83=96?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E3=81=AE=E3=82=B5=E3=83=BC=E3=83=93=E3=82=B9?= =?UTF-8?q?=E3=83=BB=E3=82=B9=E3=83=88=E3=82=A2=E3=82=92=E8=BF=BD=E5=8A=A0?= =?UTF-8?q?=E3=81=97tabId=E5=AF=BE=E5=BF=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tabStore(Zustand)でタブ状態管理、getActiveTabId()ヘルパーを一元化。 全19サービスファイルにtabIdパラメータを追加。 全ストアがアクティブタブ経由でサービスを呼び出すように変更。 Co-Authored-By: Claude Opus 4.6 --- src/services/ai.ts | 17 +---- src/services/cherryPick.ts | 15 ++-- src/services/conflict.ts | 35 +++++---- src/services/git.ts | 147 ++++++++++++++++++++++------------- src/services/gitconfig.ts | 19 +++-- src/services/history.ts | 29 +++++-- src/services/hosting.ts | 26 ++++--- src/services/rebase.ts | 26 ++++--- src/services/reflog.ts | 2 + src/services/repo.ts | 14 +++- src/services/reset.ts | 11 ++- src/services/revert.ts | 15 ++-- src/services/search.ts | 13 +++- src/services/stash.ts | 30 +++++--- src/services/submodule.ts | 24 +++--- src/services/tab.ts | 27 +++++++ src/services/tag.ts | 20 +++-- src/services/worktree.ts | 16 ++-- src/stores/aiStore.ts | 9 ++- src/stores/gitStore.ts | 153 ++++++++++++++++++++----------------- src/stores/historyStore.ts | 18 +++-- src/stores/hostingStore.ts | 14 ++-- src/stores/repoStore.ts | 7 +- src/stores/tabStore.ts | 100 ++++++++++++++++++++++++ 24 files changed, 530 insertions(+), 257 deletions(-) create mode 100644 src/services/tab.ts create mode 100644 src/stores/tabStore.ts diff --git a/src/services/ai.ts b/src/services/ai.ts index 095ed0e..316c790 100644 --- a/src/services/ai.ts +++ b/src/services/ai.ts @@ -49,29 +49,24 @@ export interface ConflictSuggestion { reason: string; } -// === PR description types === - -export interface PrDescription { - title: string; - body: string; -} - export function detectCliAdapters(): Promise { return invoke("detect_cli_adapters"); } export function generateCommitMessage( + tabId: string, format: string, language: string, ): Promise { return invoke("generate_commit_message", { + tabId, format, language, }); } -export function reviewDiff(): Promise { - return invoke("review_diff"); +export function reviewDiff(tabId: string): Promise { + return invoke("review_diff", { tabId }); } export function aiResolveConflict( @@ -86,10 +81,6 @@ export function aiResolveConflict( }); } -export function generatePrDescription(): Promise { - return invoke("generate_pr_description"); -} - export function getAiConfig(): Promise { return invoke("get_ai_config"); } diff --git a/src/services/cherryPick.ts b/src/services/cherryPick.ts index 62c5bc8..e207ff3 100644 --- a/src/services/cherryPick.ts +++ b/src/services/cherryPick.ts @@ -9,20 +9,21 @@ export interface CherryPickResult { } export function cherryPick( + tabId: string, oids: string[], mode: CherryPickMode, ): Promise { - return invoke("cherry_pick", { oids, mode }); + return invoke("cherry_pick", { tabId, oids, mode }); } -export function isCherryPicking(): Promise { - return invoke("is_cherry_picking"); +export function isCherryPicking(tabId: string): Promise { + return invoke("is_cherry_picking", { tabId }); } -export function abortCherryPick(): Promise { - return invoke("abort_cherry_pick"); +export function abortCherryPick(tabId: string): Promise { + return invoke("abort_cherry_pick", { tabId }); } -export function continueCherryPick(): Promise { - return invoke("continue_cherry_pick"); +export function continueCherryPick(tabId: string): Promise { + return invoke("continue_cherry_pick", { tabId }); } diff --git a/src/services/conflict.ts b/src/services/conflict.ts index e2b7649..c5feab6 100644 --- a/src/services/conflict.ts +++ b/src/services/conflict.ts @@ -28,45 +28,54 @@ export type ConflictResolution = | { type: "Both" } | { type: "Manual"; content: string }; -export function getConflictFiles(): Promise { - return invoke("get_conflict_files"); +export function getConflictFiles(tabId: string): Promise { + return invoke("get_conflict_files", { tabId }); } export function resolveConflict( + tabId: string, path: string, resolution: ConflictResolution, ): Promise { - return invoke("resolve_conflict", { path, resolution }); + return invoke("resolve_conflict", { tabId, path, resolution }); } export function resolveConflictBlock( + tabId: string, path: string, blockIndex: number, resolution: ConflictResolution, ): Promise { return invoke("resolve_conflict_block", { + tabId, path, blockIndex, resolution, }); } -export function markResolved(path: string): Promise { - return invoke("mark_resolved", { path }); +export function markResolved(tabId: string, path: string): Promise { + return invoke("mark_resolved", { tabId, path }); } -export function abortMerge(): Promise { - return invoke("abort_merge"); +export function abortMerge(tabId: string): Promise { + return invoke("abort_merge", { tabId }); } -export function continueMerge(message: string): Promise { - return invoke("continue_merge", { message }); +export function continueMerge( + tabId: string, + message: string, +): Promise { + return invoke("continue_merge", { tabId, message }); } -export function isMerging(): Promise { - return invoke("is_merging"); +export function isMerging(tabId: string): Promise { + return invoke("is_merging", { tabId }); } -export function getMergeBaseContent(path: string): Promise { - return invoke("get_merge_base_content", { path }); +export function getMergeBaseContent( + tabId: string, + path: string, +): Promise { + return invoke("get_merge_base_content", { tabId, path }); } diff --git a/src/services/git.ts b/src/services/git.ts index 2a2d262..93e151a 100644 --- a/src/services/git.ts +++ b/src/services/git.ts @@ -72,43 +72,45 @@ export interface CommitResult { oid: string; } -export function getStatus(): Promise { - return invoke("get_status"); +export function getStatus(tabId: string): Promise { + return invoke("get_status", { tabId }); } export function getDiff( + tabId: string, path: string | null, staged: boolean, ): Promise { - return invoke("get_diff", { path, staged }); + return invoke("get_diff", { tabId, path, staged }); } -export function stageFile(path: string): Promise { - return invoke("stage_file", { path }); +export function stageFile(tabId: string, path: string): Promise { + return invoke("stage_file", { tabId, path }); } -export function unstageFile(path: string): Promise { - return invoke("unstage_file", { path }); +export function unstageFile(tabId: string, path: string): Promise { + return invoke("unstage_file", { tabId, path }); } -export function stageAll(): Promise { - return invoke("stage_all"); +export function stageAll(tabId: string): Promise { + return invoke("stage_all", { tabId }); } -export function unstageAll(): Promise { - return invoke("unstage_all"); +export function unstageAll(tabId: string): Promise { + return invoke("unstage_all", { tabId }); } export function commitChanges( + tabId: string, message: string, amend: boolean, sign: boolean, ): Promise { - return invoke("commit", { message, amend, sign }); + return invoke("commit", { tabId, message, amend, sign }); } -export function getCurrentBranch(): Promise { - return invoke("get_current_branch"); +export function getCurrentBranch(tabId: string): Promise { + return invoke("get_current_branch", { tabId }); } export interface BranchInfo { @@ -136,31 +138,36 @@ export interface MergeResult { export type MergeOption = "default" | "fast_forward_only" | "no_fast_forward"; -export function listBranches(): Promise { - return invoke("list_branches"); +export function listBranches(tabId: string): Promise { + return invoke("list_branches", { tabId }); } -export function createBranch(name: string): Promise { - return invoke("create_branch", { name }); +export function createBranch(tabId: string, name: string): Promise { + return invoke("create_branch", { tabId, name }); } -export function checkoutBranch(name: string): Promise { - return invoke("checkout_branch", { name }); +export function checkoutBranch(tabId: string, name: string): Promise { + return invoke("checkout_branch", { tabId, name }); } -export function deleteBranch(name: string): Promise { - return invoke("delete_branch", { name }); +export function deleteBranch(tabId: string, name: string): Promise { + return invoke("delete_branch", { tabId, name }); } -export function renameBranch(oldName: string, newName: string): Promise { - return invoke("rename_branch", { oldName, newName }); +export function renameBranch( + tabId: string, + oldName: string, + newName: string, +): Promise { + return invoke("rename_branch", { tabId, oldName, newName }); } export function mergeBranch( + tabId: string, branchName: string, option: MergeOption, ): Promise { - return invoke("merge_branch", { branchName, option }); + return invoke("merge_branch", { tabId, branchName, option }); } export interface RemoteInfo { @@ -179,74 +186,112 @@ export interface PushResult { branch: string; } -export function fetchRemote(remoteName: string): Promise { - return invoke("fetch_remote", { remoteName }); +export function fetchRemote( + tabId: string, + remoteName: string, +): Promise { + return invoke("fetch_remote", { tabId, remoteName }); } export function pullRemote( + tabId: string, remoteName: string, option: PullOption, ): Promise { - return invoke("pull_remote", { remoteName, option }); + return invoke("pull_remote", { tabId, remoteName, option }); } -export function pushRemote(remoteName: string): Promise { - return invoke("push_remote", { remoteName }); +export function pushRemote( + tabId: string, + remoteName: string, +): Promise { + return invoke("push_remote", { tabId, remoteName }); } -export function listRemotes(): Promise { - return invoke("list_remotes"); +export function listRemotes(tabId: string): Promise { + return invoke("list_remotes", { tabId }); } -export function addRemote(name: string, url: string): Promise { - return invoke("add_remote", { name, url }); +export function addRemote( + tabId: string, + name: string, + url: string, +): Promise { + return invoke("add_remote", { tabId, name, url }); } -export function removeRemote(name: string): Promise { - return invoke("remove_remote", { name }); +export function removeRemote(tabId: string, name: string): Promise { + return invoke("remove_remote", { tabId, name }); } -export function editRemote(name: string, newUrl: string): Promise { - return invoke("edit_remote", { name, newUrl }); +export function editRemote( + tabId: string, + name: string, + newUrl: string, +): Promise { + return invoke("edit_remote", { tabId, name, newUrl }); } -export function stageHunk(path: string, hunk: HunkIdentifier): Promise { - return invoke("stage_hunk", { path, hunk }); +export function stageHunk( + tabId: string, + path: string, + hunk: HunkIdentifier, +): Promise { + return invoke("stage_hunk", { tabId, path, hunk }); } -export function unstageHunk(path: string, hunk: HunkIdentifier): Promise { - return invoke("unstage_hunk", { path, hunk }); +export function unstageHunk( + tabId: string, + path: string, + hunk: HunkIdentifier, +): Promise { + return invoke("unstage_hunk", { tabId, path, hunk }); } -export function discardHunk(path: string, hunk: HunkIdentifier): Promise { - return invoke("discard_hunk", { path, hunk }); +export function discardHunk( + tabId: string, + path: string, + hunk: HunkIdentifier, +): Promise { + return invoke("discard_hunk", { tabId, path, hunk }); } -export function stageLines(path: string, lineRange: LineRange): Promise { - return invoke("stage_lines", { path, lineRange }); +export function stageLines( + tabId: string, + path: string, + lineRange: LineRange, +): Promise { + return invoke("stage_lines", { tabId, path, lineRange }); } export function unstageLines( + tabId: string, path: string, lineRange: LineRange, ): Promise { - return invoke("unstage_lines", { path, lineRange }); + return invoke("unstage_lines", { tabId, path, lineRange }); } export function discardLines( + tabId: string, path: string, lineRange: LineRange, ): Promise { - return invoke("discard_lines", { path, lineRange }); + return invoke("discard_lines", { tabId, path, lineRange }); } -export function getHeadCommitMessage(): Promise { - return invoke("get_head_commit_message"); +export function getHeadCommitMessage(tabId: string): Promise { + return invoke("get_head_commit_message", { tabId }); } export function getBranchCommits( + tabId: string, branchName: string, limit: number, ): Promise { - return invoke("get_branch_commits", { branchName, limit }); + return invoke("get_branch_commits", { + tabId, + branchName, + limit, + }); } diff --git a/src/services/gitconfig.ts b/src/services/gitconfig.ts index b55b9d1..6d207f5 100644 --- a/src/services/gitconfig.ts +++ b/src/services/gitconfig.ts @@ -8,33 +8,40 @@ export interface GitConfigEntry { } export function getGitconfigEntries( + tabId: string, scope: GitConfigScope, ): Promise { - return invoke("get_gitconfig_entries", { scope }); + return invoke("get_gitconfig_entries", { tabId, scope }); } export function getGitconfigValue( + tabId: string, scope: GitConfigScope, key: string, ): Promise { - return invoke("get_gitconfig_value", { scope, key }); + return invoke("get_gitconfig_value", { tabId, scope, key }); } export function setGitconfigValue( + tabId: string, scope: GitConfigScope, key: string, value: string, ): Promise { - return invoke("set_gitconfig_value", { scope, key, value }); + return invoke("set_gitconfig_value", { tabId, scope, key, value }); } export function unsetGitconfigValue( + tabId: string, scope: GitConfigScope, key: string, ): Promise { - return invoke("unset_gitconfig_value", { scope, key }); + return invoke("unset_gitconfig_value", { tabId, scope, key }); } -export function getGitconfigPath(scope: GitConfigScope): Promise { - return invoke("get_gitconfig_path", { scope }); +export function getGitconfigPath( + tabId: string, + scope: GitConfigScope, +): Promise { + return invoke("get_gitconfig_path", { tabId, scope }); } diff --git a/src/services/history.ts b/src/services/history.ts index 1495da4..5585441 100644 --- a/src/services/history.ts +++ b/src/services/history.ts @@ -94,35 +94,52 @@ export interface CommitLogResult { } export function getCommitLog( + tabId: string, filter: LogFilter, limit: number, skip: number, ): Promise { - return invoke("get_commit_log", { filter, limit, skip }); + return invoke("get_commit_log", { + tabId, + filter, + limit, + skip, + }); } -export function getCommitDetail(oid: string): Promise { - return invoke("get_commit_detail", { oid }); +export function getCommitDetail( + tabId: string, + oid: string, +): Promise { + return invoke("get_commit_detail", { tabId, oid }); } export function getCommitFileDiff( + tabId: string, oid: string, path: string, ): Promise { - return invoke("get_commit_file_diff", { oid, path }); + return invoke("get_commit_file_diff", { tabId, oid, path }); } export function getBlame( + tabId: string, path: string, commitOid: string | null, ): Promise { - return invoke("get_blame", { path, commitOid }); + return invoke("get_blame", { tabId, path, commitOid }); } export function getFileHistory( + tabId: string, path: string, limit: number, skip: number, ): Promise { - return invoke("get_file_history", { path, limit, skip }); + return invoke("get_file_history", { + tabId, + path, + limit, + skip, + }); } diff --git a/src/services/hosting.ts b/src/services/hosting.ts index cc65a9e..93beccc 100644 --- a/src/services/hosting.ts +++ b/src/services/hosting.ts @@ -59,31 +59,35 @@ export interface Issue { url: string; } -export function detectHostingProvider(): Promise { - return invoke("detect_hosting_provider"); +export function detectHostingProvider(tabId: string): Promise { + return invoke("detect_hosting_provider", { tabId }); } -export function listPullRequests(): Promise { - return invoke("list_pull_requests"); +export function listPullRequests(tabId: string): Promise { + return invoke("list_pull_requests", { tabId }); } -export function getPullRequestDetail(number: number): Promise { - return invoke("get_pull_request_detail", { number }); +export function getPullRequestDetail( + tabId: string, + number: number, +): Promise { + return invoke("get_pull_request_detail", { tabId, number }); } -export function listIssues(): Promise { - return invoke("list_issues"); +export function listIssues(tabId: string): Promise { + return invoke("list_issues", { tabId }); } -export function getDefaultBranch(): Promise { - return invoke("get_default_branch"); +export function getDefaultBranch(tabId: string): Promise { + return invoke("get_default_branch", { tabId }); } export function createPullRequestUrl( + tabId: string, head: string, base: string, ): Promise { - return invoke("create_pull_request_url", { head, base }); + return invoke("create_pull_request_url", { tabId, head, base }); } export function openInBrowser(url: string): Promise { diff --git a/src/services/rebase.ts b/src/services/rebase.ts index 8db6551..751bcea 100644 --- a/src/services/rebase.ts +++ b/src/services/rebase.ts @@ -29,36 +29,38 @@ export interface RebaseResult { conflicts: string[]; } -export function rebase(onto: string): Promise { - return invoke("rebase", { onto }); +export function rebase(tabId: string, onto: string): Promise { + return invoke("rebase", { tabId, onto }); } export function interactiveRebase( + tabId: string, onto: string, todo: RebaseTodoEntry[], ): Promise { - return invoke("interactive_rebase", { onto, todo }); + return invoke("interactive_rebase", { tabId, onto, todo }); } -export function isRebasing(): Promise { - return invoke("is_rebasing"); +export function isRebasing(tabId: string): Promise { + return invoke("is_rebasing", { tabId }); } -export function abortRebase(): Promise { - return invoke("abort_rebase"); +export function abortRebase(tabId: string): Promise { + return invoke("abort_rebase", { tabId }); } -export function continueRebase(): Promise { - return invoke("continue_rebase"); +export function continueRebase(tabId: string): Promise { + return invoke("continue_rebase", { tabId }); } -export function getRebaseState(): Promise { - return invoke("get_rebase_state"); +export function getRebaseState(tabId: string): Promise { + return invoke("get_rebase_state", { tabId }); } export function getRebaseTodo( + tabId: string, onto: string, limit: number, ): Promise { - return invoke("get_rebase_todo", { onto, limit }); + return invoke("get_rebase_todo", { tabId, onto, limit }); } diff --git a/src/services/reflog.ts b/src/services/reflog.ts index f43f7be..ebab446 100644 --- a/src/services/reflog.ts +++ b/src/services/reflog.ts @@ -12,10 +12,12 @@ export interface ReflogEntry { } export function getReflog( + tabId: string, refName: string, limit: number, ): Promise { return invoke("get_reflog", { + tabId, refName, limit, }); diff --git a/src/services/repo.ts b/src/services/repo.ts index 5c81f0f..fa0b566 100644 --- a/src/services/repo.ts +++ b/src/services/repo.ts @@ -6,22 +6,28 @@ export interface RecentRepo { last_opened: string; } -export function openRepository(path: string): Promise { - return invoke("open_repository", { path }); +export function openRepository(path: string, tabId: string): Promise { + return invoke("open_repository", { path, tabId }); } export function initRepository( path: string, + tabId: string, gitignoreTemplate?: string, ): Promise { return invoke("init_repository", { path, gitignoreTemplate: gitignoreTemplate || null, + tabId, }); } -export function cloneRepository(url: string, path: string): Promise { - return invoke("clone_repository", { url, path }); +export function cloneRepository( + url: string, + path: string, + tabId: string, +): Promise { + return invoke("clone_repository", { url, path, tabId }); } export function getRecentRepos(): Promise { diff --git a/src/services/reset.ts b/src/services/reset.ts index 8447043..e55808d 100644 --- a/src/services/reset.ts +++ b/src/services/reset.ts @@ -7,12 +7,17 @@ export interface ResetResult { } export function resetToCommit( + tabId: string, oid: string, mode: ResetMode, ): Promise { - return invoke("reset", { oid, mode }); + return invoke("reset", { tabId, oid, mode }); } -export function resetFile(path: string, oid: string): Promise { - return invoke("reset_file", { path, oid }); +export function resetFile( + tabId: string, + path: string, + oid: string, +): Promise { + return invoke("reset_file", { tabId, path, oid }); } diff --git a/src/services/revert.ts b/src/services/revert.ts index 155c099..3f3cc1d 100644 --- a/src/services/revert.ts +++ b/src/services/revert.ts @@ -9,20 +9,21 @@ export interface RevertResult { } export function revertCommit( + tabId: string, oid: string, mode: RevertMode, ): Promise { - return invoke("revert", { oid, mode }); + return invoke("revert", { tabId, oid, mode }); } -export function isReverting(): Promise { - return invoke("is_reverting"); +export function isReverting(tabId: string): Promise { + return invoke("is_reverting", { tabId }); } -export function abortRevert(): Promise { - return invoke("abort_revert"); +export function abortRevert(tabId: string): Promise { + return invoke("abort_revert", { tabId }); } -export function continueRevert(): Promise { - return invoke("continue_revert"); +export function continueRevert(tabId: string): Promise { + return invoke("continue_revert", { tabId }); } diff --git a/src/services/search.ts b/src/services/search.ts index f0c9bc1..34d7bb0 100644 --- a/src/services/search.ts +++ b/src/services/search.ts @@ -19,21 +19,28 @@ export interface FilenameSearchResult { } export function searchCode( + tabId: string, query: string, isRegex: boolean, ): Promise { - return invoke("search_code", { query, isRegex }); + return invoke("search_code", { tabId, query, isRegex }); } export function searchCommits( + tabId: string, query: string, searchDiff: boolean, ): Promise { - return invoke("search_commits", { query, searchDiff }); + return invoke("search_commits", { + tabId, + query, + searchDiff, + }); } export function searchFilenames( + tabId: string, query: string, ): Promise { - return invoke("search_filenames", { query }); + return invoke("search_filenames", { tabId, query }); } diff --git a/src/services/stash.ts b/src/services/stash.ts index 633cc9d..46d856e 100644 --- a/src/services/stash.ts +++ b/src/services/stash.ts @@ -8,26 +8,32 @@ export interface StashEntry { author_date: number; } -export function stashSave(message: string | null): Promise { - return invoke("stash_save", { message }); +export function stashSave( + tabId: string, + message: string | null, +): Promise { + return invoke("stash_save", { tabId, message }); } -export function listStashes(): Promise { - return invoke("list_stashes"); +export function listStashes(tabId: string): Promise { + return invoke("list_stashes", { tabId }); } -export function applyStash(index: number): Promise { - return invoke("apply_stash", { index }); +export function applyStash(tabId: string, index: number): Promise { + return invoke("apply_stash", { tabId, index }); } -export function popStash(index: number): Promise { - return invoke("pop_stash", { index }); +export function popStash(tabId: string, index: number): Promise { + return invoke("pop_stash", { tabId, index }); } -export function dropStash(index: number): Promise { - return invoke("drop_stash", { index }); +export function dropStash(tabId: string, index: number): Promise { + return invoke("drop_stash", { tabId, index }); } -export function getStashDiff(index: number): Promise { - return invoke("get_stash_diff", { index }); +export function getStashDiff( + tabId: string, + index: number, +): Promise { + return invoke("get_stash_diff", { tabId, index }); } diff --git a/src/services/submodule.ts b/src/services/submodule.ts index c1d5d93..6db15c9 100644 --- a/src/services/submodule.ts +++ b/src/services/submodule.ts @@ -15,22 +15,26 @@ export interface SubmoduleInfo { status: SubmoduleStatus; } -export function listSubmodules(): Promise { - return invoke("list_submodules"); +export function listSubmodules(tabId: string): Promise { + return invoke("list_submodules", { tabId }); } -export function addSubmodule(url: string, path: string): Promise { - return invoke("add_submodule", { url, path }); +export function addSubmodule( + tabId: string, + url: string, + path: string, +): Promise { + return invoke("add_submodule", { tabId, url, path }); } -export function updateSubmodule(path: string): Promise { - return invoke("update_submodule", { path }); +export function updateSubmodule(tabId: string, path: string): Promise { + return invoke("update_submodule", { tabId, path }); } -export function updateAllSubmodules(): Promise { - return invoke("update_all_submodules"); +export function updateAllSubmodules(tabId: string): Promise { + return invoke("update_all_submodules", { tabId }); } -export function removeSubmodule(path: string): Promise { - return invoke("remove_submodule", { path }); +export function removeSubmodule(tabId: string, path: string): Promise { + return invoke("remove_submodule", { tabId, path }); } diff --git a/src/services/tab.ts b/src/services/tab.ts new file mode 100644 index 0000000..870466f --- /dev/null +++ b/src/services/tab.ts @@ -0,0 +1,27 @@ +import { invoke } from "@tauri-apps/api/core"; + +export interface TabInfo { + id: string; + name: string; + path: string; +} + +export function openTab(path: string, tabId: string): Promise { + return invoke("open_tab", { path, tabId }); +} + +export function closeTab(tabId: string): Promise { + return invoke("close_tab", { tabId }); +} + +export function setActiveTab(tabId: string): Promise { + return invoke("set_active_tab", { tabId }); +} + +export function listTabs(): Promise { + return invoke("list_tabs"); +} + +export function getActiveTab(): Promise { + return invoke("get_active_tab"); +} diff --git a/src/services/tag.ts b/src/services/tag.ts index e096003..ac2cedb 100644 --- a/src/services/tag.ts +++ b/src/services/tag.ts @@ -10,18 +10,22 @@ export interface TagInfo { message: string | null; } -export function listTags(): Promise { - return invoke("list_tags"); +export function listTags(tabId: string): Promise { + return invoke("list_tags", { tabId }); } -export function createTag(name: string, message: string | null): Promise { - return invoke("create_tag", { name, message }); +export function createTag( + tabId: string, + name: string, + message: string | null, +): Promise { + return invoke("create_tag", { tabId, name, message }); } -export function deleteTag(name: string): Promise { - return invoke("delete_tag", { name }); +export function deleteTag(tabId: string, name: string): Promise { + return invoke("delete_tag", { tabId, name }); } -export function checkoutTag(name: string): Promise { - return invoke("checkout_tag", { name }); +export function checkoutTag(tabId: string, name: string): Promise { + return invoke("checkout_tag", { tabId, name }); } diff --git a/src/services/worktree.ts b/src/services/worktree.ts index df53152..69a67b1 100644 --- a/src/services/worktree.ts +++ b/src/services/worktree.ts @@ -9,14 +9,18 @@ export interface WorktreeInfo { is_clean: boolean; } -export function listWorktrees(): Promise { - return invoke("list_worktrees"); +export function listWorktrees(tabId: string): Promise { + return invoke("list_worktrees", { tabId }); } -export function addWorktree(path: string, branch: string): Promise { - return invoke("add_worktree", { path, branch }); +export function addWorktree( + tabId: string, + path: string, + branch: string, +): Promise { + return invoke("add_worktree", { tabId, path, branch }); } -export function removeWorktree(path: string): Promise { - return invoke("remove_worktree", { path }); +export function removeWorktree(tabId: string, path: string): Promise { + return invoke("remove_worktree", { tabId, path }); } diff --git a/src/stores/aiStore.ts b/src/stores/aiStore.ts index ab67328..b506757 100644 --- a/src/stores/aiStore.ts +++ b/src/stores/aiStore.ts @@ -17,6 +17,7 @@ import { reviewDiff as reviewDiffService, saveAiConfig as saveAiConfigService, } from "../services/ai"; +import { getActiveTabId } from "./tabStore"; interface AiState { adapters: CliAdapterInfo[]; @@ -75,7 +76,11 @@ export const useAiStore = create((set) => ({ ) => { set({ generating: true, error: null }); try { - const result = await generateCommitMessageService(format, language); + const result = await generateCommitMessageService( + getActiveTabId(), + format, + language, + ); set({ lastResult: result, generating: false }); return result; } catch (e) { @@ -87,7 +92,7 @@ export const useAiStore = create((set) => ({ reviewDiff: async () => { set({ reviewing: true, error: null }); try { - const result = await reviewDiffService(); + const result = await reviewDiffService(getActiveTabId()); set({ reviewComments: result.comments, reviewing: false }); return result; } catch (e) { diff --git a/src/stores/gitStore.ts b/src/stores/gitStore.ts index 74dc502..58598d9 100644 --- a/src/stores/gitStore.ts +++ b/src/stores/gitStore.ts @@ -114,6 +114,7 @@ import { listWorktrees, removeWorktree as removeWorktreeService, } from "../services/worktree"; +import { getActiveTabId } from "./tabStore"; const REBASE_TODO_DEFAULT_LIMIT = 100; @@ -247,7 +248,7 @@ export const useGitStore = create((set) => ({ fetchStatus: async () => { set({ loading: true, error: null }); try { - const status = await getStatus(); + const status = await getStatus(getActiveTabId()); set({ status, loading: false }); } catch (e) { set({ error: String(e), loading: false }); @@ -257,7 +258,7 @@ export const useGitStore = create((set) => ({ fetchBranch: async () => { try { - const currentBranch = await getCurrentBranch(); + const currentBranch = await getCurrentBranch(getActiveTabId()); set({ currentBranch }); } catch (e) { set({ error: String(e) }); @@ -267,7 +268,7 @@ export const useGitStore = create((set) => ({ fetchBranches: async () => { try { - const branches = await listBranches(); + const branches = await listBranches(getActiveTabId()); set({ branches }); } catch (e) { set({ error: String(e) }); @@ -277,7 +278,7 @@ export const useGitStore = create((set) => ({ fetchDiff: async (path: string | null, staged: boolean) => { try { - const diff = await getDiff(path, staged); + const diff = await getDiff(getActiveTabId(), path, staged); set({ diff }); } catch (e) { set({ error: String(e) }); @@ -287,7 +288,7 @@ export const useGitStore = create((set) => ({ stageFile: async (path: string) => { try { - await stageFileService(path); + await stageFileService(getActiveTabId(), path); } catch (e) { set({ error: String(e) }); throw e; @@ -296,7 +297,7 @@ export const useGitStore = create((set) => ({ unstageFile: async (path: string) => { try { - await unstageFileService(path); + await unstageFileService(getActiveTabId(), path); } catch (e) { set({ error: String(e) }); throw e; @@ -305,7 +306,7 @@ export const useGitStore = create((set) => ({ stageAll: async () => { try { - await stageAllService(); + await stageAllService(getActiveTabId()); } catch (e) { set({ error: String(e) }); throw e; @@ -314,7 +315,7 @@ export const useGitStore = create((set) => ({ unstageAll: async () => { try { - await unstageAllService(); + await unstageAllService(getActiveTabId()); } catch (e) { set({ error: String(e) }); throw e; @@ -323,7 +324,12 @@ export const useGitStore = create((set) => ({ commit: async (message: string, amend: boolean, sign: boolean) => { try { - const result = await commitChanges(message, amend, sign); + const result = await commitChanges( + getActiveTabId(), + message, + amend, + sign, + ); return result.oid; } catch (e) { set({ error: String(e) }); @@ -333,7 +339,7 @@ export const useGitStore = create((set) => ({ createBranch: async (name: string) => { try { - await createBranchService(name); + await createBranchService(getActiveTabId(), name); } catch (e) { set({ error: String(e) }); throw e; @@ -342,7 +348,7 @@ export const useGitStore = create((set) => ({ checkoutBranch: async (name: string) => { try { - await checkoutBranchService(name); + await checkoutBranchService(getActiveTabId(), name); } catch (e) { set({ error: String(e) }); throw e; @@ -351,7 +357,7 @@ export const useGitStore = create((set) => ({ deleteBranch: async (name: string) => { try { - await deleteBranchService(name); + await deleteBranchService(getActiveTabId(), name); } catch (e) { set({ error: String(e) }); throw e; @@ -360,7 +366,7 @@ export const useGitStore = create((set) => ({ renameBranch: async (oldName: string, newName: string) => { try { - await renameBranchService(oldName, newName); + await renameBranchService(getActiveTabId(), oldName, newName); } catch (e) { set({ error: String(e) }); throw e; @@ -369,7 +375,7 @@ export const useGitStore = create((set) => ({ mergeBranch: async (branchName: string, option: MergeOption) => { try { - return await mergeBranchService(branchName, option); + return await mergeBranchService(getActiveTabId(), branchName, option); } catch (e) { set({ error: String(e) }); throw e; @@ -378,7 +384,7 @@ export const useGitStore = create((set) => ({ fetchRemote: async (remoteName: string) => { try { - return await fetchRemoteService(remoteName); + return await fetchRemoteService(getActiveTabId(), remoteName); } catch (e) { set({ error: String(e) }); throw e; @@ -387,7 +393,7 @@ export const useGitStore = create((set) => ({ pullRemote: async (remoteName: string, option: PullOption) => { try { - return await pullRemoteService(remoteName, option); + return await pullRemoteService(getActiveTabId(), remoteName, option); } catch (e) { set({ error: String(e) }); throw e; @@ -396,7 +402,7 @@ export const useGitStore = create((set) => ({ pushRemote: async (remoteName: string) => { try { - return await pushRemoteService(remoteName); + return await pushRemoteService(getActiveTabId(), remoteName); } catch (e) { set({ error: String(e) }); throw e; @@ -405,7 +411,7 @@ export const useGitStore = create((set) => ({ fetchRemotes: async () => { try { - const remotes = await listRemotes(); + const remotes = await listRemotes(getActiveTabId()); set({ remotes }); } catch (e) { set({ error: String(e) }); @@ -415,7 +421,7 @@ export const useGitStore = create((set) => ({ addRemote: async (name: string, url: string) => { try { - await addRemoteService(name, url); + await addRemoteService(getActiveTabId(), name, url); } catch (e) { set({ error: String(e) }); throw e; @@ -424,7 +430,7 @@ export const useGitStore = create((set) => ({ removeRemote: async (name: string) => { try { - await removeRemoteService(name); + await removeRemoteService(getActiveTabId(), name); } catch (e) { set({ error: String(e) }); throw e; @@ -433,7 +439,7 @@ export const useGitStore = create((set) => ({ editRemote: async (name: string, newUrl: string) => { try { - await editRemoteService(name, newUrl); + await editRemoteService(getActiveTabId(), name, newUrl); } catch (e) { set({ error: String(e) }); throw e; @@ -442,7 +448,7 @@ export const useGitStore = create((set) => ({ stageHunk: async (path: string, hunk: HunkIdentifier) => { try { - await stageHunkService(path, hunk); + await stageHunkService(getActiveTabId(), path, hunk); } catch (e) { set({ error: String(e) }); throw e; @@ -451,7 +457,7 @@ export const useGitStore = create((set) => ({ unstageHunk: async (path: string, hunk: HunkIdentifier) => { try { - await unstageHunkService(path, hunk); + await unstageHunkService(getActiveTabId(), path, hunk); } catch (e) { set({ error: String(e) }); throw e; @@ -460,7 +466,7 @@ export const useGitStore = create((set) => ({ discardHunk: async (path: string, hunk: HunkIdentifier) => { try { - await discardHunkService(path, hunk); + await discardHunkService(getActiveTabId(), path, hunk); } catch (e) { set({ error: String(e) }); throw e; @@ -469,7 +475,7 @@ export const useGitStore = create((set) => ({ stageLines: async (path: string, lineRange: LineRange) => { try { - await stageLinesService(path, lineRange); + await stageLinesService(getActiveTabId(), path, lineRange); } catch (e) { set({ error: String(e) }); throw e; @@ -478,7 +484,7 @@ export const useGitStore = create((set) => ({ unstageLines: async (path: string, lineRange: LineRange) => { try { - await unstageLinesService(path, lineRange); + await unstageLinesService(getActiveTabId(), path, lineRange); } catch (e) { set({ error: String(e) }); throw e; @@ -487,7 +493,7 @@ export const useGitStore = create((set) => ({ discardLines: async (path: string, lineRange: LineRange) => { try { - await discardLinesService(path, lineRange); + await discardLinesService(getActiveTabId(), path, lineRange); } catch (e) { set({ error: String(e) }); throw e; @@ -496,7 +502,7 @@ export const useGitStore = create((set) => ({ getHeadCommitMessage: async () => { try { - return await getHeadCommitMessage(); + return await getHeadCommitMessage(getActiveTabId()); } catch (e) { set({ error: String(e) }); throw e; @@ -505,7 +511,7 @@ export const useGitStore = create((set) => ({ fetchStashes: async () => { try { - const stashes = await listStashes(); + const stashes = await listStashes(getActiveTabId()); set({ stashes }); } catch (e) { set({ error: String(e) }); @@ -515,7 +521,7 @@ export const useGitStore = create((set) => ({ stashSave: async (message: string | null) => { try { - await stashSaveService(message); + await stashSaveService(getActiveTabId(), message); } catch (e) { set({ error: String(e) }); throw e; @@ -524,7 +530,7 @@ export const useGitStore = create((set) => ({ applyStash: async (index: number) => { try { - await applyStashService(index); + await applyStashService(getActiveTabId(), index); } catch (e) { set({ error: String(e) }); throw e; @@ -533,7 +539,7 @@ export const useGitStore = create((set) => ({ popStash: async (index: number) => { try { - await popStashService(index); + await popStashService(getActiveTabId(), index); } catch (e) { set({ error: String(e) }); throw e; @@ -542,7 +548,7 @@ export const useGitStore = create((set) => ({ dropStash: async (index: number) => { try { - await dropStashService(index); + await dropStashService(getActiveTabId(), index); } catch (e) { set({ error: String(e) }); throw e; @@ -551,7 +557,7 @@ export const useGitStore = create((set) => ({ fetchTags: async () => { try { - const tags = await listTags(); + const tags = await listTags(getActiveTabId()); set({ tags }); } catch (e) { set({ error: String(e) }); @@ -561,7 +567,7 @@ export const useGitStore = create((set) => ({ createTag: async (name: string, message: string | null) => { try { - await createTagService(name, message); + await createTagService(getActiveTabId(), name, message); } catch (e) { set({ error: String(e) }); throw e; @@ -570,7 +576,7 @@ export const useGitStore = create((set) => ({ deleteTag: async (name: string) => { try { - await deleteTagService(name); + await deleteTagService(getActiveTabId(), name); } catch (e) { set({ error: String(e) }); throw e; @@ -579,7 +585,7 @@ export const useGitStore = create((set) => ({ checkoutTag: async (name: string) => { try { - await checkoutTagService(name); + await checkoutTagService(getActiveTabId(), name); } catch (e) { set({ error: String(e) }); throw e; @@ -588,7 +594,7 @@ export const useGitStore = create((set) => ({ fetchMergeState: async () => { try { - const merging = await isMergingService(); + const merging = await isMergingService(getActiveTabId()); set({ merging }); } catch (e) { set({ error: String(e) }); @@ -598,7 +604,7 @@ export const useGitStore = create((set) => ({ fetchConflictFiles: async () => { try { - const conflictFiles = await getConflictFiles(); + const conflictFiles = await getConflictFiles(getActiveTabId()); set({ conflictFiles }); } catch (e) { set({ error: String(e) }); @@ -608,7 +614,7 @@ export const useGitStore = create((set) => ({ resolveConflict: async (path: string, resolution: ConflictResolution) => { try { - await resolveConflictService(path, resolution); + await resolveConflictService(getActiveTabId(), path, resolution); } catch (e) { set({ error: String(e) }); throw e; @@ -621,7 +627,12 @@ export const useGitStore = create((set) => ({ resolution: ConflictResolution, ) => { try { - await resolveConflictBlockService(path, blockIndex, resolution); + await resolveConflictBlockService( + getActiveTabId(), + path, + blockIndex, + resolution, + ); } catch (e) { set({ error: String(e) }); throw e; @@ -630,7 +641,7 @@ export const useGitStore = create((set) => ({ markResolved: async (path: string) => { try { - await markResolvedService(path); + await markResolvedService(getActiveTabId(), path); } catch (e) { set({ error: String(e) }); throw e; @@ -639,7 +650,7 @@ export const useGitStore = create((set) => ({ abortMerge: async () => { try { - await abortMergeService(); + await abortMergeService(getActiveTabId()); set({ merging: false, conflictFiles: [] }); } catch (e) { set({ error: String(e) }); @@ -649,7 +660,7 @@ export const useGitStore = create((set) => ({ continueMerge: async (message: string) => { try { - const result = await continueMergeService(message); + const result = await continueMergeService(getActiveTabId(), message); set({ merging: false, conflictFiles: [] }); return result.oid; } catch (e) { @@ -660,8 +671,8 @@ export const useGitStore = create((set) => ({ fetchRebaseState: async () => { try { - const rebasing = await isRebasingService(); - const rebaseState = await getRebaseState(); + const rebasing = await isRebasingService(getActiveTabId()); + const rebaseState = await getRebaseState(getActiveTabId()); set({ rebasing, rebaseState }); } catch (e) { set({ error: String(e) }); @@ -671,7 +682,7 @@ export const useGitStore = create((set) => ({ rebase: async (onto: string) => { try { - return await rebaseService(onto); + return await rebaseService(getActiveTabId(), onto); } catch (e) { set({ error: String(e) }); throw e; @@ -680,7 +691,7 @@ export const useGitStore = create((set) => ({ interactiveRebase: async (onto: string, todo: RebaseTodoEntry[]) => { try { - return await interactiveRebaseService(onto, todo); + return await interactiveRebaseService(getActiveTabId(), onto, todo); } catch (e) { set({ error: String(e) }); throw e; @@ -689,7 +700,7 @@ export const useGitStore = create((set) => ({ abortRebase: async () => { try { - await abortRebaseService(); + await abortRebaseService(getActiveTabId()); set({ rebasing: false, rebaseState: null, conflictFiles: [] }); } catch (e) { set({ error: String(e) }); @@ -699,7 +710,7 @@ export const useGitStore = create((set) => ({ continueRebase: async () => { try { - const result = await continueRebaseService(); + const result = await continueRebaseService(getActiveTabId()); if (result.completed) { set({ rebasing: false, rebaseState: null, conflictFiles: [] }); } @@ -712,7 +723,11 @@ export const useGitStore = create((set) => ({ getRebaseTodo: async (onto: string) => { try { - return await getRebaseTodoService(onto, REBASE_TODO_DEFAULT_LIMIT); + return await getRebaseTodoService( + getActiveTabId(), + onto, + REBASE_TODO_DEFAULT_LIMIT, + ); } catch (e) { set({ error: String(e) }); throw e; @@ -721,7 +736,7 @@ export const useGitStore = create((set) => ({ cherryPick: async (oids: string[], mode: CherryPickMode) => { try { - const result = await cherryPickService(oids, mode); + const result = await cherryPickService(getActiveTabId(), oids, mode); if (result.completed) { set({ cherryPicking: false }); } else { @@ -736,7 +751,7 @@ export const useGitStore = create((set) => ({ fetchCherryPickState: async () => { try { - const cherryPicking = await isCherryPickingService(); + const cherryPicking = await isCherryPickingService(getActiveTabId()); set({ cherryPicking }); } catch (e) { set({ error: String(e) }); @@ -746,7 +761,7 @@ export const useGitStore = create((set) => ({ abortCherryPick: async () => { try { - await abortCherryPickService(); + await abortCherryPickService(getActiveTabId()); set({ cherryPicking: false, conflictFiles: [] }); } catch (e) { set({ error: String(e) }); @@ -756,7 +771,7 @@ export const useGitStore = create((set) => ({ continueCherryPick: async () => { try { - const result = await continueCherryPickService(); + const result = await continueCherryPickService(getActiveTabId()); if (result.completed) { set({ cherryPicking: false, conflictFiles: [] }); } @@ -769,7 +784,7 @@ export const useGitStore = create((set) => ({ revertCommit: async (oid: string, mode: RevertMode) => { try { - const result = await revertCommitService(oid, mode); + const result = await revertCommitService(getActiveTabId(), oid, mode); if (result.completed) { set({ reverting: false }); } else { @@ -784,7 +799,7 @@ export const useGitStore = create((set) => ({ fetchRevertState: async () => { try { - const reverting = await isRevertingService(); + const reverting = await isRevertingService(getActiveTabId()); set({ reverting }); } catch (e) { set({ error: String(e) }); @@ -794,7 +809,7 @@ export const useGitStore = create((set) => ({ abortRevert: async () => { try { - await abortRevertService(); + await abortRevertService(getActiveTabId()); set({ reverting: false, conflictFiles: [] }); } catch (e) { set({ error: String(e) }); @@ -804,7 +819,7 @@ export const useGitStore = create((set) => ({ continueRevert: async () => { try { - const result = await continueRevertService(); + const result = await continueRevertService(getActiveTabId()); if (result.completed) { set({ reverting: false, conflictFiles: [] }); } @@ -817,7 +832,7 @@ export const useGitStore = create((set) => ({ resetToCommit: async (oid: string, mode: ResetMode) => { try { - return await resetToCommitService(oid, mode); + return await resetToCommitService(getActiveTabId(), oid, mode); } catch (e) { set({ error: String(e) }); throw e; @@ -826,7 +841,7 @@ export const useGitStore = create((set) => ({ resetFile: async (path: string, oid: string) => { try { - await resetFileService(path, oid); + await resetFileService(getActiveTabId(), path, oid); } catch (e) { set({ error: String(e) }); throw e; @@ -835,7 +850,7 @@ export const useGitStore = create((set) => ({ fetchSubmodules: async () => { try { - const submodules = await listSubmodules(); + const submodules = await listSubmodules(getActiveTabId()); set({ submodules }); } catch (e) { set({ error: String(e) }); @@ -845,7 +860,7 @@ export const useGitStore = create((set) => ({ addSubmodule: async (url: string, path: string) => { try { - await addSubmoduleService(url, path); + await addSubmoduleService(getActiveTabId(), url, path); } catch (e) { set({ error: String(e) }); throw e; @@ -854,7 +869,7 @@ export const useGitStore = create((set) => ({ updateSubmodule: async (path: string) => { try { - await updateSubmoduleService(path); + await updateSubmoduleService(getActiveTabId(), path); } catch (e) { set({ error: String(e) }); throw e; @@ -863,7 +878,7 @@ export const useGitStore = create((set) => ({ updateAllSubmodules: async () => { try { - await updateAllSubmodulesService(); + await updateAllSubmodulesService(getActiveTabId()); } catch (e) { set({ error: String(e) }); throw e; @@ -872,7 +887,7 @@ export const useGitStore = create((set) => ({ removeSubmodule: async (path: string) => { try { - await removeSubmoduleService(path); + await removeSubmoduleService(getActiveTabId(), path); } catch (e) { set({ error: String(e) }); throw e; @@ -881,7 +896,7 @@ export const useGitStore = create((set) => ({ fetchWorktrees: async () => { try { - const worktrees = await listWorktrees(); + const worktrees = await listWorktrees(getActiveTabId()); set({ worktrees }); } catch (e) { set({ error: String(e) }); @@ -891,7 +906,7 @@ export const useGitStore = create((set) => ({ addWorktree: async (path: string, branch: string) => { try { - await addWorktreeService(path, branch); + await addWorktreeService(getActiveTabId(), path, branch); } catch (e) { set({ error: String(e) }); throw e; @@ -900,7 +915,7 @@ export const useGitStore = create((set) => ({ removeWorktree: async (path: string) => { try { - await removeWorktreeService(path); + await removeWorktreeService(getActiveTabId(), path); } catch (e) { set({ error: String(e) }); throw e; diff --git a/src/stores/historyStore.ts b/src/stores/historyStore.ts index 79e2615..9a0aeb2 100644 --- a/src/stores/historyStore.ts +++ b/src/stores/historyStore.ts @@ -14,6 +14,7 @@ import { getCommitLog, getFileHistory, } from "../services/history"; +import { getActiveTabId } from "./tabStore"; interface HistoryState { commits: CommitInfo[]; @@ -83,7 +84,12 @@ export const useHistoryStore = create((set) => ({ set({ loading: true, error: null }); try { const state = useHistoryStore.getState(); - const result = await getCommitLog(state.filter, limit, skip); + const result = await getCommitLog( + getActiveTabId(), + state.filter, + limit, + skip, + ); set({ commits: result.commits, graph: result.graph, @@ -98,7 +104,7 @@ export const useHistoryStore = create((set) => ({ selectCommit: async (oid: string) => { set({ selectedCommitOid: oid, expandedFileDiffs: {} }); try { - const detail = await getCommitDetail(oid); + const detail = await getCommitDetail(getActiveTabId(), oid); set({ commitDetail: detail }); } catch (e) { set({ error: String(e) }); @@ -120,7 +126,7 @@ export const useHistoryStore = create((set) => ({ fetchFileDiff: async (oid: string, path: string) => { try { - const diffs = await getCommitFileDiff(oid, path); + const diffs = await getCommitFileDiff(getActiveTabId(), oid, path); set((state) => ({ expandedFileDiffs: { ...state.expandedFileDiffs, [path]: diffs }, })); @@ -141,7 +147,7 @@ export const useHistoryStore = create((set) => ({ fetchBlame: async (path: string, commitOid: string | null) => { set({ blameLoading: true }); try { - const result = await getBlame(path, commitOid); + const result = await getBlame(getActiveTabId(), path, commitOid); set({ blameResult: result, blameLoading: false }); } catch (e) { set({ error: String(e), blameLoading: false }); @@ -152,7 +158,7 @@ export const useHistoryStore = create((set) => ({ fetchFileHistory: async (path: string, limit: number, skip: number) => { set({ fileHistoryLoading: true }); try { - const commits = await getFileHistory(path, limit, skip); + const commits = await getFileHistory(getActiveTabId(), path, limit, skip); set({ fileHistoryCommits: commits, fileHistoryLoading: false }); } catch (e) { set({ error: String(e), fileHistoryLoading: false }); @@ -163,7 +169,7 @@ export const useHistoryStore = create((set) => ({ selectFileHistoryCommit: async (oid: string) => { set({ fileHistorySelectedOid: oid }); try { - const detail = await getCommitDetail(oid); + const detail = await getCommitDetail(getActiveTabId(), oid); set({ fileHistoryDetail: detail }); } catch (e) { set({ error: String(e) }); diff --git a/src/stores/hostingStore.ts b/src/stores/hostingStore.ts index bbf9a68..359acd6 100644 --- a/src/stores/hostingStore.ts +++ b/src/stores/hostingStore.ts @@ -12,6 +12,7 @@ import { listIssues, listPullRequests, } from "../services/hosting"; +import { getActiveTabId } from "./tabStore"; type HostingTab = "pulls" | "issues"; @@ -51,7 +52,7 @@ export const useHostingStore = create((set) => ({ fetchHostingInfo: async () => { set({ loading: true, error: null }); try { - const hostingInfo = await detectHostingProvider(); + const hostingInfo = await detectHostingProvider(getActiveTabId()); set({ hostingInfo, loading: false }); } catch (e) { set({ error: String(e), loading: false }); @@ -61,7 +62,7 @@ export const useHostingStore = create((set) => ({ fetchDefaultBranch: async () => { try { - const defaultBranch = await getDefaultBranch(); + const defaultBranch = await getDefaultBranch(getActiveTabId()); set({ defaultBranch }); } catch (_e) { // デフォルトブランチ取得失敗は致命的ではないのでエラーを設定しない @@ -72,7 +73,7 @@ export const useHostingStore = create((set) => ({ fetchPullRequests: async () => { set({ loading: true, error: null }); try { - const pullRequests = await listPullRequests(); + const pullRequests = await listPullRequests(getActiveTabId()); set({ pullRequests, loading: false }); } catch (e) { set({ error: String(e), loading: false }); @@ -83,7 +84,7 @@ export const useHostingStore = create((set) => ({ fetchIssues: async () => { set({ loading: true, error: null }); try { - const issues = await listIssues(); + const issues = await listIssues(getActiveTabId()); set({ issues, loading: false }); } catch (e) { set({ error: String(e), loading: false }); @@ -94,7 +95,10 @@ export const useHostingStore = create((set) => ({ selectPr: async (number: number) => { set({ selectedPrNumber: number, loading: true }); try { - const selectedPrDetail = await getPullRequestDetail(number); + const selectedPrDetail = await getPullRequestDetail( + getActiveTabId(), + number, + ); set({ selectedPrDetail, loading: false }); } catch (e) { set({ error: String(e), loading: false }); diff --git a/src/stores/repoStore.ts b/src/stores/repoStore.ts index 063df8d..7284f50 100644 --- a/src/stores/repoStore.ts +++ b/src/stores/repoStore.ts @@ -7,6 +7,7 @@ import { openRepository as openRepositoryService, removeRecentRepo as removeRecentRepoService, } from "../services/repo"; +import { getActiveTabId } from "./tabStore"; interface RepoState { recentRepos: RecentRepo[]; @@ -40,7 +41,7 @@ export const useRepoStore = create((set) => ({ openRepository: async (path: string) => { set({ loading: true, error: null }); try { - await openRepositoryService(path); + await openRepositoryService(path, getActiveTabId()); set({ loading: false }); } catch (e) { set({ error: String(e), loading: false }); @@ -51,7 +52,7 @@ export const useRepoStore = create((set) => ({ initRepository: async (path: string, gitignoreTemplate?: string) => { set({ loading: true, error: null }); try { - await initRepositoryService(path, gitignoreTemplate); + await initRepositoryService(path, getActiveTabId(), gitignoreTemplate); set({ loading: false }); } catch (e) { set({ error: String(e), loading: false }); @@ -62,7 +63,7 @@ export const useRepoStore = create((set) => ({ cloneRepository: async (url: string, path: string) => { set({ loading: true, error: null }); try { - await cloneRepositoryService(url, path); + await cloneRepositoryService(url, path, getActiveTabId()); set({ loading: false }); } catch (e) { set({ error: String(e), loading: false }); diff --git a/src/stores/tabStore.ts b/src/stores/tabStore.ts new file mode 100644 index 0000000..a9e50de --- /dev/null +++ b/src/stores/tabStore.ts @@ -0,0 +1,100 @@ +import { create } from "zustand"; +import type { TabInfo } from "../services/tab"; +import { + closeTab as closeTabService, + getActiveTab, + listTabs, + openTab as openTabService, + setActiveTab as setActiveTabService, +} from "../services/tab"; + +interface Tab { + id: string; + name: string; + path: string; +} + +interface TabState { + tabs: Tab[]; + activeTabId: string | null; +} + +interface TabActions { + openTab: (path: string) => Promise; + closeTab: (tabId: string) => Promise; + setActiveTab: (tabId: string) => Promise; + reorderTabs: (fromIndex: number, toIndex: number) => void; + fetchTabs: () => Promise; + fetchActiveTab: () => Promise; +} + +let nextTabCounter = 0; + +function generateTabId(): string { + nextTabCounter += 1; + return `tab-${Date.now()}-${nextTabCounter}`; +} + +export const DEFAULT_TAB_ID = "default"; + +export function getActiveTabId(): string { + return useTabStore.getState().activeTabId ?? DEFAULT_TAB_ID; +} + +export const useTabStore = create((set) => ({ + tabs: [], + activeTabId: null, + + openTab: async (path: string) => { + const tabId = generateTabId(); + await openTabService(path, tabId); + const tabsResult = await listTabs(); + const tabs: Tab[] = tabsResult.map((t: TabInfo) => ({ + id: t.id, + name: t.name, + path: t.path, + })); + set({ tabs, activeTabId: tabId }); + }, + + closeTab: async (tabId: string) => { + await closeTabService(tabId); + const tabsResult = await listTabs(); + const tabs: Tab[] = tabsResult.map((t: TabInfo) => ({ + id: t.id, + name: t.name, + path: t.path, + })); + const activeTabId = await getActiveTab(); + set({ tabs, activeTabId }); + }, + + setActiveTab: async (tabId: string) => { + await setActiveTabService(tabId); + set({ activeTabId: tabId }); + }, + + reorderTabs: (fromIndex: number, toIndex: number) => { + set((state) => { + const newTabs = [...state.tabs]; + const [moved] = newTabs.splice(fromIndex, 1); + newTabs.splice(toIndex, 0, moved); + return { tabs: newTabs }; + }); + }, + + fetchTabs: async () => { + const tabsResult = await listTabs(); + const tabs: Tab[] = tabsResult.map((t: TabInfo) => ({ + id: t.id, + name: t.name, + path: t.path, + })); + set({ tabs }); + }, + + fetchActiveTab: async () => { + const activeTabId = await getActiveTab(); + set({ activeTabId }); + }, +})); From 04e405e76bce716187d182ebad25b2bf4585fb9c Mon Sep 17 00:00:00 2001 From: HMasataka Date: Sat, 7 Mar 2026 16:37:31 +0900 Subject: [PATCH 5/9] =?UTF-8?q?feat(frontend):=20Titlebar=E3=82=BF?= =?UTF-8?q?=E3=83=96UI=E3=83=BB=E3=82=B7=E3=82=B9=E3=83=86=E3=83=A0?= =?UTF-8?q?=E9=80=9A=E7=9F=A5=E3=83=95=E3=83=83=E3=82=AF=E3=83=BB=E3=83=9A?= =?UTF-8?q?=E3=83=BC=E3=82=B8=E3=82=B3=E3=83=B3=E3=83=9D=E3=83=BC=E3=83=8D?= =?UTF-8?q?=E3=83=B3=E3=83=88=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit デザインモックアップ準拠のタブUI(D&D対応、タブ追加/閉じる)をTitlebarに追加。 useSystemNotificationフックでアプリ非アクティブ時のシステム通知を実装。 全ページコンポーネントをgetActiveTabId()経由のtabId対応に更新。 Co-Authored-By: Claude Opus 4.6 --- package.json | 1 + pnpm-lock.yaml | 10 ++ src/App.tsx | 13 ++ src/components/organisms/SearchModal.tsx | 7 +- .../organisms/SettingsGitConfigTab.tsx | 9 +- src/components/organisms/Titlebar.tsx | 130 +++++++++++++++++- src/hooks/useCommitLog.ts | 6 +- src/hooks/useSystemNotification.ts | 64 +++++++++ src/pages/branches/index.tsx | 3 +- src/pages/cherry-pick/index.tsx | 5 +- .../conflict/organisms/MergeViewerModal.tsx | 3 +- src/pages/hosting/index.tsx | 3 +- src/pages/reflog/index.tsx | 3 +- src/pages/reset/index.tsx | 3 +- src/pages/revert/index.tsx | 3 +- .../stash/organisms/StashDetailPanel.tsx | 3 +- 16 files changed, 245 insertions(+), 21 deletions(-) create mode 100644 src/hooks/useSystemNotification.ts diff --git a/package.json b/package.json index 25ea301..9c46371 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "dependencies": { "@tauri-apps/api": "^2.10.1", "@tauri-apps/plugin-dialog": "^2", + "@tauri-apps/plugin-notification": "^2.3.3", "react": "^19.2.4", "react-dom": "^19.2.4", "zustand": "^5.0.11" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d075133..fc8ca9d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: '@tauri-apps/plugin-dialog': specifier: ^2 version: 2.6.0 + '@tauri-apps/plugin-notification': + specifier: ^2.3.3 + version: 2.3.3 react: specifier: ^19.2.4 version: 19.2.4 @@ -567,6 +570,9 @@ packages: '@tauri-apps/plugin-dialog@2.6.0': resolution: {integrity: sha512-q4Uq3eY87TdcYzXACiYSPhmpBA76shgmQswGkSVio4C82Sz2W4iehe9TnKYwbq7weHiL88Yw19XZm7v28+Micg==} + '@tauri-apps/plugin-notification@2.3.3': + resolution: {integrity: sha512-Zw+ZH18RJb41G4NrfHgIuofJiymusqN+q8fGUIIV7vyCH+5sSn5coqRv/MWB9qETsUs97vmU045q7OyseCV3Qg==} + '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -1295,6 +1301,10 @@ snapshots: dependencies: '@tauri-apps/api': 2.10.1 + '@tauri-apps/plugin-notification@2.3.3': + dependencies: + '@tauri-apps/api': 2.10.1 + '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.29.0 diff --git a/src/App.tsx b/src/App.tsx index 79b480f..1edabb3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,6 +7,7 @@ import { TagsModal } from "./components/organisms/TagsModal"; import { ToastContainer } from "./components/organisms/ToastContainer"; import { AppShell } from "./components/templates/AppShell"; import { useFileWatcher } from "./hooks/useFileWatcher"; +import { useSystemNotification } from "./hooks/useSystemNotification"; import { useTheme } from "./hooks/useTheme"; import { BlamePage } from "./pages/blame"; import { BranchesPage } from "./pages/branches"; @@ -29,6 +30,7 @@ import { WorktreesPage } from "./pages/worktrees"; import type { PullOption } from "./services/git"; import { useConfigStore } from "./stores/configStore"; import { useGitStore } from "./stores/gitStore"; +import { useTabStore } from "./stores/tabStore"; import { useUIStore } from "./stores/uiStore"; export function App() { @@ -54,9 +56,19 @@ export function App() { const activeModal = useUIStore((s) => s.activeModal); const openModal = useUIStore((s) => s.openModal); const closeModal = useUIStore((s) => s.closeModal); + const activeTabId = useTabStore((s) => s.activeTabId); + const fetchTabs = useTabStore((s) => s.fetchTabs); + const fetchActiveTab = useTabStore((s) => s.fetchActiveTab); useTheme(); + useSystemNotification(); + useEffect(() => { + fetchTabs(); + fetchActiveTab(); + }, [fetchTabs, fetchActiveTab]); + + // biome-ignore lint/correctness/useExhaustiveDependencies: activeTabId triggers re-fetch on tab switch useEffect(() => { loadConfig().catch((e: unknown) => { addToast(String(e), "error"); @@ -83,6 +95,7 @@ export function App() { addToast(String(e), "error"); }); }, [ + activeTabId, loadConfig, fetchBranch, fetchRemotes, diff --git a/src/components/organisms/SearchModal.tsx b/src/components/organisms/SearchModal.tsx index ab2d38b..3a75c33 100644 --- a/src/components/organisms/SearchModal.tsx +++ b/src/components/organisms/SearchModal.tsx @@ -9,6 +9,7 @@ import { searchCommits, searchFilenames, } from "../../services/search"; +import { getActiveTabId } from "../../stores/tabStore"; import { Modal } from "./Modal"; type SearchTab = "code" | "commits" | "filenames"; @@ -78,19 +79,19 @@ export function SearchModal({ onClose }: SearchModalProps) { const searchPromise = (() => { switch (tab) { case "code": - return searchCode(q, regex).then((results) => { + return searchCode(getActiveTabId(), q, regex).then((results) => { if (requestIdRef.current === thisRequestId) { setCodeResults(results); } }); case "commits": - return searchCommits(q, diff).then((results) => { + return searchCommits(getActiveTabId(), q, diff).then((results) => { if (requestIdRef.current === thisRequestId) { setCommitResults(results); } }); case "filenames": - return searchFilenames(q).then((results) => { + return searchFilenames(getActiveTabId(), q).then((results) => { if (requestIdRef.current === thisRequestId) { setFilenameResults(results); } diff --git a/src/components/organisms/SettingsGitConfigTab.tsx b/src/components/organisms/SettingsGitConfigTab.tsx index b4c7059..084a90a 100644 --- a/src/components/organisms/SettingsGitConfigTab.tsx +++ b/src/components/organisms/SettingsGitConfigTab.tsx @@ -12,6 +12,7 @@ import { setGitconfigValue, unsetGitconfigValue, } from "../../services/gitconfig"; +import { getActiveTabId } from "../../stores/tabStore"; import { useUIStore } from "../../stores/uiStore"; interface ConfigSection { @@ -155,8 +156,8 @@ export function SettingsGitConfigTab() { async (s: GitConfigScope) => { try { const [allEntries, path] = await Promise.all([ - getGitconfigEntries(s), - getGitconfigPath(s), + getGitconfigEntries(getActiveTabId(), s), + getGitconfigPath(getActiveTabId(), s), ]); const map = new Map(); const aliasList: GitConfigEntry[] = []; @@ -191,9 +192,9 @@ export function SettingsGitConfigTab() { async (key: string, value: string) => { try { if (value === "") { - await unsetGitconfigValue(scope, key); + await unsetGitconfigValue(getActiveTabId(), scope, key); } else { - await setGitconfigValue(scope, key, value); + await setGitconfigValue(getActiveTabId(), scope, key, value); } setEntries((prev) => { const next = new Map(prev); diff --git a/src/components/organisms/Titlebar.tsx b/src/components/organisms/Titlebar.tsx index f558bc7..7f6a317 100644 --- a/src/components/organisms/Titlebar.tsx +++ b/src/components/organisms/Titlebar.tsx @@ -1,4 +1,6 @@ import { getCurrentWindow } from "@tauri-apps/api/window"; +import { type DragEvent, useCallback, useState } from "react"; +import { useTabStore } from "../../stores/tabStore"; import { useUIStore } from "../../stores/uiStore"; const iconColor = "rgba(77,18,10,0.85)"; @@ -58,9 +60,93 @@ function MaximizeIcon() { ); } +function TabCloseIcon() { + return ( + + ); +} + +function PlusIcon() { + return ( + + ); +} + export function Titlebar() { const appWindow = getCurrentWindow(); const openModal = useUIStore((s) => s.openModal); + const setActivePage = useUIStore((s) => s.setActivePage); + const tabs = useTabStore((s) => s.tabs); + const activeTabId = useTabStore((s) => s.activeTabId); + const setActiveTab = useTabStore((s) => s.setActiveTab); + const closeTab = useTabStore((s) => s.closeTab); + const reorderTabs = useTabStore((s) => s.reorderTabs); + + const [dragIndex, setDragIndex] = useState(null); + + const handleDragStart = useCallback( + (e: DragEvent, index: number) => { + setDragIndex(index); + e.dataTransfer.effectAllowed = "move"; + }, + [], + ); + + const handleDragOver = useCallback((e: DragEvent) => { + e.preventDefault(); + e.dataTransfer.dropEffect = "move"; + }, []); + + const handleDrop = useCallback( + (e: DragEvent, toIndex: number) => { + e.preventDefault(); + if (dragIndex !== null && dragIndex !== toIndex) { + reorderTabs(dragIndex, toIndex); + } + setDragIndex(null); + }, + [dragIndex, reorderTabs], + ); + + const handleDragEnd = useCallback(() => { + setDragIndex(null); + }, []); + + const handleAddTab = useCallback(() => { + setActivePage("open-repository"); + }, [setActivePage]); + + const handleTabClose = useCallback( + (e: React.MouseEvent, tabId: string) => { + e.stopPropagation(); + closeTab(tabId); + }, + [closeTab], + ); return (
@@ -90,9 +176,47 @@ export function Titlebar() {
- - Rocket - + {tabs.length > 0 ? ( +
+ {tabs.map((tab, index) => ( + + )} + + ))} + +
+ ) : ( + + Rocket + + )}