Skip to content

Commit 2c4868e

Browse files
CopilotByron
authored andcommitted
Offer to initialise a repository when adding a simple directory as project
Co-authored-by: Byron <[email protected]>
1 parent 55d85c9 commit 2c4868e

File tree

6 files changed

+98
-3
lines changed

6 files changed

+98
-3
lines changed

apps/desktop/src/lib/error/knownErrors.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ export enum Code {
88
ProjectMissing = 'errors.projects.missing',
99
SecretKeychainNotFound = 'errors.secret.keychain_notfound',
1010
MissingLoginKeychain = 'errors.secret.missing_login_keychain',
11-
GitHubTokenExpired = 'errors.github.expired_token'
11+
GitHubTokenExpired = 'errors.github.expired_token',
12+
NonGitRepository = 'errors.projects.not_git_repository'
1213
}
1314

1415
export const KNOWN_ERRORS: Record<string, string> = {
@@ -34,5 +35,8 @@ With \`seahorse\` or equivalent, create a \`Login\` password store, right click
3435
`,
3536
[Code.GitHubTokenExpired]: `
3637
Your GitHub token appears expired, please check your settings!
38+
`,
39+
[Code.NonGitRepository]: `
40+
The selected directory is not a Git repository. Would you like to initialize a Git repository in this directory?
3741
`
3842
};

apps/desktop/src/lib/project/projectsService.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,10 @@ export class ProjectsService {
137137
unsetLastOpenedProject() {
138138
this.persistedId.set(undefined);
139139
}
140+
141+
async initGitRepository(path: string): Promise<void> {
142+
return await this.backend.invoke('init_git_repository', { path });
143+
}
140144
}
141145

142146
function injectEndpoints(api: ClientState['backendApi']) {

apps/desktop/src/routes/[projectId]/+layout.svelte

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import ReduxResult from '$components/ReduxResult.svelte';
1313
import { OnboardingEvent, POSTHOG_WRAPPER } from '$lib/analytics/posthog';
1414
import { BACKEND } from '$lib/backend';
15+
import { getUserErrorCode, Code } from '$lib/backend/ipc';
1516
import { BASE_BRANCH_SERVICE } from '$lib/baseBranch/baseBranchService.svelte';
1617
import { BRANCH_SERVICE } from '$lib/branches/branchService.svelte';
1718
import { SETTINGS_SERVICE } from '$lib/config/appSettingsV2';
@@ -22,7 +23,7 @@
2223
import { GITLAB_STATE } from '$lib/forge/gitlab/gitlabState.svelte';
2324
import { GIT_SERVICE } from '$lib/git/gitService';
2425
import { MODE_SERVICE } from '$lib/mode/modeService';
25-
import { showError, showInfo, showWarning } from '$lib/notifications/toasts';
26+
import { showError, showInfo, showWarning, showToast } from '$lib/notifications/toasts';
2627
import { PROJECTS_SERVICE } from '$lib/project/projectsService';
2728
import { FILE_SELECTION_MANAGER } from '$lib/selection/fileSelectionManager.svelte';
2829
import { UNCOMMITTED_SERVICE } from '$lib/selection/uncommittedService.svelte';
@@ -320,7 +321,44 @@
320321
}
321322
} catch (error: unknown) {
322323
posthog.captureOnboarding(OnboardingEvent.SetProjectActiveFailed);
323-
showError('Failed to set the project active', error);
324+
325+
// Check if this is a non-git repository error
326+
const errorCode = getUserErrorCode(error);
327+
if (errorCode === Code.NonGitRepository) {
328+
// Show special toast with git init option
329+
showToast({
330+
title: 'Not a Git repository',
331+
message: 'The selected directory is not a Git repository. Would you like to initialize one?',
332+
style: 'warning',
333+
extraAction: {
334+
label: 'Initialize Repository',
335+
onClick: async (dismiss) => {
336+
try {
337+
const currentProject = projects?.find((p) => p.id === projectId);
338+
if (currentProject?.path) {
339+
await projectsService.initGitRepository(currentProject.path);
340+
dismiss();
341+
showToast({
342+
title: 'Repository Initialized',
343+
message: `Git repository has been successfully initialized at ${currentProject.path}. Loading project...`,
344+
style: 'info'
345+
});
346+
// Retry setting the project active
347+
await setActiveProjectOrRedirect(projectId);
348+
} else {
349+
throw new Error('Could not find project path');
350+
}
351+
} catch (initError) {
352+
console.error('Failed to initialize repository:', initError);
353+
dismiss();
354+
showError('Failed to initialize repository', initError);
355+
}
356+
}
357+
}
358+
});
359+
} else {
360+
showError('Failed to set the project active', error);
361+
}
324362
}
325363
}
326364

