Tauri(Rust) × AI で作る GitGUIクライアントアプリ その5
2025.04.14
どもです。
今回は前回の「Tauri(Rust) × AI で作る GitGUIクライアントアプリ その4」の続きとなり、Rust側のレポジトリのクローン処理も追加していこうと思います。
まずは、LocalディレクトリのデフォルトのパスをMacだと、
「ライブラリ>Application Support>Branchie>repository」
に該当する箇所にクローンできるように設定します。
ローカルレポジトリ作成先の指定などを調整
Cursorに以下を指示。
「git のレポジトリURL入力画面にて、Localディレクトリのデフォルトのパスをdirs::data_dir() + “Branchie/repository” にしたい。」
dirs = "5.0"
dirsクレートをinstall。
#[tauri::command] fn get_default_repository_path() -> Result<String, String> { let data_dir = dirs::data_dir() .ok_or_else(|| "データディレクトリが見つかりませんでした".to_string())?; let repo_dir = data_dir.join("Branchie").join("repository"); // ディレクトリが存在しない場合は作成 if !repo_dir.exists() { std::fs::create_dir_all(&repo_dir) .map_err(|e| format!("ディレクトリの作成に失敗しました: {}", e))?; } Ok(repo_dir.to_string_lossy().to_string()) }
デフォルトパスを取得する関数の作成。
const defaultPath = await invoke<string>("get_default_repository_path"); localPath.value = defaultPath;
フロント側で取得してセット。
これで、以下の機能が実装されました: 1.Rust側: ・dirsクレートを使用してアプリケーションデータディレクトリを取得 ・デフォルトのリポジトリパスをdata_dir() + "Branchie/repository"に設定 ・ディレクトリが存在しない場合は自動的に作成 2.フロントエンド側: ・コンポーネントマウント時にデフォルトのリポジトリパスを取得 ・取得したパスをローカルパスのデフォルト値として設定 これにより、以下のような動作になります: 1.アプリケーション起動時: ・アプリケーションデータディレクトリを取得 ・Branchie/repositoryディレクトリを作成(存在しない場合) ・フロントエンドにデフォルトのリポジトリパスを提供 2.リポジトリクローン時: ・デフォルトのリポジトリパスが自動的に設定される ・ユーザーが別のパスを指定することも可能 リンターエラーはtsconfig.jsonの設定に関連していますが、アプリケーションの動作には影響しません。必要に応じてtsconfig.jsonの設定を調整することができます。 次のステップとして、以下のような機能を追加することができます: 1.ディレクトリ選択ダイアログの実装 2.リポジトリパスのバリデーション 3.リポジトリの存在チェック これらの機能を追加しますか?
結構、グイグイきますね。それでは提案されたまま進めてみます。
「1. 2. 3の実装を進めてください」
すると、
use tauri::api::dialog::FileDialogBuilder;
#[tauri::command] async fn select_directory() -> Result<String, String> { let path = FileDialogBuilder::new() .set_title("Select Directory") .pick_folder() .await .ok_or_else(|| "ディレクトリが選択されませんでした".to_string())? .to_string_lossy() .to_string(); Ok(path) }
といった感じで、「FileDialogBuilder」を追加し、生成しようとしますが、これは多分Tauri バージョン1.0の仕様なのかと。ChatGPTにお願いした際もこちらを追加しようとして何度もエラーになりハマった覚えが。
こちらは手動で変更しました。
まず、Rust側で使用するクレートは、
Cargo.tmol
tauri-plugin-dialog = "2.2.1"
そして、src-tauri/capabilities/default.json
に”dialog:allow-open”の追加が必要。
"permissions": [ "core:default", "opener:default", "dialog:allow-open" ]
そして、tauri::Builder::default()に以下を追加
.plugin(tauri_plugin_dialog::init())
全体的にはこんな感じですかね。
tauri::Builder::default() .plugin(tauri_plugin_dialog::init()) .invoke_handler(tauri::generate_handler![ save_credentials, get_credentials, get_default_repository_path, validate_repository_path, check_repository_exists, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); }
フロント側は、呼び出しのため、“@tauri-apps/plugin-dialog”パッケージが必要となるのでインストールします。
npm install @tauri-apps/plugin-dialog
openメソッドを用いてディレクトリを選択するダイアログを表示します。
import { open } from "@tauri-apps/plugin-dialog"; .... const handleBrowse = async () => { const selected = await open({ multiple: false, directory: true, }); if (selected) { localPath.value = selected; } };
これで、ディレクトリパスにはデフォルトのパスが指定されており、パス指定するインプットの隣のアイコンをクリックすると、ディレクトリを選択するダイアログが表示するようになりました。
Gitレポジトリのクローン処理
後は実際にクローンしてくる処理を追加しましょう。
Git周りを操作するため、Git2クレートを使用します。それでは指示を。
「git2クレートを使って、実際にRepository URLを元にLocal Destinationにクローンする処理を追加してください。」
これ一発で行くかなと思ったのですが、色々とエラーが出てしまいました。
git2 = "0.18"
use git2::Repository;
git2クレートが追加されます。
#[tauri::command] fn clone_repository(url: String, path: String) -> Result<(), String> { // クローン先のパスをPathBufに変換 let path = PathBuf::from(path); // リポジトリをクローン Repository::clone(&url, &path) .map_err(|e| format!("リポジトリのクローンに失敗しました: {}", e))?; Ok(()) }
■ フロント側
// リポジトリをクローン await invoke("clone_repository", { url: repositoryUrl.value, path: localPath.value, });
といった形で追加されたのですが、これだとプライベートレポジトリにアクセス出来ないので、キーチェーンに登録されたパスワードと、loaclstorageに保存したusernameを使ってgit credentialsを設定。
さらに、指定されたディレクトリ直下にcloneしてしまう状況だったので、レポジトリ名をディレクトリ名として作成しつつ、既にcloneしている場合はpullを行う処理などを追加。
最終的に出来た処理は、フロント側
const handleSubmit = async () => { try { isLoading.value = true; error.value = ""; // リポジトリURLのバリデーション if (!repositoryUrl.value) { throw new Error("リポジトリURLを入力してください"); } // ローカルパスのバリデーション const isValid = await invoke<boolean>("validate_repository_path", { path: localPath.value, }); if (!isValid) { throw new Error("無効なローカルパスです"); } // リポジトリの存在チェック const exists = await invoke<boolean>("check_repository_exists", { path: localPath.value, }); if (exists) { throw new Error("指定されたパスには既にGitリポジトリが存在します"); } // localStorageからlastUsernameを取得 const lastUsername = localStorage.getItem("lastUsername"); if (!lastUsername) { throw new Error( "ログイン情報が見つかりません。ログインしてください。" ); } // リポジトリをクローン const result = await invoke<string>("clone_repository", { url: repositoryUrl.value, path: localPath.value, username: lastUsername, }); console.log("クローン結果:", result); emit("clone", { repositoryUrl: repositoryUrl.value, localPath: localPath.value, }); } catch (e) { console.error("クローンエラー:", e); error.value = e instanceof Error ? e.message : "エラーが発生しました"; } finally { isLoading.value = false; } };
となっており、Rust側は
■ Rust側
#[tauri::command] fn clone_repository(url: String, path: String, username: String) -> Result<String, String> { // クローン先のパスをPathBufに変換 let mut path = PathBuf::from(path); // リポジトリ名を抽出 let repo_name = url .split('/') .last() .ok_or_else(|| "リポジトリURLが無効です".to_string())? .trim_end_matches(".git"); // リポジトリ名をディレクトリ名として追加 path = path.join(repo_name); // 認証情報を取得 let password = get_credentials(username.clone())?; // 認証コールバックを設定 let mut callbacks = git2::RemoteCallbacks::new(); callbacks.credentials(move |_url, username_from_url, _allowed_types| { git2::Cred::userpass_plaintext(&username, &password) }); // フェッチオプションを設定 let mut fetch_options = git2::FetchOptions::new(); fetch_options.remote_callbacks(callbacks); // ディレクトリが存在しない場合は作成 if !path.exists() { std::fs::create_dir_all(&path) .map_err(|e| format!("ディレクトリの作成に失敗しました: {}", e))?; } // リポジトリの存在チェック let git_dir = path.join(".git"); if git_dir.exists() && git_dir.is_dir() { // すでにリポジトリがある場合は pull 等の処理 let repo = Repository::open(&path).map_err(|e| e.to_string())?; { let mut remote = repo.find_remote("origin").map_err(|e| e.to_string())?; remote .fetch(&["master"], Some(&mut fetch_options), None) .map_err(|e| e.to_string())?; } Ok(format!("Pulled existing repo at {:?}", path)) } else { // 新しいリポジトリをクローン let mut builder = git2::build::RepoBuilder::new(); builder.fetch_options(fetch_options); builder.clone(&url, &path).map_err(|e| e.to_string())?; Ok(format!("Cloned new repo at {:?}", path)) } }
といった感じに作成され、一応任意のレポジトリを任意のパスにcloneするところまで完成しました。
試しに、「open-texture-packer」をデスクトップ上にcloneしてみると。
ホーム画面に遷移し、
無事クローンされておりました。
といった感じで、殆どCursorさんにお任せして作成してきたのですが、次回からはそろそろソースコードもカオスってきたのでリファクタリングを中心に行いつつ、クローンの処理が同期的なので非同期で実行しつつ、cloneの進捗状況も取得出来るように実装を進めて行こうと思います。
ではではぁ。
またまたぁ。