diff --git a/Cargo.lock b/Cargo.lock index 8cef3bca6f..9c1ea2bb68 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -786,7 +786,6 @@ dependencies = [ "gitbutler-oplog", "gitbutler-oxidize", "gitbutler-project", - "gitbutler-repo", "gitbutler-serde", "gitbutler-stack", "gix", @@ -1404,7 +1403,6 @@ dependencies = [ "anyhow", "bstr", "but-core", - "but-db", "but-graph", "but-rebase", "but-status", @@ -1421,7 +1419,6 @@ dependencies = [ "gix-testtools", "insta", "serde", - "serde_json", "tracing", "uuid", ] @@ -3638,9 +3635,7 @@ dependencies = [ "serde_json", "strum", "tempfile", - "tokio", "tracing", - "uuid", ] [[package]] diff --git a/apps/desktop/src/lib/project/project.ts b/apps/desktop/src/lib/project/project.ts index 0c90916289..272f552395 100644 --- a/apps/desktop/src/lib/project/project.ts +++ b/apps/desktop/src/lib/project/project.ts @@ -16,6 +16,7 @@ export type Project = { title: string; description?: string; path: string; + git_dir?: string; api?: CloudProject & { sync: boolean; sync_code: boolean | undefined; diff --git a/crates/but-api/src/commands/claude.rs b/crates/but-api/src/commands/claude.rs index 845077a441..1d08ed4c44 100644 --- a/crates/but-api/src/commands/claude.rs +++ b/crates/but-api/src/commands/claude.rs @@ -72,10 +72,11 @@ pub async fn claude_get_session_details( let session_id = uuid::Uuid::parse_str(&session_id).map_err(anyhow::Error::from)?; let session = but_claude::db::get_session_by_id(&mut ctx, session_id)? .context("Could not find session")?; - let current_id = Transcript::current_valid_session_id(&project.path, &session).await?; + let worktree_dir = project.worktree_dir()?; + let current_id = Transcript::current_valid_session_id(worktree_dir, &session).await?; if let Some(current_id) = current_id { let transcript_path = - but_claude::Transcript::get_transcript_path(&project.path, current_id)?; + but_claude::Transcript::get_transcript_path(worktree_dir, current_id)?; let transcript = but_claude::Transcript::from_file(&transcript_path)?; Ok(but_claude::ClaudeSessionDetails { summary: transcript.summary(), @@ -204,8 +205,9 @@ pub fn claude_maybe_create_prompt_dir(project_id: ProjectId, path: String) -> Re #[instrument(err(Debug))] pub async fn claude_get_mcp_config(project_id: ProjectId) -> Result { let project = gitbutler_project::get(project_id)?; - let settings = ClaudeSettings::open(&project.path).await; - let mcp_config = ClaudeMcpConfig::open(&settings, &project.path).await; + let worktree_dir = project.worktree_dir()?; + let settings = ClaudeSettings::open(worktree_dir).await; + let mcp_config = ClaudeMcpConfig::open(&settings, worktree_dir).await; Ok(mcp_config.mcp_servers()) } @@ -215,7 +217,8 @@ pub async fn claude_get_sub_agents( project_id: ProjectId, ) -> Result, Error> { let project = gitbutler_project::get(project_id)?; - let sub_agents = but_claude::claude_sub_agents::read_claude_sub_agents(&project.path).await; + let sub_agents = + but_claude::claude_sub_agents::read_claude_sub_agents(project.worktree_dir()?).await; Ok(sub_agents) } @@ -229,7 +232,7 @@ pub async fn claude_verify_path(project_id: ProjectId, path: String) -> Result Result { - but_core::open_repo(gitbutler_project::get(project_id)?.path)? + gitbutler_project::get(project_id)? + .open()? .git_settings() .map(Into::into) .map_err(Into::into) @@ -22,7 +23,8 @@ pub fn get_gb_config(project_id: ProjectId) -> Result #[cfg_attr(feature = "tauri", tauri::command(async))] #[instrument(err(Debug))] pub fn set_gb_config(project_id: ProjectId, config: GitConfigSettings) -> Result<(), Error> { - but_core::open_repo(gitbutler_project::get(project_id)?.path)? + gitbutler_project::get(project_id)? + .open()? .set_git_settings(&config.into()) .map_err(Into::into) } @@ -35,7 +37,7 @@ pub fn store_author_globally_if_unset( name: String, email: String, ) -> Result<(), Error> { - let repo = but_core::open_repo(gitbutler_project::get(project_id)?.path)?; + let repo = gitbutler_project::get(project_id)?.open()?; but_rebase::commit::save_author_if_unset_in_repo( &repo, gix::config::Source::User, @@ -61,7 +63,7 @@ pub struct AuthorInfo { #[instrument(err(Debug))] /// Return the Git author information as the project repository would see it. pub fn get_author_info(project_id: ProjectId) -> Result { - let repo = but_core::open_repo(gitbutler_project::get(project_id)?.path)?; + let repo = gitbutler_project::get(project_id)?.open()?; let (name, email) = repo .author() .transpose() diff --git a/crates/but-api/src/commands/diff.rs b/crates/but-api/src/commands/diff.rs index f1dc20e718..b3e203094e 100644 --- a/crates/but-api/src/commands/diff.rs +++ b/crates/but-api/src/commands/diff.rs @@ -32,7 +32,7 @@ pub fn tree_change_diffs( let change: but_core::TreeChange = change.into(); let project = gitbutler_project::get(project_id)?; let app_settings = AppSettings::load_from_default_path_creating()?; - let repo = gix::open(project.path).map_err(anyhow::Error::from)?; + let repo = project.open()?; Ok(change.unified_diff(&repo, app_settings.context_lines)?) } @@ -53,11 +53,11 @@ pub fn commit_details( commit_id: HexHash, ) -> anyhow::Result { let project = gitbutler_project::get(project_id)?; - let repo = &gix::open(&project.path).context("Failed to open repo")?; + let repo = project.open()?; let commit = repo .find_commit(commit_id) .context("Failed for find commit")?; - let changes = but_core::diff::ui::commit_changes_by_worktree_dir(repo, commit_id.into())?; + let changes = but_core::diff::ui::commit_changes_by_worktree_dir(&repo, commit_id.into())?; let conflict_entries = Commit::from_id(commit.id())?.conflict_entries()?; Ok(CommitDetails { commit: commit.try_into()?, @@ -121,7 +121,7 @@ pub fn changes_in_worktree(project_id: ProjectId) -> anyhow::Result Result, Error> { let project = gitbutler_project::get_validated(project_id)?; - Ok(available_review_templates(&project.path, &forge)) + Ok(available_review_templates(project.worktree_dir()?, &forge)) } #[api_cmd] @@ -39,7 +37,7 @@ pub fn pr_template( if !is_valid_review_template_path(&relative_path) { return Err(anyhow::format_err!( "Invalid review template path: {:?}", - Path::join(&project.path, &relative_path) + project.worktree_dir()?.join(relative_path), ) .into()); } diff --git a/crates/but-api/src/commands/git.rs b/crates/but-api/src/commands/git.rs index c673836d05..9d088a25da 100644 --- a/crates/but-api/src/commands/git.rs +++ b/crates/but-api/src/commands/git.rs @@ -69,7 +69,9 @@ pub fn git_index_size(project_id: ProjectId) -> Result { #[cfg_attr(feature = "tauri", tauri::command(async))] #[instrument(err(Debug))] pub fn delete_all_data() -> Result<(), Error> { - for project in gitbutler_project::list().context("failed to list projects")? { + for project in gitbutler_project::dangerously_list_without_migration() + .context("failed to list projects")? + { gitbutler_project::delete(project.id) .map_err(|err| err.context("failed to delete project"))?; } diff --git a/crates/but-api/src/commands/virtual_branches.rs b/crates/but-api/src/commands/virtual_branches.rs index cb98a34c1d..50c27957bd 100644 --- a/crates/but-api/src/commands/virtual_branches.rs +++ b/crates/but-api/src/commands/virtual_branches.rs @@ -257,7 +257,10 @@ pub fn unapply_stack(project_id: ProjectId, stack_id: StackId) -> Result<(), Err let (assignments, _) = but_hunk_assignment::assignments_with_fallback( ctx, false, - Some(but_core::diff::ui::worktree_changes_by_worktree_dir(project.path)?.changes), + Some( + but_core::diff::ui::worktree_changes_by_worktree_dir(project.worktree_dir()?.into())? + .changes, + ), None, )?; let assigned_diffspec = but_workspace::flatten_diff_specs( diff --git a/crates/but-api/src/commands/workspace.rs b/crates/but-api/src/commands/workspace.rs index 1ce95a52dc..9d7a190b21 100644 --- a/crates/but-api/src/commands/workspace.rs +++ b/crates/but-api/src/commands/workspace.rs @@ -329,7 +329,7 @@ pub fn amend_commit_from_worktree_changes( ) -> Result { let project = gitbutler_project::get(project_id)?; let mut guard = project.exclusive_worktree_access(); - let repo = but_core::open_repo_for_merging(project.worktree_path())?; + let repo = project.open_for_merging()?; let app_settings = AppSettings::load_from_default_path_creating()?; let outcome = commit_engine::create_commit_and_update_refs_with_project( &repo, @@ -364,7 +364,7 @@ pub fn discard_worktree_changes( worktree_changes: Vec, ) -> Result, Error> { let project = gitbutler_project::get(project_id)?; - let repo = but_core::open_repo(project.worktree_path())?; + let repo = project.open()?; let ctx = CommandContext::open(&project, AppSettings::load_from_default_path_creating()?)?; let mut guard = project.exclusive_worktree_access(); diff --git a/crates/but-claude/src/bridge.rs b/crates/but-claude/src/bridge.rs index 892292a58b..2aac849301 100644 --- a/crates/but-claude/src/bridge.rs +++ b/crates/but-claude/src/bridge.rs @@ -253,7 +253,7 @@ impl Claudes { writer, write_stderr, session, - project.path.clone(), + project.worktree_dir()?.to_owned(), ctx.clone(), user_params, summary_to_resume, diff --git a/crates/but-claude/src/compact.rs b/crates/but-claude/src/compact.rs index 1c63ec324a..2e610ab80f 100644 --- a/crates/but-claude/src/compact.rs +++ b/crates/but-claude/src/compact.rs @@ -234,7 +234,7 @@ pub async fn generate_summary( let app_settings = ctx.lock().await.app_settings().clone(); let claude_executable = app_settings.claude.executable.clone(); let session_id = - Transcript::current_valid_session_id(&ctx.lock().await.project().path, session) + Transcript::current_valid_session_id(ctx.lock().await.project().worktree_dir()?, session) .await? .context("Cant find current session id")?; @@ -248,7 +248,7 @@ pub async fn generate_summary( command.creation_flags(CREATE_NO_WINDOW); } - command.current_dir(&ctx.lock().await.project().path); + command.current_dir(ctx.lock().await.project().worktree_dir()?); command.args(["--resume", &format!("{session_id}")]); command.arg("-p"); command.arg(SUMMARY_PROMPT); diff --git a/crates/but-claude/src/hooks/mod.rs b/crates/but-claude/src/hooks/mod.rs index 8ae0688703..3c06bb0a00 100644 --- a/crates/but-claude/src/hooks/mod.rs +++ b/crates/but-claude/src/hooks/mod.rs @@ -103,8 +103,10 @@ pub async fn handle_stop() -> anyhow::Result { .ok_or(anyhow!("No worktree found for repo"))?, )?; - let changes = - but_core::diff::ui::worktree_changes_by_worktree_dir(project.clone().path)?.changes; + let changes = but_core::diff::ui::worktree_changes_by_worktree_dir( + project.clone().worktree_dir()?.into(), + )? + .changes; // This is a naive way of handling this case. // If the user simply asks a question and there are no changes, we don't need to create a stack @@ -328,7 +330,7 @@ pub fn handle_pre_tool_call() -> anyhow::Result { .ok_or(anyhow!("No worktree found for repo"))?, )?; let relative_file_path = std::path::PathBuf::from(&input.tool_input.file_path) - .strip_prefix(project.path.clone())? + .strip_prefix(project.worktree_dir()?)? .to_string_lossy() .to_string(); input.tool_input.file_path = relative_file_path; @@ -374,7 +376,7 @@ pub fn handle_post_tool_call() -> anyhow::Result { )?; let relative_file_path = std::path::PathBuf::from(&input.tool_response.file_path) - .strip_prefix(project.path.clone())? + .strip_prefix(project.worktree_dir()?)? .to_string_lossy() .to_string(); input.tool_response.file_path = relative_file_path.clone(); @@ -404,7 +406,8 @@ pub fn handle_post_tool_call() -> anyhow::Result { let stack_id = get_or_create_session(defer.ctx, &session_id, stacks, vb_state)?; let changes = - but_core::diff::ui::worktree_changes_by_worktree_dir(project.path.clone())?.changes; + but_core::diff::ui::worktree_changes_by_worktree_dir(project.worktree_dir()?.into())? + .changes; let (assignments, _assignments_error) = but_hunk_assignment::assignments_with_fallback( defer.ctx, true, diff --git a/crates/but-claude/src/prompt_templates.rs b/crates/but-claude/src/prompt_templates.rs index 9b2cc16d32..c3bf63f3f4 100644 --- a/crates/but-claude/src/prompt_templates.rs +++ b/crates/but-claude/src/prompt_templates.rs @@ -42,10 +42,10 @@ pub struct PromptDir { /// Fetch the directories where we look up the user provided templates. /// -/// We want the precidence to be Global < Project < Project Local +/// We want the precedence to be Global < Project < Project Local /// -/// As such, items last in the array take precidence, and filters last in the -/// filters list also take precidence over earlier ones. +/// As such, items last in the array take precedence, and filters last in the +/// filters list also take precedence over earlier ones. /// /// The point of labeling these dirs is so we can also display where to find /// these directories in the frontend. @@ -58,7 +58,7 @@ pub fn prompt_dirs(project: &Project) -> Result> { }, PromptDir { label: "Project".into(), - path: project.path.join(".gitbutler/prompt-templates"), + path: project.gb_dir().join("prompt-templates"), filters: vec![".md".into(), ".local.md".into()], }, ]) @@ -124,7 +124,7 @@ pub fn maybe_create_dir(project: &Project, path: &str) -> Result<()> { let path = if path.is_absolute() { path } else { - &project.path.join(path) + &project.worktree_dir()?.join(path) }; if path.try_exists()? { diff --git a/crates/but-cursor/src/lib.rs b/crates/but-cursor/src/lib.rs index cb31383aa6..d6edf464ab 100644 --- a/crates/but-cursor/src/lib.rs +++ b/crates/but-cursor/src/lib.rs @@ -136,7 +136,8 @@ pub async fn handle_after_edit() -> anyhow::Result { but_claude::hooks::get_or_create_session(ctx, &input.conversation_id, stacks, vb_state)?; let changes = - but_core::diff::ui::worktree_changes_by_worktree_dir(project.path.clone())?.changes; + but_core::diff::ui::worktree_changes_by_worktree_dir(project.worktree_dir()?.into())? + .changes; let (assignments, _assignments_error) = but_hunk_assignment::assignments_with_fallback(ctx, true, Some(changes.clone()), None)?; @@ -186,7 +187,8 @@ pub async fn handle_stop(nightly: bool) -> anyhow::Result { )?; let changes = - but_core::diff::ui::worktree_changes_by_worktree_dir(project.clone().path)?.changes; + but_core::diff::ui::worktree_changes_by_worktree_dir(project.worktree_dir()?.into())? + .changes; if changes.is_empty() { return Ok(CursorHookOutput::default()); diff --git a/crates/but-feedback/src/lib.rs b/crates/but-feedback/src/lib.rs index 7cc0915103..d3937eca5a 100644 --- a/crates/but-feedback/src/lib.rs +++ b/crates/but-feedback/src/lib.rs @@ -26,7 +26,10 @@ impl Archival { let output_file = self .cache_dir .join(format!("project-{date}.zip", date = filesafe_date_time())); - create_zip_file_from_dir(project.path, output_file) + create_zip_file_from_dir( + project.worktree_dir().unwrap_or(project.git_dir()), + output_file, + ) } /// Create an archive commit graph behind `project_id` such that it doesn't reveal PII. diff --git a/crates/but-hunk-assignment/src/lib.rs b/crates/but-hunk-assignment/src/lib.rs index 50f38bea09..622630d839 100644 --- a/crates/but-hunk-assignment/src/lib.rs +++ b/crates/but-hunk-assignment/src/lib.rs @@ -254,7 +254,7 @@ pub fn assign( } else { &hunk_dependencies_for_workspace_changes_by_worktree_dir( ctx, - &ctx.project().path, + ctx.project().worktree_dir()?, &ctx.project().gb_dir(), None, )? @@ -344,7 +344,7 @@ pub fn assignments_with_fallback( } else { &hunk_dependencies_for_workspace_changes_by_worktree_dir( ctx, - &ctx.project().path, + ctx.project().worktree_dir()?, &ctx.project().gb_dir(), Some(worktree_changes.clone()), )? diff --git a/crates/but-hunk-dependency/tests/hunk_dependency/ui.rs b/crates/but-hunk-dependency/tests/hunk_dependency/ui.rs index e0f5b04a4d..f9a1f5ea28 100644 --- a/crates/but-hunk-dependency/tests/hunk_dependency/ui.rs +++ b/crates/but-hunk-dependency/tests/hunk_dependency/ui.rs @@ -309,7 +309,7 @@ mod util { )?; Ok(TestContext { - repo: gix::open_opts(&ctx.project().path, gix::open::Options::isolated())?, + repo: ctx.project().open_isolated()?, gitbutler_dir: ctx.project().gb_dir(), stacks_entries: stacks, }) diff --git a/crates/but-hunk-dependency/tests/hunk_dependency/workspace_dependencies.rs b/crates/but-hunk-dependency/tests/hunk_dependency/workspace_dependencies.rs index c9865fde27..5a6240992f 100644 --- a/crates/but-hunk-dependency/tests/hunk_dependency/workspace_dependencies.rs +++ b/crates/but-hunk-dependency/tests/hunk_dependency/workspace_dependencies.rs @@ -898,7 +898,7 @@ mod util { let handle = VirtualBranchesHandle::new(ctx.project().gb_dir()); Ok(TestContext { - repo: gix::open_opts(&ctx.project().path, gix::open::Options::isolated())?, + repo: ctx.project().open_isolated()?, stacks_entries: stacks, common_merge_base: handle.get_default_target()?.sha.to_gix(), }) diff --git a/crates/but-rules/src/handler.rs b/crates/but-rules/src/handler.rs index 876364c98b..6a14f24be0 100644 --- a/crates/but-rules/src/handler.rs +++ b/crates/but-rules/src/handler.rs @@ -82,7 +82,7 @@ fn handle_amend( let changes: Vec = assignments.into_iter().map(|a| a.into()).collect(); let project = ctx.project(); let mut guard = project.exclusive_worktree_access(); - let repo = but_core::open_repo_for_merging(project.worktree_path())?; + let repo = project.open_for_merging()?; let meta = VirtualBranchesTomlMetadata::from_path( ctx.project().gb_dir().join("virtual_branches.toml"), diff --git a/crates/but-rules/src/lib.rs b/crates/but-rules/src/lib.rs index 32e7fb40c3..224881e380 100644 --- a/crates/but-rules/src/lib.rs +++ b/crates/but-rules/src/lib.rs @@ -305,7 +305,7 @@ pub fn process_rules(ctx: &mut CommandContext) -> anyhow::Result<()> { let dependencies = hunk_dependencies_for_workspace_changes_by_worktree_dir( ctx, - &ctx.project().path, + ctx.project().worktree_dir()?, &ctx.project().gb_dir(), Some(wt_changes.changes.clone()), )?; diff --git a/crates/but-server/src/projects.rs b/crates/but-server/src/projects.rs index 085cbf9791..2ed73cdf92 100644 --- a/crates/but-server/src/projects.rs +++ b/crates/but-server/src/projects.rs @@ -77,7 +77,7 @@ impl ActiveProjects { let watcher = gitbutler_watcher::watch_in_background( handler, - project.worktree_path(), + project.worktree_dir()?, project.id, app_settings_sync, )?; @@ -101,14 +101,15 @@ pub struct ProjectInfo { pub async fn list_projects(extra: &Extra) -> Result { let active_projects = extra.active_projects.lock().await; // For server implementation, we don't have window state, so all projects are marked as not open - let projects_for_frontend = - gitbutler_project::assure_app_can_startup_or_fix_it(gitbutler_project::list())? - .into_iter() - .map(|project| ProjectForFrontend { - is_open: active_projects.projects.contains_key(&project.id), - inner: project.into(), - }) - .collect::>(); + let projects_for_frontend = gitbutler_project::assure_app_can_startup_or_fix_it( + gitbutler_project::dangerously_list_without_migration(), + )? + .into_iter() + .map(|project| ProjectForFrontend { + is_open: active_projects.projects.contains_key(&project.id), + inner: project.into(), + }) + .collect::>(); Ok(json!(projects_for_frontend)) } @@ -139,7 +140,7 @@ pub async fn set_project_active( })) } -#[derive(serde::Deserialize, serde::Serialize)] +#[derive(serde::Serialize)] pub struct ProjectForFrontend { #[serde(flatten)] pub inner: gitbutler_project::api::Project, diff --git a/crates/but-testing/src/command/diff.rs b/crates/but-testing/src/command/diff.rs index c5a7334331..a49631fbba 100644 --- a/crates/but-testing/src/command/diff.rs +++ b/crates/but-testing/src/command/diff.rs @@ -76,7 +76,7 @@ fn handle_normal_diff(worktree: but_core::WorktreeChanges, use_json: bool) -> an pub fn locks(current_dir: &Path, simple: bool, use_json: bool) -> anyhow::Result<()> { let project = project_from_path(current_dir)?; let ctx = CommandContext::open(&project, AppSettings::default())?; - let repo = but_core::open_repo(project.worktree_path())?; + let repo = project.open()?; let worktree_changes = but_core::diff::worktree_changes(&repo)?; let input_stacks = but_hunk_dependency::workspace_stacks_to_input_stacks( &repo, diff --git a/crates/but-testing/src/command/mod.rs b/crates/but-testing/src/command/mod.rs index ad987628a6..9a1f755d7b 100644 --- a/crates/but-testing/src/command/mod.rs +++ b/crates/but-testing/src/command/mod.rs @@ -35,10 +35,7 @@ pub fn project_from_path(path: &Path) -> anyhow::Result { pub fn project_repo(path: &Path) -> anyhow::Result { let project = project_from_path(path)?; - configured_repo( - gix::open(project.worktree_path())?, - RepositoryOpenMode::General, - ) + configured_repo(project.open()?, RepositoryOpenMode::General) } pub enum RepositoryOpenMode { @@ -77,9 +74,7 @@ pub fn repo_and_maybe_project( let work_dir = gix::path::realpath(work_dir)?; ( repo, - gitbutler_project::list()? - .into_iter() - .find(|p| p.path == work_dir), + gitbutler_project::Project::find_by_worktree_dir(&work_dir).ok(), ) } else { (repo, None) diff --git a/crates/but-workspace/src/commit_engine/mod.rs b/crates/but-workspace/src/commit_engine/mod.rs index a2e91ece5e..19f7b37009 100644 --- a/crates/but-workspace/src/commit_engine/mod.rs +++ b/crates/but-workspace/src/commit_engine/mod.rs @@ -705,7 +705,7 @@ pub fn create_commit_simple( stack_branch_name: String, perm: &mut WorktreeWritePermission, ) -> anyhow::Result { - let repo = but_core::open_repo_for_merging(ctx.project().worktree_path())?; + let repo = ctx.project().open_for_merging()?; // If parent_id was not set but a stack branch name was provided, pick the current head of that branch as parent. let parent_commit_id: Option = match parent_id { Some(id) => Some(id), diff --git a/crates/but-worktrees/Cargo.toml b/crates/but-worktrees/Cargo.toml index 37bfe1f9d9..b7c4bc8651 100644 --- a/crates/but-worktrees/Cargo.toml +++ b/crates/but-worktrees/Cargo.toml @@ -10,7 +10,6 @@ doctest = false [dependencies] anyhow.workspace = true -but-db.workspace = true but-workspace.workspace = true but-graph.workspace = true but-status.workspace = true @@ -24,7 +23,6 @@ gitbutler-oxidize.workspace = true gitbutler-workspace.workspace = true gitbutler-branch-actions.workspace = true serde.workspace = true -serde_json.workspace = true uuid.workspace = true bstr.workspace = true tracing.workspace = true diff --git a/crates/but-worktrees/src/destroy.rs b/crates/but-worktrees/src/destroy.rs index d5876bdf9d..f11c822f64 100644 --- a/crates/but-worktrees/src/destroy.rs +++ b/crates/but-worktrees/src/destroy.rs @@ -19,7 +19,7 @@ pub fn worktree_destroy_by_id( id: &WorktreeId, ) -> Result { // Remove the git worktree (force=true to handle uncommitted changes) - git_worktree_remove(&ctx.project().path, id, true)?; + git_worktree_remove(&ctx.project().common_git_dir()?, id, true)?; Ok(DestroyWorktreeOutcome { destroyed_ids: vec![id.clone()], @@ -52,7 +52,7 @@ pub fn worktree_destroy_by_reference( // Destroy each matching worktree for worktree in worktrees_to_destroy { // Remove the git worktree (force=true to handle uncommitted changes) - git_worktree_remove(&ctx.project().path, &worktree.id, true)?; + git_worktree_remove(&ctx.project().common_git_dir()?, &worktree.id, true)?; destroyed_ids.push(worktree.id); } diff --git a/crates/but-worktrees/src/integrate.rs b/crates/but-worktrees/src/integrate.rs index bf8846209b..7a29ea6c76 100644 --- a/crates/but-worktrees/src/integrate.rs +++ b/crates/but-worktrees/src/integrate.rs @@ -77,7 +77,7 @@ pub fn worktree_integrate( let vb_state = VirtualBranchesHandle::new(ctx.project().gb_dir()); update_workspace_commit(&vb_state, ctx, false)?; - git_worktree_remove(&ctx.project().path, id, true)?; + git_worktree_remove(&ctx.project().common_git_dir()?, id, true)?; Ok(()) } diff --git a/crates/but-worktrees/src/new.rs b/crates/but-worktrees/src/new.rs index adcd3aff8f..6f72241d3a 100644 --- a/crates/but-worktrees/src/new.rs +++ b/crates/but-worktrees/src/new.rs @@ -44,7 +44,7 @@ pub fn worktree_new( gix::refs::PartialName::try_from(format!("gitbutler/worktree/{}", id.as_str()))?; git_worktree_add( - &ctx.project().path, + &ctx.project().common_git_dir()?, &path, branch_name.as_ref(), to_checkout.detach(), diff --git a/crates/but-worktrees/tests/worktree/integrate.rs b/crates/but-worktrees/tests/worktree/integrate.rs index 0019e35b08..ac52904883 100644 --- a/crates/but-worktrees/tests/worktree/integrate.rs +++ b/crates/but-worktrees/tests/worktree/integrate.rs @@ -177,7 +177,7 @@ fn test_causes_conflicts_above() -> anyhow::Result<()> { fn test_causes_workdir_conflicts_simple() -> anyhow::Result<()> { let test_ctx = test_ctx("stacked-branches")?; let mut ctx = test_ctx.ctx; - let path = ctx.project().path.clone(); + let path = ctx.project().worktree_dir()?.to_owned(); let mut guard = ctx.project().exclusive_worktree_access(); @@ -233,7 +233,7 @@ fn test_causes_workdir_conflicts_simple() -> anyhow::Result<()> { fn test_causes_workdir_conflicts_complex() -> anyhow::Result<()> { let test_ctx = test_ctx("stacked-branches")?; let mut ctx = test_ctx.ctx; - let path = ctx.project().path.clone(); + let path = ctx.project().worktree_dir()?.to_owned(); let mut guard = ctx.project().exclusive_worktree_access(); diff --git a/crates/but/Cargo.toml b/crates/but/Cargo.toml index 029d742931..3ec5f96ce6 100644 --- a/crates/but/Cargo.toml +++ b/crates/but/Cargo.toml @@ -31,7 +31,6 @@ rmcp.workspace = true command-group = { version = "5.0.1", features = ["with-tokio"] } sysinfo = "0.37.1" gitbutler-project.workspace = true -gitbutler-repo.workspace = true gix.workspace = true but-core.workspace = true but-api.workspace = true diff --git a/crates/but/src/main.rs b/crates/but/src/main.rs index fbe9c67e6d..a2dec9e000 100644 --- a/crates/but/src/main.rs +++ b/crates/but/src/main.rs @@ -274,11 +274,11 @@ fn get_or_init_project( ) -> anyhow::Result { let repo = gix::discover(current_dir)?; if let Some(path) = repo.workdir() { - let project = match gitbutler_project::Project::find_by_path(path) { + let project = match gitbutler_project::Project::find_by_worktree_dir(path) { Ok(p) => Ok(p), Err(_e) => { crate::init::repo(path, false, false)?; - gitbutler_project::Project::find_by_path(path) + gitbutler_project::Project::find_by_worktree_dir(path) } }?; Ok(project) diff --git a/crates/but/src/mcp_internal/project.rs b/crates/but/src/mcp_internal/project.rs index eddc2127d8..df1a3ed5ac 100644 --- a/crates/but/src/mcp_internal/project.rs +++ b/crates/but/src/mcp_internal/project.rs @@ -9,10 +9,7 @@ pub fn project_from_path(path: &Path) -> anyhow::Result { pub fn project_repo(path: &Path) -> anyhow::Result { let project = project_from_path(path)?; - configured_repo( - gix::open(project.worktree_path())?, - RepositoryOpenMode::General, - ) + configured_repo(project.open()?, RepositoryOpenMode::General) } pub enum RepositoryOpenMode { // We'll need this later for the commit command @@ -49,9 +46,7 @@ pub fn repo_and_maybe_project( let work_dir = gix::path::realpath(work_dir)?; ( repo, - gitbutler_project::list()? - .into_iter() - .find(|p| p.path == work_dir), + gitbutler_project::Project::find_by_worktree_dir(&work_dir).ok(), ) } else { (repo, None) diff --git a/crates/but/src/rub/amend.rs b/crates/but/src/rub/amend.rs index aefaa84d70..be698a5ff9 100644 --- a/crates/but/src/rub/amend.rs +++ b/crates/but/src/rub/amend.rs @@ -68,7 +68,8 @@ pub(crate) fn assignments_to_commit( fn wt_assignments(ctx: &mut CommandContext) -> anyhow::Result> { let changes = - but_core::diff::ui::worktree_changes_by_worktree_dir(ctx.project().path.clone())?.changes; + but_core::diff::ui::worktree_changes_by_worktree_dir(ctx.project().worktree_dir()?.into())? + .changes; let (assignments, _assignments_error) = but_hunk_assignment::assignments_with_fallback(ctx, false, Some(changes.clone()), None)?; Ok(assignments) diff --git a/crates/but/src/rub/assign.rs b/crates/but/src/rub/assign.rs index 84b4d86389..56c1cd08dd 100644 --- a/crates/but/src/rub/assign.rs +++ b/crates/but/src/rub/assign.rs @@ -37,7 +37,8 @@ pub(crate) fn assign_all( // Get all assignment requests from the from_stack_id let changes = - but_core::diff::ui::worktree_changes_by_worktree_dir(ctx.project().path.clone())?.changes; + but_core::diff::ui::worktree_changes_by_worktree_dir(ctx.project().worktree_dir()?.into())? + .changes; let (assignments, _assignments_error) = but_hunk_assignment::assignments_with_fallback(ctx, false, Some(changes.clone()), None)?; @@ -107,7 +108,8 @@ fn to_assignment_request( let stack_id = branch_name_to_stack_id(ctx, branch_name)?; let changes = - but_core::diff::ui::worktree_changes_by_worktree_dir(ctx.project().path.clone())?.changes; + but_core::diff::ui::worktree_changes_by_worktree_dir(ctx.project().worktree_dir()?.into())? + .changes; let (assignments, _assignments_error) = but_hunk_assignment::assignments_with_fallback(ctx, false, Some(changes.clone()), None)?; let mut reqs = Vec::new(); diff --git a/crates/but/src/rub/mod.rs b/crates/but/src/rub/mod.rs index aa8663d3b3..cfc7e1119f 100644 --- a/crates/but/src/rub/mod.rs +++ b/crates/but/src/rub/mod.rs @@ -265,9 +265,9 @@ fn get_all_files_in_display_order(ctx: &mut CommandContext) -> anyhow::Result ColoredString { pub(crate) fn all_files(ctx: &mut CommandContext) -> anyhow::Result> { let changes = - but_core::diff::ui::worktree_changes_by_worktree_dir(ctx.project().path.clone())?.changes; + but_core::diff::ui::worktree_changes_by_worktree_dir(ctx.project().worktree_dir()?.into())? + .changes; let (assignments, _assignments_error) = but_hunk_assignment::assignments_with_fallback(ctx, false, Some(changes.clone()), None)?; let out = assignments diff --git a/crates/gitbutler-branch-actions/src/base.rs b/crates/gitbutler-branch-actions/src/base.rs index d9be89fe5a..fe5d92107a 100644 --- a/crates/gitbutler-branch-actions/src/base.rs +++ b/crates/gitbutler-branch-actions/src/base.rs @@ -113,7 +113,7 @@ pub(crate) fn set_base_branch( let sig = repo .signature() .unwrap_or(git2::Signature::now("Author", "author@email.com")?); - let mut r = git2::Repository::open(ctx.project().path.clone())?; + let mut r = ctx.project().open_git2()?; r.stash_save2(&sig, None, Some(git2::StashFlags::INCLUDE_UNTRACKED))?; } diff --git a/crates/gitbutler-branch-actions/src/file.rs b/crates/gitbutler-branch-actions/src/file.rs index 6f929d9953..b85a8a2416 100644 --- a/crates/gitbutler-branch-actions/src/file.rs +++ b/crates/gitbutler-branch-actions/src/file.rs @@ -119,7 +119,7 @@ pub(crate) fn list_virtual_commit_files( .find_real_tree(&parent, Default::default()) .context("failed to get parent tree")?; let diff = gitbutler_diff::trees(ctx.repo(), &parent_tree, &commit_tree, context_lines)?; - let hunks_by_filepath = virtual_hunks_by_file_diffs(&ctx.project().path, diff); + let hunks_by_filepath = virtual_hunks_by_file_diffs(ctx.project().worktree_dir()?, diff); Ok(virtual_hunks_into_virtual_files(hunks_by_filepath)) } diff --git a/crates/gitbutler-branch-actions/src/status.rs b/crates/gitbutler-branch-actions/src/status.rs index 8b5ea0b671..2d7b877c93 100644 --- a/crates/gitbutler-branch-actions/src/status.rs +++ b/crates/gitbutler-branch-actions/src/status.rs @@ -228,11 +228,11 @@ pub fn get_applied_status_cached( .context(format!("failed to write virtual branch {}", vbranch.name))?; } + let worktree_dir = ctx.project().worktree_dir()?; let hunks_by_branch: Vec<(Stack, HashMap>)> = hunks_by_branch .iter() .map(|(branch, hunks)| { - let hunks = - file_hunks_from_diffs(&ctx.project().path, hunks.clone(), Some(diff_dependencies)); + let hunks = file_hunks_from_diffs(worktree_dir, hunks.clone(), Some(diff_dependencies)); (branch.clone(), hunks) }) .collect(); diff --git a/crates/gitbutler-branch-actions/tests/hooks.rs b/crates/gitbutler-branch-actions/tests/hooks.rs index 0a4ee181f6..5098e6e2e4 100644 --- a/crates/gitbutler-branch-actions/tests/hooks.rs +++ b/crates/gitbutler-branch-actions/tests/hooks.rs @@ -1,9 +1,6 @@ #[cfg(test)] mod tests { - use std::{ - collections::HashMap, - path::{Path, PathBuf}, - }; + use std::{collections::HashMap, path::PathBuf}; use git2::{Repository, StatusOptions}; use gitbutler_branch_actions::hooks; @@ -59,7 +56,7 @@ if echo "$STAGED_DIFF" | grep -qE "^\+.*forbidden"; then fi "#; git2_hooks::create_hook(ctx.repo(), git2_hooks::HOOK_PRE_COMMIT, hook.as_bytes()); - std::fs::write(Path::new(&project.path).join("test.txt"), "forbidden\n")?; + std::fs::write(project.worktree_dir()?.join("test.txt"), "forbidden\n")?; // While we have changed a file to include the forbidden word, the hook should not // fail if we pass no ownership claims. These claims are used to select what hunks diff --git a/crates/gitbutler-branch-actions/tests/virtual_branches/oplog.rs b/crates/gitbutler-branch-actions/tests/virtual_branches/oplog.rs index 932a977162..24107f0415 100644 --- a/crates/gitbutler-branch-actions/tests/virtual_branches/oplog.rs +++ b/crates/gitbutler-branch-actions/tests/virtual_branches/oplog.rs @@ -291,7 +291,7 @@ fn restores_gitbutler_workspace() -> anyhow::Result<()> { let _commit1_id = gitbutler_branch_actions::create_commit(ctx, stack_entry.id, "commit one", None)?; - let repo = git2::Repository::open(&project.path)?; + let repo = project.open_git2()?; // check the workspace commit let head = repo.head().expect("never unborn"); diff --git a/crates/gitbutler-branch-actions/tests/virtual_branches/save_and_unapply_virtual_branch.rs b/crates/gitbutler-branch-actions/tests/virtual_branches/save_and_unapply_virtual_branch.rs index 482cd20a54..566acbd259 100644 --- a/crates/gitbutler-branch-actions/tests/virtual_branches/save_and_unapply_virtual_branch.rs +++ b/crates/gitbutler-branch-actions/tests/virtual_branches/save_and_unapply_virtual_branch.rs @@ -5,7 +5,7 @@ use gitbutler_testsupport::stack_details; use super::*; #[test] -fn unapply_with_data() { +fn unapply_with_data() -> anyhow::Result<()> { let Test { repo, ctx, .. } = &mut Test::default(); gitbutler_branch_actions::set_base_branch( @@ -27,9 +27,11 @@ fn unapply_with_data() { let stacks = stack_details(ctx); assert_eq!(stacks.len(), 1); - let changes = but_core::diff::ui::worktree_changes_by_worktree_dir(ctx.project().path.clone()) - .unwrap() - .changes; + let changes = but_core::diff::ui::worktree_changes_by_worktree_dir( + ctx.project().worktree_dir()?.to_owned(), + ) + .unwrap() + .changes; let (assignments, _assignments_error) = but_hunk_assignment::assignments_with_fallback(ctx, false, Some(changes.clone()), None) .unwrap(); @@ -56,6 +58,8 @@ fn unapply_with_data() { let stacks = stack_details(ctx); assert_eq!(stacks.len(), 0); + + Ok(()) } #[test] diff --git a/crates/gitbutler-branch-actions/tests/virtual_branches/undo_commit.rs b/crates/gitbutler-branch-actions/tests/virtual_branches/undo_commit.rs index 080336845e..ac9aeca954 100644 --- a/crates/gitbutler-branch-actions/tests/virtual_branches/undo_commit.rs +++ b/crates/gitbutler-branch-actions/tests/virtual_branches/undo_commit.rs @@ -44,7 +44,8 @@ fn undo_commit_simple() -> anyhow::Result<()> { // should be two uncommitted files now (file2.txt and file3.txt) let changes = - but_core::diff::ui::worktree_changes_by_worktree_dir(ctx.project().path.clone())?.changes; + but_core::diff::ui::worktree_changes_by_worktree_dir(ctx.project().worktree_dir()?.into())? + .changes; assert_eq!(changes.len(), 2); let (_, b) = stack_details(ctx) .into_iter() @@ -122,7 +123,8 @@ fn undo_commit_in_non_default_branch() -> anyhow::Result<()> { // should be two uncommitted files now (file2.txt and file3.txt) let changes = - but_core::diff::ui::worktree_changes_by_worktree_dir(ctx.project().path.clone())?.changes; + but_core::diff::ui::worktree_changes_by_worktree_dir(ctx.project().worktree_dir()?.into())? + .changes; assert_eq!(changes.len(), 2); let (_, b) = stack_details(ctx) diff --git a/crates/gitbutler-cli/src/command/project.rs b/crates/gitbutler-cli/src/command/project.rs index 03dc173d14..40285362cd 100644 --- a/crates/gitbutler-cli/src/command/project.rs +++ b/crates/gitbutler-cli/src/command/project.rs @@ -9,12 +9,12 @@ use gitbutler_reference::RemoteRefname; use crate::command::debug_print; pub fn list() -> Result<()> { - for project in gitbutler_project::list()? { + for project in gitbutler_project::dangerously_list_without_migration()? { println!( "{id} {name} {path}", id = project.id, name = project.title, - path = project.path.display() + path = project.worktree_dir()?.display() ); } Ok(()) diff --git a/crates/gitbutler-command-context/src/lib.rs b/crates/gitbutler-command-context/src/lib.rs index 99b182d079..20397b7556 100644 --- a/crates/gitbutler-command-context/src/lib.rs +++ b/crates/gitbutler-command-context/src/lib.rs @@ -59,7 +59,7 @@ impl DerefMut for VirtualBranchesTomlMetadataMut { impl CommandContext { /// Open the repository identified by `project` and perform some checks. pub fn open(project: &Project, app_settings: AppSettings) -> Result { - let repo = git2::Repository::open(&project.path)?; + let repo = project.open_git2()?; Self::open_from(project, app_settings, repo) } diff --git a/crates/gitbutler-diff/src/write.rs b/crates/gitbutler-diff/src/write.rs index 94e21ebf49..8042198ea0 100644 --- a/crates/gitbutler-diff/src/write.rs +++ b/crates/gitbutler-diff/src/write.rs @@ -57,7 +57,7 @@ where for (rel_path, hunks) in files { let rel_path = rel_path.borrow(); let hunks: Vec = hunks.borrow().iter().map(|h| h.clone().into()).collect(); - let full_path = ctx.project().worktree_path().join(rel_path); + let full_path = ctx.project().worktree_dir()?.join(rel_path); let is_submodule = full_path.is_dir() && hunks.len() == 1 @@ -118,7 +118,7 @@ where // if the link target is inside the project repository, make it relative let link_target = link_target - .strip_prefix(ctx.project().worktree_path()) + .strip_prefix(ctx.project().worktree_dir()?) .unwrap_or(&link_target); let blob_oid = git_repository.blob( diff --git a/crates/gitbutler-oplog/src/oplog.rs b/crates/gitbutler-oplog/src/oplog.rs index eb060699ef..f012107b55 100644 --- a/crates/gitbutler-oplog/src/oplog.rs +++ b/crates/gitbutler-oplog/src/oplog.rs @@ -176,9 +176,7 @@ impl OplogExt for CommandContext { oplog_commit_id: Option, exclude_kind: Vec, ) -> Result> { - let worktree_dir = self.project().path.as_path(); - let repo = gitbutler_command_context::gix_repo_for_merging(worktree_dir)?; - + let repo = self.project().open_for_merging()?; let traversal_root_id = git2_to_gix_object_id(match oplog_commit_id { Some(id) => id, None => { @@ -261,7 +259,7 @@ impl OplogExt for CommandContext { return Ok(false); } - let repo = git2::Repository::open(&self.project().path)?; + let repo = self.project().open_git2()?; if repo.workspace_ref_from_head().is_err() { return Ok(false); } @@ -269,9 +267,8 @@ impl OplogExt for CommandContext { } fn snapshot_diff(&self, sha: git2::Oid) -> Result> { - let worktree_dir = self.project().path.as_path(); - let gix_repo = gitbutler_command_context::gix_repo_for_merging(worktree_dir)?; - let repo = git2::Repository::init(worktree_dir)?; + let gix_repo = self.project().open_for_merging()?; + let repo = self.project().open_git2()?; let commit = repo.find_commit(sha)?; @@ -355,8 +352,7 @@ fn prepare_snapshot( ctx: &CommandContext, _shared_access: &WorktreeReadPermission, ) -> Result { - let worktree_dir = ctx.project().path.as_path(); - let repo = git2::Repository::open(worktree_dir)?; + let repo = ctx.project().open_git2()?; let vb_state = VirtualBranchesHandle::new(ctx.project().gb_dir()); @@ -370,7 +366,7 @@ fn prepare_snapshot( let vb_blob_id = repo.blob(&vb_content)?; // Create a tree out of the conflicts state if present - let conflicts_tree_id = write_conflicts_tree(worktree_dir, &repo)?; + let conflicts_tree_id = write_conflicts_tree(&repo)?; // write out the index as a tree to store let mut index = repo.index()?; @@ -480,15 +476,15 @@ fn prepare_snapshot( } fn commit_snapshot( - ctx: &Project, + project: &Project, snapshot_tree_id: git2::Oid, details: SnapshotDetails, _exclusive_access: &mut WorktreeWritePermission, ) -> Result { - let repo = git2::Repository::open(ctx.path.as_path())?; + let repo = project.open_git2()?; let snapshot_tree = repo.find_tree(snapshot_tree_id)?; - let oplog_state = OplogHandle::new(&ctx.gb_dir()); + let oplog_state = OplogHandle::new(&project.gb_dir()); let oplog_head_commit = oplog_state .oplog_head()? .and_then(|head_id| repo.find_commit(head_id).ok()); @@ -511,7 +507,7 @@ fn commit_snapshot( oplog_state.set_oplog_head(snapshot_commit_id)?; - set_reference_to_oplog(&ctx.path, ReflogCommits::new(ctx)?)?; + set_reference_to_oplog(project.git_dir(), ReflogCommits::new(project)?)?; Ok(snapshot_commit_id) } @@ -521,8 +517,7 @@ fn restore_snapshot( snapshot_commit_id: git2::Oid, exclusive_access: &mut WorktreeWritePermission, ) -> Result { - let worktree_dir = ctx.project().path.as_path(); - let repo = git2::Repository::open(worktree_dir)?; + let repo = ctx.project().open_git2()?; let before_restore_snapshot_result = prepare_snapshot(ctx, exclusive_access.read_permission()); let snapshot_commit = repo.find_commit(snapshot_commit_id)?; @@ -606,7 +601,7 @@ fn restore_snapshot( "We will not change a worktree which for some reason isn't on the workspace branch", )?; - let gix_repo = gitbutler_command_context::gix_repo_for_merging(worktree_dir)?; + let gix_repo = ctx.project().open_for_merging()?; let workdir_tree = repo.find_tree( get_workdir_tree(None, snapshot_commit_id.to_gix(), &gix_repo, ctx)?.to_git2(), @@ -723,11 +718,8 @@ fn restore_conflicts_tree(snapshot_tree: &git2::Tree, repo: &git2::Repository) - Ok(()) } -fn write_conflicts_tree( - worktree_dir: &std::path::Path, - repo: &git2::Repository, -) -> Result { - let git_dir = worktree_dir.join(".git"); +fn write_conflicts_tree(repo: &git2::Repository) -> Result { + let git_dir = repo.path(); let merge_parent_path = git_dir.join("base_merge_parent"); let merge_parent_blob = if merge_parent_path.exists() { let merge_parent_content = fs::read(merge_parent_path)?; diff --git a/crates/gitbutler-oplog/src/reflog.rs b/crates/gitbutler-oplog/src/reflog.rs index 9fec5fe898..514abe76f7 100644 --- a/crates/gitbutler-oplog/src/reflog.rs +++ b/crates/gitbutler-oplog/src/reflog.rs @@ -56,9 +56,8 @@ impl ReflogCommits { /// ``` /// /// The reflog entry is continuously updated to refer to the current target and oplog head commits. -pub fn set_reference_to_oplog(worktree_dir: &Path, reflog_commits: ReflogCommits) -> Result<()> { - let reflog_file_path = worktree_dir - .join(".git") +pub fn set_reference_to_oplog(git_dir: &Path, reflog_commits: ReflogCommits) -> Result<()> { + let reflog_file_path = git_dir .join("logs") .join("refs") .join("heads") @@ -66,7 +65,7 @@ pub fn set_reference_to_oplog(worktree_dir: &Path, reflog_commits: ReflogCommits .join("target"); let mut repo = gix::open_opts( - worktree_dir, + git_dir, // We may override the username as we only write a specific commit log, unrelated to the user. gix::open::Options::isolated().config_overrides({ let sig = standard_signature(); @@ -182,14 +181,15 @@ mod set_target_ref { fn reflog_present_but_empty() -> anyhow::Result<()> { let (dir, commit_id) = setup_repo()?; let worktree_dir = dir.path(); + let git_dir = worktree_dir.join(".git"); let oplog = git2::Oid::from_str("0123456789abcdef0123456789abcdef0123456")?; - set_reference_to_oplog(worktree_dir, reflog_commits(commit_id, oplog)).expect("success"); + set_reference_to_oplog(&git_dir, reflog_commits(commit_id, oplog)).expect("success"); let log_file_path = worktree_dir.join(".git/logs/refs/heads/gitbutler/target"); std::fs::write(&log_file_path, [])?; - set_reference_to_oplog(worktree_dir, reflog_commits(commit_id, oplog)).expect("success"); + set_reference_to_oplog(&git_dir, reflog_commits(commit_id, oplog)).expect("success"); let contents = std::fs::read_to_string(&log_file_path)?; assert_eq!(reflog_lines(&contents).len(), 2); @@ -203,14 +203,15 @@ mod set_target_ref { fn reflog_present_but_broken() -> anyhow::Result<()> { let (dir, commit_id) = setup_repo()?; let worktree_dir = dir.path(); + let git_dir = worktree_dir.join(".git"); let oplog = git2::Oid::from_str("0123456789abcdef0123456789abcdef0123456")?; - set_reference_to_oplog(worktree_dir, reflog_commits(commit_id, oplog)).expect("success"); + set_reference_to_oplog(&git_dir, reflog_commits(commit_id, oplog)).expect("success"); let log_file_path = worktree_dir.join(".git/logs/refs/heads/gitbutler/target"); std::fs::write(&log_file_path, b"a gobbled mess that is no reflog")?; - set_reference_to_oplog(worktree_dir, reflog_commits(commit_id, oplog)).expect("success"); + set_reference_to_oplog(&git_dir, reflog_commits(commit_id, oplog)).expect("success"); let contents = std::fs::read_to_string(&log_file_path)?; assert_eq!(reflog_lines(&contents).len(), 2); @@ -221,14 +222,15 @@ mod set_target_ref { fn reflog_present_but_branch_is_missing() -> anyhow::Result<()> { let (dir, commit_id) = setup_repo()?; let worktree_dir = dir.path(); + let git_dir = worktree_dir.join(".git"); let oplog = git2::Oid::from_str("0123456789abcdef0123456789abcdef0123456")?; - set_reference_to_oplog(worktree_dir, reflog_commits(commit_id, oplog)).expect("success"); + set_reference_to_oplog(&git_dir, reflog_commits(commit_id, oplog)).expect("success"); let loose_ref_path = worktree_dir.join(".git/refs/heads/gitbutler/target"); std::fs::remove_file(&loose_ref_path)?; - set_reference_to_oplog(worktree_dir, reflog_commits(commit_id, oplog)).expect("success"); + set_reference_to_oplog(&git_dir, reflog_commits(commit_id, oplog)).expect("success"); assert!( loose_ref_path.is_file(), "the file was recreated, just in case there is only a reflog and no branch" @@ -240,14 +242,15 @@ mod set_target_ref { fn branch_present_but_reflog_is_missing() -> anyhow::Result<()> { let (dir, commit_id) = setup_repo()?; let worktree_dir = dir.path(); + let git_dir = worktree_dir.join(".git"); let oplog = git2::Oid::from_str("0123456789abcdef0123456789abcdef0123456")?; - set_reference_to_oplog(worktree_dir, reflog_commits(commit_id, oplog)).expect("success"); + set_reference_to_oplog(&git_dir, reflog_commits(commit_id, oplog)).expect("success"); let log_file_path = worktree_dir.join(".git/logs/refs/heads/gitbutler/target"); std::fs::remove_file(&log_file_path)?; - set_reference_to_oplog(worktree_dir, reflog_commits(commit_id, oplog)) + set_reference_to_oplog(&git_dir, reflog_commits(commit_id, oplog)) .expect("missing reflog files are recreated"); assert!(log_file_path.is_file(), "the file was recreated"); @@ -263,6 +266,7 @@ mod set_target_ref { let commit_id_hex = commit_id.to_string(); let commit_id_hex: &gix::bstr::BStr = commit_id_hex.as_str().into(); let worktree_dir = dir.path(); + let git_dir = worktree_dir.join(".git"); let log_file_path = worktree_dir.join(".git/logs/refs/heads/gitbutler/target"); assert!(!log_file_path.exists()); @@ -270,7 +274,7 @@ mod set_target_ref { // Set ref for the first time let oplog_hex = "0123456789abcdef0123456789abcdef01234567"; let oplog = git2::Oid::from_str(oplog_hex)?; - set_reference_to_oplog(worktree_dir, reflog_commits(commit_id, oplog)).expect("success"); + set_reference_to_oplog(&git_dir, reflog_commits(commit_id, oplog)).expect("success"); assert!(log_file_path.exists()); let contents = std::fs::read_to_string(&log_file_path)?; let lines = reflog_lines(&contents); @@ -304,7 +308,7 @@ mod set_target_ref { // Update the oplog head only let another_oplog_hex = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; let another_oplog = git2::Oid::from_str(another_oplog_hex)?; - set_reference_to_oplog(worktree_dir, reflog_commits(commit_id, another_oplog)) + set_reference_to_oplog(&git_dir, reflog_commits(commit_id, another_oplog)) .expect("success"); let contents = std::fs::read_to_string(&log_file_path)?; @@ -334,7 +338,7 @@ mod set_target_ref { // Update the target head only let new_target_hex = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; let new_target = git2::Oid::from_str(new_target_hex)?; - set_reference_to_oplog(worktree_dir, reflog_commits(new_target, another_oplog)) + set_reference_to_oplog(&git_dir, reflog_commits(new_target, another_oplog)) .expect("success"); let contents = std::fs::read_to_string(&log_file_path)?; diff --git a/crates/gitbutler-project/Cargo.toml b/crates/gitbutler-project/Cargo.toml index 2efc47bda8..7964c7f365 100644 --- a/crates/gitbutler-project/Cargo.toml +++ b/crates/gitbutler-project/Cargo.toml @@ -5,6 +5,7 @@ edition = "2024" authors = ["GitButler "] publish = false rust-version = "1.89" + [dependencies] anyhow = "1.0.100" parking_lot = { workspace = true, features = ["arc_lock"] } @@ -18,7 +19,6 @@ gitbutler-storage.workspace = true but-core.workspace = true git2.workspace = true gix = { workspace = true, features = ["dirwalk", "credentials", "parallel"] } -uuid.workspace = true strum = { version = "0.27", features = ["derive"] } tracing.workspace = true resolve-path = "0.1.0" @@ -29,4 +29,3 @@ fslock = "0.2.1" [dev-dependencies] gitbutler-testsupport.workspace = true tempfile.workspace = true -tokio = { workspace = true, features = ["rt-multi-thread", "rt", "macros"] } diff --git a/crates/gitbutler-project/src/api.rs b/crates/gitbutler-project/src/api.rs index 2d6cb160ba..e87b941110 100644 --- a/crates/gitbutler-project/src/api.rs +++ b/crates/gitbutler-project/src/api.rs @@ -1,47 +1,12 @@ -use std::path; - use but_core::RepositoryExt; -use serde::{Deserialize, Serialize}; - -use crate::{ - ApiProject, AuthKey, CodePushState, FetchResult, ProjectId, default_true::DefaultTrue, -}; +use serde::Serialize; /// API-specific project type that can be enriched with computed/derived data /// while preserving the original project structure for persistence. -#[derive(Debug, Deserialize, Serialize, Clone, Default)] +#[derive(Debug, Serialize, Clone, Default)] pub struct Project { - pub id: ProjectId, - pub title: String, - pub description: Option, - /// The worktree directory of the project's repository. - // TODO(ST): rename this to `worktree_dir` and while at it, add a `git_dir` if it's retrieved from a repo. - // Then find `.join(".git")` and use the `git_dir` instead. - pub path: path::PathBuf, - #[serde(default)] - pub preferred_key: AuthKey, - /// if ok_with_force_push is true, we'll not try to avoid force pushing - /// for example, when updating base branch - #[serde(default)] - pub ok_with_force_push: DefaultTrue, - /// Force push protection uses safer force push flags instead of doing straight force pushes - #[serde(default)] - pub force_push_protection: bool, - pub api: Option, - #[serde(default)] - pub gitbutler_data_last_fetch: Option, - #[serde(default)] - pub gitbutler_code_push_state: Option, - #[serde(default)] - pub project_data_last_fetch: Option, - #[serde(default)] - pub omit_certificate_check: Option, - // The number of changed lines that will trigger a snapshot - pub snapshot_lines_threshold: Option, - #[serde(default)] - pub forge_override: Option, - #[serde(default)] - pub preferred_forge_user: Option, + #[serde(flatten)] + inner: crate::Project, /// Gerrit mode enabled for this project, derived from git configuration #[serde(default)] pub gerrit_mode: bool, @@ -49,7 +14,7 @@ pub struct Project { impl From for Project { fn from(project: crate::Project) -> Self { - let gerrit_mode = match gix::open(&project.path) { + let gerrit_mode = match project.open_isolated() { Ok(repo) => repo .git_settings() .ok() @@ -59,21 +24,7 @@ impl From for Project { }; Self { - id: project.id, - title: project.title, - description: project.description, - path: project.path, - preferred_key: project.preferred_key, - ok_with_force_push: project.ok_with_force_push, - force_push_protection: project.force_push_protection, - api: project.api, - gitbutler_data_last_fetch: project.gitbutler_data_last_fetch, - gitbutler_code_push_state: project.gitbutler_code_push_state, - project_data_last_fetch: project.project_data_last_fetch, - omit_certificate_check: project.omit_certificate_check, - snapshot_lines_threshold: project.snapshot_lines_threshold, - forge_override: project.forge_override, - preferred_forge_user: project.preferred_forge_user, + inner: project, gerrit_mode, } } @@ -81,23 +32,6 @@ impl From for Project { impl From for crate::Project { fn from(api_project: Project) -> Self { - Self { - id: api_project.id, - title: api_project.title, - description: api_project.description, - path: api_project.path, - preferred_key: api_project.preferred_key, - ok_with_force_push: api_project.ok_with_force_push, - force_push_protection: api_project.force_push_protection, - api: api_project.api, - gitbutler_data_last_fetch: api_project.gitbutler_data_last_fetch, - gitbutler_code_push_state: api_project.gitbutler_code_push_state, - project_data_last_fetch: api_project.project_data_last_fetch, - omit_certificate_check: api_project.omit_certificate_check, - snapshot_lines_threshold: api_project.snapshot_lines_threshold, - forge_override: api_project.forge_override, - preferred_forge_user: api_project.preferred_forge_user, - // Note: gerrit_mode is not included as it's derived, not persisted - } + api_project.inner } } diff --git a/crates/gitbutler-project/src/controller.rs b/crates/gitbutler-project/src/controller.rs index de2762377d..b7bdd92bac 100644 --- a/crates/gitbutler-project/src/controller.rs +++ b/crates/gitbutler-project/src/controller.rs @@ -60,31 +60,37 @@ impl Controller { } } - pub(crate) fn add>(&self, path: P) -> Result { - let path = path.as_ref(); + pub(crate) fn add(&self, worktree_dir: impl AsRef) -> Result { + let worktree_dir = worktree_dir.as_ref(); let all_projects = self .projects_storage .list() .context("failed to list projects from storage")?; - if let Some(existing_project) = all_projects.iter().find(|project| project.path == path) { + if let Some(existing_project) = all_projects + .iter() + .find(|project| project.worktree_dir_but_should_use_git_dir() == worktree_dir) + { return Ok(AddProjectOutcome::AlreadyExists( existing_project.to_owned(), )); } - if !path.exists() { + if !worktree_dir.exists() { return Ok(AddProjectOutcome::PathNotFound); } - if !path.is_dir() { + if !worktree_dir.is_dir() { return Ok(AddProjectOutcome::NotADirectory); } - match gix::open_opts(path, gix::open::Options::isolated()) { + let resolved_path = gix::path::realpath(worktree_dir)?; + // Make sure the repo is opened from the resolved path - it must be absolute for persistence. + let repo = match gix::open_opts(&resolved_path, gix::open::Options::isolated()) { Ok(repo) if repo.is_bare() => { return Ok(AddProjectOutcome::BareRepository); } Ok(repo) if repo.worktree().is_some_and(|wt| !wt.is_main()) => { - if path.join(".git").is_file() { + if worktree_dir.join(".git").is_file() { return Ok(AddProjectOutcome::NonMainWorktree); }; + repo } Ok(repo) => match repo.workdir() { None => { @@ -94,25 +100,25 @@ impl Controller { if !wd.join(".git").is_dir() { return Ok(AddProjectOutcome::NoDotGitDirectory); } + repo } }, Err(err) => { return Ok(AddProjectOutcome::NotAGitRepository(err.to_string())); } - } + }; let id = ProjectId::generate(); // Resolve the path first to get the actual directory name - let resolved_path = gix::path::realpath(path)?; - let title_is_not_normal_component = path + let title_is_not_normal_component = worktree_dir .components() .next_back() .is_none_or(|c| !matches!(c, Component::Normal(_))); let path_for_title = if title_is_not_normal_component { &resolved_path } else { - path + worktree_dir }; let title = path_for_title.file_name().map_or_else( @@ -123,8 +129,10 @@ impl Controller { let project = Project { id, title, - path: resolved_path, + // TODO(1.0): make this always `None`, until the field can be removed for good. + worktree_dir: resolved_path, api: None, + git_dir: repo.git_dir().to_owned(), ..Default::default() }; @@ -198,29 +206,31 @@ impl Controller { fn get_inner(&self, id: ProjectId, validate: bool) -> Result { #[cfg_attr(not(windows), allow(unused_mut))] let mut project = self.projects_storage.get(id)?; + // BACKWARD-COMPATIBLE MIGRATION if validate { - let worktree_dir = &project.path; - if gix::open_opts(worktree_dir, gix::open::Options::isolated()).is_err() { - let suffix = if !worktree_dir.exists() { + let repo = project.open_isolated(); + if repo.is_err() { + let suffix = if !project.worktree_dir.exists() { " as it does not exist" } else { "" }; return Err(anyhow!( "Could not open repository at '{}'{suffix}", - worktree_dir.display() + project.worktree_dir.display() ) .context(error::Code::ProjectMissing)); } } + project.migrate()?; if !project.gb_dir().exists() && let Err(error) = std::fs::create_dir_all(project.gb_dir()) { tracing::error!(project_id = %project.id, ?error, "failed to create \"{}\" on project get", project.gb_dir().display()); } // Clean up old virtual_branches.toml that was never used - let old_virtual_branches_path = project.path.join(".git").join("virtual_branches.toml"); + let old_virtual_branches_path = project.git_dir().join("virtual_branches.toml"); if old_virtual_branches_path.exists() && let Err(error) = std::fs::remove_file(old_virtual_branches_path) { diff --git a/crates/gitbutler-project/src/lib.rs b/crates/gitbutler-project/src/lib.rs index 24747d1153..98b4d9d3e7 100644 --- a/crates/gitbutler-project/src/lib.rs +++ b/crates/gitbutler-project/src/lib.rs @@ -77,7 +77,8 @@ pub fn add_with_path( controller.add(path) } -pub fn list() -> anyhow::Result> { +/// NOTE: call [`Project::migrated()`] if the instance should be used for actual functionality. +pub fn dangerously_list_without_migration() -> anyhow::Result> { let controller = Controller::from_path(but_path::app_data_dir()?); controller.list() } diff --git a/crates/gitbutler-project/src/project.rs b/crates/gitbutler-project/src/project.rs index 6c8cb86a1a..8e701a4f45 100644 --- a/crates/gitbutler-project/src/project.rs +++ b/crates/gitbutler-project/src/project.rs @@ -1,5 +1,5 @@ use std::{ - path::{self, Path, PathBuf}, + path::{Path, PathBuf}, time, }; @@ -14,7 +14,7 @@ use crate::default_true::DefaultTrue; pub enum AuthKey { GitCredentialsHelper, Local { - private_key_path: path::PathBuf, + private_key_path: PathBuf, }, // There used to be more auth option variants that we are deprecating and replacing with this #[serde(other)] @@ -78,9 +78,22 @@ pub struct Project { pub title: String, pub description: Option, /// The worktree directory of the project's repository. - // TODO(ST): rename this to `worktree_dir` and while at it, add a `git_dir` if it's retrieved from a repo. - // Then find `.join(".git")` and use the `git_dir` instead. - pub path: path::PathBuf, + // TODO: Make it optional for bare repo support! + // TODO: Do not actually store it, but obtain it on the fly by using a repository! + #[serde(rename = "path")] + // TODO(1.0): enable the line below to clear the value from storage - we only want the git dir, + // but need to remain compatible. The frontend shouldn't care, so we may need a specific type for that + // which already exists, but… needs cleanup. + // However, this field SHOULD STAY to present better errors when the path isn't there anymore. + // But it must still be optional. + // #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) worktree_dir: PathBuf, + /// The storage location of the Git repository itself. + /// This is the only value we need to access everything related to the Git repository. + /// + // TODO(1.0): remove the `default` which is just needed while there is project files without it. + #[serde(default)] + pub(crate) git_dir: PathBuf, #[serde(default)] pub preferred_key: AuthKey, /// if ok_with_force_push is true, we'll not try to avoid force pushing @@ -107,6 +120,58 @@ pub struct Project { pub preferred_forge_user: Option, } +/// Testing +// TODO: remove once `gitbutler-testsupport` isn't needed anymore, and `gitbutler-repo` +impl Project { + /// A special constructor needed as `worktree_dir` isn't accessible anymore. + pub fn new_for_gitbutler_testsupport(title: String, worktree_dir: PathBuf) -> Self { + Project { + id: ProjectId::generate(), + title, + worktree_dir, + ..Default::default() + } + .migrated() + .unwrap() + } + + /// A special constructor needed as `worktree_dir` isn't accessible anymore. + pub fn new_for_gitbutler_repo(worktree_dir: PathBuf, preferred_key: AuthKey) -> Self { + Project { + worktree_dir, + preferred_key, + ..Default::default() + } + .migrated() + .unwrap() + } + + /// Call this after each invocation of `list()` with manual filtering to get fields filled in. + pub fn migrated(mut self) -> anyhow::Result { + self.migrate()?; + Ok(self) + } +} + +impl Project { + pub(crate) fn migrate(&mut self) -> anyhow::Result<()> { + if !self.git_dir.as_os_str().is_empty() { + return Ok(()); + } + let repo = gix::open_opts(&self.worktree_dir, gix::open::Options::isolated()) + .context("BUG: worktree is supposed to be valid here for migration")?; + self.git_dir = repo.git_dir().to_owned(); + // NOTE: we set the worktree so the frontend is happier until this usage can be reviewed, + // probably for supporting bare repositories. + self.worktree_dir = repo.workdir().context("BUG: we currently only support non-bare repos, yet this one didn't have a worktree dir")?.to_owned(); + Ok(()) + } + + pub(crate) fn worktree_dir_but_should_use_git_dir(&self) -> &Path { + &self.worktree_dir + } +} + /// Instantiation impl Project { /// Search upwards from `path` to discover a Git worktree. @@ -115,42 +180,81 @@ impl Project { .workdir() .context("Bare repositories aren't supported")? .to_owned(); - Ok(Project { - path: worktree_dir, + Project { + worktree_dir, ..Default::default() - }) + } + .migrated() } /// Finds an existing project by its path. Errors out if not found. - pub fn find_by_path(path: &Path) -> anyhow::Result { - let mut projects = crate::list()?; + // TODO: find by git-dir instead! + pub fn find_by_worktree_dir(worktree_dir: &Path) -> anyhow::Result { + let mut projects = crate::dangerously_list_without_migration()?; // Sort projects by longest pathname to shortest. // We need to do this because users might have one gitbutler project // nexted insided of another via a gitignored folder. // We want to match on the longest project path. projects.sort_by(|a, b| { - a.path + a.worktree_dir .as_os_str() .len() - .cmp(&b.path.as_os_str().len()) + .cmp(&b.worktree_dir.as_os_str().len()) // longest first .reverse() }); - let resolved_path = if path.is_relative() { - path.canonicalize().context("Failed to canonicalize path")? + let resolved_path = if worktree_dir.is_relative() { + worktree_dir + .canonicalize() + .context("Failed to canonicalize path")? } else { - path.to_path_buf() + worktree_dir.to_path_buf() }; let project = projects .into_iter() .find(|p| { // Canonicalize project path for comparison - match p.path.canonicalize() { + match p.worktree_dir.canonicalize() { Ok(proj_canon) => resolved_path.starts_with(proj_canon), Err(_) => false, } }) .context("No project found with the given path")?; - Ok(project) + project.migrated() + } +} + +/// Repository helpers. +impl Project { + /// Open an isolated repository, one that didn't read options beyond `.git/config` and + /// knows no environment variables. + /// + /// Use it for fastest-possible access, when incomplete configuration is acceptable. + pub fn open_isolated(&self) -> anyhow::Result { + Ok(gix::open_opts( + &self.git_dir, + gix::open::Options::isolated(), + )?) + } + + /// Open a standard Git repository at the project directory, just like a real user would. + /// + /// This repository is good for standard tasks, like checking refs and traversing the commit graph, + /// and for reading objects as well. + /// + /// Diffing and merging is better done with [`Self::open_for_merging()`]. + pub fn open(&self) -> anyhow::Result { + Ok(gix::open(&self.git_dir)?) + } + + /// Calls [`but_core::open_repo_for_merging()`] + pub fn open_for_merging(&self) -> anyhow::Result { + but_core::open_repo_for_merging(&self.git_dir) + } + + /// Open a git2 repository. + /// Deprecated, but still in use. + pub fn open_git2(&self) -> anyhow::Result { + Ok(git2::Repository::open(&self.git_dir)?) } } @@ -185,15 +289,37 @@ impl Project { /// /// Normally this is `.git/gitbutler` in the project's repository. pub fn gb_dir(&self) -> PathBuf { - self.path.join(".git").join("gitbutler") + self.git_dir.join("gitbutler") } pub fn snapshot_lines_threshold(&self) -> usize { self.snapshot_lines_threshold.unwrap_or(20) } - pub fn worktree_path(&self) -> PathBuf { - self.path.clone() + // TODO(ST): Actually remove this - people should use the `gix::Repository` for worktree handling (which makes it optional, too) + pub fn worktree_dir(&self) -> anyhow::Result<&Path> { + // TODO: open a repo and get the workdir. + // For now we don't have to open a repo as we only support repos with worktree. + Ok(&self.worktree_dir) + } + + /// Set the worktree directory to `worktree_dir`. + pub fn set_worktree_dir(&mut self, worktree_dir: PathBuf) -> anyhow::Result<()> { + let repo = gix::open_opts(&worktree_dir, gix::open::Options::isolated())?; + self.worktree_dir = worktree_dir; + self.git_dir = repo.git_dir().to_owned(); + Ok(()) + } + + /// Return the path to the directory that holds the repository data and that is associated with the current worktree. + pub fn git_dir(&self) -> &Path { + &self.git_dir + } + + /// Return the path to the Git directory of the 'prime' repository, the one that holds all worktrees. + pub fn common_git_dir(&self) -> anyhow::Result { + let repo = self.open_isolated()?; + Ok(repo.common_dir().to_owned()) } } diff --git a/crates/gitbutler-project/src/storage.rs b/crates/gitbutler-project/src/storage.rs index c002af02d3..0ea34338cd 100644 --- a/crates/gitbutler-project/src/storage.rs +++ b/crates/gitbutler-project/src/storage.rs @@ -1,7 +1,7 @@ use std::path::PathBuf; use anyhow::{Context, Result}; -use serde::{Deserialize, Serialize}; +use serde::Deserialize; use crate::{ApiProject, AuthKey, CodePushState, FetchResult, Project, ProjectId}; @@ -12,7 +12,7 @@ pub(crate) struct Storage { inner: gitbutler_storage::Storage, } -#[derive(Debug, Serialize, Deserialize, Default, Clone)] +#[derive(Debug, Deserialize, Default, Clone)] pub struct UpdateRequest { pub id: ProjectId, pub title: Option, @@ -97,7 +97,7 @@ impl Storage { } if let Some(path) = &update_request.path { - project.path = path.clone(); + project.set_worktree_dir(path.clone())?; } if let Some(api) = &update_request.api { diff --git a/crates/gitbutler-project/tests/project/main.rs b/crates/gitbutler-project/tests/project/main.rs index 68440a2688..36ca3e51eb 100644 --- a/crates/gitbutler-project/tests/project/main.rs +++ b/crates/gitbutler-project/tests/project/main.rs @@ -9,18 +9,19 @@ mod add { use super::*; #[test] - fn success() { + fn success() -> anyhow::Result<()> { let tmp = paths::data_dir(); let repository = gitbutler_testsupport::TestProject::default(); let path = repository.path(); let project = gitbutler_project::add_with_path(tmp.path(), path) .unwrap() .unwrap_project(); - assert_eq!(project.path, path); + assert_eq!(project.worktree_dir()?, path); assert_eq!( project.title, path.iter().next_back().unwrap().to_str().unwrap() ); + Ok(()) } mod error { diff --git a/crates/gitbutler-repo-actions/src/repository.rs b/crates/gitbutler-repo-actions/src/repository.rs index 7671782648..de52eb3b05 100644 --- a/crates/gitbutler-repo-actions/src/repository.rs +++ b/crates/gitbutler-repo-actions/src/repository.rs @@ -186,7 +186,7 @@ impl RepoActionsExt for CommandContext { // NOTE(qix-): work around a time-sensitive change that was necessary // NOTE(qix-): without having to refactor a large portion of the codebase. if use_git_executable { - let path = self.project().worktree_path(); + let path = self.project().git_dir().to_owned(); let remote = branch.remote().to_string(); std::thread::spawn(move || { tokio::runtime::Runtime::new() @@ -282,7 +282,7 @@ impl RepoActionsExt for CommandContext { // NOTE(qix-): work around a time-sensitive change that was necessary // NOTE(qix-): without having to refactor a large portion of the codebase. if self.project().preferred_key == AuthKey::SystemExecutable { - let path = self.project().worktree_path(); + let path = self.project().git_dir().to_owned(); let remote = remote_name.to_string(); return std::thread::spawn(move || { tokio::runtime::Runtime::new() diff --git a/crates/gitbutler-repo/src/commands.rs b/crates/gitbutler-repo/src/commands.rs index bbb11b75b1..4fa4755641 100644 --- a/crates/gitbutler-repo/src/commands.rs +++ b/crates/gitbutler-repo/src/commands.rs @@ -1,6 +1,6 @@ use std::path::Path; -use anyhow::{Result, bail}; +use anyhow::{Context, Result, bail}; use base64::engine::Engine as _; use git2::Oid; use gitbutler_project::Project; @@ -125,20 +125,19 @@ pub trait RepoCommands { impl RepoCommands for Project { fn get_local_config(&self, key: &str) -> Result> { - let repo = &git2::Repository::open(&self.path)?; + let repo = &self.open_git2()?; let config: Config = repo.into(); config.get_local(key) } fn set_local_config(&self, key: &str, value: &str) -> Result<()> { - let repo = &git2::Repository::open(&self.path)?; + let repo = &self.open_git2()?; let config: Config = repo.into(); config.set_local(key, value) } fn check_signing_settings(&self) -> Result { - let repo = &git2::Repository::open(&self.path)?; - let signed = repo.sign_buffer(b"test"); + let signed = self.open_git2()?.sign_buffer(b"test"); match signed { Ok(_) => Ok(true), Err(e) => Err(e), @@ -146,7 +145,7 @@ impl RepoCommands for Project { } fn remotes(&self) -> anyhow::Result> { - let repo = &git2::Repository::open(&self.path)?; + let repo = self.open_git2()?; let remotes = repo .remotes_as_string()? .iter() @@ -159,7 +158,7 @@ impl RepoCommands for Project { } fn add_remote(&self, name: &str, url: &str) -> Result<()> { - let repo = &git2::Repository::open(&self.path)?; + let repo = self.open_git2()?; // Bail if remote with given name already exists. if repo.find_remote(name).is_ok() { @@ -189,7 +188,7 @@ impl RepoCommands for Project { ); } - let repo = &git2::Repository::open(&self.path)?; + let repo = self.open_git2()?; let tree = repo.find_commit(commit_id)?.tree()?; Ok(match tree.get_path(relative_path) { @@ -203,19 +202,21 @@ impl RepoCommands for Project { } fn read_file_from_workspace(&self, probably_relative_path: &Path) -> Result { - let repo = &git2::Repository::open(&self.path)?; - + let repo = self.open_git2()?; + let workdir = repo.workdir().context( + "BUG: can't yet handle bare repos and we shouldn't run into this until we do", + )?; let (path_in_worktree, relative_path) = if probably_relative_path.is_relative() { ( - gix::path::realpath(self.path.join(probably_relative_path))?, + gix::path::realpath(workdir.join(probably_relative_path))?, probably_relative_path.to_owned(), ) } else { - let Ok(relative_path) = probably_relative_path.strip_prefix(&self.path) else { + let Ok(relative_path) = probably_relative_path.strip_prefix(workdir) else { bail!( "Path to read from at '{}' isn't in the worktree directory '{}'", probably_relative_path.display(), - self.path.display() + workdir.display() ); }; (probably_relative_path.to_owned(), relative_path.to_owned()) diff --git a/crates/gitbutler-repo/tests/credentials.rs b/crates/gitbutler-repo/tests/credentials.rs index f31777f2df..81421a3515 100644 --- a/crates/gitbutler-repo/tests/credentials.rs +++ b/crates/gitbutler-repo/tests/credentials.rs @@ -27,11 +27,10 @@ impl TestCase<'_> { let (repo, _tmp) = test_repository(); repo.remote("origin", self.remote_url).unwrap(); - let project = projects::Project { - path: repo.workdir().unwrap().to_path_buf(), - preferred_key: self.preferred_key.clone(), - ..Default::default() - }; + let project = projects::Project::new_for_gitbutler_repo( + repo.workdir().unwrap().to_path_buf(), + self.preferred_key.clone(), + ); let ctx = CommandContext::open(&project, AppSettings::default()).unwrap(); let flow = help(&ctx, "origin").unwrap(); diff --git a/crates/gitbutler-sync/src/stack_upload.rs b/crates/gitbutler-sync/src/stack_upload.rs index 27b4fabc60..617411566e 100644 --- a/crates/gitbutler-sync/src/stack_upload.rs +++ b/crates/gitbutler-sync/src/stack_upload.rs @@ -34,7 +34,7 @@ pub fn push_stack_to_review( let Some(review_base_id) = vb_state.upsert_last_pushed_base(&repo)? else { bail!("This is impossible. If you got here, I'm sorry."); }; - set_reference_to_oplog(&ctx.project().path, ReflogCommits::new(ctx.project())?)?; + set_reference_to_oplog(ctx.project().git_dir(), ReflogCommits::new(ctx.project())?)?; let target_commit_id = vb_state.get_default_target()?.sha.to_gix(); let git2_repository = ctx.repo(); diff --git a/crates/gitbutler-tauri/src/projects.rs b/crates/gitbutler-tauri/src/projects.rs index f9a44d8c6d..9ce5aa1d8c 100644 --- a/crates/gitbutler-tauri/src/projects.rs +++ b/crates/gitbutler-tauri/src/projects.rs @@ -20,17 +20,19 @@ pub fn list_projects( window_state: State<'_, WindowState>, ) -> Result, Error> { let open_projects = window_state.open_projects(); - gitbutler_project::assure_app_can_startup_or_fix_it(gitbutler_project::list()) - .map_err(Into::into) - .map(|projects| { - projects - .into_iter() - .map(|project| ProjectForFrontend { - is_open: open_projects.contains(&project.id), - inner: project.into(), - }) - .collect() - }) + gitbutler_project::assure_app_can_startup_or_fix_it( + gitbutler_project::dangerously_list_without_migration(), + ) + .map_err(Into::into) + .map(|projects| { + projects + .into_iter() + .map(|project| ProjectForFrontend { + is_open: open_projects.contains(&project.id), + inner: project.into(), + }) + .collect() + }) } /// Additional information to help the user interface communicate what happened with the project. @@ -63,7 +65,7 @@ pub fn set_project_active( return Ok(None); } }; - let repo = git2::Repository::open(&project.path) + let repo = git2::Repository::open(project.git_dir()) // Only capture this information here to prevent spawning too many errors because of this // (the UI has many parallel calls in flight). .map_err(|err| { @@ -117,7 +119,7 @@ pub fn open_project_in_window(handle: tauri::AppHandle, id: ProjectId) -> Result Ok(()) } -#[derive(serde::Deserialize, serde::Serialize)] +#[derive(serde::Serialize)] pub struct ProjectForFrontend { #[serde(flatten)] pub inner: gitbutler_project::api::Project, diff --git a/crates/gitbutler-tauri/src/window.rs b/crates/gitbutler-tauri/src/window.rs index 1f993622f5..c12b164aa1 100644 --- a/crates/gitbutler-tauri/src/window.rs +++ b/crates/gitbutler-tauri/src/window.rs @@ -218,7 +218,7 @@ pub(crate) mod state { } let exclusive_access = project.try_exclusive_access().ok(); let handler = handler_from_app(&self.app_handle)?; - let worktree_dir = project.path.clone(); + let worktree_dir = project.worktree_dir()?; let project_id = project.id; let watcher = gitbutler_watcher::watch_in_background( handler, diff --git a/crates/gitbutler-testsupport/src/lib.rs b/crates/gitbutler-testsupport/src/lib.rs index 8ddc974950..e066d78d7e 100644 --- a/crates/gitbutler-testsupport/src/lib.rs +++ b/crates/gitbutler-testsupport/src/lib.rs @@ -73,7 +73,7 @@ pub fn init_opts_bare() -> git2::RepositoryInitOptions { pub mod writable { use but_settings::AppSettings; use gitbutler_command_context::CommandContext; - use gitbutler_project::{Project, ProjectId}; + use gitbutler_project::Project; use tempfile::TempDir; use crate::{BUT_DRIVER, DRIVER}; @@ -107,12 +107,10 @@ pub mod writable { ) .expect("script execution always succeeds"); - let project = Project { - id: ProjectId::generate(), - title: project_directory.to_owned(), - path: root.path().join(project_directory), - ..Default::default() - }; + let project = Project::new_for_gitbutler_testsupport( + project_directory.to_owned(), + root.path().join(project_directory), + ); Ok((project, root)) } @@ -150,12 +148,10 @@ pub mod writable { ) .expect("script execution always succeeds"); - let project = Project { - id: ProjectId::generate(), - title: project_directory.to_owned(), - path: root.path().join(project_directory), - ..Default::default() - }; + let project = Project::new_for_gitbutler_testsupport( + project_directory.to_owned(), + root.path().join(project_directory), + ); Ok((project, root)) } } @@ -256,7 +252,7 @@ pub mod read_only { use but_settings::{AppSettings, app_settings::FeatureFlags}; use gitbutler_command_context::CommandContext; - use gitbutler_project::{Project, ProjectId}; + use gitbutler_project::Project; use once_cell::sync::Lazy; use parking_lot::Mutex; @@ -312,12 +308,10 @@ pub mod read_only { gitbutler_project::add_with_path(tmp.path(), project_worktree_dir.as_path())?; outcome.try_project()? } else { - Project { - id: ProjectId::generate(), - title: project_directory.to_owned(), - path: project_worktree_dir, - ..Default::default() - } + Project::new_for_gitbutler_testsupport( + project_directory.to_owned(), + project_worktree_dir, + ) }; Ok(project) } diff --git a/crates/gitbutler-watcher/src/handler.rs b/crates/gitbutler-watcher/src/handler.rs index 2e703fe86d..e34fe91c0e 100644 --- a/crates/gitbutler-watcher/src/handler.rs +++ b/crates/gitbutler-watcher/src/handler.rs @@ -83,7 +83,7 @@ impl Handler { let dependencies = hunk_dependencies_for_workspace_changes_by_worktree_dir( ctx, - &ctx.project().path, + ctx.project().worktree_dir()?, &ctx.project().gb_dir(), Some(wt_changes.changes.clone()), );