fix(git): restore non-repo fallback and refine repo preflight errors

Reintroduce a local not-a-git-repo fallback state in the git log panel so non-repo errors still render the dedicated empty state when workspace state streaming is degraded.

Improve git repository preflight classification by distinguishing missing paths, permission issues, and non-directory targets before checking .git presence.

Add not_a_git_repository to the frontend AppErrorCode union for explicit typed handling.
This commit is contained in:
xintaofei
2026-04-18 23:49:42 +08:00
parent b17328b0fc
commit 95a0c527c4
3 changed files with 60 additions and 10 deletions

View File

@@ -14,7 +14,7 @@
//! Bare repositories are intentionally not supported — they have no working //! Bare repositories are intentionally not supported — they have no working
//! tree, which makes them an unusual target for a workspace-oriented editor. //! tree, which makes them an unusual target for a workspace-oriented editor.
use std::path::Path; use std::{fs, io::ErrorKind, path::Path};
use crate::app_error::AppCommandError; use crate::app_error::AppCommandError;
@@ -30,11 +30,40 @@ pub fn is_git_repo(path: &Path) -> bool {
/// when the target path is not a git working tree, so callers avoid locale- /// when the target path is not a git working tree, so callers avoid locale-
/// dependent stderr parsing for the most common "wrong folder" failure. /// dependent stderr parsing for the most common "wrong folder" failure.
pub fn ensure_git_repo(path: &str) -> Result<(), AppCommandError> { pub fn ensure_git_repo(path: &str) -> Result<(), AppCommandError> {
if is_git_repo(Path::new(path)) { let root = Path::new(path);
Ok(())
} else { let root_meta = fs::metadata(root).map_err(|err| match err.kind() {
Err(AppCommandError::not_a_git_repository(format!( ErrorKind::NotFound => {
"Not a Git repository: {path}" AppCommandError::not_found(format!("Workspace path does not exist: {path}"))
))) }
ErrorKind::PermissionDenied => {
AppCommandError::permission_denied(format!("Cannot access workspace path: {path}"))
.with_detail(err.to_string())
}
_ => AppCommandError::io(err)
.with_detail(format!("Failed to inspect workspace path: {path}")),
})?;
if !root_meta.is_dir() {
return Err(AppCommandError::invalid_input(format!(
"Workspace path is not a directory: {path}"
)));
}
let git_path = root.join(".git");
match fs::metadata(&git_path) {
Ok(_) => Ok(()),
Err(err) => match err.kind() {
ErrorKind::NotFound => Err(AppCommandError::not_a_git_repository(format!(
"Not a Git repository: {path}"
))),
ErrorKind::PermissionDenied => Err(AppCommandError::permission_denied(format!(
"Cannot access Git metadata: {}",
git_path.display()
))
.with_detail(err.to_string())),
_ => Err(AppCommandError::io(err)
.with_detail(format!("Failed to inspect Git metadata: {}", git_path.display()))),
},
} }
} }

View File

@@ -699,6 +699,7 @@ export function GitLogTab() {
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [refreshing, setRefreshing] = useState(false) const [refreshing, setRefreshing] = useState(false)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
const [notAGitRepo, setNotAGitRepo] = useState(false)
const [scrolled, setScrolled] = useState(false) const [scrolled, setScrolled] = useState(false)
const [openByCommit, setOpenByCommit] = useState<Record<string, boolean>>({}) const [openByCommit, setOpenByCommit] = useState<Record<string, boolean>>({})
const [branchesByCommit, setBranchesByCommit] = useState< const [branchesByCommit, setBranchesByCommit] = useState<
@@ -767,9 +768,9 @@ export function GitLogTab() {
// check in `git_list_all_branches` would short-circuit on non-git folders // check in `git_list_all_branches` would short-circuit on non-git folders
// anyway, but skipping the call saves an unnecessary round trip. // anyway, but skipping the call saves an unnecessary round trip.
useEffect(() => { useEffect(() => {
if (!isGitRepo) return if (!isGitRepo || notAGitRepo) return
void refreshBranches() void refreshBranches()
}, [isGitRepo, refreshBranches]) }, [isGitRepo, notAGitRepo, refreshBranches])
const fetchCommitBranches = useCallback( const fetchCommitBranches = useCallback(
async (fullHash: string) => { async (fullHash: string) => {
@@ -814,6 +815,7 @@ export function GitLogTab() {
setBranchesError({}) setBranchesError({})
} }
setError(null) setError(null)
setNotAGitRepo(false)
try { try {
const result = await gitLog(folder.path, 100, branch ?? undefined) const result = await gitLog(folder.path, 100, branch ?? undefined)
setEntries(result.entries) setEntries(result.entries)
@@ -836,6 +838,7 @@ export function GitLogTab() {
} }
} catch (e) { } catch (e) {
if (isNotAGitRepoError(e)) { if (isNotAGitRepoError(e)) {
setNotAGitRepo(true)
// Workspace state will flip isGitRepo within the next watch flush; // Workspace state will flip isGitRepo within the next watch flush;
// clear entries so stale log data does not linger while we wait. // clear entries so stale log data does not linger while we wait.
setEntries([]) setEntries([])
@@ -853,6 +856,10 @@ export function GitLogTab() {
[folder?.path, selectedBranch] [folder?.path, selectedBranch]
) )
useEffect(() => {
setNotAGitRepo(false)
}, [folder?.path])
const handleRefresh = useCallback(() => { const handleRefresh = useCallback(() => {
void fetchLog({ inline: true }) void fetchLog({ inline: true })
}, [fetchLog]) }, [fetchLog])
@@ -969,6 +976,7 @@ export function GitLogTab() {
// and either re-fetches or clears the log to stay aligned with the other // and either re-fetches or clears the log to stay aligned with the other
// workspace panels. // workspace panels.
if (!isGitRepo) { if (!isGitRepo) {
setNotAGitRepo(false)
setEntries([]) setEntries([])
setError(null) setError(null)
setLoading(false) setLoading(false)
@@ -1042,7 +1050,7 @@ export function GitLogTab() {
) )
} }
if (!isGitRepo) { if (!isGitRepo || notAGitRepo) {
return ( return (
<ScrollArea className="h-full px-3 py-3"> <ScrollArea className="h-full px-3 py-3">
<div className="flex flex-col items-center justify-center min-h-full gap-1 p-6 text-center"> <div className="flex flex-col items-center justify-center min-h-full gap-1 p-6 text-center">
@@ -1051,6 +1059,18 @@ export function GitLogTab() {
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
{t("notAGitRepoHint")} {t("notAGitRepoHint")}
</p> </p>
{isGitRepo && (
<Button
variant="ghost"
size="xs"
className="mt-2"
onClick={() => {
void fetchLog()
}}
>
{t("retry")}
</Button>
)}
</div> </div>
</ScrollArea> </ScrollArea>
) )

View File

@@ -11,6 +11,7 @@ export type AppErrorCode =
| "configuration_missing" | "configuration_missing"
| "configuration_invalid" | "configuration_invalid"
| "not_found" | "not_found"
| "not_a_git_repository"
| "already_exists" | "already_exists"
| "permission_denied" | "permission_denied"
| "dependency_missing" | "dependency_missing"