# GitButler GitLab Integration for PR Status Checking: A Complete Technical Guide

> Master GitButler GitLab integration for PR status checking. This technical guide shows how to streamline MR workflows using a unified Rust API.

- Repository: [GitButler/gitbutler](https://github.com/gitbutlerapp/gitbutler)
- Tags: how-to-guide
- Published: 2026-02-16

---

**GitButler provides native GitLab integration that allows developers to check merge request status, validate credentials, and manage MR workflows through a unified Rust API layer.**

The `gitbutlerapp/gitbutler` repository implements first-class GitLab support alongside its virtual branch management system. This integration enables both the desktop application and CLI to authenticate with GitLab, monitor merge request states, and create new MRs without leaving the GitButler workflow. The implementation spans multiple Rust crates that handle everything from encrypted token storage to HTTP client operations against the GitLab API.

## Architecture of the GitLab Integration

GitButler's GitLab functionality resides in a dedicated crate structure that separates concerns between API abstraction, HTTP transport, and credential management. The `but-api` crate provides the public interface consumed by the Tauri backend and CLI, while `but-gitlab` handles the low-level GitLab API v4 interactions.

The `but-forge-storage` crate manages encrypted persistence of Personal Access Tokens (PATs) using the OS-native keychain or equivalent secure storage. This architecture ensures that GitLab credentials remain secure while remaining accessible to both the desktop GUI and command-line interfaces.

## Authenticating with GitLab

### Storing GitLab Personal Access Tokens

Before checking PR status, GitButler requires a valid GitLab PAT. The `store_gitlab_pat` function in [`crates/but-api/src/gitlab.rs`](https://github.com/gitbutlerapp/gitbutler/blob/main/crates/but-api/src/gitlab.rs) validates and encrypts the token:

```rust
/// Stores a GitLab Personal Access Token (PAT) for gitlab.com.
/// Validates and stores the provided PAT, then returns the authenticated user.
/// <https://github.com/gitbutlerapp/gitbutler/blob/master/crates/but-api/src/gitlab.rs#L22-L25>
pub async fn store_gitlab_pat(access_token: Sensitive<String>) -> Result<AuthStatusResponse> {
    let storage = but_forge_storage::Controller::from_path(but_path::app_data_dir()?);
    but_gitlab::store_pat(&access_token, &storage).await
}

```

The `Sensitive<String>` type ensures the token is encrypted in memory before being passed to the storage controller. For self-hosted GitLab instances, use `store_gitlab_selfhosted_pat` with the additional instance URL parameter.

### Validating Stored Credentials

To verify that a stored token remains valid before attempting MR operations, use `check_gitlab_credentials`:

```rust
/// Validates stored GitLab credentials.
/// Returns a `CredentialCheckResult` indicating validity.
/// <https://github.com/gitbutlerapp/gitbutler/blob/master/crates/but-api/src/gitlab.rs#L36-L40>
pub async fn check_gitlab_credentials(
    account: but_gitlab::GitlabAccountIdentifier,
) -> Result<but_gitlab::CredentialCheckResult> {
    let storage = but_forge_storage::Controller::from_path(but_path::app_data_dir()?);
    but_gitlab::check_credentials(&account, &storage).await
}

```

This function issues a lightweight `GET /user` request to confirm the token hasn't expired or been revoked, returning `CredentialCheckResult::Valid` or `CredentialCheckResult::Invalid`.

## Checking Merge Request Status

### Listing Open Merge Requests

The `list` function in [`crates/but-gitlab/src/mr.rs`](https://github.com/gitbutlerapp/gitbutler/blob/main/crates/but-gitlab/src/mr.rs) retrieves all open MRs for a specific project:

```rust
pub async fn list(
    preferred_account: Option<&crate::GitlabAccountIdentifier>,
    project_id: GitLabProjectId,
    storage: &but_forge_storage::Controller,
) -> Result<Vec<crate::client::MergeRequest>> {
    if let Ok(gl) = GitLabClient::from_storage(storage, preferred_account) {
        gl.list_open_mrs(project_id).await.context("Failed to list open merge requests")
    } else {
        Ok(vec![])
    }
}

```

This returns a vector of `MergeRequest` structs containing status fields that indicate whether an MR is open, merged, or closed.

### Retrieving Specific MR Details

For PR status checking of a specific merge request, the `get_merge_request` method in [`crates/but-gitlab/src/client.rs`](https://github.com/gitbutlerapp/gitbutler/blob/main/crates/but-gitlab/src/client.rs) fetches detailed state information:

```rust
pub async fn get_merge_request(&self, project_id: GitLabProjectId, mr_iid: i64) -> Result<MergeRequest> {
    let url = format!("{}/projects/{}/merge_requests/{}", self.base_url, project_id, mr_iid);
    let response = self.client.get(&url).send().await?;
    if !response.status().is_success() {
        bail!("Failed to get merge request: {}", response.status());
    }
    let mr: GitLabMergeRequest = response.json().await?;
    Ok(mr.into())
}

```

The `MergeRequest` struct includes critical status fields such as `merged_at`, `closed_at`, `labels`, and `draft`, enabling the UI to display accurate PR status indicators.

### Creating Merge Requests

To programmatically create an MR from a virtual branch:

```rust
pub async fn create(
    preferred_account: Option<&crate::GitlabAccountIdentifier>,
    params: crate::client::CreateMergeRequestParams<'_>,
    storage: &but_forge_storage::Controller,
) -> Result<crate::client::MergeRequest> {
    let mr = GitLabClient::from_storage(storage, preferred_account)?
        .create_merge_request(&params)
        .await
        .context("Failed to create merge request")?;
    Ok(mr)
}

```

This function accepts parameters including title, description, source branch, target branch, and project ID, returning the created MR with its initial status.

## Practical Implementation Examples

### Validating All Stored GitLab Accounts

This complete example demonstrates listing and validating credentials for all stored GitLab accounts:

```rust
use but_api::gitlab;
use but_forge_storage::Controller;
use but_path::app_data_dir;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // Load the storage controller (app‑data dir)
    let storage = Controller::from_path(app_data_dir()?)?;

    // Retrieve stored accounts
    let accounts = gitlab::list_known_gitlab_accounts().await?;

    for acc in accounts {
        // Validate each token
        match gitlab::check_gitlab_credentials(acc.clone()).await? {
            but_gitlab::CredentialCheckResult::Valid => {
                println!("✅ {} – credentials OK", acc);
            }
            but_gitlab::CredentialCheckResult::Invalid => {
                println!("⚠️ {} – invalid/expired token", acc);
            }
            _ => println!("ℹ️ {} – no credentials stored", acc),
        }
    }
    Ok(())
}

```

### Creating a Merge Request Programmatically

This example shows the complete workflow for creating an MR from a virtual branch:

```rust
use but_api::gitlab;
use but_gitlab::GitlabAccountIdentifier;
use but_forge_storage::Controller;
use but_path::app_data_dir;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // Assume we already have a stored account identifier
    let account = GitlabAccountIdentifier::PatUsername { username: "alice".into() };

    // Parameters for the MR
    let params = but_gitlab::client::CreateMergeRequestParams {
        title: "Feature: improve UI",
        body: "Adds a new button to the toolbar.",
        source_branch: "feature/ui-improvement",
        target_branch: "main",
        project_id: 42,               // numeric GitLab project ID
    };

    // Issue the request
    let mr = gitlab::create(Some(&account), params, &Controller::from_path(app_data_dir()?)?).await?;

    println!("✅ MR created: {}", mr.web_url);
    Ok(())
}

```

## UI Integration with Svelte

GitButler's frontend consumes the same Rust APIs through Tauri commands. The `ForgeUserCard` component displays GitLab account information and MR status:

```svelte
<script lang="ts">
  import type { GitlabAccountIdentifier } from '$lib/api/gitlab';
  export let account: GitlabAccountIdentifier;
  export let username: string;
  export let avatarUrl: string | null = null;
</script>

<div class="account-card">
  <img src={avatarUrl ?? '/icons/gitlab.svg'} alt="GitLab avatar" class="avatar" />
  <div class="details">
    <strong>{username}</strong>
    <span class="provider">GitLab</span>
  </div>
  <button on:click={() => dispatch('forget', account)}>Forget</button>
</div>

<style>
  .account-card { /* styling omitted for brevity */ }
</style>

```

The corresponding story file at [`packages/ui/src/stories/components/ForgeUserCard.stories.svelte`](https://github.com/gitbutlerapp/gitbutler/blob/main/packages/ui/src/stories/components/ForgeUserCard.stories.svelte) demonstrates how the UI presents GitLab MR status data returned by the Rust backend.

## Summary

- **GitButler GitLab integration** is implemented across dedicated Rust crates: `but-api` for the public interface, `but-gitlab` for HTTP operations, and `but-forge-storage` for encrypted token persistence.
- **Authentication** uses `store_gitlab_pat` in [`crates/but-api/src/gitlab.rs`](https://github.com/gitbutlerapp/gitbutler/blob/main/crates/but-api/src/gitlab.rs) to encrypt and store Personal Access Tokens, with `check_gitlab_credentials` validating tokens via lightweight API calls.
- **PR status checking** relies on `list_open_mrs` and `get_merge_request` in [`crates/but-gitlab/src/mr.rs`](https://github.com/gitbutlerapp/gitbutler/blob/main/crates/but-gitlab/src/mr.rs) and [`client.rs`](https://github.com/gitbutlerapp/gitbutler/blob/main/client.rs), returning structs with `merged_at`, `closed_at`, and `draft` fields.
- **Both CLI and GUI** consume the same backend APIs, with the Svelte frontend accessing Rust functions through Tauri IPC commands.

## Frequently Asked Questions

### How does GitButler store GitLab authentication tokens securely?

GitButler encrypts GitLab Personal Access Tokens using the `but-forge-storage` crate before writing them to the application data directory. The `store_gitlab_pat` function in [`crates/but-api/src/gitlab.rs`](https://github.com/gitbutlerapp/gitbutler/blob/main/crates/but-api/src/gitlab.rs) wraps the token in a `Sensitive<String>` type that ensures encryption both in transit and at rest, leveraging the OS-native keychain where available.

### Can GitButler check PR status for self-hosted GitLab instances?

Yes, GitButler supports self-hosted GitLab through the `store_gitlab_selfhosted_pat` function, which accepts an instance URL parameter alongside the access token. The `but-gitlab` crate constructs API endpoints using the provided base URL rather than hardcoded `gitlab.com` paths, allowing the `check_gitlab_credentials` and MR status functions to work with private GitLab deployments.

### What specific PR status fields does GitButler retrieve from GitLab?

When checking PR status, GitButler retrieves a `MergeRequest` struct containing `merged_at`, `closed_at`, `labels`, and `draft` fields. These fields allow the UI to distinguish between open, merged, closed, and draft merge requests. The `get_merge_request` method in [`crates/but-gitlab/src/client.rs`](https://github.com/gitbutlerapp/gitbutler/blob/main/crates/but-gitlab/src/client.rs) fetches these details via the GitLab API v4 endpoint `GET /projects/:id/merge_requests/:merge_request_iid`.

### How does the GitButler UI display GitLab PR status information?

The GitButler desktop application uses Svelte components that consume the Rust backend through Tauri IPC commands. The `ForgeUserCard` component in [`packages/ui/src/stories/components/ForgeUserCard.stories.svelte`](https://github.com/gitbutlerapp/gitbutler/blob/main/packages/ui/src/stories/components/ForgeUserCard.stories.svelte) demonstrates how GitLab account data and MR status are rendered, utilizing the same `GitlabAccountIdentifier` types returned by the `list_known_gitlab_accounts` API. The [`getForgeLogo.ts`](https://github.com/gitbutlerapp/gitbutler/blob/main/getForgeLogo.ts) utility ensures the correct GitLab branding appears alongside status indicators.