掘金 后端 ( ) • 2024-04-08 10:05

如何使用Tauri和git2在Rust中实现Git克隆操作的进度显示与取消

本文将介绍如何使用Rust语言结合Tauri和git2库来实现Git克隆操作的进度显示和取消功能。

核心逻辑

首先,我们使用git2库来实现Git的克隆功能。git2是一个Rust语言的Git库,它提供了丰富的API来与Git仓库进行交互。接着,我们利用Tauri框架提供的命令和事件机制,通过命令执行下载操作,并使用原子布尔值和事件监听器来处理前端发来的取消消息。最后,我们实现了取消逻辑,确保在用户取消操作时,下载任务能够及时停止。

遇到的问题

在使用git2的remoteCallback时,我们发现其执行频率相当高。如果频繁地通过Tauri的Channel向前端发送消息,可能会导致应用程序卡顿。因此,我们需要对消息发送进行节流处理,以避免过多的消息传输影响性能。

代码实现

以下是一个简化的代码示例,展示了如何使用Tauri和git2实现Git克隆操作的进度显示和取消。

git2 实现clone和pull逻辑

// 引入标准库中的Path模块,用于处理文件路径。
use std::path::Path;
​
// 引入自定义的错误类型MyError,用于错误处理。
use crate::error::MyError;
// 引入anyhow库,它提供了一个方便的错误处理上下文。
use anyhow::Context;
// 引入derive_builder库,用于生成构建器模式的代码。
use derive_builder::Builder;
// 使用git2库,它提供了与Git仓库交互的接口。
use git2::{build::RepoBuilder, FetchOptions, Progress, RemoteCallbacks, Repository};
// 引入tracing库,用于日志记录。
use tracing::info;
​
// 定义一个常量GITHUB_PROXY,存储GitHub镜像的URL。
pub const GITHUB_PROXY: &str = "https://mirror.ghproxy.com"; 
​
// Git结构体定义,使用Builder宏来生成构建器模式。
#[derive(Debug, Builder, Clone)]
#[builder(setter(into))]
pub struct Git {
    // Git仓库的URL地址。
    url: String,
    // 本地仓库的路径。
    path: String,
    // 是否使用代理,默认为true。
    #[builder(default = "true")]
    proxy: bool,
}
​
// Git结构体的实现。
impl Git {
    // git_clone方法用于克隆Git仓库。
    pub fn git_clone<F>(&self, cb: F) -> Result<(), MyError>
    where
        F: FnMut(Progress) -> bool,
    {
        // 创建RemoteCallbacks对象,用于处理网络传输进度。
        let mut rc = RemoteCallbacks::new();
        rc.transfer_progress(cb);
​
        // 创建FetchOptions对象,用于设置获取操作的选项。
        let mut fo = FetchOptions::new();
        fo.remote_callbacks(rc);
​
        // 根据是否使用代理,构造仓库的URL。
        let url = if self.proxy {
            format!("{GITHUB_PROXY}/{}", self.url)
        } else {
            self.url.clone()
        };
        // 记录克隆操作的URL。
        info!("git clone {}", url);
        // 使用RepoBuilder克隆仓库到指定路径。
        RepoBuilder::new()
            .fetch_options(fo)
            .clone(&url, Path::new(&self.path))?;
        Ok(())
    }
​
    // git_pull方法用于从远程仓库获取更新并合并到本地仓库。
    pub fn git_pull<F>(&self, cb: F) -> Result<usize, MyError>
    where
        F: FnMut(Progress) -> bool,
    {
        // 打开本地仓库。
        let repo = Repository::open(&self.path)?;
        // 获取名为"origin"的远程仓库。
        let remote_name = "origin"; // 远程仓库的默认名称
        // 连接到远程仓库。
        let mut remote = repo.find_remote(remote_name)?;
        remote.connect(git2::Direction::Fetch)?;
​
        // 获取远程仓库的默认分支名称。
        let default_branch = remote.default_branch()?;
        let default_branch_name = default_branch.as_str().context("获取默认分支名称失败")?;
        // 设置回调函数,用于处理获取过程中的进度信息。
        let mut fetch_opts = FetchOptions::new();
        let mut rc = RemoteCallbacks::new();
        rc.transfer_progress(cb);
        fetch_opts.remote_callbacks(rc);
        // 执行获取操作。
        remote.fetch(&[default_branch_name], Some(&mut fetch_opts), None)?;
        // 找到FETCH_HEAD引用。
        let fetch_head = repo.find_reference("FETCH_HEAD")?;
        // 将FETCH_HEAD引用转换为注释提交。
        let fetch_commit = repo.reference_to_annotated_commit(&fetch_head)?;
        // 获取合并分析结果。
        let analysis = repo.merge_analysis(&[&fetch_commit])?;
        // 如果本地仓库是最新的,则返回1。
        if analysis.0.is_up_to_date() {
            return Ok(1);
        } else if analysis.0.is_fast_forward() {
            // 如果可以进行快进合并,则执行合并操作。
            let mut refrence = repo.find_reference(default_branch_name)?;
            refrence.set_target(fetch_commit.id(), "Fast forward")?;
            repo.set_head(default_branch_name)?;
            return Ok(repo
                .checkout_head(Some(git2::build::CheckoutBuilder::default().force()))
                .map(|_| 2usize)?);
        } else {
            // 如果不是快进合并,则返回0。
            return Ok(0);
        }
    }
​
    // builder方法用于创建Git结构体的构建器实例。
    pub fn builder() -> GitBuilder {
        GitBuilder::default()
    }
}

