add some features
This commit is contained in:
@@ -18,7 +18,8 @@
|
|||||||
"Bash(grep -qE \"\\(Running \\\\`target|error\\\\[|panicked|terminated\\)\" /tmp/claude-1000/-mnt-data-Development-GitLite/71a8b49f-ac61-4ad9-aaba-dc0219044038/tasks/b4ujhgmdk.output)",
|
"Bash(grep -qE \"\\(Running \\\\`target|error\\\\[|panicked|terminated\\)\" /tmp/claude-1000/-mnt-data-Development-GitLite/71a8b49f-ac61-4ad9-aaba-dc0219044038/tasks/b4ujhgmdk.output)",
|
||||||
"Bash(grep -qE \"\\(Running \\\\`target|error\\\\[|panicked|terminated\\)\" /tmp/claude-1000/-mnt-data-Development-GitLite/71a8b49f-ac61-4ad9-aaba-dc0219044038/tasks/bt54rikhh.output)",
|
"Bash(grep -qE \"\\(Running \\\\`target|error\\\\[|panicked|terminated\\)\" /tmp/claude-1000/-mnt-data-Development-GitLite/71a8b49f-ac61-4ad9-aaba-dc0219044038/tasks/bt54rikhh.output)",
|
||||||
"Bash(grep -qE \"\\(Running \\\\`target|error\\\\[|panicked|terminated\\)\" /tmp/claude-1000/-mnt-data-Development-GitLite/71a8b49f-ac61-4ad9-aaba-dc0219044038/tasks/b7nyub68s.output)",
|
"Bash(grep -qE \"\\(Running \\\\`target|error\\\\[|panicked|terminated\\)\" /tmp/claude-1000/-mnt-data-Development-GitLite/71a8b49f-ac61-4ad9-aaba-dc0219044038/tasks/b7nyub68s.output)",
|
||||||
"Bash(npm install *)"
|
"Bash(npm install *)",
|
||||||
|
"Bash(cargo check *)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+88
-2
@@ -252,16 +252,38 @@ pub fn commit(path: String, message: String) -> Result<GitStatus, String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn pull(path: String) -> Result<GitStatus, String> {
|
pub fn pull(
|
||||||
|
path: String,
|
||||||
|
username: Option<String>,
|
||||||
|
password: Option<String>,
|
||||||
|
) -> Result<GitStatus, String> {
|
||||||
let repo = resolve_repo(&path)?;
|
let repo = resolve_repo(&path)?;
|
||||||
|
match (username.as_deref(), password.as_deref()) {
|
||||||
|
(Some(u), Some(p)) if !u.is_empty() || !p.is_empty() => {
|
||||||
|
run_git_authenticated(&repo, ["pull", "--ff-only"], u, p)?;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
run_git(&repo, ["pull", "--ff-only"])?;
|
run_git(&repo, ["pull", "--ff-only"])?;
|
||||||
|
}
|
||||||
|
}
|
||||||
status_for_repo(&repo)
|
status_for_repo(&repo)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn push(path: String) -> Result<GitStatus, String> {
|
pub fn push(
|
||||||
|
path: String,
|
||||||
|
username: Option<String>,
|
||||||
|
password: Option<String>,
|
||||||
|
) -> Result<GitStatus, String> {
|
||||||
let repo = resolve_repo(&path)?;
|
let repo = resolve_repo(&path)?;
|
||||||
|
match (username.as_deref(), password.as_deref()) {
|
||||||
|
(Some(u), Some(p)) if !u.is_empty() || !p.is_empty() => {
|
||||||
|
run_git_authenticated(&repo, ["push"], u, p)?;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
run_git(&repo, ["push"])?;
|
run_git(&repo, ["push"])?;
|
||||||
|
}
|
||||||
|
}
|
||||||
status_for_repo(&repo)
|
status_for_repo(&repo)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1163,6 +1185,70 @@ fn validate_files(files: &[String]) -> Result<(), String> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
fn write_askpass_script() -> Result<std::path::PathBuf, String> {
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
let path = std::env::temp_dir().join("gitlite_askpass.sh");
|
||||||
|
let script = "#!/bin/sh\ncase \"$1\" in\n *[Uu]sername*) printf '%s\\n' \"$GIT_CRED_USER\" ;;\n *) printf '%s\\n' \"$GIT_CRED_PASS\" ;;\nesac\n";
|
||||||
|
std::fs::write(&path, script)
|
||||||
|
.map_err(|e| format!("Konnte Authentifizierungsskript nicht schreiben: {e}"))?;
|
||||||
|
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o700))
|
||||||
|
.map_err(|e| format!("Konnte Skriptrechte nicht setzen: {e}"))?;
|
||||||
|
Ok(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(unix))]
|
||||||
|
fn write_askpass_script() -> Result<std::path::PathBuf, String> {
|
||||||
|
let path = std::env::temp_dir().join("gitlite_askpass.bat");
|
||||||
|
let script = "@echo off\necho %1 | findstr /I \"sername\" >nul 2>&1\nif %errorlevel% == 0 (echo %GIT_CRED_USER%) else (echo %GIT_CRED_PASS%)\n";
|
||||||
|
std::fs::write(&path, script)
|
||||||
|
.map_err(|e| format!("Konnte Authentifizierungsskript nicht schreiben: {e}"))?;
|
||||||
|
Ok(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_git_authenticated<I, S>(
|
||||||
|
repo: &Path,
|
||||||
|
args: I,
|
||||||
|
username: &str,
|
||||||
|
password: &str,
|
||||||
|
) -> Result<Vec<u8>, String>
|
||||||
|
where
|
||||||
|
I: IntoIterator<Item = S>,
|
||||||
|
S: AsRef<OsStr>,
|
||||||
|
{
|
||||||
|
let askpass = write_askpass_script()?;
|
||||||
|
|
||||||
|
let result = Command::new("git")
|
||||||
|
.arg("-C")
|
||||||
|
.arg(repo)
|
||||||
|
.args(args)
|
||||||
|
.env("GIT_ASKPASS", &askpass)
|
||||||
|
.env("GIT_TERMINAL_PROMPT", "0")
|
||||||
|
.env("GIT_CRED_USER", username)
|
||||||
|
.env("GIT_CRED_PASS", password)
|
||||||
|
.output()
|
||||||
|
.map_err(|err| format!("Git konnte nicht gestartet werden: {err}"));
|
||||||
|
|
||||||
|
let _ = std::fs::remove_file(&askpass);
|
||||||
|
|
||||||
|
let output = result?;
|
||||||
|
|
||||||
|
if output.status.success() {
|
||||||
|
return Ok(output.stdout);
|
||||||
|
}
|
||||||
|
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
let details = if !stderr.trim().is_empty() {
|
||||||
|
stderr.trim().to_string()
|
||||||
|
} else if !stdout.trim().is_empty() {
|
||||||
|
stdout.trim().to_string()
|
||||||
|
} else {
|
||||||
|
"Unbekannter Fehler".to_string()
|
||||||
|
};
|
||||||
|
Err(format!("Git-Befehl fehlgeschlagen: {details}"))
|
||||||
|
}
|
||||||
|
|
||||||
fn run_git_with_paths(
|
fn run_git_with_paths(
|
||||||
repo: &Path,
|
repo: &Path,
|
||||||
base_args: &[&str],
|
base_args: &[&str],
|
||||||
|
|||||||
+59
-9
@@ -7,6 +7,7 @@
|
|||||||
import CommitPanel from "./lib/components/CommitPanel.svelte";
|
import CommitPanel from "./lib/components/CommitPanel.svelte";
|
||||||
import CompareDialog from "./lib/components/CompareDialog.svelte";
|
import CompareDialog from "./lib/components/CompareDialog.svelte";
|
||||||
import ComparePanel from "./lib/components/ComparePanel.svelte";
|
import ComparePanel from "./lib/components/ComparePanel.svelte";
|
||||||
|
import CredentialDialog from "./lib/components/CredentialDialog.svelte";
|
||||||
import ExplorerPanel from "./lib/components/ExplorerPanel.svelte";
|
import ExplorerPanel from "./lib/components/ExplorerPanel.svelte";
|
||||||
import FileHistoryPanel from "./lib/components/FileHistoryPanel.svelte";
|
import FileHistoryPanel from "./lib/components/FileHistoryPanel.svelte";
|
||||||
import HistoryPanel from "./lib/components/HistoryPanel.svelte";
|
import HistoryPanel from "./lib/components/HistoryPanel.svelte";
|
||||||
@@ -79,6 +80,10 @@
|
|||||||
let preparedResolutions: Record<string, PreparedResolution> = {};
|
let preparedResolutions: Record<string, PreparedResolution> = {};
|
||||||
let autoRefreshEnabled = true;
|
let autoRefreshEnabled = true;
|
||||||
let autoRefreshInFlight = false;
|
let autoRefreshInFlight = false;
|
||||||
|
let credDialogOpen = false;
|
||||||
|
let credDialogAction: "push" | "pull" | null = null;
|
||||||
|
let credDialogError = "";
|
||||||
|
let sessionCredentials: { username: string; password: string } | null = null;
|
||||||
let lastStatusFingerprint = "";
|
let lastStatusFingerprint = "";
|
||||||
const AUTO_REFRESH_INTERVAL = 4000;
|
const AUTO_REFRESH_INTERVAL = 4000;
|
||||||
let autoRefreshTimer: ReturnType<typeof setInterval> | undefined;
|
let autoRefreshTimer: ReturnType<typeof setInterval> | undefined;
|
||||||
@@ -265,26 +270,60 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function pullRepo() {
|
function openCredentialDialog(action: "push" | "pull") {
|
||||||
if (!activeRepoPath) return;
|
if (!activeRepoPath) return;
|
||||||
|
credDialogError = "";
|
||||||
|
credDialogAction = action;
|
||||||
|
credDialogOpen = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doActualPull(username: string, password: string) {
|
||||||
|
errorMessage = "";
|
||||||
await runOperation("Pulling", async () => {
|
await runOperation("Pulling", async () => {
|
||||||
applyStatus(await pull(activeRepoPath));
|
applyStatus(await pull(activeRepoPath, username, password));
|
||||||
await refreshBranchList(activeRepoPath);
|
await refreshBranchList(activeRepoPath);
|
||||||
await refreshCommitHistory(activeRepoPath);
|
await refreshCommitHistory(activeRepoPath);
|
||||||
await refreshExplorerFiles(activeRepoPath);
|
await refreshExplorerFiles(activeRepoPath);
|
||||||
await refreshFileHistory(activeRepoPath);
|
await refreshFileHistory(activeRepoPath);
|
||||||
});
|
});
|
||||||
|
if (errorMessage) { credDialogError = errorMessage; errorMessage = ""; }
|
||||||
|
else { credDialogOpen = false; credDialogAction = null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doActualPush(username: string, password: string) {
|
||||||
|
errorMessage = "";
|
||||||
|
await runOperation("Pushing", async () => {
|
||||||
|
applyStatus(await push(activeRepoPath, username, password));
|
||||||
|
await refreshBranchList(activeRepoPath);
|
||||||
|
await refreshCommitHistory(activeRepoPath);
|
||||||
|
await refreshFileHistory(activeRepoPath);
|
||||||
|
});
|
||||||
|
if (errorMessage) { credDialogError = errorMessage; errorMessage = ""; }
|
||||||
|
else { credDialogOpen = false; credDialogAction = null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCredentialSubmit(username: string, password: string, save: boolean) {
|
||||||
|
if (credDialogAction === "pull") await doActualPull(username, password);
|
||||||
|
else if (credDialogAction === "push") await doActualPush(username, password);
|
||||||
|
if (!credDialogOpen && save) sessionCredentials = { username, password };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pullRepo() {
|
||||||
|
if (!activeRepoPath) return;
|
||||||
|
if (sessionCredentials) {
|
||||||
|
await doActualPull(sessionCredentials.username, sessionCredentials.password);
|
||||||
|
} else {
|
||||||
|
openCredentialDialog("pull");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function pushRepo() {
|
async function pushRepo() {
|
||||||
if (!activeRepoPath) return;
|
if (!activeRepoPath) return;
|
||||||
await runOperation("Pushing", async () => {
|
if (sessionCredentials) {
|
||||||
applyStatus(await push(activeRepoPath));
|
await doActualPush(sessionCredentials.username, sessionCredentials.password);
|
||||||
await refreshBranchList(activeRepoPath);
|
} else {
|
||||||
await refreshCommitHistory(activeRepoPath);
|
openCredentialDialog("push");
|
||||||
await refreshExplorerFiles(activeRepoPath);
|
}
|
||||||
await refreshFileHistory(activeRepoPath);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── File staging / restore ─────────────────────────────────────────────────
|
// ── File staging / restore ─────────────────────────────────────────────────
|
||||||
@@ -697,6 +736,17 @@
|
|||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<!-- Credential dialog for push/pull -->
|
||||||
|
{#if credDialogOpen && credDialogAction}
|
||||||
|
<CredentialDialog
|
||||||
|
action={credDialogAction}
|
||||||
|
error={credDialogError}
|
||||||
|
{isBusy}
|
||||||
|
onSubmit={handleCredentialSubmit}
|
||||||
|
onCancel={() => { credDialogOpen = false; credDialogAction = null; credDialogError = ""; }}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<!-- Conflict resolve dialog -->
|
<!-- Conflict resolve dialog -->
|
||||||
{#if resolveDialogOpen}
|
{#if resolveDialogOpen}
|
||||||
<ResolveDialog
|
<ResolveDialog
|
||||||
|
|||||||
+270
@@ -636,6 +636,276 @@
|
|||||||
|
|
||||||
.prepared-tag { display: inline-flex; align-items: center; gap: 4px; margin-right: auto; color: #1f7a4d; font-size: 12px; font-weight: 700; }
|
.prepared-tag { display: inline-flex; align-items: center; gap: 4px; margin-right: auto; color: #1f7a4d; font-size: 12px; font-weight: 700; }
|
||||||
|
|
||||||
|
/* --- Credential dialog --- */
|
||||||
|
|
||||||
|
.cred-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: min(420px, 100%);
|
||||||
|
max-height: calc(100vh - 48px);
|
||||||
|
border-radius: 14px;
|
||||||
|
background: var(--color-surface);
|
||||||
|
box-shadow: 0 24px 64px rgba(20, 26, 31, 0.38), 0 2px 8px rgba(20, 26, 31, 0.12);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark hero header */
|
||||||
|
.cred-hero {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
padding: 20px 20px 20px 22px;
|
||||||
|
background: var(--color-bar);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cred-hero-icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
color: #ffffff;
|
||||||
|
box-shadow: 0 0 0 1px rgba(255,255,255,0.08) inset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cred-hero-text { flex: 1; min-width: 0; }
|
||||||
|
.cred-hero-label {
|
||||||
|
margin: 0 0 2px;
|
||||||
|
color: var(--color-bar-muted);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.cred-hero-title {
|
||||||
|
margin: 0;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cred-close {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: 1px solid rgba(255,255,255,0.12);
|
||||||
|
border-radius: 7px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--color-bar-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.12s, color 0.12s;
|
||||||
|
}
|
||||||
|
.cred-close:hover:not(:disabled) {
|
||||||
|
background: rgba(255,255,255,0.1);
|
||||||
|
border-color: rgba(255,255,255,0.2);
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form body */
|
||||||
|
.cred-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 20px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Segmented mode toggle */
|
||||||
|
.cred-segment {
|
||||||
|
display: flex;
|
||||||
|
gap: 0;
|
||||||
|
padding: 3px;
|
||||||
|
border: 1px solid var(--color-border-subtle);
|
||||||
|
border-radius: 9px;
|
||||||
|
background: var(--color-surface-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cred-seg-btn {
|
||||||
|
flex: 1;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
min-height: 34px;
|
||||||
|
padding: 0 12px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 7px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--color-ink-muted);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.14s, color 0.14s, box-shadow 0.14s, border-color 0.14s;
|
||||||
|
}
|
||||||
|
.cred-seg-btn:hover:not(.active) { color: var(--color-ink-quiet); }
|
||||||
|
.cred-seg-btn.active {
|
||||||
|
background: #ffffff;
|
||||||
|
border-color: var(--color-border);
|
||||||
|
color: var(--color-ink);
|
||||||
|
box-shadow: 0 1px 4px rgba(20, 26, 31, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fields */
|
||||||
|
.cred-fields { display: flex; flex-direction: column; gap: 12px; }
|
||||||
|
|
||||||
|
.cred-field { display: flex; flex-direction: column; gap: 5px; }
|
||||||
|
|
||||||
|
.cred-field-label {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--color-ink-quiet);
|
||||||
|
letter-spacing: 0.03em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cred-input {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.cred-input :global(.cred-field-icon) {
|
||||||
|
position: absolute;
|
||||||
|
left: 11px;
|
||||||
|
color: var(--color-ink-muted);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.cred-input input {
|
||||||
|
height: 40px;
|
||||||
|
padding-left: 34px;
|
||||||
|
padding-right: 40px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.cred-input input:focus {
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
box-shadow: 0 0 0 3px rgba(37, 111, 143, 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cred-reveal {
|
||||||
|
position: absolute;
|
||||||
|
right: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--color-ink-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.12s, color 0.12s;
|
||||||
|
}
|
||||||
|
.cred-reveal:hover { background: var(--color-surface-hover); color: var(--color-ink); }
|
||||||
|
|
||||||
|
/* Token hint */
|
||||||
|
.cred-token-hint {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #c9e0ea;
|
||||||
|
background: #edf5f9;
|
||||||
|
color: #1d5570;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
.cred-token-hint svg { flex-shrink: 0; margin-top: 1px; color: var(--color-primary); }
|
||||||
|
.cred-token-hint code {
|
||||||
|
padding: 1px 5px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: rgba(37,111,143,0.12);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--color-primary-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Error */
|
||||||
|
.cred-error {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid #fca5a5;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fff5f5;
|
||||||
|
color: #991b1b;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.cred-error svg { flex-shrink: 0; margin-top: 1px; }
|
||||||
|
|
||||||
|
/* Footer row */
|
||||||
|
.cred-footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
padding-top: 4px;
|
||||||
|
border-top: 1px solid var(--color-border-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cred-save {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 7px;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.cred-save input[type="checkbox"] { width: auto; height: auto; cursor: pointer; }
|
||||||
|
.cred-save span { font-size: 12.5px; color: var(--color-ink-quiet); }
|
||||||
|
|
||||||
|
.cred-btns { display: flex; gap: 8px; }
|
||||||
|
|
||||||
|
.cred-cancel {
|
||||||
|
min-height: 36px;
|
||||||
|
padding: 0 14px;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: 7px;
|
||||||
|
background: var(--color-surface);
|
||||||
|
color: var(--color-ink-quiet);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.12s, border-color 0.12s, color 0.12s;
|
||||||
|
}
|
||||||
|
.cred-cancel:hover:not(:disabled) {
|
||||||
|
border-color: var(--color-border-input);
|
||||||
|
background: var(--color-surface-hover);
|
||||||
|
color: var(--color-ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cred-submit {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
min-height: 36px;
|
||||||
|
padding: 0 16px;
|
||||||
|
border: 1px solid var(--color-primary-dark);
|
||||||
|
border-radius: 7px;
|
||||||
|
background: var(--color-primary);
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.12s, border-color 0.12s;
|
||||||
|
}
|
||||||
|
.cred-submit:hover:not(:disabled) {
|
||||||
|
background: var(--color-primary-dark);
|
||||||
|
border-color: #194d63;
|
||||||
|
}
|
||||||
|
.cred-submit:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||||
|
|
||||||
/* --- Diff viewer --- */
|
/* --- Diff viewer --- */
|
||||||
|
|
||||||
.diff-view {
|
.diff-view {
|
||||||
|
|||||||
@@ -0,0 +1,198 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {
|
||||||
|
AlertCircle,
|
||||||
|
Download,
|
||||||
|
Eye,
|
||||||
|
EyeOff,
|
||||||
|
Key,
|
||||||
|
LoaderCircle,
|
||||||
|
Lock,
|
||||||
|
Upload,
|
||||||
|
User,
|
||||||
|
X,
|
||||||
|
} from "@lucide/svelte";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
action: "push" | "pull";
|
||||||
|
error: string;
|
||||||
|
isBusy: boolean;
|
||||||
|
onSubmit: (username: string, password: string, save: boolean) => void;
|
||||||
|
onCancel: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
action,
|
||||||
|
error = "",
|
||||||
|
isBusy = false,
|
||||||
|
onSubmit,
|
||||||
|
onCancel,
|
||||||
|
}: Props = $props();
|
||||||
|
|
||||||
|
type Mode = "credentials" | "token";
|
||||||
|
|
||||||
|
let mode = $state<Mode>("credentials");
|
||||||
|
let username = $state("");
|
||||||
|
let password = $state("");
|
||||||
|
let showPassword = $state(false);
|
||||||
|
let saveSession = $state(false);
|
||||||
|
|
||||||
|
let canSubmit = $derived(
|
||||||
|
!isBusy &&
|
||||||
|
password.trim().length > 0 &&
|
||||||
|
(mode === "token" || username.trim().length > 0),
|
||||||
|
);
|
||||||
|
|
||||||
|
function handleSubmit(e: SubmitEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!canSubmit) return;
|
||||||
|
onSubmit(mode === "token" ? "oauth2" : username, password, saveSession);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="dialog-backdrop"
|
||||||
|
role="presentation"
|
||||||
|
onclick={(e) => { if (e.target === e.currentTarget) onCancel(); }}
|
||||||
|
>
|
||||||
|
<div class="cred-card" role="dialog" aria-modal="true" aria-label="Git-Zugangsdaten" tabindex="-1">
|
||||||
|
|
||||||
|
<!-- Hero header -->
|
||||||
|
<div class="cred-hero">
|
||||||
|
<div class="cred-hero-icon">
|
||||||
|
{#if action === "push"}
|
||||||
|
<Upload size={28} aria-hidden="true" />
|
||||||
|
{:else}
|
||||||
|
<Download size={28} aria-hidden="true" />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="cred-hero-text">
|
||||||
|
<p class="cred-hero-label">{action === "push" ? "Push" : "Pull"}</p>
|
||||||
|
<h2 class="cred-hero-title">Zugangsdaten erforderlich</h2>
|
||||||
|
</div>
|
||||||
|
<button class="cred-close" type="button" onclick={onCancel} title="Abbrechen" aria-label="Abbrechen">
|
||||||
|
<X size={16} aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Form -->
|
||||||
|
<form class="cred-body" onsubmit={handleSubmit}>
|
||||||
|
|
||||||
|
<!-- Mode segmented control -->
|
||||||
|
<div class="cred-segment" role="group" aria-label="Authentifizierungsart">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="cred-seg-btn"
|
||||||
|
class:active={mode === "credentials"}
|
||||||
|
onclick={() => { mode = "credentials"; }}
|
||||||
|
aria-pressed={mode === "credentials"}
|
||||||
|
>
|
||||||
|
<User size={13} aria-hidden="true" />
|
||||||
|
Username & Passwort
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="cred-seg-btn"
|
||||||
|
class:active={mode === "token"}
|
||||||
|
onclick={() => { mode = "token"; }}
|
||||||
|
aria-pressed={mode === "token"}
|
||||||
|
>
|
||||||
|
<Key size={13} aria-hidden="true" />
|
||||||
|
Token
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Fields -->
|
||||||
|
<div class="cred-fields">
|
||||||
|
{#if mode === "credentials"}
|
||||||
|
<div class="cred-field">
|
||||||
|
<label class="cred-field-label" for="cred-username">Username</label>
|
||||||
|
<div class="cred-input">
|
||||||
|
<User size={15} class="cred-field-icon" aria-hidden="true" />
|
||||||
|
<input
|
||||||
|
id="cred-username"
|
||||||
|
type="text"
|
||||||
|
bind:value={username}
|
||||||
|
placeholder="z. B. mein-github-username"
|
||||||
|
autocomplete="username"
|
||||||
|
disabled={isBusy}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="cred-field">
|
||||||
|
<label class="cred-field-label" for="cred-password">
|
||||||
|
{mode === "token" ? "Token" : "Passwort"}
|
||||||
|
</label>
|
||||||
|
<div class="cred-input">
|
||||||
|
<Lock size={15} class="cred-field-icon" aria-hidden="true" />
|
||||||
|
<input
|
||||||
|
id="cred-password"
|
||||||
|
type={showPassword ? "text" : "password"}
|
||||||
|
bind:value={password}
|
||||||
|
placeholder={mode === "token"
|
||||||
|
? "ghp_… oder anderer Zugangstoken"
|
||||||
|
: "Passwort oder Personal Access Token"}
|
||||||
|
autocomplete="current-password"
|
||||||
|
disabled={isBusy}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="cred-reveal"
|
||||||
|
onclick={() => { showPassword = !showPassword; }}
|
||||||
|
tabindex="-1"
|
||||||
|
aria-label={showPassword ? "Verbergen" : "Anzeigen"}
|
||||||
|
>
|
||||||
|
{#if showPassword}
|
||||||
|
<EyeOff size={14} aria-hidden="true" />
|
||||||
|
{:else}
|
||||||
|
<Eye size={14} aria-hidden="true" />
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Token hint -->
|
||||||
|
{#if mode === "token"}
|
||||||
|
<div class="cred-token-hint">
|
||||||
|
<Key size={13} aria-hidden="true" />
|
||||||
|
<span>Username wird automatisch auf <code>oauth2</code> gesetzt — funktioniert mit GitHub, GitLab & Bitbucket.</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Error banner -->
|
||||||
|
{#if error}
|
||||||
|
<div class="cred-error" role="alert">
|
||||||
|
<AlertCircle size={14} aria-hidden="true" />
|
||||||
|
<span>{error}</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<div class="cred-footer">
|
||||||
|
<label class="cred-save">
|
||||||
|
<input type="checkbox" bind:checked={saveSession} disabled={isBusy} />
|
||||||
|
<span>Für diese Sitzung merken</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="cred-btns">
|
||||||
|
<button type="button" class="cred-cancel" onclick={onCancel} disabled={isBusy}>
|
||||||
|
Abbrechen
|
||||||
|
</button>
|
||||||
|
<button class="cred-submit" type="submit" disabled={!canSubmit}>
|
||||||
|
{#if isBusy}
|
||||||
|
<LoaderCircle class="spin" size={15} aria-hidden="true" />
|
||||||
|
{:else if action === "push"}
|
||||||
|
<Upload size={15} aria-hidden="true" />
|
||||||
|
{:else}
|
||||||
|
<Download size={15} aria-hidden="true" />
|
||||||
|
{/if}
|
||||||
|
{action === "push" ? "Push" : "Pull"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
+4
-4
@@ -45,12 +45,12 @@ export function commit(path: string, message: string): Promise<GitStatus> {
|
|||||||
return invoke<GitStatus>("commit", { path, message });
|
return invoke<GitStatus>("commit", { path, message });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function pull(path: string): Promise<GitStatus> {
|
export function pull(path: string, username?: string, password?: string): Promise<GitStatus> {
|
||||||
return invoke<GitStatus>("pull", { path });
|
return invoke<GitStatus>("pull", { path, username: username ?? null, password: password ?? null });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function push(path: string): Promise<GitStatus> {
|
export function push(path: string, username?: string, password?: string): Promise<GitStatus> {
|
||||||
return invoke<GitStatus>("push", { path });
|
return invoke<GitStatus>("push", { path, username: username ?? null, password: password ?? null });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function listCommits(path: string, limit = 100): Promise<GitCommit[]> {
|
export function listCommits(path: string, limit = 100): Promise<GitCommit[]> {
|
||||||
|
|||||||
Reference in New Issue
Block a user