twin-cli 0.1.0

Git worktree wrapper with side effects (symlinks and hooks)
Documentation
/// シンボリックリンク管理モジュール
///
/// このモジュールの役割:
/// - クロスプラットフォーム対応のシンボリックリンク作成
/// - Unix: ln -s コマンドのラッパー
/// - Windows: 開発者モード対応のmklinkラッパー(フォールバック機能付き)
/// - リンクの検証と削除
use crate::core::{SymlinkInfo, TwinError, TwinResult};
use std::fs;
use std::path::Path;
#[cfg(windows)]
use std::process::Command;

/// プラットフォーム共通のトレイト
pub trait SymlinkManager {
    /// シンボリックリンクを作成
    fn create_symlink(&self, source: &Path, target: &Path) -> TwinResult<SymlinkInfo>;

    /// シンボリックリンクを削除
    fn remove_symlink(&self, path: &Path) -> TwinResult<()>;

    /// シンボリックリンクを検証
    #[allow(dead_code)]
    fn validate_symlink(&self, path: &Path) -> TwinResult<bool>;

    /// 手動作成方法の説明を取得
    #[allow(dead_code)]
    fn get_manual_instructions(&self, source: &Path, target: &Path) -> String;
}

/// プラットフォーム別の実装を選択
#[cfg(unix)]
#[allow(dead_code)]
pub type PlatformSymlinkManager = UnixSymlinkManager;

#[cfg(windows)]
#[allow(dead_code)]
pub type PlatformSymlinkManager = WindowsSymlinkManager;

/// Unix系OS用の実装
#[cfg(unix)]
pub struct UnixSymlinkManager;

#[cfg(unix)]
impl UnixSymlinkManager {
    pub fn new() -> Self {
        Self
    }
}

#[cfg(unix)]
impl Default for UnixSymlinkManager {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(unix)]
impl SymlinkManager for UnixSymlinkManager {
    fn create_symlink(&self, source: &Path, target: &Path) -> TwinResult<SymlinkInfo> {
        // 透明性のあるコマンド実行ログ
        if std::env::var("TWIN_VERBOSE").is_ok() || std::env::var("TWIN_DEBUG").is_ok() {
            eprintln!(
                "🔗 シンボリックリンク作成: {} -> {}",
                target.display(),
                source.display()
            );
        }

        // ソースファイルの存在チェック
        if !source.exists() {
            return Err(TwinError::symlink(
                format!("Source path does not exist: {}", source.display()),
                Some(source.to_path_buf()),
            ));
        }

        // 既存のリンクやファイルがある場合は削除
        if target.exists() || target.is_symlink() {
            fs::remove_file(target).ok();
        }

        // 親ディレクトリを作成
        if let Some(parent) = target.parent() {
            fs::create_dir_all(parent)?;
        }

        // シンボリックリンクを作成
        #[cfg(unix)]
        {
            use std::os::unix::fs::symlink;
            match symlink(source, target) {
                Ok(_) => {
                    if std::env::var("TWIN_VERBOSE").is_ok() || std::env::var("TWIN_DEBUG").is_ok()
                    {
                        eprintln!("✅ シンボリックリンク作成成功");
                    }
                    let mut info = SymlinkInfo::new(source.to_path_buf(), target.to_path_buf());
                    info.set_success();
                    Ok(info)
                }
                Err(e) => {
                    if std::env::var("TWIN_VERBOSE").is_ok() || std::env::var("TWIN_DEBUG").is_ok()
                    {
                        eprintln!("❌ シンボリックリンク作成失敗: {}", e);
                    }
                    let mut info = SymlinkInfo::new(source.to_path_buf(), target.to_path_buf());
                    info.set_error(format!("Failed to create symlink: {}", e));
                    Err(TwinError::symlink(
                        format!("Failed to create symlink: {}", e),
                        Some(target.to_path_buf()),
                    ))
                }
            }
        }
    }

