How to Implement Git Clone Operation Progress Display and Cancellation in Rust with Tauri and git2
Tags: rust, git, tauri
Published: 2024-04-07 12:42:44

## 
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rdq35iedt8x94s8968gw.png)

In modern software development, integrating Git as a version control system into applications for code management and updates is a common requirement. This article will introduce how to implement progress display and cancellation features for Git clone operations using the Rust language in combination with Tauri and the git2 library.

### Core Logic

Firstly, we use the git2 library to implement the Git clone functionality. git2 is a Git library in the Rust language that provides a rich set of APIs for interacting with Git repositories. Next, we leverage the command and event mechanisms provided by the Tauri framework to execute the download operation through commands and handle cancel messages from the front end using atomic boolean values and event listeners. Finally, we implement the cancellation logic to ensure that the download task can be stopped promptly when the user cancels the operation.

### Challenges Encountered

When using the remoteCallback of git2, we found that its execution frequency is quite high. If messages are frequently sent to the front end through the Tauri Channel, it may cause the application to become unresponsive. Therefore, we need to throttle message sending to avoid excessive message transmission affecting performance.

### Code Implementation

Below is a simplified code example showing how to use Tauri and git2 to implement progress display and cancellation for Git clone operations.

**`git2` Implementation of Clone and Pull Logic**

```rust
// Import the Path module from the standard library for handling file paths.
use std::path::Path;

// Import the custom error type MyError for error handling.
use crate::error::MyError;
// Import the anyhow library, which provides a convenient error handling context.
use anyhow::Context;
// Import the derive_builder library for generating builder pattern code.
use derive_builder::Builder;
// Use the git2 library, which provides interfaces for interacting with Git repositories.
use git2::{build::RepoBuilder, FetchOptions, Progress, RemoteCallbacks, Repository};
// Import the tracing library for logging.
use tracing::info;

// Define a constant GITHUB_PROXY to store the URL of the GitHub mirror.
pub const GITHUB_PROXY: &str = "https://mirror.ghproxy.com";

// Git struct definition, using the Builder macro to generate a builder pattern.
#[derive(Debug, Builder, Clone)]
#[builder(setter(into))]
pub struct Git {
    // The URL address of the Git repository.
    url: String,
    // The local repository path.
    path: String,
    // Whether to use a proxy, default to true.
    #[builder(default = "true")]
    proxy: bool,
}

// Implementation of the Git struct.
impl Git {
    // The git_clone method is used to clone a Git repository.
    pub fn git_clone<F>(&self, cb: F) -> Result<(), MyError>
    where
        F: FnMut(Progress) -> bool,
    {
        // Create a RemoteCallbacks object to handle network transfer progress.
        let mut rc = RemoteCallbacks::new();
        rc.transfer_progress(cb);

        // Create a FetchOptions object to set the options for the fetch operation.
        let mut fo = FetchOptions::new();
        fo.remote_callbacks(rc);

        // Construct the repository URL based on whether a proxy is used.
        let url = if self.proxy {
            format!("{}/{}", GITHUB_PROXY, self.url)
        } else {
            self.url.clone()
        };
        // Log the URL for the clone operation.
        info!("git clone {}", url);
        // Use RepoBuilder to clone the repository to the specified path.
        RepoBuilder::new()
            .fetch_options(fo)
            .clone(&url, Path::new(&self.path))?;
        Ok(())
    }

    // The git_pull method is used to fetch updates from the remote repository and merge them into the local repository.
    pub fn git_pull<F>(&self, cb: F) -> Result<usize, MyError>
    where
        F: FnMut(Progress) -> bool,
    {
        // Open the local repository.
        let repo = Repository::open(&self.path)?;
        // Get the remote repository named "origin".
        let remote_name = "origin"; // The default name for the remote repository
        // Connect to the remote repository.
        let mut remote = repo.find_remote(remote_name)?;
        remote.connect(git2::Direction::Fetch)?;

        // Get the default branch name of the remote repository.
        let default_branch = remote.default_branch()?;
        let default_branch_name = default_branch.as_str().context("Failed to get default branch name")?;
        // Set the callback function to handle progress information during the fetch process.
        let mut fetch_opts = FetchOptions::new();
        let mut rc = RemoteCallbacks::new();
        rc.transfer_progress(cb);
        fetch_opts.remote_callbacks(rc);
        // Execute the fetch operation.
        remote.fetch(&[default_branch_name], Some(&mut fetch_opts), None)?;
        // Find the FETCH_HEAD reference.
        let fetch_head = repo.find_reference("FETCH_HEAD")?;
        // Convert the FETCH_HEAD reference to an annotated commit.
        let fetch_commit = repo.reference_to_annotated_commit(&fetch_head)?;
        // Get the merge analysis result.
        let analysis = repo.merge_analysis(&[&fetch_commit])?;
        // If the local repository is up-to-date, then return 1.
        if analysis.0.is_up_to_date() {
            return Ok(1);
        } else if analysis.0.is_fast_forward() {
            // If a fast-forward merge is possible, then perform the merge operation.
            let mut reference = repo.find_reference(default_branch_name)?;
            reference.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 {
            // If it is not a fast-forward merge, then return 0.
            return Ok(0);
        }
    }

    // The builder method is used to create a builder instance for the Git struct.
    pub fn builder() -> GitBuilder {
        GitBuilder::default()
    }
}
```