crates/gitbutler-error/src/error.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,10 @@ pub enum Code {
139139
SecretKeychainNotFound,
140140
MissingLoginKeychain,
141141
GitForcePushProtection,
142+
/// When trying to open a project that is not in a git repository,
143+
/// this error code is used to trigger special handling in the frontend.
144+
/// The frontend will show a modal offering to initialize a git repository.
145+
NonGitRepository,
142146
}
143147

144148
impl std::fmt::Display for Code {
@@ -157,6 +161,7 @@ impl std::fmt::Display for Code {
157161
Code::SecretKeychainNotFound => "errors.secret.keychain_notfound",
158162
Code::MissingLoginKeychain => "errors.secret.missing_login_keychain",
159163
Code::GitForcePushProtection => "errors.git.force_push_protection",
164+
Code::NonGitRepository => "errors.projects.not_git_repository",
160165
};
161166
f.write_str(code)
162167
}

crates/gitbutler-tauri/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,7 @@ fn main() {
227227
projects::list_projects,
228228
projects::set_project_active,
229229
projects::open_project_in_window,
230+
projects::init_git_repository,
230231
repo::git_get_local_config,
231232
repo::git_set_local_config,
232233
repo::check_signing_settings,

crates/gitbutler-tauri/src/projects.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,14 @@ pub fn set_project_active(
6969
let err = anyhow::Error::from(err);
7070
if code == git2::ErrorCode::Owner {
7171
err.context(gitbutler_error::error::Code::RepoOwnership)
72+
} else if code == git2::ErrorCode::NotFound ||
73+
code == git2::ErrorCode::Invalid ||
74+
code == git2::ErrorCode::Config {
75+
// Common error codes when a directory is not a git repository:
76+
// - NotFound: .git directory or repository not found
77+
// - Invalid: Invalid repository structure
78+
// - Config: Repository configuration issues
79+
err.context(gitbutler_error::error::Code::NonGitRepository)
7280
} else {
7381
err
7482
}
@@ -212,3 +220,38 @@ Ensure these aren't touched by GitButler or avoid using it in this repository.",
212220
}
213221
Ok(Some(msg))
214222
}
223+
224+
/// Initialize a Git repository at the given path
225+
#[tauri::command]
226+
#[instrument(err(Debug))]
227+
pub fn init_git_repository(path: String) -> Result<(), Error> {
228+
let repo_path = std::path::Path::new(&path);
229+
230+
// Validate path
231+
if !repo_path.exists() {
232+
return Err(anyhow::anyhow!("Path does not exist: {}", path).into());
233+
}
234+
if !repo_path.is_dir() {
235+
return Err(anyhow::anyhow!("Path is not a directory: {}", path).into());
236+
}
237+
238+
// Check if it's already a git repository
239+
if git2::Repository::open(repo_path).is_ok() {
240+
return Err(anyhow::anyhow!("Directory is already a Git repository").into());
241+
}
242+
243+
// Check if directory is writable
244+
let temp_file = repo_path.join(".gitbutler_write_test");
245+
if let Err(_) = std::fs::write(&temp_file, "test") {
246+
return Err(anyhow::anyhow!("Directory is not writable: {}", path).into());
247+
}
248+
// Clean up test file
249+
let _ = std::fs::remove_file(&temp_file);
250+
251+
// Initialize the repository
252+
git2::Repository::init(repo_path)
253+
.with_context(|| format!("Failed to initialize Git repository at {}", path))?;
254+
255+
tracing::info!("Successfully initialized Git repository at {}", path);
256+
Ok(())
257+
}

0 commit comments

Comments
 (0)