fix(git): surface non-git-repo as a typed error and align all panels via workspace state
Consolidate `.git` presence detection into a shared `git_repo` module used by both the workspace state watcher and the command preflight helper, replacing duplicated local definitions. Introduce `AppErrorCode::NotAGitRepository` (HTTP 422) and preflight eleven frontend-callable git commands (log, status, list-branches, diff, diff-with-branch, show-diff, show-file, push-info, list-remotes, list-all-branches, commit-branches) so non-git folders short-circuit with a structured error instead of leaking locale-dependent git stderr. Frontend `isNotAGitRepoError` checks the error code first and falls back to a multi-language regex list centralized in `src/i18n/git-error-patterns.ts`, covering the nine languages git actually translates into. Wire the git log panel to `workspaceState.isGitRepo` rather than a local cached flag, so running `git init` or deleting `.git` externally propagates through the watcher and refreshes the panel automatically.
This commit is contained in:
@@ -9,6 +9,7 @@ pub enum AppErrorCode {
|
|||||||
ConfigurationMissing,
|
ConfigurationMissing,
|
||||||
ConfigurationInvalid,
|
ConfigurationInvalid,
|
||||||
NotFound,
|
NotFound,
|
||||||
|
NotAGitRepository,
|
||||||
AlreadyExists,
|
AlreadyExists,
|
||||||
PermissionDenied,
|
PermissionDenied,
|
||||||
DependencyMissing,
|
DependencyMissing,
|
||||||
@@ -65,6 +66,10 @@ impl AppCommandError {
|
|||||||
Self::new(AppErrorCode::NotFound, message)
|
Self::new(AppErrorCode::NotFound, message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn not_a_git_repository(message: impl Into<String>) -> Self {
|
||||||
|
Self::new(AppErrorCode::NotAGitRepository, message)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn already_exists(message: impl Into<String>) -> Self {
|
pub fn already_exists(message: impl Into<String>) -> Self {
|
||||||
Self::new(AppErrorCode::AlreadyExists, message)
|
Self::new(AppErrorCode::AlreadyExists, message)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -307,6 +307,8 @@ fn git_command_error(operation: &str, stderr: &[u8]) -> AppCommandError {
|
|||||||
AppCommandError::external_command(format!("git {operation} failed"), stderr)
|
AppCommandError::external_command(format!("git {operation} failed"), stderr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
use crate::git_repo::ensure_git_repo;
|
||||||
|
|
||||||
async fn detect_conflicts(path: &str) -> Result<Vec<String>, AppCommandError> {
|
async fn detect_conflicts(path: &str) -> Result<Vec<String>, AppCommandError> {
|
||||||
let output = crate::process::tokio_command("git")
|
let output = crate::process::tokio_command("git")
|
||||||
.args(["-c", "core.quotePath=false"])
|
.args(["-c", "core.quotePath=false"])
|
||||||
@@ -902,6 +904,8 @@ pub async fn git_fetch(
|
|||||||
|
|
||||||
#[cfg_attr(feature = "tauri-runtime", tauri::command)]
|
#[cfg_attr(feature = "tauri-runtime", tauri::command)]
|
||||||
pub async fn git_push_info(path: String) -> Result<GitPushInfo, AppCommandError> {
|
pub async fn git_push_info(path: String) -> Result<GitPushInfo, AppCommandError> {
|
||||||
|
ensure_git_repo(&path)?;
|
||||||
|
|
||||||
// Get current branch name
|
// Get current branch name
|
||||||
let branch_output = crate::process::tokio_command("git")
|
let branch_output = crate::process::tokio_command("git")
|
||||||
.args(["rev-parse", "--abbrev-ref", "HEAD"])
|
.args(["rev-parse", "--abbrev-ref", "HEAD"])
|
||||||
@@ -1186,6 +1190,8 @@ pub async fn git_reset(
|
|||||||
|
|
||||||
#[cfg_attr(feature = "tauri-runtime", tauri::command)]
|
#[cfg_attr(feature = "tauri-runtime", tauri::command)]
|
||||||
pub async fn git_list_branches(path: String) -> Result<Vec<String>, AppCommandError> {
|
pub async fn git_list_branches(path: String) -> Result<Vec<String>, AppCommandError> {
|
||||||
|
ensure_git_repo(&path)?;
|
||||||
|
|
||||||
let output = crate::process::tokio_command("git")
|
let output = crate::process::tokio_command("git")
|
||||||
.args(["branch", "--format=%(refname:short)"])
|
.args(["branch", "--format=%(refname:short)"])
|
||||||
.current_dir(&path)
|
.current_dir(&path)
|
||||||
@@ -1401,6 +1407,8 @@ pub async fn git_status(
|
|||||||
path: String,
|
path: String,
|
||||||
show_all_untracked: Option<bool>,
|
show_all_untracked: Option<bool>,
|
||||||
) -> Result<Vec<GitStatusEntry>, AppCommandError> {
|
) -> Result<Vec<GitStatusEntry>, AppCommandError> {
|
||||||
|
ensure_git_repo(&path)?;
|
||||||
|
|
||||||
let untracked_mode = if show_all_untracked.unwrap_or(false) {
|
let untracked_mode = if show_all_untracked.unwrap_or(false) {
|
||||||
"-uall"
|
"-uall"
|
||||||
} else {
|
} else {
|
||||||
@@ -1446,6 +1454,8 @@ pub async fn git_is_tracked(path: String, file: String) -> Result<bool, AppComma
|
|||||||
|
|
||||||
#[cfg_attr(feature = "tauri-runtime", tauri::command)]
|
#[cfg_attr(feature = "tauri-runtime", tauri::command)]
|
||||||
pub async fn git_diff(path: String, file: Option<String>) -> Result<String, AppCommandError> {
|
pub async fn git_diff(path: String, file: Option<String>) -> Result<String, AppCommandError> {
|
||||||
|
ensure_git_repo(&path)?;
|
||||||
|
|
||||||
let literal_file = file.as_deref().map(to_git_literal_pathspec);
|
let literal_file = file.as_deref().map(to_git_literal_pathspec);
|
||||||
let mut args = vec!["diff".to_string(), "HEAD".to_string()];
|
let mut args = vec!["diff".to_string(), "HEAD".to_string()];
|
||||||
if let Some(ref f) = literal_file {
|
if let Some(ref f) = literal_file {
|
||||||
@@ -1485,6 +1495,8 @@ pub async fn git_diff_with_branch(
|
|||||||
branch: String,
|
branch: String,
|
||||||
file: Option<String>,
|
file: Option<String>,
|
||||||
) -> Result<String, AppCommandError> {
|
) -> Result<String, AppCommandError> {
|
||||||
|
ensure_git_repo(&path)?;
|
||||||
|
|
||||||
let target_branch = branch.trim();
|
let target_branch = branch.trim();
|
||||||
if target_branch.is_empty() {
|
if target_branch.is_empty() {
|
||||||
return Err(AppCommandError::invalid_input(
|
return Err(AppCommandError::invalid_input(
|
||||||
@@ -1527,6 +1539,8 @@ pub async fn git_show_diff(
|
|||||||
commit: String,
|
commit: String,
|
||||||
file: Option<String>,
|
file: Option<String>,
|
||||||
) -> Result<String, AppCommandError> {
|
) -> Result<String, AppCommandError> {
|
||||||
|
ensure_git_repo(&path)?;
|
||||||
|
|
||||||
let literal_file = file.as_deref().map(to_git_literal_pathspec);
|
let literal_file = file.as_deref().map(to_git_literal_pathspec);
|
||||||
let mut args = vec![
|
let mut args = vec![
|
||||||
"show".to_string(),
|
"show".to_string(),
|
||||||
@@ -1559,6 +1573,8 @@ pub async fn git_show_file(
|
|||||||
file: String,
|
file: String,
|
||||||
ref_name: Option<String>,
|
ref_name: Option<String>,
|
||||||
) -> Result<String, AppCommandError> {
|
) -> Result<String, AppCommandError> {
|
||||||
|
ensure_git_repo(&path)?;
|
||||||
|
|
||||||
let git_ref = ref_name.unwrap_or_else(|| "HEAD".to_string());
|
let git_ref = ref_name.unwrap_or_else(|| "HEAD".to_string());
|
||||||
let file_spec = format!("{}:{}", git_ref, file);
|
let file_spec = format!("{}:{}", git_ref, file);
|
||||||
|
|
||||||
@@ -1783,6 +1799,8 @@ pub async fn git_add_files(path: String, files: Vec<String>) -> Result<(), AppCo
|
|||||||
|
|
||||||
#[cfg_attr(feature = "tauri-runtime", tauri::command)]
|
#[cfg_attr(feature = "tauri-runtime", tauri::command)]
|
||||||
pub async fn git_list_all_branches(path: String) -> Result<GitBranchList, AppCommandError> {
|
pub async fn git_list_all_branches(path: String) -> Result<GitBranchList, AppCommandError> {
|
||||||
|
ensure_git_repo(&path)?;
|
||||||
|
|
||||||
let local_fut = crate::process::tokio_command("git")
|
let local_fut = crate::process::tokio_command("git")
|
||||||
.args(["branch", "--format=%(refname:short)"])
|
.args(["branch", "--format=%(refname:short)"])
|
||||||
.current_dir(&path)
|
.current_dir(&path)
|
||||||
@@ -1857,6 +1875,8 @@ pub async fn git_list_all_branches(path: String) -> Result<GitBranchList, AppCom
|
|||||||
|
|
||||||
#[cfg_attr(feature = "tauri-runtime", tauri::command)]
|
#[cfg_attr(feature = "tauri-runtime", tauri::command)]
|
||||||
pub async fn git_list_remotes(path: String) -> Result<Vec<GitRemote>, AppCommandError> {
|
pub async fn git_list_remotes(path: String) -> Result<Vec<GitRemote>, AppCommandError> {
|
||||||
|
ensure_git_repo(&path)?;
|
||||||
|
|
||||||
let output = crate::process::tokio_command("git")
|
let output = crate::process::tokio_command("git")
|
||||||
.args(["remote", "-v"])
|
.args(["remote", "-v"])
|
||||||
.current_dir(&path)
|
.current_dir(&path)
|
||||||
@@ -3201,6 +3221,8 @@ pub async fn git_log(
|
|||||||
branch: Option<String>,
|
branch: Option<String>,
|
||||||
remote: Option<String>,
|
remote: Option<String>,
|
||||||
) -> Result<GitLogResult, AppCommandError> {
|
) -> Result<GitLogResult, AppCommandError> {
|
||||||
|
ensure_git_repo(&path)?;
|
||||||
|
|
||||||
const COMMIT_META_PREFIX: &str = "__COMMIT__\0";
|
const COMMIT_META_PREFIX: &str = "__COMMIT__\0";
|
||||||
const MESSAGE_END_MARKER: &str = "__COMMIT_MESSAGE_END__";
|
const MESSAGE_END_MARKER: &str = "__COMMIT_MESSAGE_END__";
|
||||||
|
|
||||||
@@ -3317,6 +3339,8 @@ pub async fn git_commit_branches(
|
|||||||
path: String,
|
path: String,
|
||||||
commit: String,
|
commit: String,
|
||||||
) -> Result<Vec<String>, AppCommandError> {
|
) -> Result<Vec<String>, AppCommandError> {
|
||||||
|
ensure_git_repo(&path)?;
|
||||||
|
|
||||||
let contains_arg = format!("--contains={commit}");
|
let contains_arg = format!("--contains={commit}");
|
||||||
let output = crate::process::tokio_command("git")
|
let output = crate::process::tokio_command("git")
|
||||||
.args([
|
.args([
|
||||||
|
|||||||
40
src-tauri/src/git_repo.rs
Normal file
40
src-tauri/src/git_repo.rs
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
//! Single source of truth for "is this path a git repository?" detection.
|
||||||
|
//!
|
||||||
|
//! The check is deliberately strict: the exact path must contain a `.git`
|
||||||
|
//! entry (directory for regular repos, file for linked worktrees and
|
||||||
|
//! submodules). We do **not** walk up to ancestors.
|
||||||
|
//!
|
||||||
|
//! Rationale: codeg scopes every workspace-facing feature (file tree
|
||||||
|
//! watcher, git changes panel, log panel) to the directory the user opens.
|
||||||
|
//! If one code path walks up and another doesn't, the UI falls into a
|
||||||
|
//! "schizophrenic" state where some panels see a repo and others don't.
|
||||||
|
//! Keeping the primitive strict forces every consumer onto the same
|
||||||
|
//! interpretation.
|
||||||
|
//!
|
||||||
|
//! Bare repositories are intentionally not supported — they have no working
|
||||||
|
//! tree, which makes them an unusual target for a workspace-oriented editor.
|
||||||
|
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use crate::app_error::AppCommandError;
|
||||||
|
|
||||||
|
/// Returns true when `path` is the root of a git working tree.
|
||||||
|
///
|
||||||
|
/// `.git` may be a directory (normal repo) or a file (worktree/submodule
|
||||||
|
/// pointer). `Path::exists` treats both as present.
|
||||||
|
pub fn is_git_repo(path: &Path) -> bool {
|
||||||
|
path.join(".git").exists()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Preflight guard for git commands. Short-circuits with a typed error code
|
||||||
|
/// when the target path is not a git working tree, so callers avoid locale-
|
||||||
|
/// dependent stderr parsing for the most common "wrong folder" failure.
|
||||||
|
pub fn ensure_git_repo(path: &str) -> Result<(), AppCommandError> {
|
||||||
|
if is_git_repo(Path::new(path)) {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(AppCommandError::not_a_git_repository(format!(
|
||||||
|
"Not a Git repository: {path}"
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ pub mod chat_channel;
|
|||||||
pub mod commands;
|
pub mod commands;
|
||||||
pub mod db;
|
pub mod db;
|
||||||
pub mod git_credential;
|
pub mod git_credential;
|
||||||
|
pub mod git_repo;
|
||||||
pub mod keyring_store;
|
pub mod keyring_store;
|
||||||
mod models;
|
mod models;
|
||||||
mod network;
|
mod network;
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ impl IntoResponse for AppCommandError {
|
|||||||
AppErrorCode::AuthenticationFailed => StatusCode::UNAUTHORIZED,
|
AppErrorCode::AuthenticationFailed => StatusCode::UNAUTHORIZED,
|
||||||
AppErrorCode::ConfigurationMissing
|
AppErrorCode::ConfigurationMissing
|
||||||
| AppErrorCode::ConfigurationInvalid
|
| AppErrorCode::ConfigurationInvalid
|
||||||
| AppErrorCode::DependencyMissing => StatusCode::UNPROCESSABLE_ENTITY,
|
| AppErrorCode::DependencyMissing
|
||||||
|
| AppErrorCode::NotAGitRepository => StatusCode::UNPROCESSABLE_ENTITY,
|
||||||
AppErrorCode::NetworkError
|
AppErrorCode::NetworkError
|
||||||
| AppErrorCode::DatabaseError
|
| AppErrorCode::DatabaseError
|
||||||
| AppErrorCode::IoError
|
| AppErrorCode::IoError
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ use tokio::sync::mpsc::error::TrySendError;
|
|||||||
|
|
||||||
use crate::app_error::AppCommandError;
|
use crate::app_error::AppCommandError;
|
||||||
use crate::commands::folders::{self, FileTreeNode};
|
use crate::commands::folders::{self, FileTreeNode};
|
||||||
|
use crate::git_repo::is_git_repo;
|
||||||
use crate::web::event_bridge::{emit_event, EventEmitter};
|
use crate::web::event_bridge::{emit_event, EventEmitter};
|
||||||
|
|
||||||
pub const WORKSPACE_STATE_PROTOCOL_VERSION: u16 = 1;
|
pub const WORKSPACE_STATE_PROTOCOL_VERSION: u16 = 1;
|
||||||
@@ -580,10 +581,6 @@ async fn git_numstat_map(path: &str) -> HashMap<String, (i32, i32)> {
|
|||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_git_repo(root: &Path) -> bool {
|
|
||||||
root.join(".git").exists()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn collect_git_snapshot(path: &str) -> Result<Vec<WorkspaceGitEntry>, AppCommandError> {
|
async fn collect_git_snapshot(path: &str) -> Result<Vec<WorkspaceGitEntry>, AppCommandError> {
|
||||||
let status_entries = folders::git_status(path.to_string(), Some(true)).await?;
|
let status_entries = folders::git_status(path.to_string(), Some(true)).await?;
|
||||||
|
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ import { Skeleton } from "@/components/ui/skeleton"
|
|||||||
import { subscribe } from "@/lib/platform"
|
import { subscribe } from "@/lib/platform"
|
||||||
import { useFolderContext } from "@/contexts/folder-context"
|
import { useFolderContext } from "@/contexts/folder-context"
|
||||||
import { useWorkspaceContext } from "@/contexts/workspace-context"
|
import { useWorkspaceContext } from "@/contexts/workspace-context"
|
||||||
|
import { useWorkspaceStateStore } from "@/hooks/use-workspace-state-store"
|
||||||
import {
|
import {
|
||||||
getGitBranch,
|
getGitBranch,
|
||||||
gitCommitBranches,
|
gitCommitBranches,
|
||||||
@@ -692,11 +693,12 @@ export function GitLogTab() {
|
|||||||
const tCommon = useTranslations("Folder.common")
|
const tCommon = useTranslations("Folder.common")
|
||||||
const { folder } = useFolderContext()
|
const { folder } = useFolderContext()
|
||||||
const { openCommitDiff, openFilePreview } = useWorkspaceContext()
|
const { openCommitDiff, openFilePreview } = useWorkspaceContext()
|
||||||
|
const workspaceState = useWorkspaceStateStore(folder?.path ?? null)
|
||||||
|
const isGitRepo = workspaceState.isGitRepo
|
||||||
const [entries, setEntries] = useState<GitLogEntry[]>([])
|
const [entries, setEntries] = useState<GitLogEntry[]>([])
|
||||||
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<
|
||||||
@@ -761,10 +763,13 @@ export function GitLogTab() {
|
|||||||
[folder?.path]
|
[folder?.path]
|
||||||
)
|
)
|
||||||
|
|
||||||
// Fetch branches on mount
|
// Fetch branches on mount and when git presence flips — the preflight
|
||||||
|
// check in `git_list_all_branches` would short-circuit on non-git folders
|
||||||
|
// anyway, but skipping the call saves an unnecessary round trip.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!isGitRepo) return
|
||||||
void refreshBranches()
|
void refreshBranches()
|
||||||
}, [refreshBranches])
|
}, [isGitRepo, refreshBranches])
|
||||||
|
|
||||||
const fetchCommitBranches = useCallback(
|
const fetchCommitBranches = useCallback(
|
||||||
async (fullHash: string) => {
|
async (fullHash: string) => {
|
||||||
@@ -809,7 +814,6 @@ 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)
|
||||||
@@ -832,7 +836,8 @@ 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;
|
||||||
|
// clear entries so stale log data does not linger while we wait.
|
||||||
setEntries([])
|
setEntries([])
|
||||||
} else {
|
} else {
|
||||||
setError(toErrorMessage(e))
|
setError(toErrorMessage(e))
|
||||||
@@ -958,8 +963,19 @@ export function GitLogTab() {
|
|||||||
])
|
])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!folder?.path) return
|
||||||
|
// Only fetch when workspaceState says we're in a git repo. When it flips
|
||||||
|
// (user runs `git init` / deletes `.git` externally), this effect re-runs
|
||||||
|
// and either re-fetches or clears the log to stay aligned with the other
|
||||||
|
// workspace panels.
|
||||||
|
if (!isGitRepo) {
|
||||||
|
setEntries([])
|
||||||
|
setError(null)
|
||||||
|
setLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
void fetchLog()
|
void fetchLog()
|
||||||
}, [fetchLog])
|
}, [folder?.path, isGitRepo, fetchLog])
|
||||||
|
|
||||||
// Refresh branches & log on branch change, commit, or push
|
// Refresh branches & log on branch change, commit, or push
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -1026,7 +1042,7 @@ export function GitLogTab() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (notAGitRepo) {
|
if (!isGitRepo) {
|
||||||
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">
|
||||||
|
|||||||
29
src/i18n/git-error-patterns.ts
Normal file
29
src/i18n/git-error-patterns.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* Locale-independent patterns that identify a "not a git repository" error
|
||||||
|
* from the raw stderr of a failed git invocation.
|
||||||
|
*
|
||||||
|
* Git translates its error messages via gettext based on the **system**
|
||||||
|
* LC_MESSAGES locale, which may differ from the user's browser locale.
|
||||||
|
* Detection therefore needs patterns for every language git might emit on
|
||||||
|
* the server, not just the one the UI is localized into.
|
||||||
|
*
|
||||||
|
* These patterns are a belt-and-suspenders fallback. The primary detection
|
||||||
|
* path is the typed `not_a_git_repository` error code returned by backend
|
||||||
|
* commands wrapped with a filesystem preflight check (see
|
||||||
|
* `src-tauri/src/commands/folders.rs::ensure_git_repo`). Patterns only apply
|
||||||
|
* when an un-preflighted command leaks raw stderr to the client.
|
||||||
|
*
|
||||||
|
* Locales covered match git's own gettext translations. Arabic falls back to
|
||||||
|
* English in upstream git (no ar translation), so no separate pattern.
|
||||||
|
*/
|
||||||
|
export const NOT_A_GIT_REPO_PATTERNS: readonly RegExp[] = [
|
||||||
|
/not a git repository/i, // en
|
||||||
|
/不是\s*git\s*仓库/i, // zh-CN
|
||||||
|
/不是\s*git\s*儲存庫/i, // zh-TW
|
||||||
|
/git\s*リポジトリではありません/i, // ja
|
||||||
|
/git\s*저장소가\s*아닙니다/i, // ko
|
||||||
|
/kein\s*git[-\s]*repository/i, // de
|
||||||
|
/pas\s*(?:un|dans\s*un)\s*d[ée]p[oô]t\s*git/i, // fr
|
||||||
|
/no\s*es\s*un\s*repositorio\s*git/i, // es
|
||||||
|
/n[ãa]o\s*[ée]\s*um\s*reposit[oó]rio\s*git/i, // pt
|
||||||
|
]
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { NOT_A_GIT_REPO_PATTERNS } from "@/i18n/git-error-patterns"
|
||||||
import type { AppCommandError } from "@/lib/types"
|
import type { AppCommandError } from "@/lib/types"
|
||||||
|
|
||||||
type ObjectLike = Record<string, unknown>
|
type ObjectLike = Record<string, unknown>
|
||||||
@@ -46,11 +47,19 @@ export function extractAppCommandError(error: unknown): AppCommandError | null {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Must mirror `AppErrorCode::NotAGitRepository` in src-tauri/src/app_error.rs.
|
||||||
|
// If the backend enum ever renames, both sides must change together.
|
||||||
|
export const NOT_A_GIT_REPO_CODE = "not_a_git_repository"
|
||||||
|
|
||||||
export function isNotAGitRepoError(error: unknown): boolean {
|
export function isNotAGitRepoError(error: unknown): boolean {
|
||||||
const appError = extractAppCommandError(error)
|
const appError = extractAppCommandError(error)
|
||||||
|
if (appError?.code === NOT_A_GIT_REPO_CODE) return true
|
||||||
|
|
||||||
const candidates = [appError?.detail, appError?.message]
|
const candidates = [appError?.detail, appError?.message]
|
||||||
return candidates.some(
|
return candidates.some(
|
||||||
(text) => typeof text === "string" && /not a git repository/i.test(text)
|
(text) =>
|
||||||
|
typeof text === "string" &&
|
||||||
|
NOT_A_GIT_REPO_PATTERNS.some((pattern) => pattern.test(text))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user