`tauri` Define Front-end Invocation Interface

```rust
// Use the Tauri framework's command macro to define a function that can be called by the front end.
#[tauri::command]
pub async fn download_plugin(
    // The app parameter is the handle of the Tauri application, used for communication with the front end.
    app: AppHandle,
    // The config parameter is a state object containing configuration information.
    config: State<'_, MyConfig>,
    // The plugin parameter is an object containing plugin information.
    plugin: Plugin,
    // The on_progress parameter is a channel used to send download progress to the front end.
    on_progress: Channel,
) -> Result<(), MyError> {
    // Clone the configuration information from the state object.
    let config = config.inner().clone();
    // Send a pre-download status message to the front end through the channel.
    on_progress
        .send(
            PluginDownloadMessage::builder()
                .status(PluginStatus::Pending)
                .build()
                .unwrap(),
        )
        .context("Send Message Error")?;

    // Create an atomic boolean value to control the cancellation of the download task.
    let cancel = Arc::new(AtomicBool::new(true));
    // Clone the cancel variable to modify its value within the closure.
    let cancel2 = cancel.clone();
    // Clone the plugin's reference information for subsequent cancellation operations.
    let plugin_reference = plugin.reference.clone();
    // Clone the channel to send messages to the front end within the closure.
    let on_progress2 = on_progress.clone();

    // Use the spawn function from the tokio library to create a new asynchronous task.
    let handler = tokio::spawn(async move {

        // Lock the configuration information for use within the asynchronous task.
        let config = config.lock().await;
        // Record the start time of the download.
        let mut start_time = Instant::now();
        // Initialize the download progress.
        let mut progress = 0f64;

        // Call the plugin's download method and provide a callback function for progress updates.
        match plugin
            .download(&config.comfyui_path, config.is_chinese(), |p| {
                // Check if the download task has been cancelled.
                let v = cancel2.load(std::sync::atomic::Ordering::SeqCst);
                // Calculate the current download progress.
                let new_progress = percent(p.received_objects(), p.total_objects());
                // To avoid blocking due to too many messages being sent, throttle the messages.
                if start_time.elapsed() > Duration::from_millis(60) && progress != new_progress {
                    start_time = Instant::now();
                    progress = new_progress;
                    // Log the current download progress.
                    info!("Download Progress: {}", new_progress);
                    // Send the current download progress to the front end through the channel.
                    on_progress
                        .send(
                            PluginDownloadMessage::builder()
                                .status(PluginStatus::Downloading)
                                .progress(new_progress)
                                .build()
                                .unwrap(),
                        )
                        .unwrap();
                }
                return v;
            })
            .await
        {
            // If the download is successful, send a 100% progress message to indicate the download is complete.
            Ok(_) => {
                on_progress.send(100f64).unwrap();
                // Wait for 500 milliseconds to ensure the message is received by the front end.
                sleep(Duration::from_millis(500)).await;
                // Send a download success status message to the front end.
                on_progress
                    .send(
                        PluginDownloadMessage::builder()
                            .status(PluginStatus::Success)
                            .build()
                            .unwrap(),
                    )
                    .unwrap();
            }
            // If the download fails, send an error status message to the front end.
            Err(e) => {
                if !cancel2.load(std::sync::atomic::Ordering::SeqCst) {
                    // Send an error status message to the front end.
                    on_progress
                        .send(
                            PluginDownloadMessage::builder()
                                .status(PluginStatus::Error)
                                .error_message(e.to_string())
                                .build()
                                .unwrap(),
                        )
                        .unwrap();
                }
            }
        }
    });

    // Listen for cancellation events from the front end.
    app.listen("plugin-cancel", move |event| {
        // Parse the reference information from the event.
        let reference = serde_json::from_str::<Value>(event.payload()).unwrap();
        // If the reference information matches the current plugin's reference information, cancel the download task.
        if reference["reference"] == plugin_reference {
            // If successfully set the cancel variable to false, it means the cancellation operation was successful.
            if cancel
                .compare_exchange(
                    true,
                    false,
                    std::sync::atomic::Ordering::SeqCst,
                    std::sync::atomic::Ordering::SeqCst,
                )
                .is_ok()
            {
                // Send a cancelled status message to the front end.
                on_progress2
                    .send(
                        PluginDownloadMessage::builder()
                            .status(PluginStatus::Canceled)
                            .build()
                            .unwrap(),
                    )
                    .context("Send Message Error")
                    .unwrap();
                // Cancel the asynchronous task.
                handler.abort();
            }
        }
    });

    // The function execution is successful, return Ok.
    Ok(())
}
```

This code provides a comprehensive guide on how to integrate Git clone operation progress display and cancellation features into a Rust application using Tauri and the git2 library. By following the outlined steps and code examples, developers can effectively manage Git operations within their applications, providing a smooth user experience and the ability to handle long-running tasks with ease.