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:
@@ -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()))),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user