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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 4 additions & 9 deletions apps/desktop/src/components/ChromeHeader.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
import { ircEnabled } from '$lib/config/uiFeatureFlags';
import { IRC_SERVICE } from '$lib/irc/ircService.svelte';
import { MODE_SERVICE } from '$lib/mode/modeService';
import { handleAddProjectOutcome } from '$lib/project/project';
import { PROJECTS_SERVICE } from '$lib/project/projectsService';
import { useAddProject } from '$lib/project/useProjects.svelte';
import { ircPath, projectPath, isWorkspacePath } from '$lib/routes/routes.svelte';
import { UI_STATE } from '$lib/state/uiState.svelte';
import { inject } from '@gitbutler/core/context';
Expand Down Expand Up @@ -47,6 +47,8 @@
const singleBranchMode = $derived($settingsStore?.featureFlags.singleBranch ?? false);
const backend = inject(BACKEND);

const { addProject } = useAddProject();

const mode = $derived(modeService.mode({ projectId }));
const currentMode = $derived(mode.response);
const currentBranchName = $derived.by(() => {
Expand Down Expand Up @@ -188,14 +190,7 @@
onClick={async () => {
newProjectLoading = true;
try {
const outcome = await projectsService.addProject();
if (!outcome) {
// User cancelled the project creation
newProjectLoading = false;
return;
}

handleAddProjectOutcome(outcome, (project) => goto(projectPath(project.id)));
await addProject();
} finally {
newProjectLoading = false;
}
Expand Down
24 changes: 10 additions & 14 deletions apps/desktop/src/components/CloneForm.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@
import { OnboardingEvent, POSTHOG_WRAPPER } from '$lib/analytics/posthog';
import { BACKEND } from '$lib/backend';
import { GIT_SERVICE } from '$lib/git/gitService';
import { handleAddProjectOutcome } from '$lib/project/project';
import { PROJECTS_SERVICE } from '$lib/project/projectsService';
import { projectPath } from '$lib/routes/routes.svelte';
import { useAddProject } from '$lib/project/useProjects.svelte';
import { parseRemoteUrl } from '$lib/url/gitUrl';
import { inject } from '@gitbutler/core/context';
import { persisted } from '@gitbutler/shared/persisted';
Expand All @@ -16,7 +14,6 @@
import * as Sentry from '@sentry/sveltekit';
import { onMount } from 'svelte';

const projectsService = inject(PROJECTS_SERVICE);
const gitService = inject(GIT_SERVICE);
const posthog = inject(POSTHOG_WRAPPER);
const backend = inject(BACKEND);
Expand Down Expand Up @@ -62,6 +59,14 @@
return String(error);
}

const { addProject } = useAddProject(() => {
posthog.captureOnboarding(
OnboardingEvent.ClonedProjectFailed,
'Failed to add project after cloning'
);
throw new Error('Failed to add project after cloning.');
});

async function cloneRepository() {
loading = true;
savedTargetDirPath.set(targetDirPath);
Expand All @@ -88,16 +93,7 @@
await gitService.cloneRepo(repositoryUrl, targetDir);

posthog.captureOnboarding(OnboardingEvent.ClonedProject);
const outcome = await projectsService.addProject(targetDir);
if (!outcome) {
posthog.captureOnboarding(
OnboardingEvent.ClonedProjectFailed,
'Failed to add project after cloning'
);
throw new Error('Failed to add project after cloning.');
}

handleAddProjectOutcome(outcome, (project) => goto(projectPath(project.id)));
await addProject(targetDir);
} catch (e) {
Sentry.captureException(e);
const errorMessage = getErrorMessage(e);
Expand Down
15 changes: 5 additions & 10 deletions apps/desktop/src/components/FileMenuAction.svelte
Original file line number Diff line number Diff line change
@@ -1,24 +1,19 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { handleAddProjectOutcome } from '$lib/project/project';
import { PROJECTS_SERVICE } from '$lib/project/projectsService';
import { clonePath, projectPath } from '$lib/routes/routes.svelte';
import { useAddProject } from '$lib/project/useProjects.svelte';
import { clonePath } from '$lib/routes/routes.svelte';
import { SHORTCUT_SERVICE } from '$lib/shortcuts/shortcutService';
import { inject } from '@gitbutler/core/context';
import { mergeUnlisten } from '@gitbutler/ui/utils/mergeUnlisten';

const projectsService = inject(PROJECTS_SERVICE);
const shortcutService = inject(SHORTCUT_SERVICE);

const { addProject } = useAddProject();

$effect(() =>
mergeUnlisten(
shortcutService.on('add-local-repo', async () => {
const outcome = await projectsService.addProject();
if (!outcome) {
// User cancelled the project creation
return;
}
handleAddProjectOutcome(outcome, (project) => goto(projectPath(project.id)));
await addProject();
}),
shortcutService.on('clone-repo', async () => {
goto(clonePath());
Expand Down
12 changes: 4 additions & 8 deletions apps/desktop/src/components/ProjectSwitcher.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { handleAddProjectOutcome } from '$lib/project/project';
import { PROJECTS_SERVICE } from '$lib/project/projectsService';
import { useAddProject } from '$lib/project/useProjects.svelte';
import { projectPath } from '$lib/routes/routes.svelte';
import { inject } from '@gitbutler/core/context';
import { Button, OptionsGroup, Select, SelectItem } from '@gitbutler/ui';
Expand All @@ -22,6 +22,8 @@

let newProjectLoading = $state(false);
let cloneProjectLoading = $state(false);

const { addProject } = useAddProject();
</script>

<div class="project-switcher">
Expand All @@ -48,13 +50,7 @@
onClick={async () => {
newProjectLoading = true;
try {
const outcome = await projectsService.addProject();
if (!outcome) {
// User cancelled the project creation
newProjectLoading = false;
return;
}
handleAddProjectOutcome(outcome, (project) => goto(projectPath(project.id)));
await addProject();
} finally {
newProjectLoading = false;
}
Expand Down
27 changes: 19 additions & 8 deletions apps/desktop/src/components/Welcome.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import { OnboardingEvent, POSTHOG_WRAPPER } from '$lib/analytics/posthog';
import cloneRepoSvg from '$lib/assets/welcome/clone-repo.svg?raw';
import newProjectSvg from '$lib/assets/welcome/new-local-project.svg?raw';
import { showToast } from '$lib/notifications/toasts';
import { handleAddProjectOutcome } from '$lib/project/project';
import { PROJECTS_SERVICE } from '$lib/project/projectsService';
import { inject } from '@gitbutler/core/context';
Expand All @@ -17,23 +18,33 @@
let newProjectLoading = $state(false);
let directoryInputElement = $state<HTMLInputElement | undefined>();

async function onNewProject() {
newProjectLoading = true;
async function addProject(path: string) {
try {
const testDirectoryPath = directoryInputElement?.value;
const outcome = await projectsService.addProject(testDirectoryPath ?? '');

const outcome = await projectsService.addProject(path);
posthog.captureOnboarding(OnboardingEvent.AddLocalProject);

if (outcome) {
handleAddProjectOutcome(outcome);
handleAddProjectOutcome(outcome, async (path: string) => {
await projectsService.initGitRepository(path);
showToast({
title: 'Repository Initialized',
message: `Git repository has been successfully initialized at ${path}. Loading project...`,
style: 'info'
});
await addProject(path);
});
}
} catch (e: unknown) {
posthog.captureOnboarding(OnboardingEvent.AddLocalProjectFailed, e);
} finally {
newProjectLoading = false;
}
}

async function onNewProject() {
newProjectLoading = true;
await addProject(directoryInputElement?.value ?? '');
newProjectLoading = false;
}

async function onCloneProject() {
goto('/onboarding/clone');
}
Expand Down
22 changes: 18 additions & 4 deletions apps/desktop/src/lib/project/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,15 +85,19 @@ export type AddProjectOutcome =
/**
* The error message received
*/
subject: string;
subject: {
path: string;
message: string;
};
};

/**
* Correctly handle the outcome of an addProject operation by passing the project to the callback or
* showing toasts as necessary.
* showing toasts as necessary.'get this - needs a refactor probably';
*/
export function handleAddProjectOutcome(
outcome: AddProjectOutcome,
onInitialize: (path: string) => Promise<void>,
onAdded?: (project: Project) => void
): true {
switch (outcome.type) {
Expand Down Expand Up @@ -163,9 +167,19 @@ export function handleAddProjectOutcome(
case 'notAGitRepository':
showToast({
testId: TestId.AddProjectNotAGitRepoModal,
style: 'warning',
title: 'Not a Git repository',
message: `Unable to add project: ${outcome.subject}`
message:
'The selected directory is not a Git repository. Would you like to initialize one?',
style: 'warning',
extraAction: {
testId: TestId.AddProjectNotAGitRepoModalInitializeButton,
label: 'Initialize Repository',
onClick: async (dismiss) => {
const projectPath = outcome.subject.path;
await onInitialize(projectPath);
dismiss();
}
}
});
return true;
}
Expand Down
4 changes: 4 additions & 0 deletions apps/desktop/src/lib/project/projectsService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,10 @@ export class ProjectsService {
unsetLastOpenedProject() {
this.persistedId.set(undefined);
}

async initGitRepository(path: string): Promise<void> {
return await this.backend.invoke('init_git_repository', { path });
}
}

function injectEndpoints(api: ClientState['backendApi']) {
Expand Down
34 changes: 34 additions & 0 deletions apps/desktop/src/lib/project/useProjects.svelte.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { goto } from '$app/navigation';
import { showToast } from '$lib/notifications/toasts';
import { handleAddProjectOutcome } from '$lib/project/project';
import { PROJECTS_SERVICE } from '$lib/project/projectsService';
import { projectPath } from '$lib/routes/routes.svelte';
import { inject } from '@gitbutler/core/context';

export function useAddProject(onMissingOutcome?: () => void) {
const projectsService = inject(PROJECTS_SERVICE);

async function addProject(path?: string) {
const outcome = await projectsService.addProject(path);

if (outcome) {
handleAddProjectOutcome(
outcome,
async (path: string) => {
await projectsService.initGitRepository(path);
showToast({
title: 'Repository Initialized',
message: `Git repository has been successfully initialized at ${path}. Loading project...`,
style: 'info'
});
await addProject(path);
},
(project) => goto(projectPath(project.id))
);
} else {
onMissingOutcome?.();
}
}

return { addProject };
}
12 changes: 12 additions & 0 deletions crates/but-api/src/commands/projects.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::error::Error;
use anyhow::Context;
use but_api_macros::api_cmd;
use gitbutler_project::{self as projects, ProjectId};
use std::path::PathBuf;
Expand Down Expand Up @@ -40,3 +41,14 @@ pub fn get_project(
pub fn delete_project(project_id: ProjectId) -> Result<(), Error> {
gitbutler_project::delete(project_id).map_err(Into::into)
}

/// Initialize a Git repository at the given path
#[api_cmd]
#[tauri::command(async)]
#[instrument(err(Debug))]
pub fn init_git_repository(path: String) -> Result<(), Error> {
let path: PathBuf = path.into();
git2::Repository::init(&path)
.with_context(|| format!("Failed to initialize Git repository at {}", path.display()))?;
Ok(())
}
1 change: 1 addition & 0 deletions crates/but-server/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ async fn handle_command(
"add_project" => iprojects::add_project_cmd(request.params),
"get_project" => iprojects::get_project_cmd(request.params),
"delete_project" => iprojects::delete_project_cmd(request.params),
"init_git_repository" => iprojects::init_git_repository_cmd(request.params),
"list_projects" => projects::list_projects(&extra).await,
"set_project_active" => {
projects::set_project_active(&app, &extra, app_settings_sync, request.params).await
Expand Down
15 changes: 12 additions & 3 deletions crates/gitbutler-project/src/controller.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ use anyhow::{anyhow, bail, Context, Result};
use gitbutler_error::error;

use super::{storage, storage::UpdateRequest, Project, ProjectId};
use crate::{project::AddProjectOutcome, AuthKey};
use crate::{
project::{AddProjectOutcome, NotAGitRepositoryOutcome},
AuthKey,
};

#[derive(Clone, Debug)]
pub(crate) struct Controller {
Expand Down Expand Up @@ -96,9 +99,15 @@ impl Controller {
}
}
},
Err(err) => {
return Ok(AddProjectOutcome::NotAGitRepository(err.to_string()));
Err(err @ gix::open::Error::NotARepository { .. }) => {
return Ok(AddProjectOutcome::NotAGitRepository(
NotAGitRepositoryOutcome {
path: path.to_path_buf(),
message: err.to_string(),
},
));
}
Err(err) => return Err(err.into()),
}

let id = ProjectId::generate();
Expand Down
10 changes: 8 additions & 2 deletions crates/gitbutler-project/src/project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,12 @@ impl Project {
}
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct NotAGitRepositoryOutcome {
pub path: PathBuf,
pub message: String,
}

#[derive(Debug, Serialize, Deserialize, Clone, strum::Display)]
#[serde(rename_all = "camelCase", tag = "type", content = "subject")]
pub enum AddProjectOutcome {
Expand All @@ -194,7 +200,7 @@ pub enum AddProjectOutcome {
NonMainWorktree,
NoWorkdir,
NoDotGitDirectory,
NotAGitRepository(String),
NotAGitRepository(NotAGitRepositoryOutcome),
}

impl AddProjectOutcome {
Expand Down Expand Up @@ -229,7 +235,7 @@ impl AddProjectOutcome {
Err(anyhow::anyhow!("no .git directory found in repository"))
}
AddProjectOutcome::NotAGitRepository(msg) => {
Err(anyhow::anyhow!("not a git repository: {}", msg))
Err(anyhow::anyhow!("not a git repository: {}", msg.message))
}
}
}
Expand Down
1 change: 1 addition & 0 deletions crates/gitbutler-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ fn main() {
but_api::projects::get_project,
but_api::projects::update_project,
but_api::projects::delete_project,
but_api::projects::init_git_repository,
projects::list_projects,
projects::set_project_active,
projects::open_project_in_window,
Expand Down
Loading
Loading