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

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 validates and encrypts the token:

/// 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:

/// 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 retrieves all open MRs for a specific project:

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 fetches detailed state information:

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:

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:

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:

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:

<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 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 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 and 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 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 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 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 utility ensures the correct GitLab branding appears alongside status indicators.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →