Tauri v1 -> v2

Refactor appSettings to accommodate new Tauri v2 API

- creates AppSettings class and injects it where needed
- avoids `window` undeclared variable during vite build process
This commit is contained in:
Mattias Granlund 2024-10-07 16:23:05 +02:00 committed by Nico Domino
parent 7be3e7a6e7
commit 2ef866baa6
52 changed files with 2669 additions and 2343 deletions

View File

@ -157,6 +157,7 @@ jobs:
run: |
sudo apt update;
sudo apt install -y \
libwebkit2gtk-4.1-dev \
build-essential \
curl \
wget \
@ -186,11 +187,10 @@ jobs:
--dist "./release" \
--version "${{ env.version }}"
env:
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_TEAM_ID: ${{ secrets.APPLE_PROVIDER_SHORT_NAME }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}

2909
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -17,6 +17,7 @@
"prepare": "svelte-kit sync"
},
"devDependencies": {
"@anthropic-ai/sdk": "^0.27.3",
"@codemirror/lang-cpp": "^6.0.2",
"@codemirror/lang-css": "^6.2.1",
"@codemirror/lang-html": "^6.4.9",
@ -42,7 +43,16 @@
"@sveltejs/adapter-static": "catalog:svelte",
"@sveltejs/kit": "catalog:svelte",
"@sveltejs/vite-plugin-svelte": "catalog:svelte",
"@tauri-apps/api": "^1.6.0",
"@tauri-apps/api": "^2.0.3",
"@tauri-apps/plugin-dialog": "^2.0.1",
"@tauri-apps/plugin-http": "^2.0.1",
"@tauri-apps/plugin-fs": "^2.0.1",
"@tauri-apps/plugin-log": "^2.0.0",
"@tauri-apps/plugin-os": "^2.0.0",
"@tauri-apps/plugin-process": "^2.0.0",
"@tauri-apps/plugin-shell": "^2.0.1",
"@tauri-apps/plugin-store": "^2.1.0",
"@tauri-apps/plugin-updater": "^2.0.0",
"@testing-library/jest-dom": "^6.4.8",
"@testing-library/svelte": "^5.2.1",
"@types/diff-match-patch": "^1.0.36",
@ -77,15 +87,12 @@
"svelte": "catalog:svelte",
"svelte-check": "catalog:svelte",
"svelte-french-toast": "^1.2.0",
"tauri-plugin-log-api": "https://github.com/tauri-apps/tauri-plugin-log#v1",
"tauri-plugin-store-api": "https://github.com/tauri-apps/tauri-plugin-store#v1",
"tinykeys": "^2.1.0",
"ts-node": "^10.9.2",
"vite": "catalog:",
"vitest": "^2.0.5"
},
"dependencies": {
"@anthropic-ai/sdk": "^0.27.3",
"openai": "^4.47.3"
}
}

View File

