支持实时处理Git凭证
This commit is contained in:
@@ -20,32 +20,98 @@ use crate::app_error::AppCommandError;
|
|||||||
use crate::db::error::DbError;
|
use crate::db::error::DbError;
|
||||||
use crate::db::service::folder_service;
|
use crate::db::service::folder_service;
|
||||||
use crate::db::AppDatabase;
|
use crate::db::AppDatabase;
|
||||||
use crate::models::{FolderDetail, FolderHistoryEntry, OpenedConversation};
|
use crate::models::{FolderDetail, FolderHistoryEntry, GitCredentials, OpenedConversation};
|
||||||
|
|
||||||
/// Inject stored GitHub credentials into a git command for a given repository.
|
/// Configure a git command for remote operations:
|
||||||
async fn inject_repo_credentials(
|
/// - Always disable interactive prompts (prevent hanging in a GUI app)
|
||||||
|
/// - If explicit credentials are provided, use them directly
|
||||||
|
/// - Otherwise, try to inject stored account credentials
|
||||||
|
async fn prepare_remote_git_cmd(
|
||||||
cmd: &mut tokio::process::Command,
|
cmd: &mut tokio::process::Command,
|
||||||
repo_path: &str,
|
repo_path: &str,
|
||||||
|
credentials: Option<&GitCredentials>,
|
||||||
db: &AppDatabase,
|
db: &AppDatabase,
|
||||||
app_handle: &tauri::AppHandle,
|
app_handle: &tauri::AppHandle,
|
||||||
) {
|
) {
|
||||||
|
cmd.env("GIT_TERMINAL_PROMPT", "0")
|
||||||
|
.stdin(Stdio::null());
|
||||||
|
|
||||||
if let Ok(data_dir) = app_handle.path().app_data_dir() {
|
if let Ok(data_dir) = app_handle.path().app_data_dir() {
|
||||||
crate::git_credential::try_inject_for_repo(cmd, repo_path, &db.conn, &data_dir).await;
|
if let Some(creds) = credentials {
|
||||||
|
// Explicit credentials provided (e.g. from credential dialog)
|
||||||
|
if let Ok(askpass) = crate::git_credential::ensure_askpass_script(&data_dir) {
|
||||||
|
crate::git_credential::inject_credentials(
|
||||||
|
cmd,
|
||||||
|
&creds.username,
|
||||||
|
&creds.password,
|
||||||
|
&askpass,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fall back to stored accounts
|
||||||
|
crate::git_credential::try_inject_for_repo(cmd, repo_path, &db.conn, &data_dir).await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Inject stored GitHub credentials for a clone URL (no repo path yet).
|
/// Same as `prepare_remote_git_cmd` but for clone (URL only, no repo yet).
|
||||||
async fn inject_url_credentials(
|
async fn prepare_remote_git_cmd_for_url(
|
||||||
cmd: &mut tokio::process::Command,
|
cmd: &mut tokio::process::Command,
|
||||||
clone_url: &str,
|
clone_url: &str,
|
||||||
|
credentials: Option<&GitCredentials>,
|
||||||
db: &AppDatabase,
|
db: &AppDatabase,
|
||||||
app_handle: &tauri::AppHandle,
|
app_handle: &tauri::AppHandle,
|
||||||
) {
|
) {
|
||||||
|
cmd.env("GIT_TERMINAL_PROMPT", "0")
|
||||||
|
.stdin(Stdio::null());
|
||||||
|
|
||||||
if let Ok(data_dir) = app_handle.path().app_data_dir() {
|
if let Ok(data_dir) = app_handle.path().app_data_dir() {
|
||||||
crate::git_credential::try_inject_for_url(cmd, clone_url, &db.conn, &data_dir).await;
|
if let Some(creds) = credentials {
|
||||||
|
if let Ok(askpass) = crate::git_credential::ensure_askpass_script(&data_dir) {
|
||||||
|
crate::git_credential::inject_credentials(
|
||||||
|
cmd,
|
||||||
|
&creds.username,
|
||||||
|
&creds.password,
|
||||||
|
&askpass,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
crate::git_credential::try_inject_for_url(cmd, clone_url, &db.conn, &data_dir).await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Classify a git remote command error, detecting authentication failures.
|
||||||
|
fn classify_remote_git_error(operation: &str, stderr: &[u8]) -> AppCommandError {
|
||||||
|
let msg = String::from_utf8_lossy(stderr).trim().to_string();
|
||||||
|
let lower = msg.to_lowercase();
|
||||||
|
|
||||||
|
if lower.contains("authentication failed")
|
||||||
|
|| lower.contains("invalid credentials")
|
||||||
|
|| lower.contains("could not read username")
|
||||||
|
|| lower.contains("could not read password")
|
||||||
|
|| lower.contains("logon failed")
|
||||||
|
|| lower.contains("401")
|
||||||
|
|| lower.contains("403")
|
||||||
|
{
|
||||||
|
return AppCommandError::authentication_failed(format!(
|
||||||
|
"git {operation}: authentication failed. Configure a GitHub account in Settings → Version Control."
|
||||||
|
))
|
||||||
|
.with_detail(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
if lower.contains("could not resolve host")
|
||||||
|
|| lower.contains("unable to access")
|
||||||
|
|| lower.contains("connection refused")
|
||||||
|
|| lower.contains("network is unreachable")
|
||||||
|
{
|
||||||
|
return AppCommandError::network(format!("git {operation}: network error"))
|
||||||
|
.with_detail(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
AppCommandError::external_command(format!("git {operation} failed"), msg)
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
pub struct GitStatusEntry {
|
pub struct GitStatusEntry {
|
||||||
pub status: String,
|
pub status: String,
|
||||||
@@ -438,6 +504,7 @@ pub async fn create_folder_directory(path: String) -> Result<(), AppCommandError
|
|||||||
pub async fn clone_repository(
|
pub async fn clone_repository(
|
||||||
url: String,
|
url: String,
|
||||||
target_dir: String,
|
target_dir: String,
|
||||||
|
credentials: Option<GitCredentials>,
|
||||||
db: tauri::State<'_, AppDatabase>,
|
db: tauri::State<'_, AppDatabase>,
|
||||||
app_handle: tauri::AppHandle,
|
app_handle: tauri::AppHandle,
|
||||||
) -> Result<(), AppCommandError> {
|
) -> Result<(), AppCommandError> {
|
||||||
@@ -449,7 +516,7 @@ pub async fn clone_repository(
|
|||||||
|
|
||||||
let mut cmd = crate::process::tokio_command("git");
|
let mut cmd = crate::process::tokio_command("git");
|
||||||
cmd.args(["clone", &url, &target_dir]);
|
cmd.args(["clone", &url, &target_dir]);
|
||||||
inject_url_credentials(&mut cmd, &url, &db, &app_handle).await;
|
prepare_remote_git_cmd_for_url(&mut cmd, &url, credentials.as_ref(), &db, &app_handle).await;
|
||||||
|
|
||||||
let output = cmd
|
let output = cmd
|
||||||
.output()
|
.output()
|
||||||
@@ -568,6 +635,7 @@ pub async fn git_init(path: String) -> Result<(), AppCommandError> {
|
|||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn git_pull(
|
pub async fn git_pull(
|
||||||
path: String,
|
path: String,
|
||||||
|
credentials: Option<GitCredentials>,
|
||||||
db: tauri::State<'_, AppDatabase>,
|
db: tauri::State<'_, AppDatabase>,
|
||||||
app_handle: tauri::AppHandle,
|
app_handle: tauri::AppHandle,
|
||||||
) -> Result<GitPullResult, AppCommandError> {
|
) -> Result<GitPullResult, AppCommandError> {
|
||||||
@@ -576,7 +644,7 @@ pub async fn git_pull(
|
|||||||
// Step 1: fetch from remote
|
// Step 1: fetch from remote
|
||||||
let mut fetch_cmd = crate::process::tokio_command("git");
|
let mut fetch_cmd = crate::process::tokio_command("git");
|
||||||
fetch_cmd.args(["fetch"]).current_dir(&path);
|
fetch_cmd.args(["fetch"]).current_dir(&path);
|
||||||
inject_repo_credentials(&mut fetch_cmd, &path, &db, &app_handle).await;
|
prepare_remote_git_cmd(&mut fetch_cmd, &path, credentials.as_ref(), &db, &app_handle).await;
|
||||||
|
|
||||||
let fetch_output = fetch_cmd
|
let fetch_output = fetch_cmd
|
||||||
.output()
|
.output()
|
||||||
@@ -584,7 +652,7 @@ pub async fn git_pull(
|
|||||||
.map_err(AppCommandError::io)?;
|
.map_err(AppCommandError::io)?;
|
||||||
|
|
||||||
if !fetch_output.status.success() {
|
if !fetch_output.status.success() {
|
||||||
return Err(git_command_error("fetch", &fetch_output.stderr));
|
return Err(classify_remote_git_error("fetch", &fetch_output.stderr));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 2: check if upstream exists
|
// Step 2: check if upstream exists
|
||||||
@@ -743,12 +811,13 @@ pub async fn git_has_merge_head(path: String) -> Result<bool, AppCommandError> {
|
|||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn git_fetch(
|
pub async fn git_fetch(
|
||||||
path: String,
|
path: String,
|
||||||
|
credentials: Option<GitCredentials>,
|
||||||
db: tauri::State<'_, AppDatabase>,
|
db: tauri::State<'_, AppDatabase>,
|
||||||
app_handle: tauri::AppHandle,
|
app_handle: tauri::AppHandle,
|
||||||
) -> Result<String, AppCommandError> {
|
) -> Result<String, AppCommandError> {
|
||||||
let mut cmd = crate::process::tokio_command("git");
|
let mut cmd = crate::process::tokio_command("git");
|
||||||
cmd.args(["fetch", "--all"]).current_dir(&path);
|
cmd.args(["fetch", "--all"]).current_dir(&path);
|
||||||
inject_repo_credentials(&mut cmd, &path, &db, &app_handle).await;
|
prepare_remote_git_cmd(&mut cmd, &path, credentials.as_ref(), &db, &app_handle).await;
|
||||||
|
|
||||||
let output = cmd
|
let output = cmd
|
||||||
.output()
|
.output()
|
||||||
@@ -756,7 +825,7 @@ pub async fn git_fetch(
|
|||||||
.map_err(AppCommandError::io)?;
|
.map_err(AppCommandError::io)?;
|
||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
return Err(git_command_error("fetch --all", &output.stderr));
|
return Err(classify_remote_git_error("fetch --all", &output.stderr));
|
||||||
}
|
}
|
||||||
Ok(String::from_utf8_lossy(&output.stderr).trim().to_string())
|
Ok(String::from_utf8_lossy(&output.stderr).trim().to_string())
|
||||||
}
|
}
|
||||||
@@ -764,6 +833,7 @@ pub async fn git_fetch(
|
|||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn git_push(
|
pub async fn git_push(
|
||||||
path: String,
|
path: String,
|
||||||
|
credentials: Option<GitCredentials>,
|
||||||
db: tauri::State<'_, AppDatabase>,
|
db: tauri::State<'_, AppDatabase>,
|
||||||
app_handle: tauri::AppHandle,
|
app_handle: tauri::AppHandle,
|
||||||
) -> Result<GitPushResult, AppCommandError> {
|
) -> Result<GitPushResult, AppCommandError> {
|
||||||
@@ -794,17 +864,17 @@ pub async fn git_push(
|
|||||||
let mut cmd = crate::process::tokio_command("git");
|
let mut cmd = crate::process::tokio_command("git");
|
||||||
cmd.args(["push", "--set-upstream", "origin", &branch])
|
cmd.args(["push", "--set-upstream", "origin", &branch])
|
||||||
.current_dir(&path);
|
.current_dir(&path);
|
||||||
inject_repo_credentials(&mut cmd, &path, &db, &app_handle).await;
|
prepare_remote_git_cmd(&mut cmd, &path, credentials.as_ref(), &db, &app_handle).await;
|
||||||
cmd.output().await.map_err(AppCommandError::io)?
|
cmd.output().await.map_err(AppCommandError::io)?
|
||||||
} else {
|
} else {
|
||||||
let mut cmd = crate::process::tokio_command("git");
|
let mut cmd = crate::process::tokio_command("git");
|
||||||
cmd.args(["push"]).current_dir(&path);
|
cmd.args(["push"]).current_dir(&path);
|
||||||
inject_repo_credentials(&mut cmd, &path, &db, &app_handle).await;
|
prepare_remote_git_cmd(&mut cmd, &path, credentials.as_ref(), &db, &app_handle).await;
|
||||||
cmd.output().await.map_err(AppCommandError::io)?
|
cmd.output().await.map_err(AppCommandError::io)?
|
||||||
};
|
};
|
||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
return Err(git_command_error("push", &output.stderr));
|
return Err(classify_remote_git_error("push", &output.stderr));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(GitPushResult {
|
Ok(GitPushResult {
|
||||||
@@ -1553,15 +1623,13 @@ pub async fn git_list_remotes(path: String) -> Result<Vec<GitRemote>, AppCommand
|
|||||||
pub async fn git_fetch_remote(
|
pub async fn git_fetch_remote(
|
||||||
path: String,
|
path: String,
|
||||||
name: String,
|
name: String,
|
||||||
|
credentials: Option<GitCredentials>,
|
||||||
db: tauri::State<'_, AppDatabase>,
|
db: tauri::State<'_, AppDatabase>,
|
||||||
app_handle: tauri::AppHandle,
|
app_handle: tauri::AppHandle,
|
||||||
) -> Result<String, AppCommandError> {
|
) -> Result<String, AppCommandError> {
|
||||||
let mut cmd = crate::process::tokio_command("git");
|
let mut cmd = crate::process::tokio_command("git");
|
||||||
cmd.args(["fetch", &name])
|
cmd.args(["fetch", &name]).current_dir(&path);
|
||||||
.current_dir(&path)
|
prepare_remote_git_cmd(&mut cmd, &path, credentials.as_ref(), &db, &app_handle).await;
|
||||||
.env("GIT_TERMINAL_PROMPT", "0")
|
|
||||||
.stdin(std::process::Stdio::null());
|
|
||||||
inject_repo_credentials(&mut cmd, &path, &db, &app_handle).await;
|
|
||||||
|
|
||||||
let output = cmd
|
let output = cmd
|
||||||
.output()
|
.output()
|
||||||
@@ -1569,7 +1637,7 @@ pub async fn git_fetch_remote(
|
|||||||
.map_err(AppCommandError::io)?;
|
.map_err(AppCommandError::io)?;
|
||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
return Err(git_command_error("fetch", &output.stderr));
|
return Err(classify_remote_git_error("fetch", &output.stderr));
|
||||||
}
|
}
|
||||||
Ok(String::from_utf8_lossy(&output.stderr).trim().to_string())
|
Ok(String::from_utf8_lossy(&output.stderr).trim().to_string())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,6 @@ pub use conversation::{
|
|||||||
pub use folder::{FolderCommandInfo, FolderDetail, FolderHistoryEntry, OpenedConversation};
|
pub use folder::{FolderCommandInfo, FolderDetail, FolderHistoryEntry, OpenedConversation};
|
||||||
pub use message::{ContentBlock, MessageRole, MessageTurn, TurnRole, TurnUsage, UnifiedMessage};
|
pub use message::{ContentBlock, MessageRole, MessageTurn, TurnRole, TurnUsage, UnifiedMessage};
|
||||||
pub use system::{
|
pub use system::{
|
||||||
GitDetectResult, GitHubAccountsSettings, GitHubTokenValidation, GitSettings,
|
GitCredentials, GitDetectResult, GitHubAccountsSettings, GitHubTokenValidation, GitSettings,
|
||||||
SystemLanguageSettings, SystemProxySettings,
|
SystemLanguageSettings, SystemProxySettings,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -39,6 +39,13 @@ pub struct SystemLanguageSettings {
|
|||||||
|
|
||||||
// --- Version Control ---
|
// --- Version Control ---
|
||||||
|
|
||||||
|
/// Explicit credentials for a single git remote operation.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct GitCredentials {
|
||||||
|
pub username: String,
|
||||||
|
pub password: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct GitDetectResult {
|
pub struct GitDetectResult {
|
||||||
pub installed: bool,
|
pub installed: bool,
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
TerminalProvider,
|
TerminalProvider,
|
||||||
useTerminalContext,
|
useTerminalContext,
|
||||||
} from "@/contexts/terminal-context"
|
} from "@/contexts/terminal-context"
|
||||||
|
import { GitCredentialProvider } from "@/contexts/git-credential-context"
|
||||||
import {
|
import {
|
||||||
WorkspaceProvider,
|
WorkspaceProvider,
|
||||||
useWorkspaceContext,
|
useWorkspaceContext,
|
||||||
@@ -642,7 +643,8 @@ function FolderLayoutInner({ children }: { children: React.ReactNode }) {
|
|||||||
initialAgentType={agentType}
|
initialAgentType={agentType}
|
||||||
>
|
>
|
||||||
<AlertProvider>
|
<AlertProvider>
|
||||||
<TaskProvider>
|
<GitCredentialProvider>
|
||||||
|
<TaskProvider>
|
||||||
<AcpConnectionsProvider>
|
<AcpConnectionsProvider>
|
||||||
<ConversationRuntimeProvider>
|
<ConversationRuntimeProvider>
|
||||||
<WorkspaceProvider key={`workspace-${normalizedFolderId}`}>
|
<WorkspaceProvider key={`workspace-${normalizedFolderId}`}>
|
||||||
@@ -677,7 +679,8 @@ function FolderLayoutInner({ children }: { children: React.ReactNode }) {
|
|||||||
</WorkspaceProvider>
|
</WorkspaceProvider>
|
||||||
</ConversationRuntimeProvider>
|
</ConversationRuntimeProvider>
|
||||||
</AcpConnectionsProvider>
|
</AcpConnectionsProvider>
|
||||||
</TaskProvider>
|
</TaskProvider>
|
||||||
|
</GitCredentialProvider>
|
||||||
</AlertProvider>
|
</AlertProvider>
|
||||||
</FolderProvider>
|
</FolderProvider>
|
||||||
)
|
)
|
||||||
|
|||||||
8
src/app/welcome/layout.tsx
Normal file
8
src/app/welcome/layout.tsx
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import type { ReactNode } from "react"
|
||||||
|
import { GitCredentialProvider } from "@/contexts/git-credential-context"
|
||||||
|
|
||||||
|
export default function WelcomeLayout({ children }: { children: ReactNode }) {
|
||||||
|
return <GitCredentialProvider>{children}</GitCredentialProvider>
|
||||||
|
}
|
||||||
@@ -92,6 +92,7 @@ import { toast } from "sonner"
|
|||||||
import { useFolderContext } from "@/contexts/folder-context"
|
import { useFolderContext } from "@/contexts/folder-context"
|
||||||
import { useTaskContext } from "@/contexts/task-context"
|
import { useTaskContext } from "@/contexts/task-context"
|
||||||
import { useAlertContext } from "@/contexts/alert-context"
|
import { useAlertContext } from "@/contexts/alert-context"
|
||||||
|
import { useGitCredential } from "@/contexts/git-credential-context"
|
||||||
|
|
||||||
interface BranchDropdownProps {
|
interface BranchDropdownProps {
|
||||||
branch: string | null
|
branch: string | null
|
||||||
@@ -120,6 +121,7 @@ export function BranchDropdown({
|
|||||||
const folderPath = folder?.path ?? ""
|
const folderPath = folder?.path ?? ""
|
||||||
const { addTask, updateTask, removeTask } = useTaskContext()
|
const { addTask, updateTask, removeTask } = useTaskContext()
|
||||||
const { pushAlert } = useAlertContext()
|
const { pushAlert } = useAlertContext()
|
||||||
|
const { withCredentialRetry } = useGitCredential()
|
||||||
const [branchList, setBranchList] = useState<GitBranchList>({
|
const [branchList, setBranchList] = useState<GitBranchList>({
|
||||||
local: [],
|
local: [],
|
||||||
remote: [],
|
remote: [],
|
||||||
@@ -335,7 +337,10 @@ export function BranchDropdown({
|
|||||||
addTask(taskId, label)
|
addTask(taskId, label)
|
||||||
updateTask(taskId, { status: "running" })
|
updateTask(taskId, { status: "running" })
|
||||||
try {
|
try {
|
||||||
const result = await gitPush(folderPath)
|
const result = await withCredentialRetry(
|
||||||
|
(creds) => gitPush(folderPath, creds),
|
||||||
|
{ folderPath }
|
||||||
|
)
|
||||||
updateTask(taskId, { status: "completed" })
|
updateTask(taskId, { status: "completed" })
|
||||||
onBranchChange()
|
onBranchChange()
|
||||||
let description: string | undefined
|
let description: string | undefined
|
||||||
@@ -368,7 +373,10 @@ export function BranchDropdown({
|
|||||||
status: "running",
|
status: "running",
|
||||||
})
|
})
|
||||||
try {
|
try {
|
||||||
const pullResult = await gitPull(folderPath)
|
const pullResult = await withCredentialRetry(
|
||||||
|
(creds) => gitPull(folderPath, creds),
|
||||||
|
{ folderPath }
|
||||||
|
)
|
||||||
if (pullResult.conflict?.has_conflicts) {
|
if (pullResult.conflict?.has_conflicts) {
|
||||||
removeTask(taskId)
|
removeTask(taskId)
|
||||||
onBranchChange()
|
onBranchChange()
|
||||||
@@ -376,7 +384,10 @@ export function BranchDropdown({
|
|||||||
} else {
|
} else {
|
||||||
// Pull succeeded, retry push
|
// Pull succeeded, retry push
|
||||||
updateTask(taskId, { status: "running" })
|
updateTask(taskId, { status: "running" })
|
||||||
const pushResult = await gitPush(folderPath)
|
const pushResult = await withCredentialRetry(
|
||||||
|
(creds) => gitPush(folderPath, creds),
|
||||||
|
{ folderPath }
|
||||||
|
)
|
||||||
updateTask(taskId, { status: "completed" })
|
updateTask(taskId, { status: "completed" })
|
||||||
onBranchChange()
|
onBranchChange()
|
||||||
let description: string | undefined
|
let description: string | undefined
|
||||||
@@ -686,7 +697,11 @@ export function BranchDropdown({
|
|||||||
onSelect={() =>
|
onSelect={() =>
|
||||||
runGitTask(
|
runGitTask(
|
||||||
t("tasks.pullCode"),
|
t("tasks.pullCode"),
|
||||||
() => gitPull(folderPath),
|
() =>
|
||||||
|
withCredentialRetry(
|
||||||
|
(creds) => gitPull(folderPath, creds),
|
||||||
|
{ folderPath }
|
||||||
|
),
|
||||||
(result) => {
|
(result) => {
|
||||||
if (result.conflict?.has_conflicts) {
|
if (result.conflict?.has_conflicts) {
|
||||||
setConflictInfo(result.conflict)
|
setConflictInfo(result.conflict)
|
||||||
@@ -708,7 +723,12 @@ export function BranchDropdown({
|
|||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
onSelect={() =>
|
onSelect={() =>
|
||||||
runGitTask(t("tasks.fetchInfo"), () => gitFetch(folderPath))
|
runGitTask(t("tasks.fetchInfo"), () =>
|
||||||
|
withCredentialRetry(
|
||||||
|
(creds) => gitFetch(folderPath, creds),
|
||||||
|
{ folderPath }
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<RefreshCw className="h-3.5 w-3.5" />
|
<RefreshCw className="h-3.5 w-3.5" />
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { open } from "@tauri-apps/plugin-dialog"
|
|||||||
import { useTranslations } from "next-intl"
|
import { useTranslations } from "next-intl"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import { cloneRepository, openFolderWindow } from "@/lib/tauri"
|
import { cloneRepository, openFolderWindow } from "@/lib/tauri"
|
||||||
|
import { useGitCredential } from "@/contexts/git-credential-context"
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -25,6 +26,7 @@ interface CloneDialogProps {
|
|||||||
|
|
||||||
export function CloneDialog({ open: isOpen, onOpenChange }: CloneDialogProps) {
|
export function CloneDialog({ open: isOpen, onOpenChange }: CloneDialogProps) {
|
||||||
const t = useTranslations("WelcomePage")
|
const t = useTranslations("WelcomePage")
|
||||||
|
const { withCredentialRetry } = useGitCredential()
|
||||||
const [url, setUrl] = useState("")
|
const [url, setUrl] = useState("")
|
||||||
const [targetDir, setTargetDir] = useState("")
|
const [targetDir, setTargetDir] = useState("")
|
||||||
const [cloning, setCloning] = useState(false)
|
const [cloning, setCloning] = useState(false)
|
||||||
@@ -55,7 +57,10 @@ export function CloneDialog({ open: isOpen, onOpenChange }: CloneDialogProps) {
|
|||||||
setError(null)
|
setError(null)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await cloneRepository(url, fullPath)
|
await withCredentialRetry(
|
||||||
|
(creds) => cloneRepository(url, fullPath, creds),
|
||||||
|
{ remoteUrl: url }
|
||||||
|
)
|
||||||
await openFolderWindow(fullPath)
|
await openFolderWindow(fullPath)
|
||||||
onOpenChange(false)
|
onOpenChange(false)
|
||||||
resetForm()
|
resetForm()
|
||||||
|
|||||||
543
src/contexts/git-credential-context.tsx
Normal file
543
src/contexts/git-credential-context.tsx
Normal file
@@ -0,0 +1,543 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import {
|
||||||
|
createContext,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
type ReactNode,
|
||||||
|
} from "react"
|
||||||
|
import { Eye, EyeOff, Github, KeyRound, Loader2 } from "lucide-react"
|
||||||
|
import { useTranslations } from "next-intl"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { Input } from "@/components/ui/input"
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog"
|
||||||
|
import { extractAppCommandError } from "@/lib/app-error"
|
||||||
|
import type { GitCredentials } from "@/lib/types"
|
||||||
|
import {
|
||||||
|
gitListRemotes,
|
||||||
|
validateGitHubToken,
|
||||||
|
getGitHubAccounts,
|
||||||
|
updateGitHubAccounts,
|
||||||
|
} from "@/lib/tauri"
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Context
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Context for identifying the remote when credentials are needed.
|
||||||
|
* - `folderPath`: detect remote from an existing repo's origin URL.
|
||||||
|
* - `remoteUrl`: use this URL directly (e.g. for clone operations).
|
||||||
|
*/
|
||||||
|
export type GitRemoteHint =
|
||||||
|
| { folderPath: string }
|
||||||
|
| { remoteUrl: string }
|
||||||
|
|
||||||
|
interface GitCredentialContextValue {
|
||||||
|
/**
|
||||||
|
* Wrap an async git operation with automatic credential retry.
|
||||||
|
*
|
||||||
|
* - For GitHub remotes: shows a token dialog, validates via API,
|
||||||
|
* saves as a GitHub account, then retries the operation.
|
||||||
|
* - For other remotes: shows a generic username/password dialog.
|
||||||
|
*/
|
||||||
|
withCredentialRetry: <T>(
|
||||||
|
operation: (credentials?: GitCredentials) => Promise<T>,
|
||||||
|
hint: GitRemoteHint
|
||||||
|
) => Promise<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
const GitCredentialContext =
|
||||||
|
createContext<GitCredentialContextValue | null>(null)
|
||||||
|
|
||||||
|
export function useGitCredential(): GitCredentialContextValue {
|
||||||
|
const ctx = useContext(GitCredentialContext)
|
||||||
|
if (!ctx) {
|
||||||
|
throw new Error(
|
||||||
|
"useGitCredential must be used within GitCredentialProvider"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function isAuthError(error: unknown): boolean {
|
||||||
|
const appError = extractAppCommandError(error)
|
||||||
|
if (appError?.code === "authentication_failed") return true
|
||||||
|
|
||||||
|
const msg = appError?.detail ?? appError?.message ?? String(error)
|
||||||
|
const lower = msg.toLowerCase()
|
||||||
|
return (
|
||||||
|
lower.includes("authentication failed") ||
|
||||||
|
lower.includes("could not read username") ||
|
||||||
|
lower.includes("could not read password") ||
|
||||||
|
lower.includes("logon failed")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractHost(url: string): string | null {
|
||||||
|
const trimmed = url.trim()
|
||||||
|
// https://github.com/...
|
||||||
|
const httpsMatch = trimmed.match(/^https?:\/\/(?:[^@]+@)?([^/:]+)/)
|
||||||
|
if (httpsMatch) return httpsMatch[1].toLowerCase()
|
||||||
|
// git@github.com:...
|
||||||
|
const sshMatch = trimmed.match(/@([^/:]+)[:/]/)
|
||||||
|
if (sshMatch) return sshMatch[1].toLowerCase()
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function isGitHubHost(host: string | null): boolean {
|
||||||
|
return host === "github.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveRemoteHost(hint: GitRemoteHint): Promise<string | null> {
|
||||||
|
if ("remoteUrl" in hint) {
|
||||||
|
return extractHost(hint.remoteUrl)
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const remotes = await gitListRemotes(hint.folderPath)
|
||||||
|
const origin = remotes.find((r) => r.name === "origin") ?? remotes[0]
|
||||||
|
if (!origin) return null
|
||||||
|
return extractHost(origin.url)
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Provider
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
type DialogMode = "github" | "generic"
|
||||||
|
|
||||||
|
interface PendingRequest {
|
||||||
|
resolve: (credentials: GitCredentials | null) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Save generic credentials as a git account for future operations. */
|
||||||
|
async function saveGenericAccount(
|
||||||
|
host: string | null,
|
||||||
|
creds: GitCredentials
|
||||||
|
): Promise<void> {
|
||||||
|
const serverUrl = host ? `https://${host}` : "https://unknown"
|
||||||
|
try {
|
||||||
|
const existing = await getGitHubAccounts()
|
||||||
|
const isDuplicate = existing.accounts.some(
|
||||||
|
(a) =>
|
||||||
|
a.username === creds.username &&
|
||||||
|
extractHost(a.server_url) === host
|
||||||
|
)
|
||||||
|
if (!isDuplicate) {
|
||||||
|
await updateGitHubAccounts({
|
||||||
|
accounts: [
|
||||||
|
...existing.accounts,
|
||||||
|
{
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
server_url: serverUrl,
|
||||||
|
username: creds.username,
|
||||||
|
token: creds.password,
|
||||||
|
scopes: [],
|
||||||
|
avatar_url: null,
|
||||||
|
is_default: existing.accounts.length === 0,
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Non-critical — just skip saving
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GitCredentialProvider({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: ReactNode
|
||||||
|
}) {
|
||||||
|
const t = useTranslations("GitCredentialDialog")
|
||||||
|
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
const [mode, setMode] = useState<DialogMode>("generic")
|
||||||
|
const [remoteHost, setRemoteHost] = useState<string | null>(null)
|
||||||
|
|
||||||
|
// Generic mode fields
|
||||||
|
const [username, setUsername] = useState("")
|
||||||
|
const [password, setPassword] = useState("")
|
||||||
|
const [showPassword, setShowPassword] = useState(false)
|
||||||
|
|
||||||
|
// GitHub mode field
|
||||||
|
const [token, setToken] = useState("")
|
||||||
|
const [showToken, setShowToken] = useState(false)
|
||||||
|
|
||||||
|
// Save credentials checkbox (generic mode)
|
||||||
|
const [saveCredentials, setSaveCredentials] = useState(true)
|
||||||
|
|
||||||
|
const [submitting, setSubmitting] = useState(false)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
|
const pendingRef = useRef<PendingRequest | null>(null)
|
||||||
|
const saveCredentialsRef = useRef(saveCredentials)
|
||||||
|
saveCredentialsRef.current = saveCredentials
|
||||||
|
const remoteHostRef = useRef(remoteHost)
|
||||||
|
remoteHostRef.current = remoteHost
|
||||||
|
const modeRef = useRef(mode)
|
||||||
|
modeRef.current = mode
|
||||||
|
|
||||||
|
const resetForm = useCallback(() => {
|
||||||
|
setUsername("")
|
||||||
|
setPassword("")
|
||||||
|
setShowPassword(false)
|
||||||
|
setToken("")
|
||||||
|
setShowToken(false)
|
||||||
|
setSaveCredentials(true)
|
||||||
|
setError(null)
|
||||||
|
setSubmitting(false)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const requestCredentials = useCallback(
|
||||||
|
(dialogMode: DialogMode, host: string | null): Promise<GitCredentials | null> => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
pendingRef.current = { resolve }
|
||||||
|
resetForm()
|
||||||
|
setMode(dialogMode)
|
||||||
|
setRemoteHost(host)
|
||||||
|
setOpen(true)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[resetForm]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleCancel = useCallback(() => {
|
||||||
|
setOpen(false)
|
||||||
|
pendingRef.current?.resolve(null)
|
||||||
|
pendingRef.current = null
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// GitHub mode: validate token → save account → return credentials
|
||||||
|
const handleGitHubSubmit = useCallback(async () => {
|
||||||
|
const trimmedToken = token.trim()
|
||||||
|
if (!trimmedToken) return
|
||||||
|
|
||||||
|
setSubmitting(true)
|
||||||
|
setError(null)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const serverUrl = remoteHost
|
||||||
|
? `https://${remoteHost}`
|
||||||
|
: "https://github.com"
|
||||||
|
|
||||||
|
const result = await validateGitHubToken(serverUrl, trimmedToken)
|
||||||
|
if (!result.success) {
|
||||||
|
setError(result.message ?? t("invalidCredentials"))
|
||||||
|
setSubmitting(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save as GitHub account
|
||||||
|
try {
|
||||||
|
const existing = await getGitHubAccounts()
|
||||||
|
const isDuplicate = existing.accounts.some(
|
||||||
|
(a) =>
|
||||||
|
a.username === result.username &&
|
||||||
|
extractHost(a.server_url) === remoteHost
|
||||||
|
)
|
||||||
|
if (!isDuplicate) {
|
||||||
|
const newAccount = {
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
server_url: serverUrl,
|
||||||
|
username: result.username ?? "unknown",
|
||||||
|
token: trimmedToken,
|
||||||
|
scopes: result.scopes,
|
||||||
|
avatar_url: result.avatar_url,
|
||||||
|
is_default: existing.accounts.length === 0,
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
}
|
||||||
|
await updateGitHubAccounts({
|
||||||
|
accounts: [...existing.accounts, newAccount],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Saving account failed — not critical, continue with auth
|
||||||
|
}
|
||||||
|
|
||||||
|
const creds: GitCredentials = {
|
||||||
|
username: result.username ?? "unknown",
|
||||||
|
password: trimmedToken,
|
||||||
|
}
|
||||||
|
pendingRef.current?.resolve(creds)
|
||||||
|
pendingRef.current = null
|
||||||
|
} catch (err) {
|
||||||
|
const msg = err instanceof Error ? err.message : String(err)
|
||||||
|
setError(msg)
|
||||||
|
setSubmitting(false)
|
||||||
|
}
|
||||||
|
}, [token, remoteHost, t])
|
||||||
|
|
||||||
|
// Generic mode: return username + password directly
|
||||||
|
const handleGenericSubmit = useCallback(() => {
|
||||||
|
if (!username.trim() || !password.trim()) return
|
||||||
|
const creds: GitCredentials = {
|
||||||
|
username: username.trim(),
|
||||||
|
password: password.trim(),
|
||||||
|
}
|
||||||
|
setSubmitting(true)
|
||||||
|
pendingRef.current?.resolve(creds)
|
||||||
|
pendingRef.current = null
|
||||||
|
}, [username, password])
|
||||||
|
|
||||||
|
const handleSubmit = useCallback(() => {
|
||||||
|
if (mode === "github") {
|
||||||
|
handleGitHubSubmit()
|
||||||
|
} else {
|
||||||
|
handleGenericSubmit()
|
||||||
|
}
|
||||||
|
}, [mode, handleGitHubSubmit, handleGenericSubmit])
|
||||||
|
|
||||||
|
const withCredentialRetry = useCallback(
|
||||||
|
async <T,>(
|
||||||
|
operation: (credentials?: GitCredentials) => Promise<T>,
|
||||||
|
hint: GitRemoteHint
|
||||||
|
): Promise<T> => {
|
||||||
|
try {
|
||||||
|
return await operation()
|
||||||
|
} catch (firstError) {
|
||||||
|
if (!isAuthError(firstError)) throw firstError
|
||||||
|
|
||||||
|
// Detect remote host to decide dialog mode
|
||||||
|
const host = await resolveRemoteHost(hint)
|
||||||
|
const dialogMode: DialogMode = isGitHubHost(host)
|
||||||
|
? "github"
|
||||||
|
: "generic"
|
||||||
|
|
||||||
|
// Show credential dialog
|
||||||
|
const creds = await requestCredentials(dialogMode, host)
|
||||||
|
if (!creds) {
|
||||||
|
setOpen(false)
|
||||||
|
throw firstError
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: save credentials after successful operation
|
||||||
|
const maybeSave = async (c: GitCredentials) => {
|
||||||
|
if (modeRef.current === "generic" && saveCredentialsRef.current) {
|
||||||
|
await saveGenericAccount(remoteHostRef.current, c)
|
||||||
|
}
|
||||||
|
// GitHub mode saves during handleGitHubSubmit, no extra work needed
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retry with credentials
|
||||||
|
try {
|
||||||
|
const result = await operation(creds)
|
||||||
|
await maybeSave(creds)
|
||||||
|
setOpen(false)
|
||||||
|
return result
|
||||||
|
} catch (retryError) {
|
||||||
|
setSubmitting(false)
|
||||||
|
if (isAuthError(retryError)) {
|
||||||
|
setError(t("invalidCredentials"))
|
||||||
|
const retryCreds = await new Promise<GitCredentials | null>(
|
||||||
|
(resolve) => {
|
||||||
|
pendingRef.current = { resolve }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if (!retryCreds) {
|
||||||
|
setOpen(false)
|
||||||
|
throw retryError
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const result = await operation(retryCreds)
|
||||||
|
await maybeSave(retryCreds)
|
||||||
|
setOpen(false)
|
||||||
|
return result
|
||||||
|
} catch (thirdError) {
|
||||||
|
setOpen(false)
|
||||||
|
throw thirdError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setOpen(false)
|
||||||
|
throw retryError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[requestCredentials, t]
|
||||||
|
)
|
||||||
|
|
||||||
|
const canSubmitGitHub = token.trim().length > 0
|
||||||
|
const canSubmitGeneric = username.trim().length > 0 && password.trim().length > 0
|
||||||
|
const canSubmit = mode === "github" ? canSubmitGitHub : canSubmitGeneric
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GitCredentialContext.Provider value={{ withCredentialRetry }}>
|
||||||
|
{children}
|
||||||
|
|
||||||
|
<Dialog open={open} onOpenChange={(v) => !v && handleCancel()}>
|
||||||
|
<DialogContent className="sm:max-w-md">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
{mode === "github" ? (
|
||||||
|
<Github className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<KeyRound className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
{mode === "github" ? t("githubTitle") : t("title")}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{mode === "github" ? t("githubDescription") : t("description")}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-4 py-2">
|
||||||
|
{mode === "github" ? (
|
||||||
|
/* ---- GitHub Token Mode ---- */
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-xs font-medium text-muted-foreground">
|
||||||
|
{t("githubToken")}
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<Input
|
||||||
|
type={showToken ? "text" : "password"}
|
||||||
|
value={token}
|
||||||
|
onChange={(e) => {
|
||||||
|
setToken(e.target.value)
|
||||||
|
setError(null)
|
||||||
|
}}
|
||||||
|
placeholder={t("githubTokenPlaceholder")}
|
||||||
|
disabled={submitting}
|
||||||
|
className="pr-9"
|
||||||
|
autoFocus
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter" && canSubmitGitHub) handleSubmit()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="xs"
|
||||||
|
className="absolute right-1 top-1/2 -translate-y-1/2 h-6 w-6 p-0"
|
||||||
|
onClick={() => setShowToken(!showToken)}
|
||||||
|
tabIndex={-1}
|
||||||
|
>
|
||||||
|
{showToken ? (
|
||||||
|
<EyeOff className="h-3.5 w-3.5" />
|
||||||
|
) : (
|
||||||
|
<Eye className="h-3.5 w-3.5" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<p className="text-[11px] text-muted-foreground">
|
||||||
|
{t("githubTokenHint")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
/* ---- Generic Mode ---- */
|
||||||
|
<>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-xs font-medium text-muted-foreground">
|
||||||
|
{t("username")}
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => {
|
||||||
|
setUsername(e.target.value)
|
||||||
|
setError(null)
|
||||||
|
}}
|
||||||
|
placeholder={t("usernamePlaceholder")}
|
||||||
|
disabled={submitting}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-xs font-medium text-muted-foreground">
|
||||||
|
{t("password")}
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<Input
|
||||||
|
type={showPassword ? "text" : "password"}
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => {
|
||||||
|
setPassword(e.target.value)
|
||||||
|
setError(null)
|
||||||
|
}}
|
||||||
|
placeholder={t("passwordPlaceholder")}
|
||||||
|
disabled={submitting}
|
||||||
|
className="pr-9"
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter" && canSubmitGeneric)
|
||||||
|
handleSubmit()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="xs"
|
||||||
|
className="absolute right-1 top-1/2 -translate-y-1/2 h-6 w-6 p-0"
|
||||||
|
onClick={() => setShowPassword(!showPassword)}
|
||||||
|
tabIndex={-1}
|
||||||
|
>
|
||||||
|
{showPassword ? (
|
||||||
|
<EyeOff className="h-3.5 w-3.5" />
|
||||||
|
) : (
|
||||||
|
<Eye className="h-3.5 w-3.5" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<p className="text-[11px] text-muted-foreground">
|
||||||
|
{t("passwordHint")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<label className="inline-flex items-center gap-2 text-xs cursor-pointer select-none">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={saveCredentials}
|
||||||
|
onChange={(e) => setSaveCredentials(e.target.checked)}
|
||||||
|
disabled={submitting}
|
||||||
|
/>
|
||||||
|
{t("saveCredentials")}
|
||||||
|
</label>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="rounded-md border border-red-500/30 bg-red-500/5 px-3 py-2 text-xs text-red-400">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleCancel}
|
||||||
|
disabled={submitting}
|
||||||
|
>
|
||||||
|
{t("cancel")}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleSubmit} disabled={submitting || !canSubmit}>
|
||||||
|
{submitting ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||||
|
{t("authenticating")}
|
||||||
|
</>
|
||||||
|
) : mode === "github" ? (
|
||||||
|
t("githubAuthenticate")
|
||||||
|
) : (
|
||||||
|
t("authenticate")
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</GitCredentialContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -57,6 +57,26 @@
|
|||||||
"clone": "استنساخ"
|
"clone": "استنساخ"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"GitCredentialDialog": {
|
||||||
|
"title": "المصادقة مطلوبة",
|
||||||
|
"description": "يتطلب الخادم البعيد بيانات اعتماد. أدخل اسم المستخدم وكلمة المرور (أو رمز الوصول الشخصي).",
|
||||||
|
"username": "اسم المستخدم",
|
||||||
|
"usernamePlaceholder": "اسم المستخدم أو البريد الإلكتروني",
|
||||||
|
"password": "كلمة المرور / الرمز",
|
||||||
|
"passwordPlaceholder": "كلمة المرور أو رمز الوصول الشخصي",
|
||||||
|
"passwordHint": "أدخل اسم المستخدم وكلمة المرور للخادم.",
|
||||||
|
"cancel": "إلغاء",
|
||||||
|
"authenticate": "مصادقة",
|
||||||
|
"authenticating": "جارٍ المصادقة...",
|
||||||
|
"invalidCredentials": "بيانات الاعتماد غير صالحة. يرجى المحاولة مرة أخرى.",
|
||||||
|
"saveCredentials": "حفظ بيانات الاعتماد للعمليات المستقبلية",
|
||||||
|
"githubTitle": "مصادقة GitHub",
|
||||||
|
"githubDescription": "أدخل رمز وصول شخصي للاتصال بـ GitHub. سيتم التحقق من الرمز وحفظه تلقائيًا.",
|
||||||
|
"githubToken": "رمز الوصول الشخصي",
|
||||||
|
"githubTokenPlaceholder": "ghp_xxxxxxxxxxxx",
|
||||||
|
"githubTokenHint": "أنشئ رمزًا في GitHub → Settings → Developer settings → Personal access tokens.",
|
||||||
|
"githubAuthenticate": "التحقق والاتصال"
|
||||||
|
},
|
||||||
"SettingsShell": {
|
"SettingsShell": {
|
||||||
"title": "الإعدادات",
|
"title": "الإعدادات",
|
||||||
"preferences": "التفضيلات",
|
"preferences": "التفضيلات",
|
||||||
|
|||||||
@@ -57,6 +57,26 @@
|
|||||||
"clone": "Klonen"
|
"clone": "Klonen"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"GitCredentialDialog": {
|
||||||
|
"title": "Authentifizierung erforderlich",
|
||||||
|
"description": "Der Remote-Server erfordert Anmeldedaten. Geben Sie Ihren Benutzernamen und Ihr Passwort (oder persönliches Zugriffstoken) ein.",
|
||||||
|
"username": "Benutzername",
|
||||||
|
"usernamePlaceholder": "Benutzername oder E-Mail",
|
||||||
|
"password": "Passwort / Token",
|
||||||
|
"passwordPlaceholder": "Passwort oder persönliches Zugriffstoken",
|
||||||
|
"passwordHint": "Geben Sie Benutzername und Passwort des Servers ein.",
|
||||||
|
"cancel": "Abbrechen",
|
||||||
|
"authenticate": "Authentifizieren",
|
||||||
|
"authenticating": "Authentifizierung...",
|
||||||
|
"invalidCredentials": "Ungültige Anmeldedaten. Bitte versuchen Sie es erneut.",
|
||||||
|
"saveCredentials": "Anmeldedaten für zukünftige Vorgänge speichern",
|
||||||
|
"githubTitle": "GitHub-Authentifizierung",
|
||||||
|
"githubDescription": "Geben Sie ein persönliches Zugriffstoken ein, um sich mit GitHub zu verbinden. Das Token wird validiert und automatisch gespeichert.",
|
||||||
|
"githubToken": "Persönliches Zugriffstoken",
|
||||||
|
"githubTokenPlaceholder": "ghp_xxxxxxxxxxxx",
|
||||||
|
"githubTokenHint": "Erstellen Sie ein Token unter GitHub → Settings → Developer settings → Personal access tokens.",
|
||||||
|
"githubAuthenticate": "Validieren & verbinden"
|
||||||
|
},
|
||||||
"SettingsShell": {
|
"SettingsShell": {
|
||||||
"title": "Einstellungen",
|
"title": "Einstellungen",
|
||||||
"preferences": "Präferenzen",
|
"preferences": "Präferenzen",
|
||||||
|
|||||||
@@ -57,6 +57,26 @@
|
|||||||
"clone": "Clone"
|
"clone": "Clone"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"GitCredentialDialog": {
|
||||||
|
"title": "Authentication Required",
|
||||||
|
"description": "The remote server requires credentials. Enter your username and password (or personal access token).",
|
||||||
|
"username": "Username",
|
||||||
|
"usernamePlaceholder": "Username or email",
|
||||||
|
"password": "Password / Token",
|
||||||
|
"passwordPlaceholder": "Password or personal access token",
|
||||||
|
"passwordHint": "For non-GitHub servers, enter your username and password.",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"authenticate": "Authenticate",
|
||||||
|
"authenticating": "Authenticating...",
|
||||||
|
"invalidCredentials": "Invalid credentials. Please try again.",
|
||||||
|
"saveCredentials": "Save credentials for future operations",
|
||||||
|
"githubTitle": "GitHub Authentication",
|
||||||
|
"githubDescription": "Enter a personal access token to authenticate with GitHub. The token will be validated and saved to your accounts.",
|
||||||
|
"githubToken": "Personal Access Token",
|
||||||
|
"githubTokenPlaceholder": "ghp_xxxxxxxxxxxx",
|
||||||
|
"githubTokenHint": "Generate a token at GitHub → Settings → Developer settings → Personal access tokens.",
|
||||||
|
"githubAuthenticate": "Validate & Connect"
|
||||||
|
},
|
||||||
"SettingsShell": {
|
"SettingsShell": {
|
||||||
"title": "Settings",
|
"title": "Settings",
|
||||||
"preferences": "Preferences",
|
"preferences": "Preferences",
|
||||||
|
|||||||
@@ -57,6 +57,26 @@
|
|||||||
"clone": "Clonar"
|
"clone": "Clonar"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"GitCredentialDialog": {
|
||||||
|
"title": "Autenticación requerida",
|
||||||
|
"description": "El servidor remoto requiere credenciales. Introduce tu nombre de usuario y contraseña (o token de acceso personal).",
|
||||||
|
"username": "Nombre de usuario",
|
||||||
|
"usernamePlaceholder": "Usuario o correo electrónico",
|
||||||
|
"password": "Contraseña / Token",
|
||||||
|
"passwordPlaceholder": "Contraseña o token de acceso personal",
|
||||||
|
"passwordHint": "Introduce el nombre de usuario y contraseña del servidor.",
|
||||||
|
"cancel": "Cancelar",
|
||||||
|
"authenticate": "Autenticar",
|
||||||
|
"authenticating": "Autenticando...",
|
||||||
|
"invalidCredentials": "Credenciales inválidas. Inténtalo de nuevo.",
|
||||||
|
"saveCredentials": "Guardar credenciales para futuras operaciones",
|
||||||
|
"githubTitle": "Autenticación de GitHub",
|
||||||
|
"githubDescription": "Introduce un token de acceso personal para conectarte a GitHub. El token se validará y guardará automáticamente.",
|
||||||
|
"githubToken": "Token de acceso personal",
|
||||||
|
"githubTokenPlaceholder": "ghp_xxxxxxxxxxxx",
|
||||||
|
"githubTokenHint": "Genera un token en GitHub → Settings → Developer settings → Personal access tokens.",
|
||||||
|
"githubAuthenticate": "Validar y conectar"
|
||||||
|
},
|
||||||
"SettingsShell": {
|
"SettingsShell": {
|
||||||
"title": "Configuración",
|
"title": "Configuración",
|
||||||
"preferences": "Preferencias",
|
"preferences": "Preferencias",
|
||||||
|
|||||||
@@ -57,6 +57,26 @@
|
|||||||
"clone": "Cloner"
|
"clone": "Cloner"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"GitCredentialDialog": {
|
||||||
|
"title": "Authentification requise",
|
||||||
|
"description": "Le serveur distant nécessite des identifiants. Entrez votre nom d'utilisateur et votre mot de passe (ou jeton d'accès personnel).",
|
||||||
|
"username": "Nom d'utilisateur",
|
||||||
|
"usernamePlaceholder": "Nom d'utilisateur ou e-mail",
|
||||||
|
"password": "Mot de passe / Jeton",
|
||||||
|
"passwordPlaceholder": "Mot de passe ou jeton d'accès personnel",
|
||||||
|
"passwordHint": "Entrez le nom d'utilisateur et le mot de passe du serveur.",
|
||||||
|
"cancel": "Annuler",
|
||||||
|
"authenticate": "Authentifier",
|
||||||
|
"authenticating": "Authentification...",
|
||||||
|
"invalidCredentials": "Identifiants invalides. Veuillez réessayer.",
|
||||||
|
"saveCredentials": "Enregistrer les identifiants pour les opérations futures",
|
||||||
|
"githubTitle": "Authentification GitHub",
|
||||||
|
"githubDescription": "Entrez un jeton d'accès personnel pour vous connecter à GitHub. Le jeton sera validé et enregistré automatiquement.",
|
||||||
|
"githubToken": "Jeton d'accès personnel",
|
||||||
|
"githubTokenPlaceholder": "ghp_xxxxxxxxxxxx",
|
||||||
|
"githubTokenHint": "Générez un jeton dans GitHub → Settings → Developer settings → Personal access tokens.",
|
||||||
|
"githubAuthenticate": "Valider et connecter"
|
||||||
|
},
|
||||||
"SettingsShell": {
|
"SettingsShell": {
|
||||||
"title": "Paramètres",
|
"title": "Paramètres",
|
||||||
"preferences": "Préférences",
|
"preferences": "Préférences",
|
||||||
|
|||||||
@@ -57,6 +57,26 @@
|
|||||||
"clone": "クローン"
|
"clone": "クローン"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"GitCredentialDialog": {
|
||||||
|
"title": "認証が必要です",
|
||||||
|
"description": "リモートサーバーが認証情報を要求しています。ユーザー名とパスワード(またはアクセストークン)を入力してください。",
|
||||||
|
"username": "ユーザー名",
|
||||||
|
"usernamePlaceholder": "ユーザー名またはメールアドレス",
|
||||||
|
"password": "パスワード / トークン",
|
||||||
|
"passwordPlaceholder": "パスワードまたはアクセストークン",
|
||||||
|
"passwordHint": "サーバーのユーザー名とパスワードを入力してください。",
|
||||||
|
"cancel": "キャンセル",
|
||||||
|
"authenticate": "認証",
|
||||||
|
"authenticating": "認証中...",
|
||||||
|
"invalidCredentials": "認証情報が無効です。再試行してください。",
|
||||||
|
"saveCredentials": "今後の操作のために認証情報を保存する",
|
||||||
|
"githubTitle": "GitHub 認証",
|
||||||
|
"githubDescription": "個人アクセストークンを入力して GitHub に接続します。トークン検証後、自動的にアカウントに保存されます。",
|
||||||
|
"githubToken": "個人アクセストークン",
|
||||||
|
"githubTokenPlaceholder": "ghp_xxxxxxxxxxxx",
|
||||||
|
"githubTokenHint": "GitHub → Settings → Developer settings → Personal access tokens でトークンを生成してください。",
|
||||||
|
"githubAuthenticate": "検証して接続"
|
||||||
|
},
|
||||||
"SettingsShell": {
|
"SettingsShell": {
|
||||||
"title": "設定",
|
"title": "設定",
|
||||||
"preferences": "環境設定",
|
"preferences": "環境設定",
|
||||||
|
|||||||
@@ -57,6 +57,26 @@
|
|||||||
"clone": "클론"
|
"clone": "클론"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"GitCredentialDialog": {
|
||||||
|
"title": "인증 필요",
|
||||||
|
"description": "원격 서버에서 자격 증명을 요구합니다. 사용자 이름과 비밀번호(또는 개인 액세스 토큰)를 입력하세요.",
|
||||||
|
"username": "사용자 이름",
|
||||||
|
"usernamePlaceholder": "사용자 이름 또는 이메일",
|
||||||
|
"password": "비밀번호 / 토큰",
|
||||||
|
"passwordPlaceholder": "비밀번호 또는 개인 액세스 토큰",
|
||||||
|
"passwordHint": "서버의 사용자 이름과 비밀번호를 입력하세요.",
|
||||||
|
"cancel": "취소",
|
||||||
|
"authenticate": "인증",
|
||||||
|
"authenticating": "인증 중...",
|
||||||
|
"invalidCredentials": "자격 증명이 유효하지 않습니다. 다시 시도하세요.",
|
||||||
|
"saveCredentials": "향후 작업을 위해 자격 증명 저장",
|
||||||
|
"githubTitle": "GitHub 인증",
|
||||||
|
"githubDescription": "개인 액세스 토큰을 입력하여 GitHub에 연결합니다. 토큰 확인 후 계정에 자동으로 저장됩니다.",
|
||||||
|
"githubToken": "개인 액세스 토큰",
|
||||||
|
"githubTokenPlaceholder": "ghp_xxxxxxxxxxxx",
|
||||||
|
"githubTokenHint": "GitHub → Settings → Developer settings → Personal access tokens에서 토큰을 생성하세요.",
|
||||||
|
"githubAuthenticate": "확인 및 연결"
|
||||||
|
},
|
||||||
"SettingsShell": {
|
"SettingsShell": {
|
||||||
"title": "설정",
|
"title": "설정",
|
||||||
"preferences": "환경설정",
|
"preferences": "환경설정",
|
||||||
|
|||||||
@@ -57,6 +57,26 @@
|
|||||||
"clone": "Clonar"
|
"clone": "Clonar"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"GitCredentialDialog": {
|
||||||
|
"title": "Autenticação necessária",
|
||||||
|
"description": "O servidor remoto requer credenciais. Insira seu nome de usuário e senha (ou token de acesso pessoal).",
|
||||||
|
"username": "Nome de usuário",
|
||||||
|
"usernamePlaceholder": "Nome de usuário ou e-mail",
|
||||||
|
"password": "Senha / Token",
|
||||||
|
"passwordPlaceholder": "Senha ou token de acesso pessoal",
|
||||||
|
"passwordHint": "Insira o nome de usuário e a senha do servidor.",
|
||||||
|
"cancel": "Cancelar",
|
||||||
|
"authenticate": "Autenticar",
|
||||||
|
"authenticating": "Autenticando...",
|
||||||
|
"invalidCredentials": "Credenciais inválidas. Tente novamente.",
|
||||||
|
"saveCredentials": "Salvar credenciais para operações futuras",
|
||||||
|
"githubTitle": "Autenticação do GitHub",
|
||||||
|
"githubDescription": "Insira um token de acesso pessoal para se conectar ao GitHub. O token será validado e salvo automaticamente.",
|
||||||
|
"githubToken": "Token de acesso pessoal",
|
||||||
|
"githubTokenPlaceholder": "ghp_xxxxxxxxxxxx",
|
||||||
|
"githubTokenHint": "Gere um token em GitHub → Settings → Developer settings → Personal access tokens.",
|
||||||
|
"githubAuthenticate": "Validar e conectar"
|
||||||
|
},
|
||||||
"SettingsShell": {
|
"SettingsShell": {
|
||||||
"title": "Configurações",
|
"title": "Configurações",
|
||||||
"preferences": "Preferências",
|
"preferences": "Preferências",
|
||||||
|
|||||||
@@ -57,6 +57,26 @@
|
|||||||
"clone": "克隆"
|
"clone": "克隆"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"GitCredentialDialog": {
|
||||||
|
"title": "需要身份验证",
|
||||||
|
"description": "远程服务器要求输入凭据。请输入用户名和密码(或个人访问令牌)。",
|
||||||
|
"username": "用户名",
|
||||||
|
"usernamePlaceholder": "用户名或邮箱",
|
||||||
|
"password": "密码 / 令牌",
|
||||||
|
"passwordPlaceholder": "密码或个人访问令牌",
|
||||||
|
"passwordHint": "请输入服务器的用户名和密码。",
|
||||||
|
"cancel": "取消",
|
||||||
|
"authenticate": "认证",
|
||||||
|
"authenticating": "认证中...",
|
||||||
|
"invalidCredentials": "凭据无效,请重试。",
|
||||||
|
"saveCredentials": "保存凭据以供后续操作使用",
|
||||||
|
"githubTitle": "GitHub 身份验证",
|
||||||
|
"githubDescription": "输入个人访问令牌以连接 GitHub。令牌验证成功后将自动保存到账号列表。",
|
||||||
|
"githubToken": "个人访问令牌",
|
||||||
|
"githubTokenPlaceholder": "ghp_xxxxxxxxxxxx",
|
||||||
|
"githubTokenHint": "在 GitHub → Settings → Developer settings → Personal access tokens 中生成令牌。",
|
||||||
|
"githubAuthenticate": "验证并连接"
|
||||||
|
},
|
||||||
"SettingsShell": {
|
"SettingsShell": {
|
||||||
"title": "设置",
|
"title": "设置",
|
||||||
"preferences": "偏好设置",
|
"preferences": "偏好设置",
|
||||||
|
|||||||
@@ -57,6 +57,26 @@
|
|||||||
"clone": "複製"
|
"clone": "複製"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"GitCredentialDialog": {
|
||||||
|
"title": "需要身份驗證",
|
||||||
|
"description": "遠端伺服器要求輸入憑據。請輸入使用者名稱和密碼(或個人存取權杖)。",
|
||||||
|
"username": "使用者名稱",
|
||||||
|
"usernamePlaceholder": "使用者名稱或電子郵件",
|
||||||
|
"password": "密碼 / 權杖",
|
||||||
|
"passwordPlaceholder": "密碼或個人存取權杖",
|
||||||
|
"passwordHint": "請輸入伺服器的使用者名稱和密碼。",
|
||||||
|
"cancel": "取消",
|
||||||
|
"authenticate": "驗證",
|
||||||
|
"authenticating": "驗證中...",
|
||||||
|
"invalidCredentials": "憑據無效,請重試。",
|
||||||
|
"saveCredentials": "儲存憑據以供後續操作使用",
|
||||||
|
"githubTitle": "GitHub 身份驗證",
|
||||||
|
"githubDescription": "輸入個人存取權杖以連線 GitHub。權杖驗證成功後將自動儲存至帳號列表。",
|
||||||
|
"githubToken": "個人存取權杖",
|
||||||
|
"githubTokenPlaceholder": "ghp_xxxxxxxxxxxx",
|
||||||
|
"githubTokenHint": "在 GitHub → Settings → Developer settings → Personal access tokens 中產生權杖。",
|
||||||
|
"githubAuthenticate": "驗證並連線"
|
||||||
|
},
|
||||||
"SettingsShell": {
|
"SettingsShell": {
|
||||||
"title": "設定",
|
"title": "設定",
|
||||||
"preferences": "偏好設定",
|
"preferences": "偏好設定",
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ import type {
|
|||||||
GitLogEntry,
|
GitLogEntry,
|
||||||
SystemLanguageSettings,
|
SystemLanguageSettings,
|
||||||
SystemProxySettings,
|
SystemProxySettings,
|
||||||
|
GitCredentials,
|
||||||
GitDetectResult,
|
GitDetectResult,
|
||||||
GitSettings,
|
GitSettings,
|
||||||
GitHubAccountsSettings,
|
GitHubAccountsSettings,
|
||||||
@@ -487,9 +488,14 @@ export async function createFolderDirectory(path: string): Promise<void> {
|
|||||||
|
|
||||||
export async function cloneRepository(
|
export async function cloneRepository(
|
||||||
url: string,
|
url: string,
|
||||||
targetDir: string
|
targetDir: string,
|
||||||
|
credentials?: GitCredentials | null
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
return invoke("clone_repository", { url, targetDir })
|
return invoke("clone_repository", {
|
||||||
|
url,
|
||||||
|
targetDir,
|
||||||
|
credentials: credentials ?? null,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getGitBranch(path: string): Promise<string | null> {
|
export async function getGitBranch(path: string): Promise<string | null> {
|
||||||
@@ -500,8 +506,11 @@ export async function gitInit(path: string): Promise<void> {
|
|||||||
return invoke("git_init", { path })
|
return invoke("git_init", { path })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function gitPull(path: string): Promise<GitPullResult> {
|
export async function gitPull(
|
||||||
return invoke("git_pull", { path })
|
path: string,
|
||||||
|
credentials?: GitCredentials | null
|
||||||
|
): Promise<GitPullResult> {
|
||||||
|
return invoke("git_pull", { path, credentials: credentials ?? null })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function gitStartPullMerge(
|
export async function gitStartPullMerge(
|
||||||
@@ -515,12 +524,18 @@ export async function gitHasMergeHead(path: string): Promise<boolean> {
|
|||||||
return invoke("git_has_merge_head", { path })
|
return invoke("git_has_merge_head", { path })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function gitFetch(path: string): Promise<string> {
|
export async function gitFetch(
|
||||||
return invoke("git_fetch", { path })
|
path: string,
|
||||||
|
credentials?: GitCredentials | null
|
||||||
|
): Promise<string> {
|
||||||
|
return invoke("git_fetch", { path, credentials: credentials ?? null })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function gitPush(path: string): Promise<GitPushResult> {
|
export async function gitPush(
|
||||||
return invoke("git_push", { path })
|
path: string,
|
||||||
|
credentials?: GitCredentials | null
|
||||||
|
): Promise<GitPushResult> {
|
||||||
|
return invoke("git_push", { path, credentials: credentials ?? null })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function gitNewBranch(
|
export async function gitNewBranch(
|
||||||
@@ -683,9 +698,14 @@ export async function gitListRemotes(path: string): Promise<GitRemote[]> {
|
|||||||
|
|
||||||
export async function gitFetchRemote(
|
export async function gitFetchRemote(
|
||||||
path: string,
|
path: string,
|
||||||
name: string
|
name: string,
|
||||||
|
credentials?: GitCredentials | null
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
return invoke("git_fetch_remote", { path, name })
|
return invoke("git_fetch_remote", {
|
||||||
|
path,
|
||||||
|
name,
|
||||||
|
credentials: credentials ?? null,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function gitAddRemote(
|
export async function gitAddRemote(
|
||||||
|
|||||||
@@ -525,6 +525,11 @@ export interface SystemLanguageSettings {
|
|||||||
|
|
||||||
// --- Version Control ---
|
// --- Version Control ---
|
||||||
|
|
||||||
|
export interface GitCredentials {
|
||||||
|
username: string
|
||||||
|
password: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface GitDetectResult {
|
export interface GitDetectResult {
|
||||||
installed: boolean
|
installed: boolean
|
||||||
version: string | null
|
version: string | null
|
||||||
|
|||||||
Reference in New Issue
Block a user