    fn remove_symlink(&self, path: &Path) -> TwinResult<()> {
        if std::env::var("TWIN_VERBOSE").is_ok() || std::env::var("TWIN_DEBUG").is_ok() {
            eprintln!("🗑️  シンボリックリンク削除: {}", path.display());
        }

        if path.is_symlink() {
            fs::remove_file(path)?;
            if std::env::var("TWIN_VERBOSE").is_ok() || std::env::var("TWIN_DEBUG").is_ok() {
                eprintln!("✅ シンボリックリンク削除成功");
            }
        }
        Ok(())
    }

    #[allow(dead_code)]
    fn validate_symlink(&self, path: &Path) -> TwinResult<bool> {
        if !path.exists() {
            return Ok(false);
        }

        // シンボリックリンクかどうか確認
        let metadata = fs::symlink_metadata(path)?;
        if !metadata.file_type().is_symlink() {
            return Ok(false);
        }

        // リンク先が存在するか確認
        match fs::metadata(path) {
            Ok(_) => Ok(true),
            Err(_) => Ok(false), // 壊れたリンク
        }
    }

    #[allow(dead_code)]
    fn get_manual_instructions(&self, source: &Path, target: &Path) -> String {
        format!(
            "To manually create the symlink, run:\n  ln -s \"{}\" \"{}\"",
            source.display(),
            target.display()
        )
    }
}

/// Windows用の実装
#[cfg(windows)]
pub struct WindowsSymlinkManager {
    /// 開発者モードが有効かどうか
    developer_mode: bool,
    /// 管理者権限で実行されているか
    is_elevated: bool,
}

#[cfg(windows)]
impl Default for WindowsSymlinkManager {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(windows)]
impl WindowsSymlinkManager {
    pub fn new() -> Self {
        Self {
            developer_mode: Self::check_developer_mode(),
            is_elevated: Self::check_elevation(),
        }
    }

    /// 開発者モードが有効か確認
    fn check_developer_mode() -> bool {
        // レジストリをチェック
        let output = Command::new("reg")
            .args([
                "query",
                "HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\AppModelUnlock",
                "/v",
                "AllowDevelopmentWithoutDevLicense",
            ])
            .output();

        if let Ok(output) = output {
            let stdout = String::from_utf8_lossy(&output.stdout);
            return stdout.contains("0x1");
        }

        false
    }

    /// 管理者権限で実行されているか確認
    fn check_elevation() -> bool {
        // 管理者権限が必要な操作を試みる
        Command::new("net")
            .args(["session"])
            .output()
            .map(|o| o.status.success())
            .unwrap_or(false)
    }

    /// ファイルをコピー
    fn copy_file(&self, source: &Path, target: &Path) -> TwinResult<()> {
        if let Some(parent) = target.parent() {
            fs::create_dir_all(parent)?;
        }

        fs::copy(source, target)?;
        Ok(())
    }
}

