Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ members = [
"crates/but-github", # 📄A thin wrapper of the GitHub API, for authentication and resource access.
# 👉lacks top-level docs and docs.
"crates/but-gitlab", # 📄A thin wrapper of the GitLab API, for authentication and resource access.
# 👉Has basic tests and top-level docs; purpose is intentionally narrow.
"crates/but-gitea", # 📄A thin wrapper of the Gitea API, for authentication and resource access.
# 👉No tests, lacks top-level docs, purpose somewhat unclear.
"crates/but-cursor", # 📄Integration with Cursor
# 👉Kind of no docs, no tests, and unclear purpose.
Expand Down Expand Up @@ -174,6 +176,7 @@ but-cherry-apply = { path = "crates/but-cherry-apply" }
but-irc = { path = "crates/but-irc" }
but-github = { path = "crates/but-github" }
but-gitlab = { path = "crates/but-gitlab" }
but-gitea = { path = "crates/but-gitea" }
but-error = { path = "crates/but-error" }
but-serde = { path = "crates/but-serde" }
but-schemars = { path = "crates/but-schemars" }
Expand Down
30 changes: 30 additions & 0 deletions apps/desktop/src/components/GiteaAccountBadge.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<script lang="ts">
import { Badge } from "@gitbutler/ui";
import type { ButGiteaToken } from "@gitbutler/core/api";

type Props = {
account: ButGiteaToken.GiteaAccountIdentifier;
class?: string;
};

const { account, class: className }: Props = $props();

function normalizedHost(host: string): string {
return host
.replace(/^https?:\/\//, "")
.replace(/\/api\/v1\/?$/, "")
.replace(/\/$/, "");
}

export function badgeText(account: ButGiteaToken.GiteaAccountIdentifier): string {
return normalizedHost(account.host);
}

export function tooltipText(account: ButGiteaToken.GiteaAccountIdentifier): string {
return `Gitea instance: ${normalizedHost(account.host)}`;
}
</script>

<Badge class={className} tooltip={tooltipText(account)}>
{badgeText(account)}
</Badge>
175 changes: 175 additions & 0 deletions apps/desktop/src/components/GiteaIntegration.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
<script lang="ts">
import GiteaUserLoginState from "$components/GiteaUserLoginState.svelte";
import ReduxResult from "$components/ReduxResult.svelte";
import { GITEA_USER_SERVICE } from "$lib/forge/gitea/giteaUserService.svelte";
import { inject } from "@gitbutler/core/context";
import { Button, CardGroup, Textbox } from "@gitbutler/ui";
import { fade } from "svelte/transition";

const giteaUserService = inject(GITEA_USER_SERVICE);

const [storeAccount, storeAccountResult] = giteaUserService.storeGiteaAccount;
const accounts = giteaUserService.accounts();

let showingFlow = $state(false);
let hostInput = $state<string>();
let patInput = $state<string>();
let hostError = $state<string>();
let patError = $state<string>();
let retryingAccounts = $state(false);

function cleanupFlow() {
showingFlow = false;
hostInput = undefined;
patInput = undefined;
hostError = undefined;
patError = undefined;
}

function startFlow() {
showingFlow = true;
}

async function retryLoadAccounts() {
retryingAccounts = true;
try {
await accounts.result.refetch();
} finally {
retryingAccounts = false;
}
}

async function storeToken() {
if (!hostInput || !patInput) return;
hostError = undefined;
patError = undefined;
try {
await storeAccount({ host: hostInput, accessToken: patInput });
cleanupFlow();
} catch (err: any) {
console.error("Failed to store Gitea token:", err);
hostError = "Invalid host or network error";
patError = "Invalid token";
}
}
</script>

<div class="stack-v gap-8">
<CardGroup>
<ReduxResult result={accounts.result}>
{#snippet error()}
<CardGroup.Item>
{#snippet title()}
Failed to load Gitea accounts
{/snippet}
<Button style="pop" onclick={retryLoadAccounts} loading={retryingAccounts}
>Try again</Button
>
</CardGroup.Item>
{/snippet}

{#snippet children(accounts)}
{@const noAccounts = accounts.length === 0}
{#each accounts as account}
<GiteaUserLoginState {account} />
{/each}

<CardGroup.Item background={accounts.length > 0 ? "var(--clr-bg-2)" : undefined}>
{#snippet iconSide()}
<div class="icon-wrapper__logo">GT</div>
{/snippet}

{#snippet title()}
Gitea
{/snippet}

{#snippet caption()}
Store a personal access token for Codeberg or any Gitea-compatible instance.
{/snippet}

{#snippet actions()}
<Button
style="pop"
onclick={startFlow}
disabled={showingFlow}
loading={storeAccountResult.current.isLoading}
>
{noAccounts ? "Add account" : "Add another"}
</Button>
{/snippet}
</CardGroup.Item>
{/snippet}
</ReduxResult>
</CardGroup>

{#if showingFlow}
<div in:fade={{ duration: 100 }}>
<CardGroup>
<CardGroup.Item>
{#snippet title()}
Add Gitea Account
{/snippet}

{#snippet caption()}
Use the web base URL for your instance, such as `https://codeberg.org` or your
self-hosted Gitea host.
{/snippet}

<Textbox
label="Instance URL"
size="large"
value={hostInput}
oninput={(value) => (hostInput = value)}
placeholder="https://codeberg.org"
error={hostError}
/>
<Textbox
label="Personal Access Token"
size="large"
type="password"
value={patInput}
oninput={(value) => (patInput = value)}
error={patError}
/>
</CardGroup.Item>
<CardGroup.Item>
<div class="flex justify-end gap-6">
<Button style="gray" kind="outline" onclick={cleanupFlow}>Cancel</Button>
<Button
style="pop"
disabled={!hostInput || !patInput}
loading={storeAccountResult.current.isLoading}
onclick={storeToken}
>
Add account
</Button>
</div>
</CardGroup.Item>
</CardGroup>
</div>
{/if}
</div>

<p class="text-12 text-body integration-settings__text">
🔒 Credentials are persisted locally in your OS Keychain / Credential Manager.
</p>

<style lang="postcss">
.icon-wrapper__logo {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border-radius: 999px;
background: var(--clr-bg-3);
color: var(--clr-text-1);
font-weight: 700;
font-size: 11px;
letter-spacing: 0.08em;
}

.integration-settings__text {
color: var(--clr-text-2);
}
</style>
70 changes: 70 additions & 0 deletions apps/desktop/src/components/GiteaUserLoginState.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<script lang="ts">
import GiteaAccountBadge from "$components/GiteaAccountBadge.svelte";
import ReduxResult from "$components/ReduxResult.svelte";
import { GITEA_USER_SERVICE } from "$lib/forge/gitea/giteaUserService.svelte";
import { inject } from "@gitbutler/core/context";
import { ForgeUserCard } from "@gitbutler/ui";
import { QueryStatus } from "@reduxjs/toolkit/query";
import type { ButGiteaToken } from "@gitbutler/core/api";

type Props = {
account: ButGiteaToken.GiteaAccountIdentifier;
};

const { account }: Props = $props();

const giteaUserService = inject(GITEA_USER_SERVICE);

const [forget, forgetting] = giteaUserService.forgetGiteaAccount;
const giteaUser = $derived(giteaUserService.authenticatedUser(account));

const isError = $derived(giteaUser.result?.status === QueryStatus.rejected);
const isLoading = $derived(giteaUser.result?.status === QueryStatus.pending);
const username = $derived(account.username);
</script>

<ReduxResult result={giteaUser.result}>
{#snippet loading()}
<ForgeUserCard
{username}
avatarUrl={null}
isError={false}
isLoading={true}
onForget={() => forget(account)}
isForgetLoading={forgetting.current.isLoading}
>
{#snippet badge()}
<GiteaAccountBadge {account} />
{/snippet}
</ForgeUserCard>
{/snippet}
{#snippet error()}
<ForgeUserCard
{username}
avatarUrl={null}
isError={true}
isLoading={false}
onForget={() => forget(account)}
isForgetLoading={forgetting.current.isLoading}
>
{#snippet badge()}
<GiteaAccountBadge {account} />
{/snippet}
</ForgeUserCard>
{/snippet}
{#snippet children(user)}
<ForgeUserCard
{username}
avatarUrl={user?.avatarUrl ?? null}
email={user?.email}
{isError}
{isLoading}
onForget={() => forget(account)}
isForgetLoading={forgetting.current.isLoading}
>
{#snippet badge()}
<GiteaAccountBadge {account} />
{/snippet}
</ForgeUserCard>
{/snippet}
</ReduxResult>
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<script lang="ts">
import GiteaIntegration from "$components/GiteaIntegration.svelte";
import GithubIntegration from "$components/GithubIntegration.svelte";
import GitlabIntegration from "$components/GitlabIntegration.svelte";
import { SETTINGS_SERVICE } from "$lib/config/appSettingsV2";
Expand All @@ -17,6 +18,7 @@

<GithubIntegration />
<GitlabIntegration />
<GiteaIntegration />
<Spacer />
<CardGroup>
<CardGroup.Item labelFor="autoFillPrDescription">
Expand Down
3 changes: 3 additions & 0 deletions apps/desktop/src/lib/bootstrap/deps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
} from "$lib/dragging/stackingReorderDropzoneManager";
import { FILE_SERVICE, FileService } from "$lib/files/fileService";
import { DefaultForgeFactory, DEFAULT_FORGE_FACTORY } from "$lib/forge/forgeFactory.svelte";
import { GITEA_USER_SERVICE, GiteaUserService } from "$lib/forge/gitea/giteaUserService.svelte";
import { GITHUB_CLIENT, GitHubClient } from "$lib/forge/github/githubClient";
import { GitHubUserService, GITHUB_USER_SERVICE } from "$lib/forge/github/githubUserService.svelte";
import { GITLAB_CLIENT, GitLabClient } from "$lib/forge/gitlab/gitlabClient.svelte";
Expand Down Expand Up @@ -137,6 +138,7 @@ export function initDependencies(args: {

const clientState = new ClientState(backend, gitHubClient, gitLabClient, posthog);
const githubUserService = new GitHubUserService(clientState.backendApi);
const giteaUserService = new GiteaUserService(clientState.backendApi);
const gitlabUserService = new GitLabUserService(clientState.backendApi, secretsService);

const uiState = new UiState(
Expand Down Expand Up @@ -346,6 +348,7 @@ export function initDependencies(args: {
[FOCUS_MANAGER, focusManager],
[GITHUB_CLIENT, gitHubClient],
[GITHUB_USER_SERVICE, githubUserService],
[GITEA_USER_SERVICE, giteaUserService],
[GITLAB_USER_SERVICE, gitlabUserService],
[GITLAB_CLIENT, gitLabClient],
[GIT_CONFIG_SERVICE, gitConfig],
Expand Down
Loading
Loading