Files
zed/crates/git_ui/src/clone.rs
T
2ab4a856a3 git: Improve handling of unsafe repositories (#43693)
These changes update Zed's Git Panel to be able to detect unsafe git
repositories, where the user running Zed doesn't own the `.git`
directory, and show a dedicated empty view in the Git Panel that not
only explains the situation but also allows the user to choose whether
to trust this directory, which will end up running `git config --global
--add safe.directory <path>`.

While testing those changes, it was noted that attempting to add or
reset files after trusting the directory would fail, as expected, but
the UI wouldn't react to the fact that those operations failed. What
this means is, if the user tried to add a file, the UI would show a
checkmark for that file and, after the operation failed, the checkmark
would remain. We now revert that, for both `git add` and `git reset`.

Some more technical notes on this change:

- Introduce `project::git_store::GitAccess` enum to express whether we
actually have access to the repository, exposed via
`git::Repository::access`, which probes the backend with a `git status`
command and classifies the result.
- On unsafe repositories,
`project::git_store::LocalRepositoryState::new` fails to spawn the git
worker, which now cleanly signals `GitAccess::No` rather than leaving
the panel in a broken state.
- Updated `git_ui::git_panel::GitPanel::render_empty_state` with a third
alternative, when `GitAccess::No`, that basically renders a view
explaining why the repository is considered unsafe, together with
buttons for git's documentation on safe directories and a button to add
the repository's folder as a safe directory
- Updated `project::git_store::GitStore` to now watch `~/.gitconfig` and
`$XDG_CONFIG_HOME/git/config`. Watching this files allows the Git Panel
to react when the git configuration is updated, which will be the case
if the user decides to trust the repository. When either changes, a
`GitStoreEvent::GlobalConfigurationUpdated` event is emitted and the
panel refetches repository state.
- Added `project::git_store::Repository::refetch_repo_state` field,
which stores a closure to allow recreating the
`Repository::repository_state` and the job sender after the repository's
directory is trusted, without requiring a project reload.
- Added a `fs::Fs::git_config` trait method, wrapping a real `git
config` invocation. In order to be able to call this, both
`git_store::GitStore::git_config` and `project::Project::git_config`
wrappers have also been introduced. Worth mentioning that this isn't yet
supported for remote projects or collab guests.
- Updated `fs::Fs::git_clone` argument order to match
`fs::Fs::git_config` and `fs::Fs::git_init`.
- Added a new
`project::git_store::pending_op::PendingOps::last_op_errored` method
that allows determining whether the last pending operation failed. This
now allows us to filter out failed operations when determining whether
`git add` or `git reset` failed, so that we can fall through to the real
git status.
- Updated `git_ui::git_panel::GitPanel::change_file_stage` to now call
`update_counts` in its error branch so the cached staged counters stay
consistent with the reverted per-entry state, seeing as we now handle
reverting the UI state if an operation fails.
- Fixed `fs::RealFs::git_init` to fall back to the provided branch name
when `git config --global --get init.defaultBranch` fails, for example,
when the user hsan't configured one.

Co-authored-by: cameron <cameron.studdstreet@gmail.com>

Closes #42286

Release Notes:

- Added a dedicated empty state in the Git Panel for unsafe
repositories, with a "Trust Directory" button that adds the repository
to `safe.directory`
- Fixed stage and unstage checkboxes in the Git Panel not reverting when
a `git add` or `git reset` command failed

---------

Co-authored-by: cameron <cameron.studdstreet@gmail.com>
Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
2026-04-24 17:14:24 +00:00

160 lines
6.2 KiB
Rust

use gpui::{App, Context, WeakEntity, Window};
use notifications::status_toast::StatusToast;
use std::sync::Arc;
use ui::{Color, Icon, IconName, IconSize, SharedString};
use util::ResultExt;
use workspace::{self, Workspace};
pub fn clone_and_open(
repo_url: SharedString,
workspace: WeakEntity<Workspace>,
window: &mut Window,
cx: &mut App,
on_success: Arc<
dyn Fn(&mut Workspace, &mut Window, &mut Context<Workspace>) + Send + Sync + 'static,
>,
) {
let destination_prompt = cx.prompt_for_paths(gpui::PathPromptOptions {
files: false,
directories: true,
multiple: false,
prompt: Some("Select as Repository Destination".into()),
});
window
.spawn(cx, async move |cx| {
let mut paths = destination_prompt.await.ok()?.ok()??;
let mut destination_dir = paths.pop()?;
let repo_name = repo_url
.split('/')
.next_back()
.map(|name| name.strip_suffix(".git").unwrap_or(name))
.unwrap_or("repository")
.to_owned();
let clone_task = workspace
.update(cx, |workspace, cx| {
let fs = workspace.app_state().fs.clone();
let destination_dir = destination_dir.clone();
let repo_url = repo_url.clone();
cx.spawn(async move |_workspace, _cx| {
fs.git_clone(destination_dir.as_path(), &repo_url).await
})
})
.ok()?;
if let Err(error) = clone_task.await {
workspace
.update(cx, |workspace, cx| {
let toast = StatusToast::new(error.to_string(), cx, |this, _| {
this.icon(
Icon::new(IconName::XCircle)
.size(IconSize::Small)
.color(Color::Error),
)
.dismiss_button(true)
});
workspace.toggle_status_toast(toast, cx);
})
.log_err();
return None;
}
let has_worktrees = workspace
.read_with(cx, |workspace, cx| {
workspace.project().read(cx).worktrees(cx).next().is_some()
})
.ok()?;
let prompt_answer = if has_worktrees {
cx.update(|window, cx| {
window.prompt(
gpui::PromptLevel::Info,
&format!("Git Clone: {}", repo_name),
None,
&["Add repo to project", "Open repo in new project"],
cx,
)
})
.ok()?
.await
.ok()?
} else {
// Don't ask if project is empty
0
};
destination_dir.push(&repo_name);
match prompt_answer {
0 => {
workspace
.update_in(cx, |workspace, window, cx| {
let create_task = workspace.project().update(cx, |project, cx| {
project.create_worktree(destination_dir.as_path(), true, cx)
});
let workspace_weak = cx.weak_entity();
let on_success = on_success.clone();
cx.spawn_in(window, async move |_window, cx| {
if create_task.await.log_err().is_some() {
workspace_weak
.update_in(cx, |workspace, window, cx| {
(on_success)(workspace, window, cx);
})
.ok();
}
})
.detach();
})
.ok()?;
}
1 => {
workspace
.update(cx, move |workspace, cx| {
let app_state = workspace.app_state().clone();
let destination_path = destination_dir.clone();
let on_success = on_success.clone();
workspace::open_new(
Default::default(),
app_state,
cx,
move |workspace, window, cx| {
cx.activate(true);
let create_task =
workspace.project().update(cx, |project, cx| {
project.create_worktree(
destination_path.as_path(),
true,
cx,
)
});
let workspace_weak = cx.weak_entity();
cx.spawn_in(window, async move |_window, cx| {
if create_task.await.log_err().is_some() {
workspace_weak
.update_in(cx, |workspace, window, cx| {
(on_success)(workspace, window, cx);
})
.ok();
}
})
.detach();
},
)
.detach();
})
.ok();
}
_ => {}
}
Some(())
})
.detach();
}