#[cfg(windows)]
impl SymlinkManager for WindowsSymlinkManager {
    fn create_symlink(&self, source: &Path, target: &Path) -> TwinResult<SymlinkInfo> {
        // 透明性のあるコマンド実行ログ
        if std::env::var("TWIN_VERBOSE").is_ok() || std::env::var("TWIN_DEBUG").is_ok() {
            eprintln!(
                "🔗 シンボリックリンク作成: {} -> {}",
                target.display(),
                source.display()
            );
        }

        // ソースファイルの存在チェック
        if !source.exists() {
            return Err(TwinError::symlink(
                format!("Source path does not exist: {}", source.display()),
                Some(source.to_path_buf()),
            ));
        }

        // 既存のファイルを削除
        if target.exists() {
            fs::remove_file(target).ok();
            fs::remove_dir(target).ok();
        }

        // 親ディレクトリを作成
        if let Some(parent) = target.parent() {
            fs::create_dir_all(parent)?;
        }

        // 開発者モードまたは管理者権限があればシンボリックリンクを作成
        let result = if self.developer_mode || self.is_elevated {
            // 標準ライブラリのAPI を使用
            #[cfg(windows)]
            {
                use std::os::windows::fs::{symlink_dir, symlink_file};
                if source.is_dir() {
                    symlink_dir(source, target).map_err(|e| {
                        TwinError::symlink(
                            format!("Failed to create directory symlink: {e}"),
                            Some(target.to_path_buf()),
                        )
                    })
                } else {
                    symlink_file(source, target).map_err(|e| {
                        TwinError::symlink(
                            format!("Failed to create file symlink: {e}"),
                            Some(target.to_path_buf()),
                        )
                    })
                }
            }
        } else {
            // 開発者モードが無効な場合、エラーメッセージを表示してコピー
            eprintln!(
                "⚠️  Warning: Symbolic link creation requires Developer Mode or Administrator privileges"
            );
            eprintln!("⚠️  Falling back to file copy instead");
            self.copy_file(source, target)
        };

        let mut info = SymlinkInfo::new(source.to_path_buf(), target.to_path_buf());

        match result {
            Ok(_) => {
                if std::env::var("TWIN_VERBOSE").is_ok() || std::env::var("TWIN_DEBUG").is_ok() {
                    eprintln!("✅ シンボリックリンク作成成功");
                }
                info.set_success();
                Ok(info)
            }
            Err(e) => {
                if std::env::var("TWIN_VERBOSE").is_ok() || std::env::var("TWIN_DEBUG").is_ok() {
                    eprintln!("❌ シンボリックリンク作成失敗: {e}");
                }
                info.set_error(e.to_string());
                Err(e)
            }
        }
    }

    fn remove_symlink(&self, path: &Path) -> TwinResult<()> {
        if std::env::var("TWIN_VERBOSE").is_ok() || std::env::var("TWIN_DEBUG").is_ok() {
            eprintln!("🗑️  シンボリックリンク削除: {}", path.display());
        }

        if path.exists() {
            let metadata = fs::symlink_metadata(path)?;
            if metadata.is_dir() {
                fs::remove_dir(path)?;
            } else {
                fs::remove_file(path)?;
            }
            if std::env::var("TWIN_VERBOSE").is_ok() || std::env::var("TWIN_DEBUG").is_ok() {
                eprintln!("✅ シンボリックリンク削除成功");
            }
        }
        Ok(())
    }

    #[allow(dead_code)]
    fn validate_symlink(&self, path: &Path) -> TwinResult<bool> {
        if !path.exists() {
            return Ok(false);
        }

        // シンボリックリンクかジャンクションか確認
        #[cfg(windows)]
        {
            use std::os::windows::fs::MetadataExt;
            let metadata = fs::symlink_metadata(path)?;
            let attrs = metadata.file_attributes();

            // FILE_ATTRIBUTE_REPARSE_POINT をチェック
            const FILE_ATTRIBUTE_REPARSE_POINT: u32 = 0x400;
            if attrs & FILE_ATTRIBUTE_REPARSE_POINT != 0 {
                // リンク先が存在するか確認
                return match fs::metadata(path) {
                    Ok(_) => Ok(true),
                    Err(_) => Ok(false),
                };
            }
        }

        Ok(false)
    }

    #[allow(dead_code)]
    fn get_manual_instructions(&self, source: &Path, target: &Path) -> String {
        if source.is_dir() {
            format!(
                "mklink /D \"{}\" \"{}\"",
                target.display(),
                source.display()
            )
        } else {
            format!("mklink \"{}\" \"{}\"", target.display(), source.display())
        }
    }
}

/// ファクトリ関数
pub fn create_symlink_manager() -> Box<dyn SymlinkManager> {
    #[cfg(unix)]
    {
        Box::new(UnixSymlinkManager::new())
    }

    #[cfg(windows)]
    {
        Box::new(WindowsSymlinkManager::new())
    }
}