Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6434a9f10c | ||
|
|
cc0f1c076f |
+2
-14
@@ -2432,8 +2432,6 @@ const CRED_SERVICE: &str = "tauri_git_lite";
|
|||||||
pub struct StoredCredential {
|
pub struct StoredCredential {
|
||||||
pub username: String,
|
pub username: String,
|
||||||
pub password: String,
|
pub password: String,
|
||||||
#[serde(default, rename = "expiresAt", skip_serializing_if = "Option::is_none")]
|
|
||||||
pub expires_at: Option<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cred_entry(key: &str) -> Result<keyring::Entry, String> {
|
fn cred_entry(key: &str) -> Result<keyring::Entry, String> {
|
||||||
@@ -2616,19 +2614,9 @@ pub fn cred_load(key: String) -> Result<Option<StoredCredential>, String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command(async)]
|
#[tauri::command(async)]
|
||||||
pub fn cred_save(
|
pub fn cred_save(key: String, username: String, password: String) -> Result<(), String> {
|
||||||
key: String,
|
|
||||||
username: String,
|
|
||||||
password: String,
|
|
||||||
expires_at: Option<String>,
|
|
||||||
) -> Result<(), String> {
|
|
||||||
let entry = cred_entry(&key)?;
|
let entry = cred_entry(&key)?;
|
||||||
let expires_at = expires_at.filter(|value| !value.trim().is_empty());
|
let cred = StoredCredential { username, password };
|
||||||
let cred = StoredCredential {
|
|
||||||
username,
|
|
||||||
password,
|
|
||||||
expires_at,
|
|
||||||
};
|
|
||||||
let json = serde_json::to_string(&cred)
|
let json = serde_json::to_string(&cred)
|
||||||
.map_err(|err| format!("Could not serialize credentials: {err}"))?;
|
.map_err(|err| format!("Could not serialize credentials: {err}"))?;
|
||||||
entry
|
entry
|
||||||
|
|||||||
+6
-20
@@ -180,7 +180,6 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
orgKeyFromUrl,
|
orgKeyFromUrl,
|
||||||
isCredentialExpired,
|
|
||||||
isAuthError,
|
isAuthError,
|
||||||
stripAuthPrefix,
|
stripAuthPrefix,
|
||||||
summarizeGitError,
|
summarizeGitError,
|
||||||
@@ -2219,11 +2218,10 @@
|
|||||||
|
|
||||||
if (!username && !password) {
|
if (!username && !password) {
|
||||||
const stored = await loadStoredCredential(credentialKey);
|
const stored = await loadStoredCredential(credentialKey);
|
||||||
if (stored && !isCredentialExpired(stored)) {
|
if (stored) {
|
||||||
await cloneRepo(remoteUrl, parentPath, directoryName, stored.username, stored.password, credentialKey, true);
|
await cloneRepo(remoteUrl, parentPath, directoryName, stored.username, stored.password, credentialKey, true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (stored && credentialKey) await credDelete(credentialKey).catch(() => {});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
operation = "Cloning repository";
|
operation = "Cloning repository";
|
||||||
@@ -2921,7 +2919,7 @@
|
|||||||
if (!activeRepoPath || isBusy) return;
|
if (!activeRepoPath || isBusy) return;
|
||||||
const key = await currentCredKey();
|
const key = await currentCredKey();
|
||||||
const stored = await loadStoredCredential(key);
|
const stored = await loadStoredCredential(key);
|
||||||
const credential = stored && !isCredentialExpired(stored) ? stored : null;
|
const credential = stored ?? null;
|
||||||
|
|
||||||
await runOperation(`Pushing tag ${tag.name}`, async () => {
|
await runOperation(`Pushing tag ${tag.name}`, async () => {
|
||||||
try {
|
try {
|
||||||
@@ -3028,12 +3026,7 @@
|
|||||||
async function storedCredentialForNoteRemote(remote: string, direction: "fetch" | "push") {
|
async function storedCredentialForNoteRemote(remote: string, direction: "fetch" | "push") {
|
||||||
const config = commitNoteRemotes.find((item) => item.name === remote);
|
const config = commitNoteRemotes.find((item) => item.name === remote);
|
||||||
const key = orgKeyFromUrl(direction === "push" ? (config?.push_url ?? "") : (config?.fetch_url ?? ""));
|
const key = orgKeyFromUrl(direction === "push" ? (config?.push_url ?? "") : (config?.fetch_url ?? ""));
|
||||||
const stored = await loadStoredCredential(key);
|
return loadStoredCredential(key);
|
||||||
if (stored && isCredentialExpired(stored)) {
|
|
||||||
if (key) await credDelete(key).catch(() => {});
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return stored;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function commitNoteRemoteError(error: unknown): string {
|
function commitNoteRemoteError(error: unknown): string {
|
||||||
@@ -3271,12 +3264,7 @@
|
|||||||
handleRemoteResult("push", key, fromStore);
|
handleRemoteResult("push", key, fromStore);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleCredentialSubmit(
|
async function handleCredentialSubmit(username: string, password: string, save: boolean) {
|
||||||
username: string,
|
|
||||||
password: string,
|
|
||||||
save: boolean,
|
|
||||||
expiresAt: string | null,
|
|
||||||
) {
|
|
||||||
const key = credDialogKey;
|
const key = credDialogKey;
|
||||||
if (credDialogAction === "pull") await doActualPull(username, password, key, false);
|
if (credDialogAction === "pull") await doActualPull(username, password, key, false);
|
||||||
else if (credDialogAction === "push") await doActualPush(username, password, key, false);
|
else if (credDialogAction === "push") await doActualPush(username, password, key, false);
|
||||||
@@ -3296,7 +3284,7 @@
|
|||||||
// Only persist once the operation actually succeeded (dialog has closed).
|
// Only persist once the operation actually succeeded (dialog has closed).
|
||||||
if (!credDialogOpen && save && key) {
|
if (!credDialogOpen && save && key) {
|
||||||
try {
|
try {
|
||||||
await credSave(key, username, password, expiresAt);
|
await credSave(key, username, password);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
errorMessage = errorToMessage(error);
|
errorMessage = errorToMessage(error);
|
||||||
}
|
}
|
||||||
@@ -3311,15 +3299,13 @@
|
|||||||
const key = await currentCredKey();
|
const key = await currentCredKey();
|
||||||
const stored = await loadStoredCredential(key);
|
const stored = await loadStoredCredential(key);
|
||||||
|
|
||||||
if (stored && !isCredentialExpired(stored)) {
|
if (stored) {
|
||||||
if (action === "pull") await doActualPull(stored.username, stored.password, key, true);
|
if (action === "pull") await doActualPull(stored.username, stored.password, key, true);
|
||||||
else if (action === "fetch") await doActualFetch(stored.username, stored.password, key, true);
|
else if (action === "fetch") await doActualFetch(stored.username, stored.password, key, true);
|
||||||
else await doActualPush(stored.username, stored.password, key, true);
|
else await doActualPush(stored.username, stored.password, key, true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Expired entry → clean it up before prompting again.
|
|
||||||
if (stored && key) await credDelete(key).catch(() => {});
|
|
||||||
await openCredentialDialog(action, key);
|
await openCredentialDialog(action, key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+26
-2
@@ -2559,8 +2559,10 @@
|
|||||||
}
|
}
|
||||||
.branch-ref-cluster {
|
.branch-ref-cluster {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
flex: 0 1 auto;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
margin-left: -10px;
|
margin-left: -10px;
|
||||||
}
|
}
|
||||||
.compact-ref-chip {
|
.compact-ref-chip {
|
||||||
@@ -2664,7 +2666,8 @@
|
|||||||
.graph-row:hover .compact-ref-chip.branch,
|
.graph-row:hover .compact-ref-chip.branch,
|
||||||
.graph-row.selected .compact-ref-chip.branch,
|
.graph-row.selected .compact-ref-chip.branch,
|
||||||
.graph-row:focus-within .compact-ref-chip.branch {
|
.graph-row:focus-within .compact-ref-chip.branch {
|
||||||
max-width: min(62%, 260px);
|
flex: 1 1 auto;
|
||||||
|
max-width: 100%;
|
||||||
padding-right: 8px;
|
padding-right: 8px;
|
||||||
border-color: color-mix(in srgb, var(--ref-lane-color, #69a7ff) 64%, transparent);
|
border-color: color-mix(in srgb, var(--ref-lane-color, #69a7ff) 64%, transparent);
|
||||||
background:
|
background:
|
||||||
@@ -2673,6 +2676,18 @@
|
|||||||
inset 2px 0 0 var(--ref-lane-color, #69a7ff),
|
inset 2px 0 0 var(--ref-lane-color, #69a7ff),
|
||||||
0 3px 12px color-mix(in srgb, var(--ref-lane-color, #69a7ff) 12%, rgba(0,0,0,.24));
|
0 3px 12px color-mix(in srgb, var(--ref-lane-color, #69a7ff) 12%, rgba(0,0,0,.24));
|
||||||
}
|
}
|
||||||
|
.graph-row:hover .commit-ref-strip,
|
||||||
|
.graph-row.selected .commit-ref-strip,
|
||||||
|
.graph-row:focus-within .commit-ref-strip {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
row-gap: 4px;
|
||||||
|
}
|
||||||
|
.graph-row:hover .branch-ref-cluster,
|
||||||
|
.graph-row.selected .branch-ref-cluster,
|
||||||
|
.graph-row:focus-within .branch-ref-cluster {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
max-width: min(100%, 320px);
|
||||||
|
}
|
||||||
.graph-row:hover .compact-ref-branch-icon,
|
.graph-row:hover .compact-ref-branch-icon,
|
||||||
.graph-row.selected .compact-ref-branch-icon,
|
.graph-row.selected .compact-ref-branch-icon,
|
||||||
.graph-row:focus-within .compact-ref-branch-icon {
|
.graph-row:focus-within .compact-ref-branch-icon {
|
||||||
@@ -3069,8 +3084,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (hover: none) {
|
@media (hover: none) {
|
||||||
|
.commit-ref-strip {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
row-gap: 4px;
|
||||||
|
}
|
||||||
|
.branch-ref-cluster {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
max-width: min(100%, 320px);
|
||||||
|
}
|
||||||
.compact-ref-chip.branch {
|
.compact-ref-chip.branch {
|
||||||
max-width: min(62%, 260px);
|
flex: 1 1 auto;
|
||||||
|
max-width: 100%;
|
||||||
padding-right: 8px;
|
padding-right: 8px;
|
||||||
}
|
}
|
||||||
.compact-ref-chip.branch > span,
|
.compact-ref-chip.branch > span,
|
||||||
|
|||||||
@@ -85,7 +85,7 @@
|
|||||||
const key = CRED_KEYS[target];
|
const key = CRED_KEYS[target];
|
||||||
const trimmed = value.trim();
|
const trimmed = value.trim();
|
||||||
if (trimmed) {
|
if (trimmed) {
|
||||||
await credSave(key, "api-key", trimmed, null);
|
await credSave(key, "api-key", trimmed);
|
||||||
} else {
|
} else {
|
||||||
await credDelete(key);
|
await credDelete(key);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
action: "push" | "pull" | "fetch" | "clone";
|
action: "push" | "pull" | "fetch" | "clone";
|
||||||
error: string;
|
error: string;
|
||||||
isBusy: boolean;
|
isBusy: boolean;
|
||||||
onSubmit: (username: string, password: string, save: boolean, expiresAt: string | null) => void;
|
onSubmit: (username: string, password: string, save: boolean) => void;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,7 +36,6 @@
|
|||||||
let password = $state("");
|
let password = $state("");
|
||||||
let showPassword = $state(false);
|
let showPassword = $state(false);
|
||||||
let saveSession = $state(true);
|
let saveSession = $state(true);
|
||||||
let expiresAt = $state("");
|
|
||||||
|
|
||||||
let canSubmit = $derived(
|
let canSubmit = $derived(
|
||||||
!isBusy &&
|
!isBusy &&
|
||||||
@@ -62,12 +61,7 @@
|
|||||||
function handleSubmit(e: SubmitEvent) {
|
function handleSubmit(e: SubmitEvent) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!canSubmit) return;
|
if (!canSubmit) return;
|
||||||
onSubmit(
|
onSubmit(mode === "token" ? "oauth2" : username, password, saveSession);
|
||||||
mode === "token" ? "oauth2" : username,
|
|
||||||
password,
|
|
||||||
saveSession,
|
|
||||||
saveSession && expiresAt ? expiresAt : null,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -191,19 +185,6 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if saveSession}
|
|
||||||
<div class="cred-expiry">
|
|
||||||
<label class="cred-field-label" for="cred-expiry">Expiration date (optional)</label>
|
|
||||||
<input
|
|
||||||
id="cred-expiry"
|
|
||||||
type="date"
|
|
||||||
bind:value={expiresAt}
|
|
||||||
disabled={isBusy}
|
|
||||||
/>
|
|
||||||
<span class="cred-expiry-hint">After this date you'll automatically be asked to log in again.</span>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div class="cred-footer">
|
<div class="cred-footer">
|
||||||
<label class="cred-save">
|
<label class="cred-save">
|
||||||
<input type="checkbox" bind:checked={saveSession} disabled={isBusy} />
|
<input type="checkbox" bind:checked={saveSession} disabled={isBusy} />
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Check, Cherry, ChevronDown, ChevronRight, CloudOff, EllipsisVertical, GitBranch, GitMerge, LoaderCircle, RotateCcw, StickyNote, Tag, X } from "@lucide/svelte";
|
import { Cherry, ChevronDown, ChevronRight, CloudOff, EllipsisVertical, GitBranch, GitMerge, LoaderCircle, RotateCcw, StickyNote, Tag, X } from "@lucide/svelte";
|
||||||
import type { FileStatusKind, GitCommit, GitCommitFile } from "../types";
|
import type { FileStatusKind, GitCommit, GitCommitFile } from "../types";
|
||||||
|
|
||||||
interface GraphSegment {
|
interface GraphSegment {
|
||||||
@@ -544,10 +544,6 @@
|
|||||||
return !localBranchNameSet.has(label) && /(?:^|\/)HEAD(?:\s*->|$)/.test(ref);
|
return !localBranchNameSet.has(label) && /(?:^|\/)HEAD(?:\s*->|$)/.test(ref);
|
||||||
}
|
}
|
||||||
|
|
||||||
function remoteName(branch: string): string {
|
|
||||||
return branch.split("/", 1)[0] ?? branch;
|
|
||||||
}
|
|
||||||
|
|
||||||
function configuredUpstreamForBranch(local: string): string {
|
function configuredUpstreamForBranch(local: string): string {
|
||||||
return localBranchUpstreams[local] ?? (local === activeBranch ? activeUpstream : "");
|
return localBranchUpstreams[local] ?? (local === activeBranch ? activeUpstream : "");
|
||||||
}
|
}
|
||||||
@@ -649,7 +645,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function branchStatusLabel(branch: CommitBranchDecoration): string {
|
function branchStatusLabel(branch: CommitBranchDecoration): string {
|
||||||
if (branch.trackedRemote) return `✓ ${remoteName(branch.trackedRemote)}`;
|
|
||||||
if (branch.label === activeBranch) {
|
if (branch.label === activeBranch) {
|
||||||
const parts = [];
|
const parts = [];
|
||||||
if (activeAhead > 0) parts.push(`↑${activeAhead}`);
|
if (activeAhead > 0) parts.push(`↑${activeAhead}`);
|
||||||
@@ -661,9 +656,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function branchDecorationTitle(branch: CommitBranchDecoration): string {
|
function branchDecorationTitle(branch: CommitBranchDecoration): string {
|
||||||
if (branch.localOnly) return `${branch.label} · Local only — not published yet`;
|
if (branch.localOnly) {
|
||||||
if (branch.trackedRemote) return `${branch.label} · up to date with ${branch.trackedRemote}`;
|
|
||||||
const status = branchStatusLabel(branch);
|
const status = branchStatusLabel(branch);
|
||||||
|
return `${branch.label} · Local only — not published yet${status ? ` · ${status}` : ""}`;
|
||||||
|
}
|
||||||
|
const status = branchStatusLabel(branch);
|
||||||
|
if (branch.trackedRemote) {
|
||||||
|
return `${branch.label} · Tracks ${branch.trackedRemote}${status ? ` · ${status}` : ""}`;
|
||||||
|
}
|
||||||
if (status) return `${branch.label} · ${status}`;
|
if (status) return `${branch.label} · ${status}`;
|
||||||
return branch.kind === "remote" ? `Remote branch ${branch.label}` : `Local branch ${branch.label}`;
|
return branch.kind === "remote" ? `Remote branch ${branch.label}` : `Local branch ${branch.label}`;
|
||||||
}
|
}
|
||||||
@@ -865,16 +865,13 @@
|
|||||||
>
|
>
|
||||||
<GitBranch class="compact-ref-branch-icon" size={10} aria-hidden="true" />
|
<GitBranch class="compact-ref-branch-icon" size={10} aria-hidden="true" />
|
||||||
<span>{refSummary.primaryBranch.label}</span>
|
<span>{refSummary.primaryBranch.label}</span>
|
||||||
{#if branchStatusLabel(refSummary.primaryBranch)}
|
{#if !refSummary.primaryBranch.localOnly && branchStatusLabel(refSummary.primaryBranch)}
|
||||||
<small class:up-to-date={Boolean(refSummary.primaryBranch.trackedRemote)}>
|
<small>{branchStatusLabel(refSummary.primaryBranch)}</small>
|
||||||
{#if refSummary.primaryBranch.trackedRemote}<Check size={9} aria-hidden="true" />{/if}
|
|
||||||
{branchStatusLabel(refSummary.primaryBranch).replace(/^✓\s*/, "")}
|
|
||||||
</small>
|
|
||||||
{/if}
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
{#if refSummary.primaryBranch.localOnly}
|
{#if refSummary.primaryBranch.localOnly}
|
||||||
<span class="compact-ref-local-marker" title="This branch exists only locally and has not been published yet">
|
<span class="compact-ref-local-marker" title="This branch exists only locally and has not been published yet">
|
||||||
<CloudOff size={9} aria-hidden="true" />LOCAL
|
LOCAL
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import type { StoredCredential } from "./types";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Derives a credential key from a remote URL, scoped to host + organisation —
|
* Derives a credential key from a remote URL, scoped to host + organisation —
|
||||||
* the same granularity Azure DevOps / GitHub use. Examples:
|
* the same granularity Azure DevOps / GitHub use. Examples:
|
||||||
@@ -37,13 +35,6 @@ export function orgKeyFromUrl(raw: string): string | null {
|
|||||||
return org ? `${host}/${org}` : host;
|
return org ? `${host}/${org}` : host;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A stored credential is expired only if it carries a past expiry date. */
|
|
||||||
export function isCredentialExpired(cred: StoredCredential): boolean {
|
|
||||||
if (!cred.expiresAt) return false;
|
|
||||||
const time = new Date(cred.expiresAt).getTime();
|
|
||||||
return !Number.isNaN(time) && time < Date.now();
|
|
||||||
}
|
|
||||||
|
|
||||||
const AUTH_PREFIX = "AUTH_FAILED:";
|
const AUTH_PREFIX = "AUTH_FAILED:";
|
||||||
|
|
||||||
export function isAuthError(message: string): boolean {
|
export function isAuthError(message: string): boolean {
|
||||||
|
|||||||
+2
-7
@@ -381,13 +381,8 @@ export function credLoad(key: string): Promise<StoredCredential | null> {
|
|||||||
return invoke<StoredCredential | null>("cred_load", { key });
|
return invoke<StoredCredential | null>("cred_load", { key });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function credSave(
|
export function credSave(key: string, username: string, password: string): Promise<void> {
|
||||||
key: string,
|
return invoke<void>("cred_save", { key, username, password });
|
||||||
username: string,
|
|
||||||
password: string,
|
|
||||||
expiresAt: string | null,
|
|
||||||
): Promise<void> {
|
|
||||||
return invoke<void>("cred_save", { key, username, password, expiresAt });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function credDelete(key: string): Promise<void> {
|
export function credDelete(key: string): Promise<void> {
|
||||||
|
|||||||
@@ -310,5 +310,4 @@ export interface ReflogEntry {
|
|||||||
export interface StoredCredential {
|
export interface StoredCredential {
|
||||||
username: string;
|
username: string;
|
||||||
password: string;
|
password: string;
|
||||||
expiresAt?: string | null;
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user