tauri定义前端调用接口

// 使用Tauri框架的命令宏来定义一个可以被前端调用的函数。
#[tauri::command]
pub async fn download_plugin(
    // app参数是Tauri应用的句柄,用于与前端通信。
    app: AppHandle,
    // config参数是一个包含配置信息的状态对象。
    config: State<'_, MyConfig>,
    // plugin参数是一个包含插件信息的对象。
    plugin: Plugin,
    // on_progress参数是一个通道,用于将下载进度发送到前端。
    on_progress: Channel,
) -> Result<(), MyError> {
    // 从状态对象中克隆配置信息。
    let config = config.inner().clone();
    // 通过通道发送一个下载前的状态消息到前端。
    on_progress
        .send(
            PluginDownloadMessage::builder()
                .status(PluginStatus::Pending)
                .build()
                .unwrap(),
        )
        .context("Send Message Error")?;
​
    // 创建一个原子布尔值,用于控制下载任务的取消。
    let cancel = Arc::new(AtomicBool::new(true));
    // 克隆cancel变量,用于在闭包内部修改其值。
    let cancel2 = cancel.clone();
    // 克隆插件的引用信息,用于后续的取消操作。
    let plugin_reference = plugin.reference.clone();
    // 克隆通道,用于在闭包内部发送消息到前端。
    let on_progress2 = on_progress.clone();
​
    // 使用tokio库的spawn函数创建一个新的异步任务。
    let handler = tokio::spawn(async move {
​
        // 锁定配置信息,以便在异步任务中使用。
        let config = config.lock().await;
        // 记录下载开始的时间。
        let mut start_time = Instant::now();
        // 初始化下载进度。
        let mut progress = 0f64;
​
        // 调用插件的下载方法,并提供进度更新的回调函数。
        match plugin
            .download(&config.comfyui_path, config.is_chinese(), |p| {
                // 检查下载任务是否已被取消。
                let v = cancel2.load(std::sync::atomic::Ordering::SeqCst);
                // 计算当前的下载进度。
                let new_progress = percent(p.received_objects(), p.total_objects());
                // 为了避免发送过多消息导致阻塞,对消息进行节流。
                if start_time.elapsed() > Duration::from_millis(60) && progress != new_progress {
                    start_time = Instant::now();
                    progress = new_progress;
                    // 记录当前的下载进度。
                    info!("Download Progress: {}", new_progress);
                    // 通过通道发送当前的下载进度到前端。
                    on_progress
                        .send(
                            PluginDownloadMessage::builder()
                                .status(PluginStatus::Downloading)
                                .progress(new_progress)
                                .build()
                                .unwrap(),
                        )
                        .unwrap();
                }
                return v;
            })
            .await
        {
            // 如果下载成功,发送100%的进度消息表示下载完成。
            Ok(_) => {
                on_progress.send(100f64).unwrap();
                // 等待500毫秒,确保消息能够被前端接收。
                sleep(Duration::from_millis(500)).await;
                // 发送下载成功的状态消息到前端。
                on_progress
                    .send(
                        PluginDownloadMessage::builder()
                            .status(PluginStatus::Success)
                            .build()
                            .unwrap(),
                    )
                    .unwrap();
            }
            // 如果下载失败,发送错误状态消息到前端。
            Err(e) => {
                if !cancel2.load(std::sync::atomic::Ordering::SeqCst) {
                    // 发送错误状态消息到前端。
                    on_progress
                        .send(
                            PluginDownloadMessage::builder()
                                .status(PluginStatus::Error)
                                .error_message(e.to_string())
                                .build()
                                .unwrap(),
                        )
                        .unwrap();
                }
            }
        }
    });
​
    // 监听来自前端的取消事件。
    app.listen("plugin-cancel", move |event| {
        // 从事件中解析出引用信息。
        let reference = serde_json::from_str::<Value>(event.payload()).unwrap();
        // 如果引用信息与当前插件的引用信息匹配,取消下载任务。
        if reference["reference"] == plugin_reference {
            // 如果成功将cancel变量设置为false,表示取消操作成功。
            if cancel
                .compare_exchange(
                    true,
                    false,
                    std::sync::atomic::Ordering::SeqCst,
                    std::sync::atomic::Ordering::SeqCst,
                )
                .is_ok()
            {
                // 发送已取消的状态消息到前端。
                on_progress2
                    .send(
                        PluginDownloadMessage::builder()
                            .status(PluginStatus::Canceled)
                            .build()
                            .unwrap(),
                    )
                    .context("Send Message Error")
                    .unwrap();
                // 取消异步任务。
                handler.abort();
            }
        }
    });
​
    // 函数执行成功,返回Ok。
    Ok(())
}