дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 19:25:54 +02:00
fix(vcs): Route a nested repository's own entry to the enclosing one and harden multi-root
Review fixes for several repositories in one project: - A path equal to a nested repository's root (a submodule's entry, an untracked clone) belongs to the enclosing repository, in the backend and the Commit view. It was routed to the nested repository as ".", so Rollback ran `git restore` and `git clean` over the whole submodule and Commit staged everything in it. - Repository-scoped commands take the repository root exactly and fail when it is gone instead of running in an enclosing repository; path-based ones resolve the file's repository (Project::with_repo_of). - The rescan on a repository appearing or going watches `.git/HEAD`: the watcher never reports a `.git` path itself, so the old check never fired. - Status skips a repository that cannot be read; refresh falls back to the primary repository when the selected one is gone instead of failing every time. - Commit and Compare-with-Local diffs resolve repository-relative paths against the selected repository's root, not the project root. - Update Project and Fetch go on after a failing repository and report the failures together; Update skips repositories with a detached HEAD or no upstream. - The Push dialog leaves out a repository that cannot be read. - Shelve and patches fail clearly for changes of a nested repository instead of leaving them out silently (they still support the main repository only). - Fix the repository discovery test on Windows (path separators); Chinese text for the new strings.
Цей коміт міститься в:
1 джерело
c868ec8b68
коміт
b8f422d451
11 files changed
+211
-68
No files matched your search
@@ -74,7 +74,7 @@ pub async fn vcs_branch_info(
|
||||
pub fn vcs_line_hunks(window: Window, state: State<'_, AppState>, path: PathBuf) -> IdeResult<Vec<LineHunk>> {
|
||||
let project = state.project(window.label())?;
|
||||
let text = project.text_of(&path)?;
|
||||
project.with_repo_at(Some(&path), |r| r.line_hunks(&path, &text, false))
|
||||
project.with_repo_of(&path, |r| r.line_hunks(&path, &text, false))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -86,7 +86,7 @@ pub async fn vcs_file_at(
|
||||
rev: Option<String>,
|
||||
) -> IdeResult<Option<String>> {
|
||||
let project = state.project(window.label())?;
|
||||
blocking(move || project.with_repo_at(Some(&path), |r| r.file_at(&path, rev.as_deref()))).await
|
||||
blocking(move || project.with_repo_of(&path, |r| r.file_at(&path, rev.as_deref()))).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -94,7 +94,7 @@ pub async fn vcs_file_at(
|
||||
pub async fn vcs_blame(window: Window, state: State<'_, AppState>, path: PathBuf) -> IdeResult<Vec<BlameLine>> {
|
||||
let project = state.project(window.label())?;
|
||||
let text = project.documents.text_of(&path);
|
||||
blocking(move || project.with_repo_at(Some(&path), |r| r.blame(&path, text.as_deref()))).await
|
||||
blocking(move || project.with_repo_of(&path, |r| r.blame(&path, text.as_deref()))).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -401,7 +401,7 @@ pub async fn vcs_conflict_versions(
|
||||
) -> IdeResult<ConflictVersions> {
|
||||
let project = state.project(window.label())?;
|
||||
blocking(move || {
|
||||
project.with_repo_at(Some(&path), |r| {
|
||||
project.with_repo_of(&path, |r| {
|
||||
r.conflict_versions(&path).map(|(base, ours, theirs)| ConflictVersions { base, ours, theirs })
|
||||
})
|
||||
})
|
||||
@@ -417,7 +417,7 @@ pub async fn vcs_diff_text(
|
||||
staged: bool,
|
||||
) -> IdeResult<String> {
|
||||
let project = state.project(window.label())?;
|
||||
blocking(move || project.with_repo_at(Some(&path), |r| r.diff_text(&path, staged))).await
|
||||
blocking(move || project.with_repo_of(&path, |r| r.diff_text(&path, staged))).await
|
||||
}
|
||||
|
||||
/// Line hunks between two arbitrary texts (diff viewer, commit dialog).
|
||||
@@ -434,7 +434,7 @@ pub fn vcs_diff_hunks(old_text: String, new_text: String) -> Vec<LineHunk> {
|
||||
pub fn vcs_stage_hunks(window: Window, state: State<'_, AppState>, path: PathBuf, hunks: Vec<u32>) -> IdeResult<()> {
|
||||
let project = state.project(window.label())?;
|
||||
let current = ide_vfs::load_text(&path)?.text;
|
||||
project.with_repo_at(Some(&path), |r| {
|
||||
project.with_repo_of(&path, |r| {
|
||||
let index_text = r.file_at(&path, Some(":0"))?.unwrap_or_default();
|
||||
let rel = path.strip_prefix(r.root()).unwrap_or(&path).to_string_lossy().replace('\\', "/");
|
||||
let selected: Vec<usize> = hunks.iter().map(|&h| h as usize).collect();
|
||||
@@ -639,6 +639,20 @@ fn per_repo(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Shelf and patches work in the primary repository only: changes of a nested one
|
||||
/// fail instead of being left out silently.
|
||||
fn in_primary_repo(project: &Project, paths: &[PathBuf]) -> IdeResult<()> {
|
||||
let repos = project.repos();
|
||||
let primary = repos.primary().map(|r| r.root());
|
||||
match paths.iter().find(|p| repos.for_path(p).map(|r| r.root()) != primary) {
|
||||
Some(p) => Err(ide_core::IdeError::invalid(format!(
|
||||
"{} is in another git repository; shelving and patches support the project's main repository only",
|
||||
p.display()
|
||||
))),
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
async fn blocking<T: Send + 'static>(f: impl FnOnce() -> IdeResult<T> + Send + 'static) -> IdeResult<T> {
|
||||
tauri::async_runtime::spawn_blocking(f).await.map_err(|e| ide_core::IdeError::internal(e.to_string()))?
|
||||
}
|
||||
@@ -662,6 +676,7 @@ pub async fn vcs_create_patch(window: Window, state: State<'_, AppState>, source
|
||||
project.with_repo(|r| match source {
|
||||
PatchSource::Changes { paths, reverse } => {
|
||||
let paths: Vec<PathBuf> = paths.into_iter().map(Into::into).collect();
|
||||
in_primary_repo(&project, &paths)?;
|
||||
r.create_patch(&paths, reverse)
|
||||
}
|
||||
PatchSource::Commits { commits, reverse } => r.create_commits_patch(&commits, reverse),
|
||||
@@ -709,6 +724,7 @@ pub async fn vcs_shelve(
|
||||
) -> IdeResult<ShelvedChangeList> {
|
||||
let project = state.project(window.label())?;
|
||||
blocking(move || {
|
||||
in_primary_repo(&project, &paths)?;
|
||||
keep_private(&project.root);
|
||||
project.with_repo_recorded(None, Some("Shelve"), |r| r.shelve(&shelf_dir(&project.root), &name, &paths))
|
||||
})
|
||||
|
||||
@@ -310,9 +310,16 @@ impl AppState {
|
||||
sink.emit(IdeEvent::ProjectRootsChanged { root: root.clone().into() });
|
||||
}
|
||||
}
|
||||
// A repository was cloned / initialised / removed below the project.
|
||||
let git_dirs =
|
||||
changed.iter().chain(&batch.removed).any(|p| p.file_name().is_some_and(|n| n == ".git"));
|
||||
// A repository was cloned / initialised / removed below the project: its
|
||||
// `.git/HEAD` (the watcher reports nothing else of `.git`) appeared or went.
|
||||
let known = repos.read().roots();
|
||||
let git_dirs = changed.iter().chain(&batch.removed).any(|p| {
|
||||
let Some(dir) = p.ends_with(".git/HEAD").then(|| p.parent().and_then(Path::parent)).flatten()
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
known.iter().any(|r| r == dir) != p.is_file()
|
||||
});
|
||||
if git_dirs {
|
||||
scan_repositories(&root, &repos, &settings, &sink);
|
||||
}
|
||||
@@ -429,23 +436,38 @@ impl Project {
|
||||
self.repos.read().clone()
|
||||
}
|
||||
|
||||
/// Runs `f` with the repository containing `at` (a path in it or its root), the
|
||||
/// project's primary repository when `None`; fails when there is none.
|
||||
/// Runs `f` with the repository whose root is `at`, the project's primary repository
|
||||
/// when `None`; fails when there is none (a repository-scoped command never falls back
|
||||
/// to another repository).
|
||||
pub fn with_repo_at<T>(&self, at: Option<&Path>, f: impl FnOnce(&Repo) -> IdeResult<T>) -> IdeResult<T> {
|
||||
let repos = self.repos();
|
||||
match at {
|
||||
Some(root) => match repos.at_root(root) {
|
||||
Some(repo) => self.run_in(repo, f),
|
||||
None => {
|
||||
Err(IdeError::unavailable(format!("{} is not a git repository of the project", root.display())))
|
||||
}
|
||||
},
|
||||
None => match repos.primary() {
|
||||
Some(repo) => self.run_in(repo, f),
|
||||
None => Err(IdeError::unavailable("project is not a git repository")),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs `f` with the repository `path` (a file or folder) belongs to.
|
||||
pub fn with_repo_of<T>(&self, path: &Path, f: impl FnOnce(&Repo) -> IdeResult<T>) -> IdeResult<T> {
|
||||
match self.repos().for_path(path) {
|
||||
Some(repo) => self.run_in(repo, f),
|
||||
None => Err(IdeError::unavailable(format!("{} is not in a git repository", path.display()))),
|
||||
}
|
||||
}
|
||||
|
||||
fn run_in<T>(&self, repo: &Repo, f: impl FnOnce(&Repo) -> IdeResult<T>) -> IdeResult<T> {
|
||||
let settings = self.settings.effective();
|
||||
crate::commands::vcs::apply_git_executable(&settings);
|
||||
let repos = self.repos();
|
||||
let repo = match at {
|
||||
Some(path) => repos.for_path(path),
|
||||
None => repos.primary(),
|
||||
};
|
||||
match repo {
|
||||
Some(repo) => {
|
||||
repo.set_ignored_names(settings.ignored_names());
|
||||
f(repo)
|
||||
}
|
||||
None => Err(IdeError::unavailable("project is not a git repository")),
|
||||
}
|
||||
repo.set_ignored_names(settings.ignored_names());
|
||||
f(repo)
|
||||
}
|
||||
|
||||
/// Runs `f` with the project's primary git repository, or fails when the project is not under git.
|
||||
|
||||
Reference in new issue
Заблокувати користувача