@ -1,6 +1,6 @@
import { showError } from '$lib/notifications/toasts';
import { captureException } from '@sentry/sveltekit';
import { error as logErrorToFile } from 'tauri-plugin-log-api';
import { error as logErrorToFile } from '@tauri-apps/plugin-log';
import type { HandleClientError } from '@sveltejs/kit';
// SvelteKit error handler.
@ -25,6 +25,7 @@ window.onunhandledrejection = (e: PromiseRejectionEvent) => {
};
function logError(error: unknown) {
try {
let message = error instanceof Error ? error.message : String(error);
const stack = error instanceof Error ? error.stack : undefined;
@ -38,7 +39,9 @@ function logError(error: unknown) {
if (stack) message = `${message}\n${stack}\n`;
logErrorToFile(message);
console.error(message);
showError('Something went wrong', message);
return id;
} catch (err: unknown) {
console.error('Error while trying to log error.', err);
}
}

View File

@ -4,13 +4,10 @@ import {
SHORT_DEFAULT_BRANCH_TEMPLATE,
SHORT_DEFAULT_PR_TEMPLATE
} from '$lib/ai/prompts';
import {
type AIClient,
type AIEvalOptions,
type AnthropicModelName,
type Prompt
} from '$lib/ai/types';
import { andThenAsync, ok, wrapAsync, type Result } from '$lib/result';
import { type AIEvalOptions } from '$lib/ai/types';
import { type AIClient, type AnthropicModelName, type Prompt } from '$lib/ai/types';
import { andThenAsync, wrapAsync } from '$lib/result';
import { ok, type Result } from '$lib/result';
import Anthropic from '@anthropic-ai/sdk';
import type { RawMessageStreamEvent } from '@anthropic-ai/sdk/resources/messages.mjs';
import type { Stream } from '@anthropic-ai/sdk/streaming.mjs';

View File

@ -6,7 +6,7 @@ import {
import { MessageRole, type PromptMessage, type AIClient, type Prompt } from '$lib/ai/types';
import { andThen, buildFailureFromAny, ok, wrap, wrapAsync, type Result } from '$lib/result';
import { isNonEmptyObject } from '@gitbutler/ui/utils/typeguards';
import { fetch, Body, Response } from '@tauri-apps/api/http';
import { fetch } from '@tauri-apps/plugin-http';
export const DEFAULT_OLLAMA_ENDPOINT = 'http://127.0.0.1:11434';
export const DEFAULT_OLLAMA_MODEL_NAME = 'llama3';
@ -137,9 +137,9 @@ ${JSON.stringify(OLLAMA_CHAT_MESSAGE_FORMAT_SCHEMA, null, 2)}`
* @param request - The OllamaChatRequest object containing the request details.
* @returns A Promise that resolves to the Response object.
*/
private async fetchChat(request: OllamaChatRequest): Promise<Result<Response<any>, Error>> {
private async fetchChat(request: OllamaChatRequest): Promise<Result<any, Error>> {
const url = new URL(OllamaAPEndpoint.Chat, this.endpoint);
const body = Body.json(request);
const body = JSON.stringify(request);
return await wrapAsync(
async () =>
await fetch(url.toString(), {

View File

@ -1,4 +1,4 @@
import { type Prompt, MessageRole } from '$lib/ai/types';
import { type Prompt, MessageRole } from './types';
export const SHORT_DEFAULT_COMMIT_TEMPLATE: Prompt = [
{

View File

@ -1,4 +1,12 @@
import { DEFAULT_PR_SUMMARY_MAIN_DIRECTIVE, getPrTemplateDirective } from './prompts';
import {
OpenAIModelName,
type AIClient,
AnthropicModelName,
ModelKind,
MessageRole,
type Prompt
} from './types';
import { AnthropicAIClient } from '$lib/ai/anthropicClient';
import { ButlerAIClient } from '$lib/ai/butlerClient';
import {
@ -7,14 +15,6 @@ import {
OllamaClient
} from '$lib/ai/ollamaClient';
import { OpenAIClient } from '$lib/ai/openAIClient';
import {
OpenAIModelName,
type AIClient,
AnthropicModelName,
ModelKind,
MessageRole,
type Prompt
} from '$lib/ai/types';
import { buildFailureFromAny, isFailure, ok, type Result } from '$lib/result';
import { splitMessage } from '$lib/utils/commitMessage';
import { get } from 'svelte/store';

View File

@ -1,30 +1,18 @@
import { initPostHog } from '$lib/analytics/posthog';
import { initSentry } from '$lib/analytics/sentry';
import { appAnalyticsConfirmed } from '$lib/config/appSettings';
import {
appMetricsEnabled,
appErrorReportingEnabled,
appNonAnonMetricsEnabled
} from '$lib/config/appSettings';
import { AppSettings } from '$lib/config/appSettings';
import posthog from 'posthog-js';
export function initAnalyticsIfEnabled() {
const analyticsConfirmed = appAnalyticsConfirmed();
analyticsConfirmed.onDisk().then((confirmed) => {
export function initAnalyticsIfEnabled(appSettings: AppSettings) {
appSettings.appAnalyticsConfirmed.onDisk().then((confirmed) => {
if (confirmed) {
appErrorReportingEnabled()
.onDisk()
.then((enabled) => {
appSettings.appErrorReportingEnabled.onDisk().then((enabled) => {
if (enabled) initSentry();
});
appMetricsEnabled()
.onDisk()
.then((enabled) => {
appSettings.appMetricsEnabled.onDisk().then((enabled) => {
if (enabled) initPostHog();
});
appNonAnonMetricsEnabled()
.onDisk()
.then((enabled) => {
appSettings.appNonAnonMetricsEnabled.onDisk().then((enabled) => {
if (enabled) {
posthog.capture('nonAnonMetricsEnabled');
} else {

View File

@ -1,5 +1,5 @@
import { invoke as invokeTauri } from '@tauri-apps/api/core';
import { listen as listenTauri } from '@tauri-apps/api/event';
import { invoke as invokeTauri } from '@tauri-apps/api/tauri';
import type { EventCallback, EventName } from '@tauri-apps/api/event';
export enum Code {

View File

@ -2,7 +2,7 @@ import { invoke } from '$lib/backend/ipc';
import { showError } from '$lib/notifications/toasts';
import * as toasts from '$lib/utils/toasts';
import { persisted } from '@gitbutler/shared/persisted';
import { open } from '@tauri-apps/api/dialog';
import { open } from '@tauri-apps/plugin-dialog';
import { plainToInstance } from 'class-transformer';
import { derived, get, writable, type Readable } from 'svelte/store';
import type { ForgeType } from './forge';

View File

@ -1,11 +1,10 @@
import { invoke as invokeIpc, listen as listenIpc } from './ipc';
import { getVersion } from '@tauri-apps/api/app';
import { checkUpdate, onUpdaterEvent } from '@tauri-apps/api/updater';
import { check } from '@tauri-apps/plugin-updater';
export class Tauri {
invoke = invokeIpc;
listen = listenIpc;
checkUpdate = checkUpdate;
onUpdaterEvent = onUpdaterEvent;
checkUpdate = check;
currentVersion = getVersion;
}

View File

@ -2,6 +2,7 @@ import { Tauri } from './tauri';
import { UPDATE_INTERVAL_MS, UpdaterService } from './updater';
import { get } from 'svelte/store';
import { expect, test, describe, vi, beforeEach, afterEach } from 'vitest';
import type { Update } from '@tauri-apps/plugin-updater';
/**
* It is important to understand the sync `get` method performs a store subscription
@ -16,7 +17,6 @@ describe('Updater', () => {
tauri = new Tauri();
updater = new UpdaterService(tauri);
vi.spyOn(tauri, 'listen').mockReturnValue(async () => {});
vi.spyOn(tauri, 'onUpdaterEvent').mockReturnValue(Promise.resolve(() => {}));
vi.spyOn(tauri, 'currentVersion').mockReturnValue(Promise.resolve('0.1'));
});
@ -28,38 +28,33 @@ describe('Updater', () => {
test('should not show up-to-date on interval check', async () => {
vi.spyOn(tauri, 'checkUpdate').mockReturnValue(
Promise.resolve({
shouldUpdate: false
})
available: false
} as Update)
);
vi.spyOn(tauri, 'onUpdaterEvent').mockImplementation(async (handler) => {
handler({ status: 'UPTODATE' });
return await Promise.resolve(() => {});
});
await updater.checkForUpdate();
expect(get(updater.update)).toHaveProperty('status', undefined);
expect(get(updater.update)).toMatchObject({});
});
test('should show up-to-date on manual check', async () => {
vi.spyOn(tauri, 'checkUpdate').mockReturnValue(
Promise.resolve({
shouldUpdate: false
})
available: false,
version: '1'
} as Update)
);
await updater.checkForUpdate(true); // manual = true;
expect(get(updater.update)).toHaveProperty('status', 'UPTODATE');
expect(get(updater.update)).toHaveProperty('status', 'Up-to-date');
});
test('should prompt again on new version', async () => {
const body = 'release notes';
const date = '2024-01-01';
vi.spyOn(tauri, 'checkUpdate').mockReturnValue(
Promise.resolve({
shouldUpdate: true,
manifest: { version: '1', body, date }
})
available: true,
version: '1',
body
} as Update)
);
await updater.checkForUpdate();
@ -70,9 +65,10 @@ describe('Updater', () => {
vi.spyOn(tauri, 'checkUpdate').mockReturnValue(
Promise.resolve({
shouldUpdate: true,
manifest: { version: '2', body, date }
})
available: true,
version: '2',
body
} as Update)
);
await updater.checkForUpdate();
const update2 = get(updater.update);
@ -83,13 +79,13 @@ describe('Updater', () => {
test('should not prompt download for seen version', async () => {
const version = '1';
const body = 'release notes';
const date = '2024-01-01';
vi.spyOn(tauri, 'checkUpdate').mockReturnValue(
Promise.resolve({
shouldUpdate: true,
manifest: { version, body, date }
})
available: true,
version,
body
} as Update)
);
const updater = new UpdaterService(tauri);
await updater.checkForUpdate();
@ -101,22 +97,20 @@ describe('Updater', () => {
updater.dismiss();
await updater.checkForUpdate();
const update2 = get(updater.update);
expect(update2).toHaveProperty('version', undefined);
expect(update2).toHaveProperty('releaseNotes', undefined);
expect(update2).toMatchObject({});
});
test('should check for updates continously', async () => {
const mock = vi.spyOn(tauri, 'checkUpdate').mockReturnValue(
Promise.resolve({
shouldUpdate: false
})
available: false
} as Update)
);
const unsubscribe = updater.update.subscribe(() => {});
await vi.advanceTimersToNextTimerAsync();
expect(mock).toHaveBeenCalledOnce();
for (let i = 2; i < 24; i++) {
for (let i = 2; i < 12; i++) {
await vi.advanceTimersByTimeAsync(UPDATE_INTERVAL_MS);
expect(mock).toHaveBeenCalledTimes(i);
}

View File

@ -1,17 +1,30 @@
import { Tauri } from './tauri';
import { showToast } from '$lib/notifications/toasts';
import { relaunch } from '@tauri-apps/api/process';
import {
installUpdate,
type UpdateResult,
type UpdateManifest,
type UpdateStatus
} from '@tauri-apps/api/updater';
import { relaunch } from '@tauri-apps/plugin-process';
import { type DownloadEvent, Update } from '@tauri-apps/plugin-updater';
import posthog from 'posthog-js';
import { derived, readable, writable } from 'svelte/store';
import { writable } from 'svelte/store';
type Status = UpdateStatus | 'DOWNLOADED';
const TIMEOUT_SECONDS = 30;
type UpdateStatus = {
version?: string;
releaseNotes?: string;
status?: InstallStatus | undefined;
};
export type InstallStatus =
| 'Checking'
| 'Downloading'
| 'Downloaded'
| 'Installing'
| 'Done'
| 'Up-to-date'
| 'Error';
const downloadStatusMap: { [K in DownloadEvent['event']]: InstallStatus } = {
Started: 'Downloading',
Progress: 'Downloading',
Finished: 'Downloaded'
};
export const UPDATE_INTERVAL_MS = 3600000; // Hourly
@ -19,39 +32,23 @@ export const UPDATE_INTERVAL_MS = 3600000; // Hourly
* Note that the Tauri API `checkUpdate` hangs indefinitely in dev mode, build
* a nightly if you want to test the updater manually.
*
* export TAURI_PRIVATE_KEY=doesnot
* export TAURI_KEY_PASSWORD=matter
* export TAURI_SIGNING_PRIVATE_KEY=doesnot
* export TAURI_SIGNING_PRIVATE_KEY_PASSWORD=matter
* ./scripts/release.sh --channel nightly --version "0.5.678"
*/
export class UpdaterService {
readonly loading = writable(false);
readonly status = writable<Status | undefined>();
private manifest = writable<UpdateManifest | undefined>(undefined, () => {
readonly update = writable<UpdateStatus>({}, () => {
this.start();
return () => {
this.stop();
};
});
private currentVersion = readable<string | undefined>(undefined, (set) => {
this.tauri.currentVersion().then((version) => set(version));
});
readonly update = derived(
[this.manifest, this.status, this.currentVersion],
([manifest, status, currentVersion]) => {
return {
version: manifest?.version,
releaseNotes: manifest?.body,
status,
currentVersion
};
}
);
private intervalId: any;
private seenVersion: string | undefined;
private lastCheckWasManual = false;
private tauriDownload: Update['download'] | undefined;
private tauriInstall: Update['install'] | undefined;
unlistenStatus?: () => void;
unlistenMenu?: () => void;
@ -62,18 +59,6 @@ export class UpdaterService {
this.unlistenMenu = this.tauri.listen<string>('menu://global/update/clicked', () => {
this.checkForUpdate(true);
});
this.unlistenStatus = await this.tauri.onUpdaterEvent((event) => {
const { error, status } = event;
if (status !== 'UPTODATE' || this.lastCheckWasManual) {
this.status.set(status);
}
if (error) {
handleError(error, false);
posthog.capture('App Update Status Error', { error });
}
});
setInterval(async () => await this.checkForUpdate(), UPDATE_INTERVAL_MS);
this.checkForUpdate();
}
@ -81,7 +66,6 @@ export class UpdaterService {
private async stop() {
this.unlistenStatus?.();
this.unlistenMenu?.();
if (this.intervalId) {
clearInterval(this.intervalId);
this.intervalId = undefined;
@ -90,55 +74,81 @@ export class UpdaterService {
async checkForUpdate(manual = false) {
this.loading.set(true);
this.lastCheckWasManual = manual;
try {
const update = await Promise.race([
this.tauri.checkUpdate(), // In DEV mode this never returns.
new Promise<UpdateResult>((_resolve, reject) =>
// For manual testing use resolve instead of reject here.
setTimeout(
() => reject(`Timed out after ${TIMEOUT_SECONDS} seconds.`),
TIMEOUT_SECONDS * 1000
)
)
]);
await this.processUpdate(update, manual);
this.handleUpdate(await this.tauri.checkUpdate(), manual); // In DEV mode this never returns.
} catch (err: unknown) {
// No toast unless manually invoked.
if (manual) {
handleError(err, true);
} else {
console.error(err);
}
handleError(err, manual);
} finally {
this.loading.set(false);
}
}
private async processUpdate(update: UpdateResult, manual: boolean) {
const { shouldUpdate, manifest } = update;
if (shouldUpdate === false && manual) {
this.status.set('UPTODATE');
private handleUpdate(update: Update | null, manual: boolean) {
if (update === null) {
this.update.set({});
return;
}
if (manifest && manifest.version !== this.seenVersion) {
this.manifest.set(manifest);
this.seenVersion = manifest.version;
if (!update.available && manual) {
this.setStatus('Up-to-date');
} else if (
update.available &&
update.version !== this.seenVersion &&
update.currentVersion !== '0.0.0' // DEV mode.
) {
const { version, body, download, install } = update;
this.tauriDownload = download.bind(update);
this.tauriInstall = install.bind(update);
this.seenVersion = version;
this.update.set({
version,
releaseNotes: body,
status: undefined
});
}
}
async installUpdate() {
async downloadAndInstall() {
this.loading.set(true);
try {
await installUpdate();
await this.download();
await this.install();
posthog.capture('App Update Successful');
} catch (err: any) {
} catch (error: any) {
// We expect toast to be shown by error handling in `onUpdaterEvent`
posthog.capture('App Update Install Error', { error: err });
handleError(error, true);
this.update.set({ status: 'Error' });
posthog.capture('App Update Install Error', { error });
} finally {
this.loading.set(false);
}
}
private async download() {
if (!this.tauriDownload) {
throw new Error('Download function not available.');
}
this.setStatus('Downloading');
await this.tauriDownload((progress: DownloadEvent) => {
this.setStatus(downloadStatusMap[progress.event]);
});
this.setStatus('Downloaded');
}
private async install() {
if (!this.tauriInstall) {
throw new Error('Install function not available.');
}
this.setStatus('Installing');
await this.tauriInstall();
this.setStatus('Done');
}
private setStatus(status: InstallStatus) {
this.update.update((update) => {
return { ...update, status };
});
}
async relaunchApp() {
try {
await relaunch();
@ -148,8 +158,7 @@ export class UpdaterService {
}
dismiss() {
this.manifest.set(undefined);
this.status.set(undefined);
this.update.set({});
}
}

View File

@ -1,5 +1,5 @@
<script lang="ts">
import { UpdaterService } from '$lib/backend/updater';
import { UpdaterService, type InstallStatus } from '$lib/backend/updater';
import { showToast } from '$lib/notifications/toasts';
import { getContext } from '@gitbutler/shared/context';
import Button from '@gitbutler/ui/Button.svelte';
@ -11,10 +11,10 @@
let version = $state<string | undefined>();
let releaseNotes = $state<string | undefined>();
let status = $state<string | undefined>();
let status = $state<InstallStatus | undefined>();
$effect(() => {
({ version, releaseNotes, status } = $update || {});
({ version, releaseNotes, status } = $update);
});
function handleDismiss() {
@ -22,14 +22,14 @@
}
</script>
{#if version || status === 'UPTODATE'}
{#if version || status === 'Up-to-date'}
<div class="update-banner" data-testid="update-banner" class:busy={$loading}>
<div class="floating-button">
<Button icon="cross-small" style="ghost" onclick={handleDismiss} />
</div>
<div class="img">
<div class="circle-img">
{#if status !== 'DONE' && status !== 'UPTODATE'}
{#if status !== 'Done' && status !== 'Up-to-date'}
<svg
class="arrow-img"
width="12"
@ -96,17 +96,19 @@
</div>
<h4 class="text-13 label">
{#if status === 'UPTODATE'}
{#if status === 'Up-to-date'}
You are up-to-date!
{:else if status === 'PENDING'}
Downloading update...
{:else if status === 'DOWNLOADED'}
Installing update...
{:else if status === 'DONE'}
{:else if status === 'Downloading'}
Downloading update…
{:else if status === 'Downloaded'}
Update downloaded
{:else if status === 'Installing'}
Installing update…
{:else if status === 'Done'}
Install complete
{:else if status === 'CHECKING'}
{:else if status === 'Checking'}
Checking for update…
{:else if status === 'ERROR'}
{:else if status === 'Error'}
Error occurred
{:else if version}
New version available
@ -130,7 +132,7 @@
</Button>
{/if}
<div class="status-section">
{#if status !== 'ERROR' && status !== 'UPTODATE'}
{#if status !== 'Error' && status !== 'Up-to-date'}
<div class="sliding-gradient"></div>
{/if}
<div class="cta-btn" transition:fade={{ duration: 100 }}>
@ -141,12 +143,12 @@
kind="solid"
testId="download-update"
onmousedown={async () => {
await updaterService.installUpdate();
await updaterService.downloadAndInstall();
}}
>
Update to {version}
</Button>
{:else if status === 'UPTODATE'}
{:else if status === 'Up-to-date'}
<Button
wide
style="pop"
@ -158,7 +160,7 @@
>
Got it!
</Button>
{:else if status === 'DONE'}
{:else if status === 'Done'}
<Button
style="pop"
kind="solid"

View File

@ -2,7 +2,9 @@ import AppUpdater from './AppUpdater.svelte';
import { Tauri } from '$lib/backend/tauri';
import { UpdaterService } from '$lib/backend/updater';
import { render, screen } from '@testing-library/svelte';
import { get } from 'svelte/store';
import { expect, test, describe, vi, beforeEach, afterEach } from 'vitest';
import type { Update } from '@tauri-apps/plugin-updater';
describe('AppUpdater', () => {
let tauri: Tauri;
@ -15,7 +17,6 @@ describe('AppUpdater', () => {
updater = new UpdaterService(tauri);
context = new Map([[UpdaterService, updater]]);
vi.spyOn(tauri, 'listen').mockReturnValue(async () => {});
vi.spyOn(tauri, 'onUpdaterEvent').mockReturnValue(Promise.resolve(() => {}));
vi.spyOn(tauri, 'currentVersion').mockReturnValue(Promise.resolve('0.1'));
});
@ -27,8 +28,8 @@ describe('AppUpdater', () => {
test('should be hidden if no update', async () => {
vi.spyOn(tauri, 'checkUpdate').mockReturnValue(
Promise.resolve({
shouldUpdate: false
})
version: '1'
} as Update)
);
render(AppUpdater, { context });
@ -41,13 +42,10 @@ describe('AppUpdater', () => {
test('should display download button', async () => {
vi.spyOn(tauri, 'checkUpdate').mockReturnValue(
Promise.resolve({
shouldUpdate: true,
manifest: {
available: true,
version: '1',
body: 'release notes',
date: '2024-01-01'
}
})
body: 'release notes'
} as Update)
);
render(AppUpdater, { context });
@ -60,8 +58,8 @@ describe('AppUpdater', () => {
test('should display up-to-date on manaul check', async () => {
vi.spyOn(tauri, 'checkUpdate').mockReturnValue(
Promise.resolve({
shouldUpdate: false
})
available: false
} as Update)
);
render(AppUpdater, { context });
updater.checkForUpdate(true);
@ -74,18 +72,31 @@ describe('AppUpdater', () => {
test('should display restart button on install complete', async () => {
vi.spyOn(tauri, 'checkUpdate').mockReturnValue(
Promise.resolve({
shouldUpdate: true,
manifest: { version: '1', body: 'release notes', date: '2024-01-01' }
})
available: true,
currentVersion: '1',
version: '2',
body: 'release notes',
download: () => {
console.log('HELLO');
},
install: () => {
console.log('WORLD');
}
} as Update)
);
vi.spyOn(tauri, 'onUpdaterEvent').mockImplementation(async (handler) => {
handler({ status: 'DONE' });
return () => {};
});
render(AppUpdater, { context });
updater.checkForUpdate(true);
await updater.checkForUpdate(true);
await vi.runOnlyPendingTimersAsync();
console.log('download and install');
await updater.downloadAndInstall();
await vi.runOnlyPendingTimersAsync();
await vi.advanceTimersToNextTimerAsync();
await vi.advanceTimersToNextTimerAsync();
await vi.advanceTimersToNextTimerAsync();
await vi.advanceTimersToNextTimerAsync();
await vi.advanceTimersToNextTimerAsync();
console.log(get(updater.update));
const button = screen.getByTestId('restart-app');
expect(button).toBeVisible();

View File

@ -27,7 +27,7 @@
loading = true;
try {
// TODO: Refactor temporary solution to forcing Windows to use system executable
if ($platformName === 'win32') {
if (platformName === 'windows') {
project.preferred_key = 'systemExecutable';
await projectsService.updateProject(project);
await baseBranchService.refresh();
@ -41,7 +41,7 @@
</script>
<DecorativeSplitView img={newProjectSvg}>
{#if selectedBranch[0] && selectedBranch[0] !== '' && $platformName !== 'win32'}
{#if selectedBranch[0] && selectedBranch[0] !== '' && platformName !== 'windows'}
{@const [remoteName, branchName] = selectedBranch[0].split(/\/(.*)/s)}
<KeysForm {remoteName} {branchName} disabled={loading} />
<div class="actions">
@ -59,7 +59,7 @@
on:branchSelected={async (e) => {
selectedBranch = e.detail;
// TODO: Temporary solution to forcing Windows to use system executable
if ($platformName === 'win32') {
if (platformName === 'windows') {
setTarget();
}
}}

View File

@ -254,7 +254,7 @@
testId="set-base-branch"
id="set-base-branch"
>
{#if $platformName === 'win32'}
{#if platformName === 'windows'}
Let's go
{:else}
Continue

View File

@ -54,8 +54,8 @@
}
async function readZipFile(path: string, filename?: string): Promise<File | Blob> {
const { readBinaryFile } = await import('@tauri-apps/api/fs');
const file = await readBinaryFile(path);
const { readFile } = await import('@tauri-apps/plugin-fs');
const file = await readFile(path);
const fileName = filename ?? path.split('/').pop();
return fileName
? new File([file], fileName, { type: 'application/zip' })

View File

@ -2,53 +2,58 @@
* This file contains functions for managing application settings.
* Settings are persisted in <Application Data>/settings.json and are used by both the UI and the backend.
*
* @module appSettings
* TODO: Rewrite this to be an injectable object so we don't need `storeInstance`.
*/
import { Store } from '@tauri-apps/plugin-store';
import { writable, type Writable } from 'svelte/store';
import { Store } from 'tauri-plugin-store-api';
const store = new Store('settings.json');
export async function loadAppSettings() {
const diskStore = await Store.load('settings.json', { autoSave: true });
return new AppSettings(diskStore);
}
export class AppSettings {
constructor(private diskStore: Store) {}
/**
* Persisted confirmation that user has confirmed their analytics settings.
*/
export function appAnalyticsConfirmed() {
return persisted(false, 'appAnalyticsConfirmed');
}
readonly appAnalyticsConfirmed = this.persisted(false, 'appAnalyticsConfirmed');
/**
* Provides a writable store for obtaining or setting the current state of application metrics.
* The application metrics can be enabled or disabled by setting the value of the store to true or false.
* @returns A writable store with the appMetricsEnabled config.
*/
export function appMetricsEnabled() {
return persisted(true, 'appMetricsEnabled');
}
readonly appMetricsEnabled = this.persisted(true, 'appMetricsEnabled');
/**
* Provides a writable store for obtaining or setting the current state of application error reporting.
* The application error reporting can be enabled or disabled by setting the value of the store to true or false.
* @returns A writable store with the appErrorReportingEnabled config.
*/
export function appErrorReportingEnabled() {
return persisted(true, 'appErrorReportingEnabled');
}
readonly appErrorReportingEnabled = this.persisted(true, 'appErrorReportingEnabled');
/**
* Provides a writable store for obtaining or setting the current state of non-anonemous application metrics.
* The setting can be enabled or disabled by setting the value of the store to true or false.
* @returns A writable store with the appNonAnonMetricsEnabled config.
*/
export function appNonAnonMetricsEnabled() {
return persisted(false, 'appNonAnonMetricsEnabled');
}
readonly appNonAnonMetricsEnabled = this.persisted(false, 'appNonAnonMetricsEnabled');
private persisted<T>(initial: T, key: string): Writable<T> & { onDisk: () => Promise<T> } {
const diskStore = this.diskStore;
const storeValueWithDefault = this.storeValueWithDefault.bind(this);
const keySpecificStore = writable<T>(initial, (set) => {
synchronize(set);
});
const subscribe = keySpecificStore.subscribe;
function persisted<T>(initial: T, key: string): Writable<T> & { onDisk: () => Promise<T> } {
async function setAndPersist(value: T, set: (value: T) => void) {
await store.set(key, value);
await store.save();
diskStore?.set(key, value);
set(value);
}
@ -57,44 +62,23 @@ function persisted<T>(initial: T, key: string): Writable<T> & { onDisk: () => Pr
set(value);
}
function update() {
throw 'Not implemented';
}
const thisStore = writable<T>(initial, (set) => {
synchronize(set);
});
async function set(value: T) {
setAndPersist(value, thisStore.set);
setAndPersist(value, keySpecificStore.set);
}
async function onDisk() {
return await storeValueWithDefault(initial, key);
}
const subscribe = thisStore.subscribe;
return {
subscribe,
set,
update,
onDisk
};
function update() {
throw 'Not implemented';
}
async function storeValueWithDefault<T>(initial: T, key: string): Promise<T> {
try {
await store.load();
} catch {
// If file does not exist, reset it
store.reset();
return { subscribe, set, update, onDisk };
}
const stored = (await store.get(key)) as T;
if (stored === null) {
return initial;
} else {
return stored;
async storeValueWithDefault<T>(initial: T, key: string): Promise<T> {
const stored = this.diskStore?.get(key) as T;
return stored === null ? initial : stored;
}
}

View File

@ -1,4 +1,4 @@
import { invoke } from '@tauri-apps/api/tauri';
import { invoke } from '@tauri-apps/api/core';
import { readable } from 'svelte/store';
export const editor = readable<string>('vscode', (set) => {

View File

@ -113,9 +113,9 @@
role="menu"
>
<!-- condition prevents split second UI shift -->
{#if $platformName || env.PUBLIC_TESTING}
{#if platformName || env.PUBLIC_TESTING}
<div class="navigation-top">
{#if $platformName === 'darwin'}
{#if platformName === 'macos'}
<div class="drag-region" data-tauri-drag-region></div>
{/if}
<ProjectSelector isNavCollapsed={$isNavCollapsed} />

View File

@ -10,9 +10,9 @@
import Spacer from '@gitbutler/ui/Spacer.svelte';
import Textbox from '@gitbutler/ui/Textbox.svelte';
import * as Sentry from '@sentry/sveltekit';
import { open } from '@tauri-apps/api/dialog';
import { documentDir } from '@tauri-apps/api/path';
import { join } from '@tauri-apps/api/path';
import { open } from '@tauri-apps/plugin-dialog';
import { posthog } from 'posthog-js';
import { onMount } from 'svelte';
import { goto } from '$app/navigation';

View File

@ -1,6 +1,3 @@
import { platform } from '@tauri-apps/api/os';
import { readable } from 'svelte/store';
import { platform } from '@tauri-apps/plugin-os';
export const platformName = readable<string | undefined>(undefined, (set) => {
platform().then((platform) => set(platform));
});
export const platformName = platform();

View File

@ -1,13 +1,7 @@
import { AISecretHandle } from '$lib/ai/service';
import { invoke } from '$lib/backend/ipc';
import { buildContext } from '@gitbutler/shared/context';
import type { GitConfigService } from '$lib/backend/gitConfigService';
const MIGRATION_HANDLES = [
AISecretHandle.AnthropicKey.toString(),
AISecretHandle.OpenAIKey.toString()
];
export type SecretsService = {
get(handle: string): Promise<string | undefined>;
set(handle: string, secret: string): Promise<void>;
@ -22,12 +16,6 @@ export class RustSecretService implements SecretsService {
async get(handle: string) {
const secret = await invoke<string>('secret_get_global', { handle });
if (secret) return secret;
if (MIGRATION_HANDLES.includes(handle)) {
const key = 'gitbutler.' + handle;
const migratedSecret = await this.migrate(key, handle);
if (migratedSecret !== undefined) return migratedSecret;
}
}
async set(handle: string, secret: string) {

View File

@ -1,10 +1,12 @@
<script lang="ts">
import { initAnalyticsIfEnabled } from '$lib/analytics/analytics';
import { AppSettings } from '$lib/config/appSettings';
import AnalyticsSettings from '$lib/settings/AnalyticsSettings.svelte';
import { getContext } from '@gitbutler/shared/context';
import Button from '@gitbutler/ui/Button.svelte';
import type { Writable } from 'svelte/store';
export let analyticsConfirmed: Writable<boolean>;
const appSettings = getContext(AppSettings);
const analyticsConfirmed = appSettings.appAnalyticsConfirmed;
</script>
<div class="analytics-confirmation">
@ -19,7 +21,7 @@
icon="chevron-right-small"
onclick={() => {
$analyticsConfirmed = true;
initAnalyticsIfEnabled();
initAnalyticsIfEnabled(appSettings);
}}
>
Continue

View File

@ -1,16 +1,14 @@
<script lang="ts">
import SectionCard from '$lib/components/SectionCard.svelte';
import {
appErrorReportingEnabled,
appMetricsEnabled,
appNonAnonMetricsEnabled
} from '$lib/config/appSettings';
import { AppSettings } from '$lib/config/appSettings';
import Link from '$lib/shared/Link.svelte';
import { getContext } from '@gitbutler/shared/context';
import Toggle from '@gitbutler/ui/Toggle.svelte';
const errorReportingEnabled = appErrorReportingEnabled();
const metricsEnabled = appMetricsEnabled();
const nonAnonMetricsEnabled = appNonAnonMetricsEnabled();
const appSettings = getContext(AppSettings);
const errorReportingEnabled = appSettings.appErrorReportingEnabled;
const metricsEnabled = appSettings.appMetricsEnabled;
const nonAnonMetricsEnabled = appSettings.appNonAnonMetricsEnabled;
</script>
<div class="analytics-settings__content">

View File

@ -12,7 +12,7 @@
import Button from '@gitbutler/ui/Button.svelte';
import Textbox from '@gitbutler/ui/Textbox.svelte';
import Toggle from '@gitbutler/ui/Toggle.svelte';
import { invoke } from '@tauri-apps/api/tauri';
import { invoke } from '@tauri-apps/api/core';
import { onMount } from 'svelte';
const projectsService = getContext(ProjectsService);

View File

@ -26,7 +26,7 @@
<Section>
<CommitSigningForm />
{#if $platformName !== 'win32'}
{#if platformName !== 'windows'}
<Spacer />
<KeysForm showProjectName={false} />
{/if}

View File

@ -1,5 +1,5 @@
import { persisted } from '@gitbutler/shared/persisted';
import { get, type Readable } from 'svelte/store';
import { get, type Readable, type Writable } from 'svelte/store';
import type { Project } from '$lib/backend/projects';
import type { GitHostIssueService } from '$lib/gitHost/interface/gitHostIssueService';
@ -12,15 +12,13 @@ export type Topic = {
};
export class TopicService {
topics = persisted<Topic[]>([], this.localStorageKey);
topics: Writable<Topic[]>;
constructor(
private project: Project,
private issueService: Readable<GitHostIssueService | undefined>
) {}
private get localStorageKey(): string {
return `TopicService--${this.project.id}`;
) {
this.topics = persisted<Topic[]>([], `TopicService--${this.project.id}`);
}
create(title: string, body: string, hasIssue: boolean = false): Topic {

View File

@ -1,7 +1,7 @@
import { listen } from '$lib/backend/ipc';
import { parseRemoteFiles } from '$lib/vbranches/remoteCommits';
import { RemoteFile } from '$lib/vbranches/types';
import { invoke } from '@tauri-apps/api/tauri';
import { invoke } from '@tauri-apps/api/core';
import { plainToInstance } from 'class-transformer';
import { readable, type Readable } from 'svelte/store';
import type { Project } from '$lib/backend/projects';

View File

@ -1,6 +1,7 @@
import { appWindow, type Theme } from '@tauri-apps/api/window';
import { getCurrentWindow, type Theme } from '@tauri-apps/api/window';
import { writable, type Writable } from 'svelte/store';
import type { Settings } from '$lib/settings/userSettings';
const appWindow = getCurrentWindow();
export const theme = writable('dark');

View File

@ -22,6 +22,7 @@
import AppUpdater from '$lib/components/AppUpdater.svelte';
import PromptModal from '$lib/components/PromptModal.svelte';
import ShareIssueModal from '$lib/components/ShareIssueModal.svelte';
import { AppSettings } from '$lib/config/appSettings';
import {
createGitHubUserServiceStore as createGitHubUserServiceStore,
GitHubUserService
@ -70,6 +71,7 @@
setContext(AIPromptService, data.aiPromptService);
setContext(LineManagerFactory, data.lineManagerFactory);
setContext(StackingLineManagerFactory, data.stackingLineManagerFactory);
setContext(AppSettings, data.appSettings);
const webRoutesService = new WebRoutesService(true, env.PUBLIC_CLOUD_BASE_URL);
const desktopRoutesService = new DesktopRoutesService(webRoutesService);

View File

@ -8,6 +8,7 @@ import { ProjectsService } from '$lib/backend/projects';
import { PromptService } from '$lib/backend/prompt';
import { Tauri } from '$lib/backend/tauri';
import { UpdaterService } from '$lib/backend/updater';
import { loadAppSettings } from '$lib/config/appSettings';
import { RemotesService } from '$lib/remotes/service';
import { RustSecretService } from '$lib/secrets/secretsService';
import { TokenMemoryService } from '$lib/stores/tokenMemoryService';
@ -28,7 +29,10 @@ export const csr = true;
// eslint-disable-next-line
export const load: LayoutLoad = async () => {
initAnalyticsIfEnabled();
// Awaited and will block initial render, but it is necessary in order to respect the user
// settings on telemetry.
const appSettings = await loadAppSettings();
initAnalyticsIfEnabled(appSettings);
// TODO: Find a workaround to avoid this dynamic import
// https://github.com/sveltejs/kit/issues/905
@ -57,6 +61,7 @@ export const load: LayoutLoad = async () => {
return {
commandService,
tokenMemoryService,
appSettings,
authService,
cloud: httpClient,
projectsService,

View File

@ -3,16 +3,18 @@
import newProjectSvg from '$lib/assets/illustrations/new-project.svg?raw';
import DecorativeSplitView from '$lib/components/DecorativeSplitView.svelte';
import Welcome from '$lib/components/Welcome.svelte';
import { appAnalyticsConfirmed } from '$lib/config/appSettings';
import { AppSettings } from '$lib/config/appSettings';
import AnalyticsConfirmation from '$lib/settings/AnalyticsConfirmation.svelte';
import { getContext } from '@gitbutler/shared/context';
const analyticsConfirmed = appAnalyticsConfirmed();
const appSettings = getContext(AppSettings);
const analyticsConfirmed = appSettings.appAnalyticsConfirmed;
</script>
<DecorativeSplitView img={$analyticsConfirmed ? newProjectSvg : analyticsSvg}>
{#if $analyticsConfirmed}
<Welcome />
{:else}
<AnalyticsConfirmation {analyticsConfirmed} />
<AnalyticsConfirmation />
{/if}
</DecorativeSplitView>

View File

@ -45,7 +45,7 @@ export default defineConfig({
strict: false
}
},
// to make use of `TAURI_DEBUG` and other env variables
// to make use of `TAURI_ENV_DEBUG` and other env variables
// https://tauri.studio/v1/api/config#buildconfig.beforedevcommand
envPrefix: ['VITE_', 'TAURI_'],
resolve: {
@ -53,9 +53,9 @@ export default defineConfig({
},
build: {
// Tauri supports es2021
target: process.env.TAURI_PLATFORM === 'windows' ? 'chrome105' : 'safari13',
target: process.env.TAURI_ENV_PLATFORM === 'windows' ? 'chrome105' : 'safari13',
// minify production builds
minify: !process.env.TAURI_DEBUG ? 'esbuild' : false,
minify: !process.env.TAURI_ENV_DEBUG ? 'esbuild' : false,
// ship sourcemaps for better sentry error reports
sourcemap: true
},

View File

@ -1 +1,5 @@
/gitbutler-git-*
# Tauri generated output.
gen/
# Extra binaries, figure out why they end up in this directory on build.
gitbutler-git-*

View File

@ -7,6 +7,7 @@ publish = false
[lib]
doctest = false
crate-type = ["lib", "staticlib", "cdylib"]
[[bin]]
name = "gitbutler-tauri"
@ -14,7 +15,7 @@ path = "src/main.rs"
test = false
[build-dependencies]
tauri-build = { version = "1.5.5", features = [] }
tauri-build = { version = "2.0.2", features = [] }
[dev-dependencies]
pretty_assertions = "1.4"
@ -37,11 +38,18 @@ once_cell = "1.20"
reqwest = { version = "0.12.8", features = ["json"] }
serde.workspace = true
serde_json = { version = "1.0", features = ["std", "arbitrary_precision"] }
tauri-plugin-context-menu = { git = "https://github.com/c2r0b/tauri-plugin-context-menu", branch = "main" }
tauri-plugin-single-instance = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
tauri-plugin-window-state = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
tauri-plugin-store = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
tauri-plugin-log = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
tauri = { version = "^2.0.6", features = ["unstable"] }
tauri-plugin-dialog = "2.0.3"
tauri-plugin-fs = "2.0.3"
tauri-plugin-http = "2.0.3"
tauri-plugin-log = "2.0.1"
tauri-plugin-os = "2.0.1"
tauri-plugin-process = "2.0.1"
tauri-plugin-shell = "2.0.2"
tauri-plugin-single-instance = "2.0.1"
tauri-plugin-store = "2.1.0"
tauri-plugin-updater = "2.0.2"
tauri-plugin-window-state = "2.0.1"
parking_lot.workspace = true
log = "^0.4"
thiserror.workspace = true
@ -78,22 +86,6 @@ gitbutler-forge.workspace = true
open = "5"
url = "2.5.2"
[dependencies.tauri]
version = "1.8.0"
features = [
"http-all",
"os-all",
"dialog-open",
"fs-read-file",
"path-all",
"process-relaunch",
"protocol-asset",
"window-maximize",
"window-start-dragging",
"window-unmaximize",
"shell-open",
]
[lints.clippy]
all = "deny"
perf = "deny"

View File

@ -0,0 +1,18 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "main",
"description": "permissions for gitbutler tauri",
"windows": ["main"],
"local": true,
"permissions": [
"core:default",
"core:window:allow-start-dragging",
"core:window:default",
"dialog:allow-open",
"log:default",
"process:default",
"shell:allow-open",
"store:default",
"updater:default"
]
}

View File

@ -0,0 +1,7 @@
{
"identifier": "migrated",
"description": "permissions that were migrated from v1",
"local": true,
"windows": ["main"],
"permissions": ["core:default"]
}

View File

@ -7,7 +7,7 @@ use tracing_subscriber::{fmt::format::FmtSpan, layer::SubscriberExt, Layer};
pub fn init(app_handle: &AppHandle, performance_logging: bool) {
let logs_dir = app_handle
.path_resolver()
.path()
.app_log_dir()
.expect("failed to get logs dir");
fs::create_dir_all(&logs_dir).expect("failed to create logs dir");
@ -88,11 +88,7 @@ pub fn init(app_handle: &AppHandle, performance_logging: bool) {
fn get_server_addr(app_handle: &AppHandle) -> (Ipv4Addr, u16) {
let config = app_handle.config();
let product_name = config
.package
.product_name
.as_ref()
.expect("product name not set");
let product_name = config.product_name.as_ref().expect("product name not set");
let port = if product_name.eq("GitButler") {
6667
} else if product_name.eq("GitButler Nightly") {

View File

@ -15,16 +15,15 @@ use gitbutler_tauri::{
askpass, commands, config, forge, github, logs, menu, modes, open, projects, remotes, repo,
secret, stack, undo, users, virtual_branches, zip, App, WindowState,
};
use tauri::Emitter;
use tauri::{generate_context, Manager};
use tauri_plugin_log::LogTarget;
use tauri_plugin_log::{Target, TargetKind};
fn main() {
let performance_logging = std::env::var_os("GITBUTLER_PERFORMANCE_LOG").is_some();
gitbutler_project::configure_git2();
let tauri_context = generate_context!();
gitbutler_secret::secret::set_application_namespace(
&tauri_context.config().tauri.bundle.identifier,
);
gitbutler_secret::secret::set_application_namespace(&tauri_context.config().identifier);
tokio::runtime::Builder::new_multi_thread()
.enable_all()
@ -34,33 +33,30 @@ fn main() {
tauri::async_runtime::set(tokio::runtime::Handle::current());
let log = tauri_plugin_log::Builder::default()
.log_name("ui-logs")
.target(LogTarget::LogDir)
.target(Target::new(TargetKind::LogDir {
file_name: Some("ui-logs".to_string()),
}))
.level(log::LevelFilter::Error);
let builder = tauri::Builder::default()
.setup(move |tauri_app| {
let window = gitbutler_tauri::window::create(
&tauri_app.handle(),
tauri_app.handle(),
"main",
"index.html".into(),
)
.expect("Failed to create window");
#[cfg(debug_assertions)]
window.open_devtools();
tokio::task::spawn(async move {
let mut six_hours =
tokio::time::interval(tokio::time::Duration::new(6 * 60 * 60, 0));
loop {
six_hours.tick().await;
_ = window.emit_and_trigger("tauri://update", ());
// TODO(mtsgrd): Is there a better way to disable devtools in E2E tests?
#[cfg(debug_assertions)]
if tauri_app.config().product_name != Some("GitButler Test".to_string()) {
window.open_devtools();
}
});
let app_handle = tauri_app.handle();
logs::init(&app_handle, performance_logging);
logs::init(app_handle, performance_logging);
tracing::info!(
"system git executable for fetch/push: {git:?}",
git = gix::path::env::exe_invocation(),
@ -81,14 +77,14 @@ fn main() {
let handle = app_handle.clone();
move |event| {
handle
.emit_all("git_prompt", event)
.emit("git_prompt", event)
.expect("tauri event emission doesn't fail in practice")
}
});
}
let (app_data_dir, app_cache_dir, app_log_dir) = {
let paths = app_handle.path_resolver();
let paths = app_handle.path();
(
paths.app_data_dir().expect("missing app data dir"),
paths.app_cache_dir().expect("missing app cache dir"),
@ -116,10 +112,20 @@ fn main() {
});
app_handle.manage(app);
tauri_app.on_menu_event(move |_handle, event| {
menu::handle_event(&window.clone(), &event)
});
Ok(())
})
.plugin(tauri_plugin_http::init())
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_os::init())
.plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_single_instance::init(|_, _, _| {}))
.plugin(tauri_plugin_context_menu::init())
.plugin(tauri_plugin_updater::Builder::new().build())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_fs::init())
// .plugin(tauri_plugin_context_menu::init())
.plugin(tauri_plugin_store::Builder::default().build())
.plugin(log.build())
.invoke_handler(tauri::generate_handler![
@ -221,17 +227,12 @@ fn main() {
forge::commands::get_available_review_templates,
forge::commands::get_review_template_contents,
])
.menu(menu::build(tauri_context.package_info()))
.on_menu_event(|event| menu::handle_event(&event))
.on_window_event(|event| {
let window = event.window();
match event.event() {
.menu(menu::build)
.on_window_event(|window, event| match event {
#[cfg(target_os = "macos")]
tauri::WindowEvent::CloseRequested { api, .. } => {
if window.app_handle().windows().len() == 1 {
tracing::debug!(
"Hiding all application windows and preventing exit"
);
tracing::debug!("Hiding all application windows and preventing exit");
window.app_handle().hide().ok();
api.prevent_close();
}
@ -250,7 +251,6 @@ fn main() {
.ok();
}
_ => {}
}
});
#[cfg(not(target_os = "linux"))]

View File

@ -1,14 +1,14 @@
use std::{env, fs};
use crate::open::open_that as open_url;
use anyhow::Context;
use gitbutler_error::{error, error::Code};
use gitbutler_error::error::{self, Code};
use serde_json::json;
#[cfg(target_os = "macos")]
use tauri::AboutMetadata;
use tauri::menu::AboutMetadata;
use tauri::Emitter;
use tauri::{
AppHandle, CustomMenuItem, Manager, Menu, MenuItem, PackageInfo, Runtime, Submenu,
WindowMenuEvent,
menu::{Menu, MenuEvent, MenuItemBuilder, PredefinedMenuItem, Submenu, SubmenuBuilder},
AppHandle, Manager, Runtime, WebviewWindow,
};
use tracing::instrument;
@ -16,21 +16,22 @@ use crate::error::Error;
#[tauri::command(async)]
#[instrument(skip(handle), err(Debug))]
pub fn menu_item_set_enabled(
handle: AppHandle,
menu_item_id: &str,
enabled: bool,
) -> Result<(), Error> {
pub fn menu_item_set_enabled(handle: AppHandle, id: &str, enabled: bool) -> Result<(), Error> {
let window = handle
.get_window("main")
.expect("main window always present");
let menu_item = window
.menu_handle()
.try_get_item(menu_item_id)
.with_context(|| error::Context::new(format!("menu item not found: {}", menu_item_id)))?;
.menu()
.context("menu not found")?
.get(id)
.with_context(|| error::Context::new(format!("menu item not found: {}", id)))?;
menu_item.set_enabled(enabled).context(Code::Unknown)?;
menu_item
.as_menuitem()
.context(Code::Unknown)?
.set_enabled(enabled)
.context(Code::Unknown)?;
Ok(())
}
@ -56,275 +57,264 @@ fn check_if_installed(executable_name: &str) -> bool {
}
}
pub fn build(_package_info: &PackageInfo) -> Menu {
let mut menu = Menu::new();
// Used in different menus depending on target os.
let check_for_updates = CustomMenuItem::new("global/update", "Check for updates…");
pub fn build<R: Runtime>(handle: &AppHandle<R>) -> tauri::Result<tauri::menu::Menu<R>> {
let check_for_updates =
MenuItemBuilder::with_id("global/update", "Check for updates…").build(handle)?;
#[cfg(target_os = "macos")]
{
let app_name = &_package_info.name;
menu = menu.add_submenu(Submenu::new(
app_name,
Menu::new()
.add_native_item(MenuItem::About(
app_name.to_string(),
AboutMetadata::default(),
))
.add_native_item(MenuItem::Separator)
.add_item(CustomMenuItem::new("global/settings", "Settings").accelerator("Cmd+,"))
.add_item(check_for_updates)
.add_native_item(MenuItem::Separator)
.add_native_item(MenuItem::Services)
.add_native_item(MenuItem::Separator)
.add_native_item(MenuItem::Hide)
.add_native_item(MenuItem::HideOthers)
.add_native_item(MenuItem::ShowAll)
.add_native_item(MenuItem::Separator)
.add_native_item(MenuItem::Quit),
));
}
let mut file_menu = Menu::new();
file_menu = file_menu.add_item(
CustomMenuItem::new("file/add-local-repo", "Add Local Repository…")
.accelerator("CmdOrCtrl+O"),
);
file_menu = file_menu.add_item(
CustomMenuItem::new("file/clone-repo", "Clone Repository…")
.accelerator("CmdOrCtrl+Shift+O"),
);
let app_name = handle
.config()
.product_name
.clone()
.context("App name not defined.")?;
#[cfg(target_os = "macos")]
{
// NB: macOS has the concept of having an app running but its
// window closed, but other platforms do not
file_menu = file_menu.add_native_item(MenuItem::Separator);
file_menu = file_menu.add_native_item(MenuItem::CloseWindow);
}
let mac_menu = &SubmenuBuilder::new(handle, app_name)
.about(Some(AboutMetadata::default()))
.separator()
.text("global/settings", "Settings")
.item(&check_for_updates)
.separator()
.services()
.separator()
.hide()
.hide_others()
.show_all()
.separator()
.quit()
.build()?;
let file_menu = &SubmenuBuilder::new(handle, "File")
.items(&[
&MenuItemBuilder::with_id("file/add-local-repo", "Add Local Repository…")
.accelerator("CmdOrCtrl+O")
.build(handle)?,
&MenuItemBuilder::with_id("file/clone-repo", "Clone Repository…")
.accelerator("CmdOrCtrl+Shift+O")
.build(handle)?,
&PredefinedMenuItem::separator(handle)?,
])
.build()?;
#[cfg(target_os = "macos")]
file_menu.append(&PredefinedMenuItem::close_window(handle, None)?)?;
#[cfg(not(target_os = "macos"))]
{
file_menu = file_menu.add_native_item(MenuItem::Separator);
file_menu = file_menu.add_native_item(MenuItem::Quit);
file_menu = file_menu.add_item(check_for_updates)
}
menu = menu.add_submenu(Submenu::new("File", file_menu));
file_menu.append_items(&[&PredefinedMenuItem::quit(handle, None)?, &check_for_updates])?;
#[cfg(not(target_os = "linux"))]
let mut edit_menu = Menu::new();
let edit_menu = &Submenu::new(handle, "Edit", true)?;
#[cfg(target_os = "macos")]
{
edit_menu = edit_menu.add_native_item(MenuItem::Undo);
edit_menu = edit_menu.add_native_item(MenuItem::Redo);
edit_menu = edit_menu.add_native_item(MenuItem::Separator);
edit_menu.append_items(&[
&PredefinedMenuItem::undo(handle, None)?,
&PredefinedMenuItem::redo(handle, None)?,
&PredefinedMenuItem::separator(handle)?,
])?;
}
#[cfg(not(target_os = "linux"))]
{
edit_menu = edit_menu.add_native_item(MenuItem::Cut);
edit_menu = edit_menu.add_native_item(MenuItem::Copy);
edit_menu = edit_menu.add_native_item(MenuItem::Paste);
edit_menu.append_items(&[
&PredefinedMenuItem::cut(handle, None)?,
&PredefinedMenuItem::copy(handle, None)?,
&PredefinedMenuItem::paste(handle, None)?,
])?;
}
#[cfg(target_os = "macos")]
{
edit_menu = edit_menu.add_native_item(MenuItem::SelectAll);
}
edit_menu.append(&PredefinedMenuItem::select_all(handle, None)?)?;
#[cfg(not(target_os = "linux"))]
{
menu = menu.add_submenu(Submenu::new("Edit", edit_menu));
}
let mut view_menu = Menu::new();
let view_menu = &Submenu::new(handle, "View", true)?;
#[cfg(target_os = "macos")]
{
view_menu = view_menu.add_native_item(MenuItem::EnterFullScreen);
}
view_menu = view_menu.add_item(
CustomMenuItem::new("view/switch-theme", "Switch Theme").accelerator("CmdOrCtrl+T"),
);
view_menu = view_menu.add_native_item(MenuItem::Separator);
view_menu = view_menu
.add_item(CustomMenuItem::new("view/zoom-in", "Zoom In").accelerator("CmdOrCtrl+="));
view_menu = view_menu
.add_item(CustomMenuItem::new("view/zoom-out", "Zoom Out").accelerator("CmdOrCtrl+-"));
view_menu = view_menu
.add_item(CustomMenuItem::new("view/zoom-reset", "Reset Zoom").accelerator("CmdOrCtrl+0"));
view_menu = view_menu.add_native_item(MenuItem::Separator);
view_menu.append(&PredefinedMenuItem::fullscreen(handle, None)?)?;
view_menu.append_items(&[
&MenuItemBuilder::with_id("view/switch-theme", "Switch Theme")
.accelerator("CmdOrCtrl+T")
.build(handle)?,
&PredefinedMenuItem::separator(handle)?,
&MenuItemBuilder::with_id("view/zoom-in", "Zoom In")
.accelerator("CmdOrCtrl+=")
.build(handle)?,
&MenuItemBuilder::with_id("view/zoom-out", "Zoom Out")
.accelerator("CmdOrCtrl+-")
.build(handle)?,
&MenuItemBuilder::with_id("view/zoom-reset", "Reset Zoom")
.accelerator("CmdOrCtrl+0")
.build(handle)?,
&PredefinedMenuItem::separator(handle)?,
])?;
#[cfg(any(debug_assertions, feature = "devtools"))]
{
view_menu = view_menu.add_item(CustomMenuItem::new("view/devtools", "Developer Tools"));
}
view_menu.append_items(&[
&MenuItemBuilder::with_id("view/devtools", "Developer Tools").build(handle)?,
&MenuItemBuilder::with_id("view/reload", "Reload View")
.accelerator("CmdOrCtrl+R")
.build(handle)?,
])?;
view_menu = view_menu
.add_item(CustomMenuItem::new("view/reload", "Reload View").accelerator("CmdOrCtrl+R"));
menu = menu.add_submenu(Submenu::new("View", view_menu));
let mut project_menu = Menu::new();
project_menu = project_menu.add_item(
CustomMenuItem::new("project/history", "Project History").accelerator("CmdOrCtrl+Shift+H"),
);
project_menu = project_menu.add_item(CustomMenuItem::new(
"project/open-in-vscode",
"Open in VS Code",
));
project_menu = project_menu.add_native_item(MenuItem::Separator);
project_menu =
project_menu.add_item(CustomMenuItem::new("project/settings", "Project Settings"));
menu = menu.add_submenu(Submenu::new("Project", project_menu));
let project_menu = &SubmenuBuilder::new(handle, "Project")
.item(
&MenuItemBuilder::with_id("project/history", "Project History")
.accelerator("CmdOrCtrl+Shift+H")
.build(handle)?,
)
.text("project/open-in-vscode", "Open in VS Code")
.separator()
.text("project/settings", "Project Settings")
.build()?;
#[cfg(target_os = "macos")]
{
let mut window_menu = Menu::new();
window_menu = window_menu.add_native_item(MenuItem::Minimize);
let window_menu = &SubmenuBuilder::new(handle, "Window")
.items(&[
&PredefinedMenuItem::minimize(handle, None)?,
&PredefinedMenuItem::maximize(handle, None)?,
&PredefinedMenuItem::separator(handle)?,
&PredefinedMenuItem::close_window(handle, None)?,
])
.build()?;
window_menu = window_menu.add_native_item(MenuItem::Zoom);
window_menu = window_menu.add_native_item(MenuItem::Separator);
window_menu = window_menu.add_native_item(MenuItem::CloseWindow);
menu = menu.add_submenu(Submenu::new("Window", window_menu));
}
let mut help_menu = Menu::new();
help_menu = help_menu.add_item(CustomMenuItem::new("help/documentation", "Documentation"));
help_menu = help_menu.add_item(CustomMenuItem::new("help/github", "Source Code"));
help_menu = help_menu.add_item(CustomMenuItem::new("help/release-notes", "Release Notes"));
help_menu = help_menu.add_native_item(MenuItem::Separator);
help_menu = help_menu.add_item(CustomMenuItem::new(
"help/share-debug-info",
"Share Debug Info…",
));
help_menu = help_menu.add_item(CustomMenuItem::new("help/report-issue", "Report an Issue…"));
help_menu = help_menu.add_native_item(MenuItem::Separator);
help_menu = help_menu.add_item(CustomMenuItem::new("help/discord", "Discord"));
help_menu = help_menu.add_item(CustomMenuItem::new("help/youtube", "YouTube"));
help_menu = help_menu.add_item(CustomMenuItem::new("help/x", "X"));
help_menu = help_menu.add_native_item(MenuItem::Separator);
help_menu = help_menu.add_item(disabled_menu_item(
let help_menu = &SubmenuBuilder::new(handle, "Help")
.text("help/documentation", "Documentation")
.text("help/github", "Source Code")
.text("help/release-notes", "Release Notes")
.separator()
.text("help/share-debug-info", "Share Debug Info…")
.text("help/report-issue", "Report an Issue…")
.separator()
.text("help/discord", "Discord")
.text("help/youtube", "YouTube")
.text("help/x", "X")
.separator()
.item(
&MenuItemBuilder::with_id(
"help/version",
&format!("Version {}", _package_info.version),
));
menu = menu.add_submenu(Submenu::new("Help", help_menu));
format!("Version {}", handle.package_info().version),
)
.enabled(false)
.build(handle)?,
)
.build()?;
menu
Menu::with_items(
handle,
&[
#[cfg(target_os = "macos")]
mac_menu,
file_menu,
#[cfg(not(target_os = "linux"))]
edit_menu,
view_menu,
project_menu,
#[cfg(target_os = "macos")]
window_menu,
help_menu,
],
)
}
fn disabled_menu_item(id: &str, title: &str) -> CustomMenuItem {
let mut item = CustomMenuItem::new(id, title);
item.enabled = false;
item
}
pub fn handle_event<R: Runtime>(event: &WindowMenuEvent<R>) {
if event.menu_item_id() == "file/add-local-repo" {
emit(event.window(), "menu://file/add-local-repo/clicked");
pub fn handle_event(webview: &WebviewWindow, event: &MenuEvent) {
if event.id() == "file/add-local-repo" {
emit(webview, "menu://file/add-local-repo/clicked");
return;
}
if event.menu_item_id() == "file/clone-repo" {
emit(event.window(), "menu://file/clone-repo/clicked");
if event.id() == "file/clone-repo" {
emit(webview, "menu://file/clone-repo/clicked");
return;
}
#[cfg(any(debug_assertions, feature = "devtools"))]
{
if event.menu_item_id() == "view/devtools" {
event.window().open_devtools();
if event.id() == "view/devtools" {
webview.open_devtools();
return;
}
}
if event.menu_item_id() == "view/switch-theme" {
emit(event.window(), "menu://view/switch-theme/clicked");
if event.id() == "view/switch-theme" {
emit(webview, "menu://view/switch-theme/clicked");
return;
}
if event.menu_item_id() == "view/reload" {
emit(event.window(), "menu://view/reload/clicked");
if event.id() == "view/reload" {
emit(webview, "menu://view/reload/clicked");
return;
}
if event.menu_item_id() == "view/zoom-in" {
emit(event.window(), "menu://view/zoom-in/clicked");
if event.id() == "view/zoom-in" {
emit(webview, "menu://view/zoom-in/clicked");
return;
}
if event.menu_item_id() == "view/zoom-out" {
emit(event.window(), "menu://view/zoom-out/clicked");
if event.id() == "view/zoom-out" {
emit(webview, "menu://view/zoom-out/clicked");
return;
}
if event.menu_item_id() == "view/zoom-reset" {
emit(event.window(), "menu://view/zoom-reset/clicked");
if event.id() == "view/zoom-reset" {
emit(webview, "menu://view/zoom-reset/clicked");
return;
}
if event.menu_item_id() == "help/share-debug-info" {
emit(event.window(), "menu://help/share-debug-info/clicked");
if event.id() == "help/share-debug-info" {
emit(webview, "menu://help/share-debug-info/clicked");
return;
}
if event.menu_item_id() == "project/history" {
emit(event.window(), "menu://project/history/clicked");
if event.id() == "project/history" {
emit(webview, "menu://project/history/clicked");
return;
}
if event.menu_item_id() == "project/open-in-vscode" {
emit(event.window(), "menu://project/open-in-vscode/clicked");
if event.id() == "project/open-in-vscode" {
emit(webview, "menu://project/open-in-vscode/clicked");
return;
}
if event.menu_item_id() == "project/settings" {
emit(event.window(), "menu://project/settings/clicked");
if event.id() == "project/settings" {
emit(webview, "menu://project/settings/clicked");
return;
}
if event.menu_item_id() == "global/settings" {
emit(event.window(), "menu://global/settings/clicked");
if event.id() == "global/settings" {
emit(webview, "menu://global/settings/clicked");
return;
}
if event.menu_item_id() == "global/update" {
emit(event.window(), "menu://global/update/clicked");
if event.id() == "global/update" {
emit(webview, "menu://global/update/clicked");
return;
}
'open_link: {
let result = match event.menu_item_id() {
"help/documentation" => open_url("https://docs.gitbutler.com"),
"help/github" => open_url("https://github.com/gitbutlerapp/gitbutler"),
"help/release-notes" => open_url("https://github.com/gitbutlerapp/gitbutler/releases"),
"help/report-issue" => open_url("https://github.com/gitbutlerapp/gitbutler/issues/new"),
"help/discord" => open_url("https://discord.com/invite/MmFkmaJ42D"),
"help/youtube" => open_url("https://www.youtube.com/@gitbutlerapp"),
"help/x" => open_url("https://x.com/gitbutler"),
let result = match event.id().0.as_str() {
"help/documentation" => open::that("https://docs.gitbutler.com"),
"help/github" => open::that("https://github.com/gitbutlerapp/gitbutler"),
"help/release-notes" => {
open::that("https://discord.com/channels/1060193121130000425/1183737922785116161")
}
"help/report-issue" => {
open::that("https://github.com/gitbutlerapp/gitbutler/issues/new")
}
"help/discord" => open::that("https://discord.com/invite/MmFkmaJ42D"),
"help/youtube" => open::that("https://www.youtube.com/@gitbutlerapp"),
"help/x" => open::that("https://x.com/gitbutler"),
_ => break 'open_link,
};
if let Err(err) = result {
tracing::error!(error = ?err, "failed to open url for {}", event.menu_item_id());
tracing::error!(error = ?err, "failed to open url for {}", event.id().0);
}
return;
}
tracing::error!("unhandled 'help' menu event: {}", event.menu_item_id());
tracing::error!("unhandled 'help' menu event: {}", event.id().0);
}
fn emit<R: Runtime>(window: &tauri::Window<R>, event: &str) {
fn emit<R: Runtime>(window: &tauri::WebviewWindow<R>, event: &str) {
if let Err(err) = window.emit(event, json!({})) {
tracing::error!(error = ?err, "failed to emit event");
}

View File

@ -12,7 +12,7 @@ pub(super) mod state {
use anyhow::{Context, Result};
use gitbutler_project::ProjectId;
use gitbutler_watcher::Change;
use tauri::Manager;
use tauri::Emitter;
/// A change we want to inform the frontend about.
#[derive(Debug, Clone, PartialEq, Eq)]
@ -64,7 +64,7 @@ pub(super) mod state {
impl ChangeForFrontend {
pub(super) fn send(&self, app_handle: &tauri::AppHandle) -> Result<()> {
app_handle
.emit_all(&self.name, Some(&self.payload))
.emit(&self.name, Some(&self.payload))
.context("emit event")?;
tracing::trace!(event_name = self.name);
Ok(())
@ -204,16 +204,16 @@ pub fn create(
handle: &tauri::AppHandle,
label: &state::WindowLabelRef,
window_relative_url: String,
) -> tauri::Result<tauri::Window> {
) -> tauri::Result<tauri::WebviewWindow> {
tracing::info!("creating window '{label}' created at '{window_relative_url}'");
let window = tauri::WindowBuilder::new(
let window = tauri::WebviewWindowBuilder::new(
handle,
label,
tauri::WindowUrl::App(window_relative_url.into()),
tauri::WebviewUrl::App(window_relative_url.into()),
)
.resizable(true)
.title(handle.package_info().name.clone())
.disable_file_drop_handler()
.disable_drag_drop_handler()
.min_inner_size(800.0, 600.0)
.inner_size(1160.0, 720.0)
.build()?;
@ -225,19 +225,19 @@ pub fn create(
handle: &tauri::AppHandle,
label: &state::WindowLabelRef,
window_relative_url: String,
) -> tauri::Result<tauri::Window> {
) -> tauri::Result<tauri::WebviewWindow> {
tracing::info!("creating window '{label}' created at '{window_relative_url}'");
let window = tauri::WindowBuilder::new(
let window = tauri::WebviewWindowBuilder::new(
handle,
label,
tauri::WindowUrl::App(window_relative_url.into()),
tauri::WebviewUrl::App(window_relative_url.into()),
)
.resizable(true)
.title(handle.package_info().name.clone())
.min_inner_size(800.0, 600.0)
.inner_size(1160.0, 720.0)
.hidden_title(true)
.disable_file_drop_handler()
.disable_drag_drop_handler()
.title_bar_style(tauri::TitleBarStyle::Overlay)
.build()?;
Ok(window)

View File

@ -1,60 +1,18 @@
{
"productName": "GitButler Dev",
"identifier": "com.gitbutler.app.dev",
"build": {
"beforeDevCommand": "pnpm dev:internal-tauri",
"beforeBuildCommand": "[ $CI = true ] || pnpm build:desktop -- --mode development",
"devPath": "http://localhost:1420",
"distDir": "../../apps/desktop/build",
"withGlobalTauri": false
},
"package": {
"productName": "GitButler Dev"
},
"tauri": {
"allowlist": {
"fs": {
"readFile": true,
"scope": ["$APPCACHE/archives/*", "$RESOURCE/_up_/scripts/*"]
},
"dialog": {
"open": true
},
"os": {
"all": true
},
"protocol": {
"asset": true,
"assetScope": ["$APPCACHE/images/*"]
},
"process": {
"relaunch": true
},
"window": {
"startDragging": true,
"maximize": true,
"unmaximize": true
},
"path": {
"all": true
},
"http": {
"all": true,
"request": true,
"scope": [
"https://api.anthropic.com/v1/messages",
"http://127.0.0.1:11434/api/chat",
"http://127.0.0.1:11434/api/generate",
"http://127.0.0.1:11434/api/embeddings"
]
},
"shell": {
"open": true
}
"frontendDist": "../../apps/desktop/build",
"devUrl": "http://localhost:1420"
},
"bundle": {
"active": true,
"identifier": "com.gitbutler.app.dev",
"active": false,
"category": "DeveloperTool",
"copyright": "Copyright © 2023-2024 GitButler. All rights reserved.",
"createUpdaterArtifacts": "v1Compatible",
"targets": ["app", "dmg", "appimage", "deb", "rpm", "msi"],
"icon": [
"icons/dev/32x32.png",
"icons/dev/128x128.png",
@ -62,13 +20,34 @@
"icons/dev/icon.icns",
"icons/dev/icon.ico"
],
"targets": ["app", "dmg", "appimage", "deb", "rpm", "updater", "msi"]
"windows": {
"certificateThumbprint": null
},
"linux": {
"rpm": {
"depends": ["webkit2gtk4.1-devel"]
},
"deb": {
"depends": ["libwebkit2gtk-4.1-dev", "libgtk-3-dev"]
}
}
},
"plugins": {
"updater": {
"endpoints": [
"https://app.gitbutler.com/releases/nightly/{{target}}-{{arch}}/{{current_version}}"
],
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDYwNTc2RDhBM0U0MjM4RUIKUldUck9FSStpbTFYWUE5UkJ3eXhuekZOL2V2RnpKaFUxbGJRNzBMVmF5V0gzV1JvN3hRblJMRDIK"
}
},
"app": {
"withGlobalTauri": false,
"enableGTKAppId": true,
"security": {
"csp": {
"default-src": "'self'",
"img-src": "'self' asset: https://asset.localhost data: tauri://localhost https://avatars.githubusercontent.com https://*.gitbutler.com https://gitbutler-public.s3.amazonaws.com https://*.gravatar.com https://lh3.googleusercontent.com",
"connect-src": "'self' https://eu.posthog.com https://eu.i.posthog.com https://app.gitbutler.com https://o4504644069687296.ingest.sentry.io ws://localhost:7703 https://github.com https://api.github.com",
"img-src": "'self' asset: https://asset.localhost data: tauri://localhost https://avatars.githubusercontent.com https://*.gitbutler.com https://gitbutler-public.s3.amazonaws.com https://*.gravatar.com https://io.wp.com https://i0.wp.com https://i1.wp.com https://i2.wp.com https://i3.wp.com https://github.com https://*.googleusercontent.com",
"connect-src": "'self' ipc: https://eu.posthog.com https://eu.i.posthog.com https://app.gitbutler.com https://o4504644069687296.ingest.sentry.io ws://localhost:7703 https://github.com https://api.github.com https://api.openai.com",
"script-src": "'self' https://eu.posthog.com https://eu.i.posthog.com",
"style-src": "'self' 'unsafe-inline'"
}

View File

@ -1,13 +1,11 @@
{
"build": {
"beforeBuildCommand": "[ $CI = true ] || pnpm build:desktop -- --mode nightly && cargo build --release -p gitbutler-git && bash ./gitbutler-tauri/inject-git-binaries.sh"
},
"package": {
"productName": "GitButler Nightly"
},
"tauri": {
"bundle": {
"productName": "GitButler Nightly",
"identifier": "com.gitbutler.app.nightly",
"build": {
"beforeBuildCommand": "[ $CI = true ] || pnpm build:desktop -- --mode nightly && cargo build --release -p gitbutler-git && bash ./crates/gitbutler-tauri/inject-git-binaries.sh"
},
"bundle": {
"active": true,
"icon": [
"icons/32x32.png",
"icons/128x128.png",
@ -15,29 +13,9 @@
"icons/icon.icns",
"icons/icon.ico"
],
"externalBin": ["gitbutler-git-setsid", "gitbutler-git-askpass"],
"windows": {
"certificateThumbprint": null,
"wix": {
"template": "templates/installer.wxs"
}
},
"rpm": {
"depends": ["webkit2gtk4.0-devel"]
},
"deb": {
"depends": ["libwebkit2gtk-4.0-dev", "libgtk-3-dev"]
}
},
"security": {
"csp": {
"default-src": "'self'",
"img-src": "'self' asset: https://asset.localhost data: tauri://localhost https://avatars.githubusercontent.com https://*.gitbutler.com https://gitbutler-public.s3.amazonaws.com https://*.gravatar.com https://io.wp.com https://i0.wp.com https://i1.wp.com https://i2.wp.com https://i3.wp.com https://github.com https://*.googleusercontent.com",
"connect-src": "'self' https://eu.posthog.com https://eu.i.posthog.com https://app.gitbutler.com https://o4504644069687296.ingest.sentry.io ws://localhost:7703 https://github.com https://api.github.com https://api.openai.com https://api.anthropic.com/v1/messages",
"script-src": "'self' https://eu.posthog.com https://eu.i.posthog.com",
"style-src": "'self' 'unsafe-inline'"
}
"externalBin": ["gitbutler-git-setsid", "gitbutler-git-askpass"]
},
"plugins": {
"updater": {
"active": true,
"dialog": false,

View File

@ -1,13 +1,11 @@
{
"build": {
"beforeBuildCommand": "[ $CI = true ] || pnpm build:desktop -- --mode production && cargo build --release -p gitbutler-git && bash ./gitbutler-tauri/inject-git-binaries.sh"
},
"package": {
"productName": "GitButler"
},
"tauri": {
"bundle": {
"productName": "GitButler",
"identifier": "com.gitbutler.app",
"build": {
"beforeBuildCommand": "[ $CI = true ] || pnpm build:desktop -- --mode production && cargo build --release -p gitbutler-git && bash ./crates/gitbutler-tauri/inject-git-binaries.sh"
},
"bundle": {
"active": true,
"icon": [
"icons/32x32.png",
"icons/128x128.png",
@ -15,29 +13,9 @@
"icons/icon.icns",
"icons/icon.ico"
],
"externalBin": ["gitbutler-git-setsid", "gitbutler-git-askpass"],
"windows": {
"certificateThumbprint": null,
"wix": {
"template": "templates/installer.wxs"
}
},
"rpm": {
"depends": ["webkit2gtk4.0-devel"]
},
"deb": {
"depends": ["libwebkit2gtk-4.0-dev", "libgtk-3-dev"]
}
},
"security": {
"csp": {
"default-src": "'self'",
"img-src": "'self' asset: https://asset.localhost data: tauri://localhost https://avatars.githubusercontent.com https://*.gitbutler.com https://gitbutler-public.s3.amazonaws.com https://*.gravatar.com https://io.wp.com https://i0.wp.com https://i1.wp.com https://i2.wp.com https://i3.wp.com https://github.com https://*.googleusercontent.com",
"connect-src": "'self' https://eu.posthog.com https://eu.i.posthog.com https://app.gitbutler.com https://o4504644069687296.ingest.sentry.io ws://localhost:7703 https://github.com https://api.github.com https://api.openai.com https://api.anthropic.com/v1/messages",
"script-src": "'self' https://eu.posthog.com https://eu.i.posthog.com",
"style-src": "'self' 'unsafe-inline'"
}
"externalBin": ["gitbutler-git-setsid", "gitbutler-git-askpass"]
},
"plugins": {
"updater": {
"active": true,
"dialog": false,

View File

@ -1,51 +1,7 @@
{
"productName": "GitButler Test",
"identifier": "com.gitbutler.app.test",
"build": {
"beforeBuildCommand": "[ $CI = true ] || pnpm build:desktop -- --mode development",
"distDir": "../../apps/desktop/build"
},
"package": {
"productName": "GitButler Dev"
},
"tauri": {
"allowlist": {
"fs": {
"readFile": true,
"scope": ["$APPCACHE/archives/*", "$RESOURCE/_up_/scripts/*"]
},
"dialog": {
"open": true
},
"os": {
"all": true
},
"protocol": {
"asset": true,
"assetScope": ["$APPCACHE/images/*"]
},
"process": {
"relaunch": true
},
"window": {
"startDragging": true,
"maximize": true,
"unmaximize": true
},
"path": {
"all": true
},
"http": {
"all": true,
"request": true,
"scope": [
"https://api.anthropic.com/v1/messages",
"http://127.0.0.1:11434/api/chat",
"http://127.0.0.1:11434/api/generate",
"http://127.0.0.1:11434/api/embeddings"
]
}
},
"bundle": {
"active": false
}
"beforeBuildCommand": "[ $CI = true ] || pnpm build:desktop -- --mode development && cargo build -p gitbutler-git"
}
}

View File

@ -1,350 +0,0 @@
<?if $(sys.BUILDARCH)="x86"?>
<?define Win64 = "no" ?>
<?define PlatformProgramFilesFolder = "ProgramFilesFolder" ?>
<?elseif $(sys.BUILDARCH)="x64"?>
<?define Win64 = "yes" ?>
<?define PlatformProgramFilesFolder = "ProgramFiles64Folder" ?>
<?elseif $(sys.BUILDARCH)="arm64"?>
<?define Win64 = "yes" ?>
<?define PlatformProgramFilesFolder = "ProgramFiles64Folder" ?>
<?else?>
<?error Unsupported value of sys.BUILDARCH=$(sys.BUILDARCH)?>
<?endif?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
<Product
Id="*"
Name="{{product_name}}"
UpgradeCode="{{upgrade_code}}"
Language="!(loc.TauriLanguage)"
Manufacturer="{{manufacturer}}"
Version="{{version}}">
<Package Id="*"
Keywords="Installer"
InstallerVersion="450"
Languages="0"
Compressed="yes"
InstallScope="perMachine"
SummaryCodepage="!(loc.TauriCodepage)"/>
<!-- https://docs.microsoft.com/en-us/windows/win32/msi/reinstallmode -->
<!-- reinstall all files; rewrite all registry entries; reinstall all shortcuts -->
<Property Id="REINSTALLMODE" Value="amus" />
<!-- Auto launch app after installation, useful for passive mode which usually used in updates -->
<Property Id="AUTOLAUNCHAPP" Secure="yes" />
<!-- Property to forward cli args to the launched app to not lose those of the pre-update instance -->
<Property Id="LAUNCHAPPARGS" Secure="yes" />
{{#if allow_downgrades}}
<MajorUpgrade Schedule="afterInstallInitialize" AllowDowngrades="yes" />
{{else}}
<MajorUpgrade Schedule="afterInstallInitialize" DowngradeErrorMessage="!(loc.DowngradeErrorMessage)" AllowSameVersionUpgrades="yes" />
{{/if}}
<InstallExecuteSequence>
<RemoveShortcuts>Installed AND NOT UPGRADINGPRODUCTCODE</RemoveShortcuts>
</InstallExecuteSequence>
<Media Id="1" Cabinet="app.cab" EmbedCab="yes" />
{{#if banner_path}}
<WixVariable Id="WixUIBannerBmp" Value="{{banner_path}}" />
{{/if}}
{{#if dialog_image_path}}
<WixVariable Id="WixUIDialogBmp" Value="{{dialog_image_path}}" />
{{/if}}
{{#if license}}
<WixVariable Id="WixUILicenseRtf" Value="{{license}}" />
{{/if}}
<Icon Id="ProductIcon" SourceFile="{{icon_path}}"/>
<Property Id="ARPPRODUCTICON" Value="ProductIcon" />
<Property Id="ARPNOREPAIR" Value="yes" Secure="yes" /> <!-- Remove repair -->
<SetProperty Id="ARPNOMODIFY" Value="1" After="InstallValidate" Sequence="execute"/>
{{#if homepage}}
<Property Id="ARPURLINFOABOUT" Value="{{homepage}}"/>
<Property Id="ARPHELPLINK" Value="{{homepage}}"/>
<Property Id="ARPURLUPDATEINFO" Value="{{homepage}}"/>
{{/if}}
<!-- initialize with previous InstallDir -->
<Property Id="INSTALLDIR">
<RegistrySearch Id="PrevInstallDirReg" Root="HKCU" Key="Software\\{{manufacturer}}\\{{product_name}}" Name="InstallDir" Type="raw"/>
</Property>
<!-- launch app checkbox -->
<Property Id="WIXUI_EXITDIALOGOPTIONALCHECKBOXTEXT" Value="!(loc.LaunchApp)" />
<Property Id="WIXUI_EXITDIALOGOPTIONALCHECKBOX" Value="1"/>
<CustomAction Id="LaunchApplication" Impersonate="yes" FileKey="Path" ExeCommand="[LAUNCHAPPARGS]" Return="asyncNoWait" />
<UI>
<!-- launch app checkbox -->
<Publish Dialog="ExitDialog" Control="Finish" Event="DoAction" Value="LaunchApplication">WIXUI_EXITDIALOGOPTIONALCHECKBOX = 1 and NOT Installed</Publish>
<Property Id="WIXUI_INSTALLDIR" Value="INSTALLDIR" />
{{#unless license}}
<!-- Skip license dialog -->
<Publish Dialog="WelcomeDlg"
Control="Next"
Event="NewDialog"
Value="InstallDirDlg"
Order="2">1</Publish>
<Publish Dialog="InstallDirDlg"
Control="Back"
Event="NewDialog"
Value="WelcomeDlg"
Order="2">1</Publish>
{{/unless}}
</UI>
<UIRef Id="WixUI_InstallDir" />
<Directory Id="TARGETDIR" Name="SourceDir">
<Directory Id="DesktopFolder" Name="Desktop">
<Component Id="ApplicationShortcutDesktop" Guid="*">
<Shortcut Id="ApplicationDesktopShortcut" Name="{{product_name}}" Description="Runs {{product_name}}" Target="[!Path]" WorkingDirectory="INSTALLDIR" />
<RemoveFolder Id="DesktopFolder" On="uninstall" />
<RegistryValue Root="HKCU" Key="Software\\{{manufacturer}}\\{{product_name}}" Name="Desktop Shortcut" Type="integer" Value="1" KeyPath="yes" />
</Component>
</Directory>
<Directory Id="$(var.PlatformProgramFilesFolder)" Name="PFiles">
<Directory Id="INSTALLDIR" Name="{{product_name}}"/>
</Directory>
<Directory Id="ProgramMenuFolder">
<Directory Id="ApplicationProgramsFolder" Name="{{product_name}}"/>
</Directory>
</Directory>
<DirectoryRef Id="INSTALLDIR">
<Component Id="RegistryEntries" Guid="*">
<RegistryKey Root="HKCU" Key="Software\\{{manufacturer}}\\{{product_name}}">
<RegistryValue Name="InstallDir" Type="string" Value="[INSTALLDIR]" KeyPath="yes" />
</RegistryKey>
<!-- Change the Root to HKCU for perUser installations -->
{{#each deep_link_protocols as |protocol| ~}}
<RegistryKey Root="HKLM" Key="Software\Classes\\{{protocol}}">
<RegistryValue Type="string" Name="URL Protocol" Value=""/>
<RegistryValue Type="string" Value="URL:{{bundle_id}} protocol"/>
<RegistryKey Key="DefaultIcon">
<RegistryValue Type="string" Value="&quot;[!Path]&quot;,0" />
</RegistryKey>
<RegistryKey Key="shell\open\command">
<RegistryValue Type="string" Value="&quot;[!Path]&quot; &quot;%1&quot;" />
</RegistryKey>
</RegistryKey>
{{/each~}}
</Component>
<Component Id="Path" Guid="{{path_component_guid}}" Win64="$(var.Win64)">
<File Id="Path" Source="{{app_exe_source}}" KeyPath="yes" Checksum="yes"/>
{{#each file_associations as |association| ~}}
{{#each association.ext as |ext| ~}}
<ProgId Id="{{../../product_name}}.{{ext}}" Advertise="yes" Description="{{association.description}}">
<Extension Id="{{ext}}" Advertise="yes">
<Verb Id="open" Command="Open with {{../../product_name}}" Argument="&quot;%1&quot;" />
</Extension>
</ProgId>
{{/each~}}
{{/each~}}
</Component>
{{#each binaries as |bin| ~}}
<Component Id="{{ bin.id }}" Guid="{{bin.guid}}" Win64="$(var.Win64)">
<File Id="Bin_{{ bin.id }}" Source="{{bin.path}}" KeyPath="yes"/>
</Component>
{{/each~}}
{{#if enable_elevated_update_task}}
<Component Id="UpdateTask" Guid="C492327D-9720-4CD5-8DB8-F09082AF44BE" Win64="$(var.Win64)">
<File Id="UpdateTask" Source="update.xml" KeyPath="yes" Checksum="yes"/>
</Component>
<Component Id="UpdateTaskInstaller" Guid="011F25ED-9BE3-50A7-9E9B-3519ED2B9932" Win64="$(var.Win64)">
<File Id="UpdateTaskInstaller" Source="install-task.ps1" KeyPath="yes" Checksum="yes"/>
</Component>
<Component Id="UpdateTaskUninstaller" Guid="D4F6CC3F-32DC-5FD0-95E8-782FFD7BBCE1" Win64="$(var.Win64)">
<File Id="UpdateTaskUninstaller" Source="uninstall-task.ps1" KeyPath="yes" Checksum="yes"/>
</Component>
{{/if}}
{{resources}}
<Component Id="CMP_UninstallShortcut" Guid="*">
<Shortcut Id="UninstallShortcut"
Name="Uninstall {{product_name}}"
Description="Uninstalls {{product_name}}"
Target="[System64Folder]msiexec.exe"
Arguments="/x [ProductCode]" />
<RemoveFolder Id="INSTALLDIR"
On="uninstall" />
<RegistryValue Root="HKCU"
Key="Software\\{{manufacturer}}\\{{product_name}}"
Name="Uninstaller Shortcut"
Type="integer"
Value="1"
KeyPath="yes" />
</Component>
</DirectoryRef>
<DirectoryRef Id="ApplicationProgramsFolder">
<Component Id="ApplicationShortcut" Guid="*">
<Shortcut Id="ApplicationStartMenuShortcut"
Name="{{product_name}}"
Description="Runs {{product_name}}"
Target="[!Path]"
Icon="ProductIcon"
WorkingDirectory="INSTALLDIR">
<ShortcutProperty Key="System.AppUserModel.ID" Value="{{bundle_id}}"/>
</Shortcut>
<RemoveFolder Id="ApplicationProgramsFolder" On="uninstall"/>
<RegistryValue Root="HKCU" Key="Software\\{{manufacturer}}\\{{product_name}}" Name="Start Menu Shortcut" Type="integer" Value="1" KeyPath="yes"/>
</Component>
</DirectoryRef>
{{#each merge_modules as |msm| ~}}
<DirectoryRef Id="TARGETDIR">
<Merge Id="{{ msm.name }}" SourceFile="{{ msm.path }}" DiskId="1" Language="!(loc.TauriLanguage)" />
</DirectoryRef>
<Feature Id="{{ msm.name }}" Title="{{ msm.name }}" AllowAdvertise="no" Display="hidden" Level="1">
<MergeRef Id="{{ msm.name }}"/>
</Feature>
{{/each~}}
<Feature
Id="MainProgram"
Title="Application"
Description="!(loc.InstallAppFeature)"
Level="1"
ConfigurableDirectory="INSTALLDIR"
AllowAdvertise="no"
Display="expand"
Absent="disallow">
<ComponentRef Id="RegistryEntries"/>
{{#each resource_file_ids as |resource_file_id| ~}}
<ComponentRef Id="{{ resource_file_id }}"/>
{{/each~}}
{{#if enable_elevated_update_task}}
<ComponentRef Id="UpdateTask" />
<ComponentRef Id="UpdateTaskInstaller" />
<ComponentRef Id="UpdateTaskUninstaller" />
{{/if}}
<Feature Id="ShortcutsFeature"
Title="Shortcuts"
Level="1">
<ComponentRef Id="Path"/>
<ComponentRef Id="CMP_UninstallShortcut" />
<ComponentRef Id="ApplicationShortcut" />
<ComponentRef Id="ApplicationShortcutDesktop" />
</Feature>
<Feature
Id="Environment"
Title="PATH Environment Variable"
Description="!(loc.PathEnvVarFeature)"
Level="1"
Absent="allow">
<ComponentRef Id="Path"/>
{{#each binaries as |bin| ~}}
<ComponentRef Id="{{ bin.id }}"/>
{{/each~}}
</Feature>
</Feature>
<Feature Id="External" AllowAdvertise="no" Absent="disallow">
{{#each component_group_refs as |id| ~}}
<ComponentGroupRef Id="{{ id }}"/>
{{/each~}}
{{#each component_refs as |id| ~}}
<ComponentRef Id="{{ id }}"/>
{{/each~}}
{{#each feature_group_refs as |id| ~}}
<FeatureGroupRef Id="{{ id }}"/>
{{/each~}}
{{#each feature_refs as |id| ~}}
<FeatureRef Id="{{ id }}"/>
{{/each~}}
{{#each merge_refs as |id| ~}}
<MergeRef Id="{{ id }}"/>
{{/each~}}
</Feature>
{{#if install_webview}}
<!-- WebView2 -->
<Property Id="WVRTINSTALLED">
<RegistrySearch Id="WVRTInstalledSystem" Root="HKLM" Key="SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" Name="pv" Type="raw" Win64="no" />
<RegistrySearch Id="WVRTInstalledUser" Root="HKCU" Key="SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" Name="pv" Type="raw"/>
</Property>
{{#if download_bootstrapper}}
<CustomAction Id='DownloadAndInvokeBootstrapper' Directory="INSTALLDIR" Execute="deferred" ExeCommand='powershell.exe -NoProfile -windowstyle hidden try [\{] [\[]Net.ServicePointManager[\]]::SecurityProtocol = [\[]Net.SecurityProtocolType[\]]::Tls12 [\}] catch [\{][\}]; Invoke-WebRequest -Uri "https://go.microsoft.com/fwlink/p/?LinkId=2124703" -OutFile "$env:TEMP\MicrosoftEdgeWebview2Setup.exe" ; Start-Process -FilePath "$env:TEMP\MicrosoftEdgeWebview2Setup.exe" -ArgumentList ({{webview_installer_args}} &apos;/install&apos;) -Wait' Return='check'/>
<InstallExecuteSequence>
<Custom Action='DownloadAndInvokeBootstrapper' Before='InstallFinalize'>
<![CDATA[NOT(REMOVE OR WVRTINSTALLED)]]>
</Custom>
</InstallExecuteSequence>
{{/if}}
<!-- Embedded webview bootstrapper mode -->
{{#if webview2_bootstrapper_path}}
<Binary Id="MicrosoftEdgeWebview2Setup.exe" SourceFile="{{webview2_bootstrapper_path}}"/>
<CustomAction Id='InvokeBootstrapper' BinaryKey='MicrosoftEdgeWebview2Setup.exe' Execute="deferred" ExeCommand='{{webview_installer_args}} /install' Return='check' />
<InstallExecuteSequence>
<Custom Action='InvokeBootstrapper' Before='InstallFinalize'>
<![CDATA[NOT(REMOVE OR WVRTINSTALLED)]]>
</Custom>
</InstallExecuteSequence>
{{/if}}
<!-- Embedded offline installer -->
{{#if webview2_installer_path}}
<Binary Id="MicrosoftEdgeWebView2RuntimeInstaller.exe" SourceFile="{{webview2_installer_path}}"/>
<CustomAction Id='InvokeStandalone' BinaryKey='MicrosoftEdgeWebView2RuntimeInstaller.exe' Execute="deferred" ExeCommand='{{webview_installer_args}} /install' Return='check' />
<InstallExecuteSequence>
<Custom Action='InvokeStandalone' Before='InstallFinalize'>
<![CDATA[NOT(REMOVE OR WVRTINSTALLED)]]>
</Custom>
</InstallExecuteSequence>
{{/if}}
{{/if}}
{{#if enable_elevated_update_task}}
<!-- Install an elevated update task within Windows Task Scheduler -->
<CustomAction
Id="CreateUpdateTask"
Return="check"
Directory="INSTALLDIR"
Execute="commit"
Impersonate="yes"
ExeCommand="powershell.exe -WindowStyle hidden .\install-task.ps1" />
<InstallExecuteSequence>
<Custom Action='CreateUpdateTask' Before='InstallFinalize'>
NOT(REMOVE)
</Custom>
</InstallExecuteSequence>
<!-- Remove elevated update task during uninstall -->
<CustomAction
Id="DeleteUpdateTask"
Return="check"
Directory="INSTALLDIR"
ExeCommand="powershell.exe -WindowStyle hidden .\uninstall-task.ps1" />
<InstallExecuteSequence>
<Custom Action="DeleteUpdateTask" Before='InstallFinalize'>
(REMOVE = "ALL") AND NOT UPGRADINGPRODUCTCODE
</Custom>
</InstallExecuteSequence>
{{/if}}
<InstallExecuteSequence>
<Custom Action="LaunchApplication" After="InstallFinalize">AUTOLAUNCHAPP AND NOT Installed</Custom>
</InstallExecuteSequence>
<SetProperty Id="ARPINSTALLLOCATION" Value="[INSTALLDIR]" After="CostFinalize"/>
</Product>
</Wix>

View File

@ -32,8 +32,8 @@
},
"devDependencies": {
"@eslint/js": "^9.5.0",
"@tauri-apps/cli": "^1.6.2",
"@types/eslint": "9.6.0",
"@tauri-apps/cli": "^2.0.1",
"@types/eslint__js": "^8.42.3",
"@types/node": "^22.3.0",
"@typescript-eslint/parser": "^7.13.1",

View File

@ -34,8 +34,8 @@ importers:
specifier: ^9.5.0
version: 9.5.0
'@tauri-apps/cli':
specifier: ^1.6.2
version: 1.6.2
specifier: ^2.0.1
version: 2.0.1
'@types/eslint':
specifier: 9.6.0
version: 9.6.0
@ -90,13 +90,13 @@ importers:
apps/desktop:
dependencies:
'@anthropic-ai/sdk':
specifier: ^0.27.3
version: 0.27.3
openai:
specifier: ^4.47.3
version: 4.47.3
devDependencies:
'@anthropic-ai/sdk':
specifier: ^0.27.3
version: 0.27.3
'@codemirror/lang-cpp':
specifier: ^6.0.2
version: 6.0.2
@ -173,8 +173,35 @@ importers:
specifier: catalog:svelte
version: 4.0.0-next.6(svelte@5.0.0-next.243)(vite@5.2.13(@types/node@22.3.0))
'@tauri-apps/api':
specifier: ^1.6.0
version: 1.6.0
specifier: ^2.0.3
version: 2.0.3
'@tauri-apps/plugin-dialog':
specifier: ^2.0.1
version: 2.0.1
'@tauri-apps/plugin-fs':
specifier: ^2.0.1
version: 2.0.1
'@tauri-apps/plugin-http':
specifier: ^2.0.1
version: 2.0.1
'@tauri-apps/plugin-log':
specifier: ^2.0.0
version: 2.0.0
'@tauri-apps/plugin-os':
specifier: ^2.0.0
version: 2.0.0
'@tauri-apps/plugin-process':
specifier: ^2.0.0
version: 2.0.0
'@tauri-apps/plugin-shell':
specifier: ^2.0.1
version: 2.0.1
'@tauri-apps/plugin-store':
specifier: ^2.1.0
version: 2.1.0
'@tauri-apps/plugin-updater':
specifier: ^2.0.0
version: 2.0.0
'@testing-library/jest-dom':
specifier: ^6.4.8
version: 6.4.8
@ -277,12 +304,6 @@ importers:
svelte-french-toast:
specifier: ^1.2.0
version: 1.2.0(svelte@5.0.0-next.243)
tauri-plugin-log-api:
specifier: https://github.com/tauri-apps/tauri-plugin-log#v1
version: https://codeload.github.com/tauri-apps/tauri-plugin-log/tar.gz/cc86b2d9878d6ec02c9f25bd48292621a4bc2a6f
tauri-plugin-store-api:
specifier: https://github.com/tauri-apps/tauri-plugin-store#v1
version: https://codeload.github.com/tauri-apps/tauri-plugin-store/tar.gz/0b7079b8c55bf25f6d9d9e8c57812d03b48a9788
tinykeys:
specifier: ^2.1.0
version: 2.1.0
@ -1788,75 +1809,101 @@ packages:
resolution: {integrity: sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==}
engines: {node: '>=14.16'}
'@tauri-apps/api@1.6.0':
resolution: {integrity: sha512-rqI++FWClU5I2UBp4HXFvl+sBWkdigBkxnpJDQUWttNyG7IZP4FwQGhTNL5EOw0vI8i6eSAJ5frLqO7n7jbJdg==}
engines: {node: '>= 14.6.0', npm: '>= 6.6.0', yarn: '>= 1.19.1'}
'@tauri-apps/api@2.0.3':
resolution: {integrity: sha512-840qk6n8rbXBXMA5/aAgTYsg5JAubKO0nXw5wf7IzGnUuYKGbB4oFBIZtXOIWy+E0kNTDI3qhq5iqsoMJfwp8g==}
'@tauri-apps/cli-darwin-arm64@1.6.2':
resolution: {integrity: sha512-6mdRyf9DaLqlZvj8kZB09U3rwY+dOHSGzTZ7+GDg665GJb17f4cb30e8dExj6/aghcsOie9EGpgiURcDUvLNSQ==}
'@tauri-apps/cli-darwin-arm64@2.0.1':
resolution: {integrity: sha512-oWjCZoFbm57V0eLEkIbc6aUmB4iW65QF7J8JVh5sNzH4xHGP9rzlQarbkg7LOn89z7mFSZpaLJAWlaaZwoV2Ug==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
'@tauri-apps/cli-darwin-x64@1.6.2':
resolution: {integrity: sha512-PLxZY5dn38H3R9VRmBN/l0ZDB5JFanCwlK4rmpzDQPPg3tQmbu5vjSCP6TVj5U6aLKsj79kFyULblPr5Dn9+vw==}
'@tauri-apps/cli-darwin-x64@2.0.1':
resolution: {integrity: sha512-bARd5yAnDGpG/FPhSh87+tzQ6D0TPyP2mZ5bg6cioeoXDmry68nT/FBzp87ySR1/KHvuhEQYWM/4RPrDjvI1Yg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
'@tauri-apps/cli-linux-arm-gnueabihf@1.6.2':
resolution: {integrity: sha512-xnpj4BLeeGOh5I/ewCQlYJZwHH0CBNBN+4q8BNWNQ9MKkjN9ST366RmHRzl2ANNgWwijOPxyce7GiUmvuH8Atw==}
'@tauri-apps/cli-linux-arm-gnueabihf@2.0.1':
resolution: {integrity: sha512-OK3/RpxujoZAUbV7GHe4IPAUsIO6IuWEHT++jHXP+YW5Y7QezGGjQRc43IlWaQYej/yE8wfcrwrbqisc5wtiCw==}
engines: {node: '>= 10'}
cpu: [arm]
os: [linux]
'@tauri-apps/cli-linux-arm64-gnu@1.6.2':
resolution: {integrity: sha512-uaiRE0vE2P+tdsCngfKt+7yKr3VZXIq/t3w01DzSdnBgHSp0zmRsRR4AhZt7ibvoEgA8GzBP+eSHJdFNZsTU9w==}
'@tauri-apps/cli-linux-arm64-gnu@2.0.1':
resolution: {integrity: sha512-MGSQJduiMEApspMK97mFt4kr6ig0OtxO5SUFpPDfYPw/XmY9utaRa9CEG6LcH8e0GN9xxYMhCv+FeU48spYPhA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@tauri-apps/cli-linux-arm64-musl@1.6.2':
resolution: {integrity: sha512-o9JunVrMrhqTBLrdvEbS64W0bo1dPm0lxX51Mx+6x9SmbDjlEWGgaAHC3iKLK9khd5Yu1uO1e+8TJltAcScvmw==}
'@tauri-apps/cli-linux-arm64-musl@2.0.1':
resolution: {integrity: sha512-R6+vgxaPpxgGi4suMkQgGuhjMbZzMJfVyWfv2DOE/xxOzSK1BAOc54/HOjfOLxlnkA6uD6V69MwCwXgxW00A2g==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@tauri-apps/cli-linux-x64-gnu@1.6.2':
resolution: {integrity: sha512-jL9f+o61DdQmNYKIt2Q3BA8YJ+hyC5+GdNxqDf7j5SoQ85j//YfUWbmp9ZgsPHVBxgSGZVvgGMNvf64Ykp0buQ==}
'@tauri-apps/cli-linux-x64-gnu@2.0.1':
resolution: {integrity: sha512-xrasYQnUZVhKJhBxHAeu4KxZbofaQlsG9KfZ9p1Bx+hmjs5BuujzwMnXsVD2a4l6GPW6gwblf2a6d600rySmWQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@tauri-apps/cli-linux-x64-musl@1.6.2':
resolution: {integrity: sha512-xsa4Pu9YMHKAX0J8pIoXfN/uhvAAAoECZDixDhWw8zi57VZ4QX28ycqolS+NscdD9NAGSgHk45MpBZWdvRtvjQ==}
'@tauri-apps/cli-linux-x64-musl@2.0.1':
resolution: {integrity: sha512-SPk+EzRTlbvk46p5aURc7O4GihzxbqG80m74vstm0rolnmQ0FX3qqIh3as3cQpDiZWLod4j6EEmX0mTU3QpvXA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@tauri-apps/cli-win32-arm64-msvc@1.6.2':
resolution: {integrity: sha512-eJtUOx2UFhJpCCkm5M5+4Co9JbjvgIHTdyS/hTSZfOEdT58CNEGVJXMA39FsSZXYoxYPE+9K7Km6haMozSmlxw==}
'@tauri-apps/cli-win32-arm64-msvc@2.0.1':
resolution: {integrity: sha512-LAELK01eOMyEt+JZLmx4EUOdRuPYr1a+mHjlxAxCnCaS3dpeg/c5/NMZfbRAJbAH4id+STRHIfPXTdCT2zUNAw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
'@tauri-apps/cli-win32-ia32-msvc@1.6.2':
resolution: {integrity: sha512-9Jwx3PrhNw3VKOgPISRRXPkvoEAZP+7rFRHXIo49dvlHy2E/o9qpWi1IntE33HWeazP6KhvsCjvXB2Ai4eGooA==}
'@tauri-apps/cli-win32-ia32-msvc@2.0.1':
resolution: {integrity: sha512-eMUgOS4mAusk5njU2TBxBjCUO1P4cV4uzY5CHihysoXSL2TVQdWrXT42VGeoahJh+yeQWkYFka2s4Bu0iWDMXg==}
engines: {node: '>= 10'}
cpu: [ia32]
os: [win32]
'@tauri-apps/cli-win32-x64-msvc@1.6.2':
resolution: {integrity: sha512-5Z+ZjRFJE8MXghJe1UXvGephY5ZcgVhiTI9yuMi9xgX3CEaAXASatyXllzsvGJ9EDaWMEpa0PHjAzi7LBAWROw==}
'@tauri-apps/cli-win32-x64-msvc@2.0.1':
resolution: {integrity: sha512-U9esAOcFIv80/slzlpwjkG31Wx1OqbfDgC5KjGT1Dd9iUOSuJZCwbiY7m3rYG2I6RWLfd9zhNu86CVohsKjBfA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
'@tauri-apps/cli@1.6.2':
resolution: {integrity: sha512-zpfZdxhm20s7d/Uejpg/T3a9sqLVe3Ih2ztINfy8v6iLw9Ohowkb9g+agZffYKlEWfOSpmCy69NFyBLj7OZL0A==}
'@tauri-apps/cli@2.0.1':
resolution: {integrity: sha512-fCheW0iWYWUtFV3ui3HlMhk3ZJpAQ5KJr7B7UmfhDzBSy1h5JBdrCtvDwy+3AcPN+Fg5Ey3JciF8zEP8eBx+vQ==}
engines: {node: '>= 10'}
hasBin: true
'@tauri-apps/plugin-dialog@2.0.1':
resolution: {integrity: sha512-fnUrNr6EfvTqdls/ufusU7h6UbNFzLKvHk/zTuOiBq01R3dTODqwctZlzakdbfSp/7pNwTKvgKTAgl/NAP/Z0Q==}
'@tauri-apps/plugin-fs@2.0.1':
resolution: {integrity: sha512-PkeZG2WAob9Xpmr66aPvj+McDVgFjV2a7YBzYVZjiCvbGeMs6Yk09tlXhCe3EyZdT/pwWMSi8lXUace+hlsjsw==}
'@tauri-apps/plugin-http@2.0.1':
resolution: {integrity: sha512-j6IA3pVBybSCwPpsihpX4z8bs6PluuGtr06ahL/xy4D8HunNBTmRmadJrFOQi0gOAbaig4MkQ15nzNLBLy8R1A==}
'@tauri-apps/plugin-log@2.0.0':
resolution: {integrity: sha512-C+NII9vzswqnOQE8k7oRtnaw0z5TZsMmnirRhXkCKDEhQQH9841Us/PC1WHtGiAaJ8za1A1JB2xXndEq/47X/w==}
'@tauri-apps/plugin-os@2.0.0':
resolution: {integrity: sha512-M7hG/nNyQYTJxVG/UhTKhp9mpXriwWzrs9mqDreB8mIgqA3ek5nHLdwRZJWhkKjZrnDT4v9CpA9BhYeplTlAiA==}
'@tauri-apps/plugin-process@2.0.0':
resolution: {integrity: sha512-OYzi0GnkrF4NAnsHZU7U3tjSoP0PbeAlO7T1Z+vJoBUH9sFQ1NSLqWYWQyf8hcb3gVWe7P1JggjiskO+LST1ug==}
'@tauri-apps/plugin-shell@2.0.1':
resolution: {integrity: sha512-akU1b77sw3qHiynrK0s930y8zKmcdrSD60htjH+mFZqv5WaakZA/XxHR3/sF1nNv9Mgmt/Shls37HwnOr00aSw==}
'@tauri-apps/plugin-store@2.1.0':
resolution: {integrity: sha512-GADqrc17opUKYIAKnGHIUgEeTZ2wJGu1ZITKQ1WMuOFdv8fvXRFBAqsqPjE3opgWohbczX6e1NpwmZK1AnuWVw==}
'@tauri-apps/plugin-updater@2.0.0':
resolution: {integrity: sha512-N0cl71g7RPr7zK2Fe5aoIwzw14NcdLcz7XMGFWZVjprsqgDRWoxbnUkknyCQMZthjhGkppCd/wN2MIsUz+eAhQ==}
'@terrazzo/cli@0.0.11':
resolution: {integrity: sha512-VJTzZ+uw5bzFcvX73g9kvsEtMebRAP/J1oUB7uB8KZYkHtbjKt153jL7YZl5N6B5pHRFLFRE3RU6XPslSvG29g==}
hasBin: true
@ -5535,14 +5582,6 @@ packages:
tar-stream@3.1.7:
resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==}
tauri-plugin-log-api@https://codeload.github.com/tauri-apps/tauri-plugin-log/tar.gz/cc86b2d9878d6ec02c9f25bd48292621a4bc2a6f:
resolution: {tarball: https://codeload.github.com/tauri-apps/tauri-plugin-log/tar.gz/cc86b2d9878d6ec02c9f25bd48292621a4bc2a6f}
version: 0.0.0
tauri-plugin-store-api@https://codeload.github.com/tauri-apps/tauri-plugin-store/tar.gz/0b7079b8c55bf25f6d9d9e8c57812d03b48a9788:
resolution: {tarball: https://codeload.github.com/tauri-apps/tauri-plugin-store/tar.gz/0b7079b8c55bf25f6d9d9e8c57812d03b48a9788}
version: 0.0.0
telejson@7.2.0:
resolution: {integrity: sha512-1QTEcJkJEhc8OnStBx/ILRu5J2p0GjvWsBx56bmZRqnrkdBMUe+nX92jxV+p3dB4CP6PZCdJMQJwCggkNBMzkQ==}
@ -7725,50 +7764,86 @@ snapshots:
dependencies:
defer-to-connect: 2.0.1
'@tauri-apps/api@1.6.0': {}
'@tauri-apps/api@2.0.3': {}
'@tauri-apps/cli-darwin-arm64@1.6.2':
'@tauri-apps/cli-darwin-arm64@2.0.1':
optional: true
'@tauri-apps/cli-darwin-x64@1.6.2':
'@tauri-apps/cli-darwin-x64@2.0.1':
optional: true
'@tauri-apps/cli-linux-arm-gnueabihf@1.6.2':
'@tauri-apps/cli-linux-arm-gnueabihf@2.0.1':
optional: true
'@tauri-apps/cli-linux-arm64-gnu@1.6.2':
'@tauri-apps/cli-linux-arm64-gnu@2.0.1':
optional: true
'@tauri-apps/cli-linux-arm64-musl@1.6.2':
'@tauri-apps/cli-linux-arm64-musl@2.0.1':
optional: true
'@tauri-apps/cli-linux-x64-gnu@1.6.2':
'@tauri-apps/cli-linux-x64-gnu@2.0.1':
optional: true
'@tauri-apps/cli-linux-x64-musl@1.6.2':
'@tauri-apps/cli-linux-x64-musl@2.0.1':
optional: true
'@tauri-apps/cli-win32-arm64-msvc@1.6.2':
'@tauri-apps/cli-win32-arm64-msvc@2.0.1':
optional: true
'@tauri-apps/cli-win32-ia32-msvc@1.6.2':
'@tauri-apps/cli-win32-ia32-msvc@2.0.1':
optional: true
'@tauri-apps/cli-win32-x64-msvc@1.6.2':
'@tauri-apps/cli-win32-x64-msvc@2.0.1':
optional: true
'@tauri-apps/cli@1.6.2':
'@tauri-apps/cli@2.0.1':
optionalDependencies:
'@tauri-apps/cli-darwin-arm64': 1.6.2
'@tauri-apps/cli-darwin-x64': 1.6.2
'@tauri-apps/cli-linux-arm-gnueabihf': 1.6.2
'@tauri-apps/cli-linux-arm64-gnu': 1.6.2
'@tauri-apps/cli-linux-arm64-musl': 1.6.2
'@tauri-apps/cli-linux-x64-gnu': 1.6.2
'@tauri-apps/cli-linux-x64-musl': 1.6.2
'@tauri-apps/cli-win32-arm64-msvc': 1.6.2
'@tauri-apps/cli-win32-ia32-msvc': 1.6.2
'@tauri-apps/cli-win32-x64-msvc': 1.6.2
'@tauri-apps/cli-darwin-arm64': 2.0.1
'@tauri-apps/cli-darwin-x64': 2.0.1
'@tauri-apps/cli-linux-arm-gnueabihf': 2.0.1
'@tauri-apps/cli-linux-arm64-gnu': 2.0.1
'@tauri-apps/cli-linux-arm64-musl': 2.0.1
'@tauri-apps/cli-linux-x64-gnu': 2.0.1
'@tauri-apps/cli-linux-x64-musl': 2.0.1
'@tauri-apps/cli-win32-arm64-msvc': 2.0.1
'@tauri-apps/cli-win32-ia32-msvc': 2.0.1
'@tauri-apps/cli-win32-x64-msvc': 2.0.1
'@tauri-apps/plugin-dialog@2.0.1':
dependencies:
'@tauri-apps/api': 2.0.3
'@tauri-apps/plugin-fs@2.0.1':
dependencies:
'@tauri-apps/api': 2.0.3
'@tauri-apps/plugin-http@2.0.1':
dependencies:
'@tauri-apps/api': 2.0.3
'@tauri-apps/plugin-log@2.0.0':
dependencies:
'@tauri-apps/api': 2.0.3
'@tauri-apps/plugin-os@2.0.0':
dependencies:
'@tauri-apps/api': 2.0.3
'@tauri-apps/plugin-process@2.0.0':
dependencies:
'@tauri-apps/api': 2.0.3
'@tauri-apps/plugin-shell@2.0.1':
dependencies:
'@tauri-apps/api': 2.0.3
'@tauri-apps/plugin-store@2.1.0':
dependencies:
'@tauri-apps/api': 2.0.3
'@tauri-apps/plugin-updater@2.0.0':
dependencies:
'@tauri-apps/api': 2.0.3
'@terrazzo/cli@0.0.11':
dependencies:
@ -12044,14 +12119,6 @@ snapshots:
fast-fifo: 1.3.2
streamx: 2.18.0
tauri-plugin-log-api@https://codeload.github.com/tauri-apps/tauri-plugin-log/tar.gz/cc86b2d9878d6ec02c9f25bd48292621a4bc2a6f:
dependencies:
'@tauri-apps/api': 1.6.0
tauri-plugin-store-api@https://codeload.github.com/tauri-apps/tauri-plugin-store/tar.gz/0b7079b8c55bf25f6d9d9e8c57812d03b48a9788:
dependencies:
'@tauri-apps/api': 1.6.0
telejson@7.2.0:
dependencies:
memoizerific: 1.11.3

View File

@ -111,27 +111,22 @@ done
[ -z "${VERSION-}" ] && error "--version is not set"
[ -z "${TAURI_PRIVATE_KEY-}" ] && error "$TAURI_PRIVATE_KEY is not set"
[ -z "${TAURI_KEY_PASSWORD-}" ] && error "$TAURI_KEY_PASSWORD is not set"
[ -z "${TAURI_SIGNING_PRIVATE_KEY-}" ] && error "$TAURI_SIGNING_PRIVATE_KEY is not set"
[ -z "${TAURI_SIGNING_PRIVATE_KEY_PASSWORD-}" ] && error "$TAURI_SIGNING_PRIVATE_KEY_PASSWORD is not set"
if [ "$CHANNEL" != "release" ] && [ "$CHANNEL" != "nightly" ]; then
error "--channel must be either 'release' or 'nightly'"
fi
export TAURI_PRIVATE_KEY="$TAURI_PRIVATE_KEY"
export TAURI_KEY_PASSWORD="$TAURI_KEY_PASSWORD"
if [ "$DO_SIGN" = "true" ]; then
if [ "$OS" = "macos" ]; then
[ -z "${APPLE_CERTIFICATE-}" ] && error "$APPLE_CERTIFICATE is not set"
[ -z "${APPLE_CERTIFICATE_PASSWORD-}" ] && error "$APPLE_CERTIFICATE_PASSWORD is not set"
[ -z "${APPLE_SIGNING_IDENTITY-}" ] && error "$APPLE_SIGNING_IDENTITY is not set"
[ -z "${APPLE_ID-}" ] && error "$APPLE_ID is not set"
[ -z "${APPLE_TEAM_ID-}" ] && error "$APPLE_TEAM_ID is not set"
[ -z "${APPLE_PASSWORD-}" ] && error "$APPLE_PASSWORD is not set"
export APPLE_CERTIFICATE="$APPLE_CERTIFICATE"
export APPLE_CERTIFICATE_PASSWORD="$APPLE_CERTIFICATE_PASSWORD"
export APPLE_SIGNING_IDENTITY="$APPLE_SIGNING_IDENTITY"
export APPLE_ID="$APPLE_ID"
export APPLE_TEAM_ID="$APPLE_TEAM_ID"
export APPLE_PASSWORD="$APPLE_PASSWORD"
@ -163,7 +158,7 @@ trap 'rm -rf "$TMP_DIR"' exit
CONFIG_PATH=$(readlink -f "$PWD/../crates/gitbutler-tauri/tauri.conf.$CHANNEL.json")
# update the version in the tauri release config
jq '.package.version="'"$VERSION"'"' "$CONFIG_PATH" >"$TMP_DIR/tauri.conf.json"
jq '.version="'"$VERSION"'"' "$CONFIG_PATH" >"$TMP_DIR/tauri.conf.json"
if [ "$OS" = "windows" ]; then
FEATURES="windows"