Merge branch 'new_featrues'
This commit is contained in:
+170
-7
@@ -36,10 +36,12 @@
|
||||
import ResolveDialog from "./lib/components/ResolveDialog.svelte";
|
||||
import StashPanel from "./lib/components/StashPanel.svelte";
|
||||
import StatusPanel from "./lib/components/StatusPanel.svelte";
|
||||
import SyncSettingsDialog from "./lib/components/SyncSettingsDialog.svelte";
|
||||
import UpdateToast from "./lib/components/UpdateToast.svelte";
|
||||
|
||||
import {
|
||||
amendCommit,
|
||||
addRemote,
|
||||
checkoutBranch,
|
||||
cherryPickAbort,
|
||||
cherryPickCommit,
|
||||
@@ -59,6 +61,8 @@
|
||||
createTag,
|
||||
deleteBranch,
|
||||
deleteTag,
|
||||
deleteRemoteBranch,
|
||||
initRepository,
|
||||
diffFileAgainstWorkingTree,
|
||||
compareFileToParent,
|
||||
fetchRemote,
|
||||
@@ -66,6 +70,7 @@
|
||||
getStatus,
|
||||
lastCommitMessage,
|
||||
listBranches,
|
||||
listRemotes,
|
||||
listStashes,
|
||||
listTags,
|
||||
listCommits,
|
||||
@@ -74,12 +79,18 @@
|
||||
listReflog,
|
||||
listRepositoryFiles,
|
||||
mergeBranch,
|
||||
mergeAbort,
|
||||
mergeContinue,
|
||||
openRepoInExplorer,
|
||||
openRepositoryFile,
|
||||
openRepositoryBundle,
|
||||
pull,
|
||||
push,
|
||||
pushTag,
|
||||
removeRemote,
|
||||
revertCommit,
|
||||
setBranchUpstream,
|
||||
updateRemote,
|
||||
renameBranch,
|
||||
rebaseAbort,
|
||||
rebaseBranch,
|
||||
@@ -126,6 +137,8 @@
|
||||
GitDiffFile,
|
||||
GitFileStatus,
|
||||
GitRepositoryFile,
|
||||
GitRemote,
|
||||
PullStrategy,
|
||||
GitSearchHit,
|
||||
GitStash,
|
||||
GitStatus,
|
||||
@@ -231,6 +244,12 @@
|
||||
let repoStatusCache: Record<string, RepoTab> = {};
|
||||
let repoSearch = "";
|
||||
let cloneDialogOpen = false;
|
||||
let pullStrategy: PullStrategy = (localStorage.getItem("gitlite.pullStrategy") as PullStrategy) || "merge";
|
||||
let selectedRemote = localStorage.getItem("gitlite.selectedRemote") || "";
|
||||
let remoteActionForceWithLease = false;
|
||||
let remoteActionPrune = false;
|
||||
let syncSettingsOpen = false;
|
||||
let syncSettingsRemotes: GitRemote[] = [];
|
||||
let cloneDialogError = "";
|
||||
let cloneDialogErrorTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
let pendingClone: CloneRequest | null = null;
|
||||
@@ -393,6 +412,7 @@
|
||||
$: hasConflicts = conflictedFiles.length > 0;
|
||||
$: rebaseInProgress = status?.rebase_in_progress ?? false;
|
||||
$: cherryPickInProgress = status?.cherry_pick_in_progress ?? false;
|
||||
$: mergeInProgress = status?.merge_in_progress ?? false;
|
||||
// Amending/undoing is only offered while the last commit hasn't reached a
|
||||
// remote yet: no upstream at all, or the branch is still ahead of it.
|
||||
$: canAmend = hasRepository && commits.length > 0 && !hasConflicts && !rebaseInProgress && !cherryPickInProgress
|
||||
@@ -2260,7 +2280,27 @@
|
||||
|
||||
async function confirmDeleteBranch() {
|
||||
const branch = deleteBranchTarget;
|
||||
if (!activeRepoPath || !branch || branch.remote || branch.current || isBusy) return;
|
||||
if (!activeRepoPath || !branch || branch.current || isBusy) return;
|
||||
|
||||
if (branch.remote) {
|
||||
const slash = branch.name.indexOf("/");
|
||||
if (slash < 1) { errorMessage = "Could not determine remote name."; return; }
|
||||
const remote = branch.name.slice(0, slash);
|
||||
const remoteBranch = branch.name.slice(slash + 1);
|
||||
operation = `Deleting ${branch.name} from remote`;
|
||||
errorMessage = "";
|
||||
try {
|
||||
applyStatus(await deleteRemoteBranch(activeRepoPath, remote, remoteBranch));
|
||||
deleteBranchTarget = null;
|
||||
await refreshRefsAndCommitGraph(activeRepoPath);
|
||||
trackEvent("remote_branch_deleted");
|
||||
} catch (error) {
|
||||
errorMessage = errorToMessage(error);
|
||||
} finally {
|
||||
operation = "";
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
operation = `${deleteBranchForce ? "Force deleting" : "Deleting"} ${branch.name}`;
|
||||
errorMessage = "";
|
||||
@@ -2318,8 +2358,11 @@
|
||||
|
||||
async function merge(branch: GitBranchInfo) {
|
||||
if (!activeRepoPath || branch.current) return;
|
||||
const strategy = (window.prompt("Merge strategy: default, squash, ff-only, or no-ff", "default") ?? "").trim();
|
||||
if (!strategy) return;
|
||||
if (!["default", "squash", "ff-only", "no-ff"].includes(strategy)) { errorMessage = "Unknown merge strategy."; return; }
|
||||
await runOperation(`Merging ${branch.name}`, async () => {
|
||||
applyStatus(await mergeBranch(activeRepoPath, branch.name));
|
||||
applyStatus(await mergeBranch(activeRepoPath, branch.name, strategy as import("./lib/types").MergeStrategy));
|
||||
await refreshBranchList(activeRepoPath);
|
||||
await refreshCommitHistory(activeRepoPath);
|
||||
await refreshExplorerFiles(activeRepoPath);
|
||||
@@ -2624,7 +2667,7 @@
|
||||
) {
|
||||
errorMessage = "";
|
||||
await runOperation("Pulling", async () => {
|
||||
applyStatus(await pull(activeRepoPath, username, password));
|
||||
applyStatus(await pull(activeRepoPath, username, password, pullStrategy, selectedRemote || undefined));
|
||||
await refreshBranchList(activeRepoPath);
|
||||
await refreshCommitHistory(activeRepoPath);
|
||||
await refreshExplorerFiles(activeRepoPath);
|
||||
@@ -2645,7 +2688,8 @@
|
||||
) {
|
||||
errorMessage = "";
|
||||
await runOperation("Fetching", async () => {
|
||||
applyStatus(await fetchRemote(activeRepoPath, username, password));
|
||||
applyStatus(await fetchRemote(activeRepoPath, username, password, remoteActionPrune, selectedRemote || undefined));
|
||||
remoteActionPrune = false;
|
||||
await refreshRefsAndCommitGraph(activeRepoPath);
|
||||
trackEvent("repository_fetched", {
|
||||
from_stored_credential: fromStore ? 1 : 0,
|
||||
@@ -2664,7 +2708,8 @@
|
||||
) {
|
||||
errorMessage = "";
|
||||
await runOperation("Pushing", async () => {
|
||||
applyStatus(await push(activeRepoPath, username, password));
|
||||
applyStatus(await push(activeRepoPath, username, password, remoteActionForceWithLease, selectedRemote || undefined));
|
||||
remoteActionForceWithLease = false;
|
||||
await refreshBranchList(activeRepoPath);
|
||||
await refreshCommitHistory(activeRepoPath);
|
||||
await refreshFileHistory(activeRepoPath);
|
||||
@@ -2788,6 +2833,88 @@
|
||||
await startRemoteAction("push");
|
||||
}
|
||||
|
||||
async function deleteTrackedRemoteBranch(branch: GitBranchInfo) {
|
||||
if (!activeRepoPath || !branch.remote) return;
|
||||
if (import.meta.env.DEV) console.info("[Gitty remote] remote branch delete requested", branch);
|
||||
deleteBranchTarget = branch;
|
||||
deleteBranchForce = false;
|
||||
trackEvent("remote_branch_delete_dialog_opened");
|
||||
}
|
||||
|
||||
async function initializeRepository() {
|
||||
const selected = await openDialog({ directory: true, multiple: false, title: "Choose an empty or existing folder" });
|
||||
if (typeof selected !== "string") return;
|
||||
const branch = window.prompt("Initial branch name", "main")?.trim(); if (!branch) return;
|
||||
await runOperation("Initializing repository", async () => { await initRepository(selected, branch); await openRepo(selected); });
|
||||
}
|
||||
|
||||
async function revertHistoryCommit(commit: GitCommit) {
|
||||
if (!activeRepoPath || !window.confirm(`Revert commit ${commit.short_hash} (${commit.summary}) with a new commit?`)) return;
|
||||
await runOperation(`Reverting ${commit.short_hash}`, async () => {
|
||||
applyStatus(await revertCommit(activeRepoPath, commit.hash));
|
||||
await refreshCommitHistory(activeRepoPath); await refreshExplorerFiles(activeRepoPath); await refreshFileHistory(activeRepoPath);
|
||||
});
|
||||
}
|
||||
|
||||
async function continueMerge() {
|
||||
if (!activeRepoPath) return;
|
||||
await runOperation("Continuing merge", async () => { applyStatus(await mergeContinue(activeRepoPath)); await refreshCommitHistory(activeRepoPath); });
|
||||
}
|
||||
|
||||
async function abortMerge() {
|
||||
if (!activeRepoPath || !window.confirm("Abort the current merge and restore the pre-merge state?")) return;
|
||||
await runOperation("Aborting merge", async () => { applyStatus(await mergeAbort(activeRepoPath)); await refreshCommitHistory(activeRepoPath); await refreshExplorerFiles(activeRepoPath); });
|
||||
}
|
||||
|
||||
async function fetchPruneRepo() {
|
||||
remoteActionPrune = true;
|
||||
await startRemoteAction("fetch");
|
||||
}
|
||||
|
||||
async function forcePushRepo() {
|
||||
if (!window.confirm("Push the current branch with --force-with-lease? This is intended for a branch whose history you rebased.")) return;
|
||||
remoteActionForceWithLease = true;
|
||||
await startRemoteAction("push");
|
||||
}
|
||||
|
||||
async function openSyncOptions() {
|
||||
if (!activeRepoPath) return;
|
||||
try {
|
||||
syncSettingsRemotes = await listRemotes(activeRepoPath);
|
||||
syncSettingsOpen = true;
|
||||
} catch (error) { errorMessage = errorToMessage(error); }
|
||||
}
|
||||
|
||||
async function saveSyncSettings(strategy: PullStrategy, remote: string, upstream: string) {
|
||||
if (!activeRepoPath || !status?.current_branch) return;
|
||||
await runOperation("Saving sync settings", async () => {
|
||||
pullStrategy = strategy; selectedRemote = remote;
|
||||
localStorage.setItem("gitlite.pullStrategy", strategy); localStorage.setItem("gitlite.selectedRemote", remote);
|
||||
applyStatus(await setBranchUpstream(activeRepoPath, status!.current_branch!, upstream || undefined));
|
||||
await refreshBranchList(activeRepoPath); syncSettingsOpen = false;
|
||||
});
|
||||
}
|
||||
|
||||
async function addSyncRemote(name: string, url: string) { if (!activeRepoPath) return; syncSettingsRemotes = await addRemote(activeRepoPath, name, url); await refreshBranchList(activeRepoPath); }
|
||||
async function updateSyncRemote(name: string, url: string) { if (!activeRepoPath) return; syncSettingsRemotes = await updateRemote(activeRepoPath, name, url); }
|
||||
async function removeSyncRemote(name: string) {
|
||||
if (!activeRepoPath) return;
|
||||
operation = `Removing remote ${name}`;
|
||||
try {
|
||||
syncSettingsRemotes = await removeRemote(activeRepoPath, name);
|
||||
if (syncSettingsRemotes.some((remote) => remote.name === name)) throw new Error(`Remote '${name}' still exists after removal.`);
|
||||
if (selectedRemote === name) { selectedRemote = ""; localStorage.setItem("gitlite.selectedRemote", ""); }
|
||||
applyStatus(await getStatus(activeRepoPath));
|
||||
await refreshBranchList(activeRepoPath);
|
||||
} catch (error) {
|
||||
const message = errorToMessage(error);
|
||||
if (import.meta.env.DEV) console.error("[Gitty remote] remove_remote failed", { name, path: activeRepoPath, error });
|
||||
throw new Error(message);
|
||||
} finally {
|
||||
operation = "";
|
||||
}
|
||||
}
|
||||
|
||||
async function saveStash(message: string, includeUntracked: boolean) {
|
||||
if (!activeRepoPath || changedFiles.length === 0) return;
|
||||
const stashedFiles = changedFiles.length;
|
||||
@@ -3576,6 +3703,7 @@
|
||||
}
|
||||
|
||||
function handleWindowContextMenu(event: MouseEvent) {
|
||||
if (import.meta.env.DEV) return;
|
||||
event.preventDefault();
|
||||
if (repoTabContextMenu) closeRepoTabContextMenu();
|
||||
}
|
||||
@@ -3632,6 +3760,9 @@
|
||||
onInteractiveRebase={openInteractiveRebase}
|
||||
onReflog={openReflog}
|
||||
onOpenInExplorer={openActiveRepoInExplorer}
|
||||
onFetchPrune={fetchPruneRepo}
|
||||
onForcePush={forcePushRepo}
|
||||
onSyncOptions={openSyncOptions}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -3677,7 +3808,15 @@
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
{#if workspaceActive && hasConflicts && !rebaseInProgress && !cherryPickInProgress}
|
||||
{#if workspaceActive && mergeInProgress}
|
||||
<section class="notice conflict" role="status">
|
||||
<GitMerge size={17} aria-hidden="true" />
|
||||
<span>{hasConflicts ? "Merge in progress. Resolve all conflicts, then continue." : "Merge is ready to be completed."}</span>
|
||||
{#if hasConflicts}<button type="button" onclick={openResolveDialog} disabled={isBusy}>Resolve conflicts</button>{/if}
|
||||
<button type="button" onclick={continueMerge} disabled={isBusy || hasConflicts}>Continue</button>
|
||||
<button type="button" onclick={abortMerge} disabled={isBusy}>Abort</button>
|
||||
</section>
|
||||
{:else if workspaceActive && hasConflicts && !rebaseInProgress && !cherryPickInProgress}
|
||||
<section class="notice conflict" role="alert">
|
||||
<GitMerge size={17} aria-hidden="true" />
|
||||
<span>{conflictedFiles.length} {conflictedFiles.length === 1 ? "file has" : "files have"} conflicts.</span>
|
||||
@@ -3735,6 +3874,9 @@
|
||||
<Download size={15} aria-hidden="true" />
|
||||
Clone
|
||||
</button>
|
||||
<button class="btn-secondary" type="button" onclick={initializeRepository} disabled={isBusy}>
|
||||
<Plus size={15} aria-hidden="true" /> Init
|
||||
</button>
|
||||
<button class="btn-secondary" type="button" onclick={chooseRepositoryFolder} disabled={isBusy}>
|
||||
<FolderOpen size={15} aria-hidden="true" />
|
||||
Browse
|
||||
@@ -3910,6 +4052,7 @@
|
||||
onCreateBranch={createNewBranch}
|
||||
onRenameBranch={renameLocalBranch}
|
||||
onDeleteBranch={deleteLocalBranch}
|
||||
onDeleteRemoteBranch={deleteTrackedRemoteBranch}
|
||||
onCreateTag={createNewTag}
|
||||
onDeleteTag={deleteLocalTag}
|
||||
onPushTag={pushLocalTag}
|
||||
@@ -4121,6 +4264,7 @@
|
||||
onPreviewCommitFile={previewCommitFileFromHistory}
|
||||
onCreateBranchFromCommit={openNewBranchDialog}
|
||||
onCherryPickCommit={cherryPickFromCommit}
|
||||
onRevertCommit={revertHistoryCommit}
|
||||
onToggleCommitFiles={(hash) => {
|
||||
const next = new Set(expandedCommitHashes);
|
||||
if (next.has(hash)) next.delete(hash); else next.add(hash);
|
||||
@@ -4297,7 +4441,7 @@
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<!-- Delete a local branch from the branch context menu -->
|
||||
<!-- Confirm deletion of a local or remote branch from the shared branch context menu -->
|
||||
{#if deleteBranchTarget}
|
||||
<BranchDeleteConfirmDialog
|
||||
branch={deleteBranchTarget}
|
||||
@@ -4390,6 +4534,25 @@
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<!-- Clone repository dialog -->
|
||||
{#if syncSettingsOpen}
|
||||
<SyncSettingsDialog
|
||||
remotes={syncSettingsRemotes}
|
||||
remoteBranches={remoteBranches.map((branch) => branch.name)}
|
||||
currentBranch={status?.current_branch ?? ""}
|
||||
currentUpstream={status?.upstream ?? ""}
|
||||
strategy={pullStrategy}
|
||||
{selectedRemote}
|
||||
{isBusy}
|
||||
language={appLanguage}
|
||||
onSaveSync={saveSyncSettings}
|
||||
onAddRemote={addSyncRemote}
|
||||
onUpdateRemote={updateSyncRemote}
|
||||
onRemoveRemote={removeSyncRemote}
|
||||
onClose={() => { if (!isBusy) syncSettingsOpen = false; }}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<!-- Clone repository dialog -->
|
||||
{#if cloneDialogOpen}
|
||||
<CloneRepositoryDialog
|
||||
|
||||
+105
-24
@@ -1703,20 +1703,23 @@
|
||||
.stash-toggle {
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
width: 26px;
|
||||
min-width: 26px;
|
||||
min-height: 26px;
|
||||
width: 28px;
|
||||
min-width: 28px;
|
||||
height: 28px;
|
||||
min-height: 28px;
|
||||
aspect-ratio: 1;
|
||||
flex: 0 0 28px;
|
||||
padding: 0;
|
||||
border-color: rgba(94,110,156,0.18);
|
||||
border-radius: 7px;
|
||||
border-radius: 6px;
|
||||
color: var(--color-ink-dim);
|
||||
background: rgba(255,255,255,0.035);
|
||||
background: rgba(255,255,255,0.018);
|
||||
}
|
||||
|
||||
.stash-toggle:hover:not(:disabled) {
|
||||
border-color: rgba(65,209,255,0.28);
|
||||
color: var(--color-ink);
|
||||
background: rgba(65,209,255,0.08);
|
||||
background: rgba(65,209,255,0.055);
|
||||
}
|
||||
|
||||
.stash-create {
|
||||
@@ -1847,19 +1850,24 @@
|
||||
}
|
||||
|
||||
.branch-create-toggle {
|
||||
width: 26px;
|
||||
min-width: 26px;
|
||||
min-height: 26px;
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
width: 28px;
|
||||
min-width: 28px;
|
||||
height: 28px;
|
||||
min-height: 28px;
|
||||
aspect-ratio: 1;
|
||||
flex: 0 0 28px;
|
||||
padding: 0;
|
||||
border-color: rgba(65,209,255,0.2);
|
||||
border-radius: 7px;
|
||||
border-color: rgba(94,110,156,0.18);
|
||||
border-radius: 6px;
|
||||
color: var(--color-ink-dim);
|
||||
background: rgba(65,209,255,0.06);
|
||||
background: rgba(255,255,255,0.018);
|
||||
}
|
||||
.branch-create-toggle:hover:not(:disabled) {
|
||||
border-color: rgba(65,209,255,0.45);
|
||||
color: #ffffff;
|
||||
background: rgba(65,209,255,0.13);
|
||||
border-color: rgba(65,209,255,0.3);
|
||||
color: var(--color-ink);
|
||||
background: rgba(65,209,255,0.06);
|
||||
}
|
||||
|
||||
.branch-list { gap: 8px; }
|
||||
@@ -2124,19 +2132,24 @@
|
||||
|
||||
.explorer-head-actions { display: flex; align-items: center; gap: 5px; flex: 0 0 auto; }
|
||||
.explorer-bulk-button {
|
||||
width: 26px;
|
||||
min-width: 26px;
|
||||
min-height: 26px;
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
width: 28px;
|
||||
min-width: 28px;
|
||||
height: 28px;
|
||||
min-height: 28px;
|
||||
aspect-ratio: 1;
|
||||
flex: 0 0 28px;
|
||||
padding: 0;
|
||||
border-color: rgba(65,209,255,0.2);
|
||||
border-radius: 7px;
|
||||
border-color: rgba(94,110,156,0.18);
|
||||
border-radius: 6px;
|
||||
color: var(--color-ink-dim);
|
||||
background: rgba(65,209,255,0.06);
|
||||
background: rgba(255,255,255,0.018);
|
||||
}
|
||||
.explorer-bulk-button:hover:not(:disabled) {
|
||||
border-color: rgba(65,209,255,0.45);
|
||||
color: #ffffff;
|
||||
background: rgba(65,209,255,0.13);
|
||||
border-color: rgba(65,209,255,0.3);
|
||||
color: var(--color-ink);
|
||||
background: rgba(65,209,255,0.06);
|
||||
}
|
||||
|
||||
.explorer-row {
|
||||
@@ -6011,6 +6024,74 @@ input:focus, textarea:focus, select:focus { box-shadow: 0 0 0 3px color-mix(in s
|
||||
.ai-review-finding-body > p { margin: 6px 0 8px; color: var(--color-ink-muted); font-size: 12px; line-height: 1.5; }
|
||||
.ai-review-location { display: flex; align-items: center; gap: 5px; color: var(--color-primary); }
|
||||
.ai-review-location code { overflow-wrap: anywhere; font: 10.5px/1.4 var(--font-mono); }
|
||||
|
||||
/* Sync settings: one place for pull behavior, upstream and remote connections. */
|
||||
.sync-settings-dialog { width: min(820px, calc(100vw - 32px)); max-height: min(760px, calc(100vh - 32px)); display: flex; flex-direction: column; overflow: hidden; border: 1px solid var(--color-border); border-radius: 14px; background: var(--app-dialog-bg); box-shadow: 0 28px 80px rgba(0,0,0,.48); }
|
||||
.sync-settings-head { display: flex; align-items: center; justify-content: space-between; padding: 18px 20px; border-bottom: 1px solid var(--color-border-subtle); background: var(--app-dialog-chrome); }
|
||||
.sync-settings-title { display: flex; align-items: center; gap: 12px; }
|
||||
.sync-settings-title h2 { margin: 2px 0 0; color: var(--color-ink); font-size: 19px; }
|
||||
.sync-settings-icon { display: grid; place-items: center; width: 38px; height: 38px; border: 1px solid rgba(90,140,248,.28); border-radius: 10px; color: var(--color-accent); background: rgba(90,140,248,.1); }
|
||||
.sync-settings-body { display: grid; gap: 12px; min-height: 0; padding: 14px; overflow: auto; }
|
||||
.sync-settings-card { padding: 16px; border: 1px solid var(--color-border-subtle); border-radius: 11px; background: var(--color-surface-raised); }
|
||||
.sync-card-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 14px; margin-bottom: 13px; }
|
||||
.sync-card-heading h3 { margin: 0; color: var(--color-ink); font-size: 13px; }
|
||||
.sync-card-heading p { margin: 4px 0 0; color: var(--color-ink-faint); font-size: 11px; }
|
||||
.count-pill { min-width: 24px; padding: 3px 7px; border-radius: 999px; color: var(--color-ink-muted); background: var(--color-surface-hover); font: 700 10px var(--font-mono); text-align: center; }
|
||||
.strategy-options { display: grid; grid-template-columns: repeat(3, minmax(0,1fr)); gap: 8px; }
|
||||
.strategy-options label { position: relative; display: grid; grid-template-columns: 18px 1fr; gap: 8px; min-height: 94px; padding: 12px; border: 1px solid var(--color-border-subtle); border-radius: 9px; color: var(--color-ink-muted); background: var(--app-input-bg); cursor: pointer; }
|
||||
.strategy-options label.active { border-color: rgba(90,140,248,.65); box-shadow: inset 0 0 0 1px rgba(90,140,248,.18); background: rgba(90,140,248,.08); }
|
||||
.strategy-options input { margin-top: 2px; accent-color: var(--color-accent); }
|
||||
.strategy-options span { display: grid; align-content: start; gap: 5px; }
|
||||
.strategy-options strong { color: var(--color-ink); font-size: 12px; }
|
||||
.strategy-options small, .sync-fields small { color: var(--color-ink-faint); font-size: 10px; line-height: 1.45; }
|
||||
.sync-fields { display: grid; grid-template-columns: repeat(2,minmax(0,1fr)); gap: 10px; margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--color-border-subtle); }
|
||||
.sync-fields label { display: grid; gap: 6px; color: var(--color-ink-muted); font-size: 11px; font-weight: 700; }
|
||||
.sync-fields select, .remote-add input, .remote-edit input { width: 100%; height: 35px; border: 1px solid var(--color-border-input); border-radius: 7px; color: var(--color-ink); background: var(--app-input-bg); font-size: 11px; }
|
||||
.sync-fields select { padding: 0 9px; }
|
||||
.remote-list { display: grid; gap: 5px; }
|
||||
.sync-action-error { display: grid; gap: 4px; margin-bottom: 5px; padding: 10px 11px; border: 1px solid rgba(232,96,96,.28); border-radius: 8px; color: #ef8888; background: rgba(232,96,96,.08); }
|
||||
.sync-action-error strong { font-size: 11px; }
|
||||
.sync-action-error span { font: 9.5px/1.45 var(--font-mono); overflow-wrap: anywhere; }
|
||||
.sync-action-status { margin-bottom: 5px; padding: 9px 11px; border: 1px solid rgba(90,140,248,.28); border-radius: 8px; color: var(--color-accent); background: rgba(90,140,248,.08); font-size: 10.5px; font-weight: 700; }
|
||||
.remote-row { display: grid; grid-template-columns: 28px minmax(0,1fr) auto auto; align-items: center; gap: 7px; min-height: 48px; padding: 6px 7px; border: 1px solid var(--color-border-subtle); border-radius: 8px; background: var(--app-input-bg); }
|
||||
.remote-mark { display: grid; place-items: center; color: var(--color-accent); }
|
||||
.remote-main { display: grid; gap: 3px; min-width: 0; padding: 0; border: 0; color: var(--color-ink); background: transparent; text-align: left; }
|
||||
.remote-main strong, .remote-edit strong { font-size: 11.5px; }
|
||||
.remote-main span { overflow: hidden; color: var(--color-ink-faint); font: 10px var(--font-mono); text-overflow: ellipsis; white-space: nowrap; }
|
||||
.remote-edit { display: grid; grid-template-columns: 80px minmax(0,1fr); align-items: center; gap: 8px; }
|
||||
.remote-confirm { display: grid; gap: 3px; min-width: 0; }
|
||||
.remote-confirm strong { color: var(--color-ink); font-size: 11px; }
|
||||
.remote-confirm span { color: var(--color-ink-faint); font-size: 9.5px; }
|
||||
.remote-edit input, .remote-add input { padding: 0 9px; }
|
||||
.remote-delete { display: inline-flex; align-items: center; gap: 5px; height: 30px; padding: 0 8px; border: 0; border-radius: 6px; color: #e86060; background: transparent; font-size: 10px; font-weight: 750; }
|
||||
.remote-delete:hover { background: rgba(235,87,87,.1); }
|
||||
.btn-danger { display: inline-flex; align-items: center; justify-content: center; gap: 5px; min-height: 34px; padding: 0 11px; border: 1px solid rgba(232,96,96,.38); border-radius: 7px; color: #fff; background: #c84f4f; font-size: 10.5px; font-weight: 750; }
|
||||
.btn-danger:hover:not(:disabled) { background: #dd5b5b; }
|
||||
.remote-empty { margin: 4px 0 10px; color: var(--color-ink-faint); font-size: 11px; }
|
||||
.remote-add { display: grid; grid-template-columns: 120px minmax(180px,1fr) auto; gap: 7px; margin-top: 9px; padding-top: 10px; border-top: 1px solid var(--color-border-subtle); }
|
||||
.sync-settings-footer { display: flex; align-items: center; justify-content: space-between; gap: 18px; padding: 13px 20px; border-top: 1px solid var(--color-border-subtle); background: var(--app-dialog-chrome); }
|
||||
.sync-settings-footer p { max-width: 440px; margin: 0; color: var(--color-ink-faint); font-size: 9.5px; line-height: 1.4; }
|
||||
.sync-settings-footer > div { display: flex; gap: 8px; }
|
||||
@media (max-width: 720px) { .strategy-options, .sync-fields { grid-template-columns: 1fr; } .remote-add { grid-template-columns: 1fr; } .sync-settings-footer { align-items: stretch; flex-direction: column; } .sync-settings-footer > div { justify-content: flex-end; } }
|
||||
|
||||
/* Compact square controls shared by the Branches, Stash and Explorer headers. */
|
||||
.branch-head-actions .branch-create-toggle,
|
||||
.stash-head-actions .stash-toggle,
|
||||
.explorer-head-actions .explorer-bulk-button {
|
||||
box-sizing: border-box;
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
inline-size: 24px;
|
||||
min-inline-size: 24px;
|
||||
max-inline-size: 24px;
|
||||
block-size: 24px;
|
||||
min-block-size: 24px;
|
||||
max-block-size: 24px;
|
||||
flex: 0 0 24px;
|
||||
aspect-ratio: 1 / 1;
|
||||
padding: 0;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.ai-review-suggestion { display: grid; gap: 3px; margin-top: 9px; padding: 8px 9px; border-radius: 5px; background: var(--color-surface-dim); }
|
||||
.ai-review-suggestion strong { color: var(--color-ink-dim); font-size: 9.5px; text-transform: uppercase; letter-spacing: .05em; }
|
||||
.ai-review-suggestion span { color: var(--color-ink-muted); font-size: 11.5px; line-height: 1.45; }
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
RefreshCw,
|
||||
Search,
|
||||
Upload,
|
||||
Settings2,
|
||||
} from "@lucide/svelte";
|
||||
|
||||
export let hasRepository: boolean = false;
|
||||
@@ -28,8 +29,12 @@
|
||||
export let onInteractiveRebase: () => void = () => {};
|
||||
export let onReflog: () => void = () => {};
|
||||
export let onOpenInExplorer: () => void = () => {};
|
||||
export let onFetchPrune: () => void = () => {};
|
||||
export let onForcePush: () => void = () => {};
|
||||
export let onSyncOptions: () => void = () => {};
|
||||
|
||||
let historyOpen = false;
|
||||
let syncOpen = false;
|
||||
let toolbarElement: HTMLDivElement;
|
||||
|
||||
$: isGerman = language === "de";
|
||||
@@ -40,9 +45,7 @@
|
||||
}
|
||||
|
||||
function handleWindowClick(event: MouseEvent) {
|
||||
if (historyOpen && toolbarElement && !toolbarElement.contains(event.target as Node)) {
|
||||
historyOpen = false;
|
||||
}
|
||||
if (toolbarElement && !toolbarElement.contains(event.target as Node)) { historyOpen = false; syncOpen = false; }
|
||||
}
|
||||
|
||||
function handleWindowKeydown(event: KeyboardEvent) {
|
||||
@@ -108,6 +111,18 @@
|
||||
<span class="repo-action-label">Push</span>
|
||||
{#if ahead > 0}<span class="repo-action-count ahead">↑{ahead}</span>{/if}
|
||||
</button>
|
||||
<div class="repo-history-wrap">
|
||||
<button class="repo-action" type="button" onclick={() => { syncOpen = !syncOpen; historyOpen = false; }} disabled={!hasRepository || isBusy} aria-label={isGerman ? "Sync-Optionen" : "Sync options"} aria-haspopup="menu">
|
||||
<ChevronDown size={14} aria-hidden="true" />
|
||||
</button>
|
||||
{#if syncOpen}
|
||||
<div class="repo-history-menu" role="menu">
|
||||
<button type="button" role="menuitem" onclick={() => { syncOpen = false; onFetchPrune(); }}><CloudDownload size={15} /><span><strong>Fetch + Prune</strong><small>{isGerman ? "Veraltete Remote-Branches entfernen" : "Remove stale remote branches"}</small></span></button>
|
||||
<button type="button" role="menuitem" onclick={() => { syncOpen = false; onForcePush(); }}><Upload size={15} /><span><strong>Force with lease</strong><small>{isGerman ? "Sicheres Pushen nach Rebase" : "Safe push after rebase"}</small></span></button>
|
||||
<button type="button" role="menuitem" onclick={() => { syncOpen = false; onSyncOptions(); }}><Settings2 size={15} /><span><strong>{isGerman ? "Remotes & Strategien" : "Remotes & strategies"}</strong><small>{isGerman ? "Upstream, Pull und Remote verwalten" : "Manage upstream, pull and remotes"}</small></span></button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
onClose = () => {},
|
||||
}: Props = $props();
|
||||
|
||||
let title = $derived(force ? "Force delete branch?" : "Delete branch?");
|
||||
let title = $derived(branch.remote ? "Delete remote branch?" : force ? "Force delete branch?" : "Delete branch?");
|
||||
let remoteParts = $derived(branch.remote ? branch.name.split(/\/(.+)/) : []);
|
||||
|
||||
</script>
|
||||
|
||||
@@ -26,7 +27,7 @@
|
||||
<div class="dialog branch-delete-dialog" role="dialog" aria-modal="true" aria-label={title}>
|
||||
<header class="dialog-header">
|
||||
<div>
|
||||
<span class="eyebrow">{force ? "Force delete" : "Delete branch"}</span>
|
||||
<span class="eyebrow">{branch.remote ? "Remote branch" : force ? "Force delete" : "Delete branch"}</span>
|
||||
<p class="dialog-title">{title}</p>
|
||||
</div>
|
||||
<button class="btn-sm dialog-close" type="button" onclick={onClose} disabled={isBusy} aria-label="Close">
|
||||
@@ -41,7 +42,9 @@
|
||||
|
||||
<div class="discard-confirm-copy">
|
||||
<p>
|
||||
{#if force}
|
||||
{#if branch.remote}
|
||||
Delete this branch from the remote server?
|
||||
{:else if force}
|
||||
This branch is not fully merged. Force deleting removes the branch pointer even if some commits are only reachable from this branch.
|
||||
{:else}
|
||||
Delete this local branch from the repository?
|
||||
@@ -52,7 +55,9 @@
|
||||
{branch.name}
|
||||
</code>
|
||||
<p class="discard-warning-text">
|
||||
{#if force}
|
||||
{#if branch.remote}
|
||||
This affects everyone using <strong>{remoteParts[0] || "the remote"}</strong>. Your local commits and local branches are kept.
|
||||
{:else if force}
|
||||
Make sure you no longer need the unique commits on this branch.
|
||||
{:else}
|
||||
Git will refuse if the branch is not fully merged.
|
||||
@@ -69,7 +74,7 @@
|
||||
{:else}
|
||||
<Trash2 size={15} aria-hidden="true" />
|
||||
{/if}
|
||||
{force ? "Force delete" : "Delete"}
|
||||
{branch.remote ? "Delete from remote" : force ? "Force delete" : "Delete"}
|
||||
</button>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
onCreateBranch: (branchName: string) => void | Promise<void>;
|
||||
onRenameBranch: (branch: GitBranchInfo) => void | Promise<void>;
|
||||
onDeleteBranch: (branch: GitBranchInfo) => void | Promise<void>;
|
||||
onDeleteRemoteBranch: (branch: GitBranchInfo) => void | Promise<void>;
|
||||
onCreateTag: (name: string, message: string) => void | Promise<void>;
|
||||
onDeleteTag: (tag: GitTag) => void | Promise<void>;
|
||||
onPushTag: (tag: GitTag) => void | Promise<void>;
|
||||
@@ -74,6 +75,7 @@
|
||||
onCreateBranch = () => {},
|
||||
onRenameBranch = () => {},
|
||||
onDeleteBranch = () => {},
|
||||
onDeleteRemoteBranch = () => {},
|
||||
onCreateTag = () => {},
|
||||
onDeleteTag = () => {},
|
||||
onPushTag = () => {},
|
||||
@@ -264,9 +266,9 @@
|
||||
|
||||
async function deleteContextBranch() {
|
||||
const branch = contextBranch;
|
||||
if (!branch || branch.current || branch.remote || isBusy) return;
|
||||
if (!branch || branch.current || isBusy) return;
|
||||
closeBranchContextMenu();
|
||||
await onDeleteBranch(branch);
|
||||
if (branch.remote) await onDeleteRemoteBranch(branch); else await onDeleteBranch(branch);
|
||||
}
|
||||
|
||||
async function checkoutContextBranch() {
|
||||
@@ -669,11 +671,11 @@
|
||||
type="button"
|
||||
role="menuitem"
|
||||
onclick={deleteContextBranch}
|
||||
disabled={isBusy || contextBranch.current || contextBranch.remote}
|
||||
title={contextBranch.current ? "Current branch cannot be deleted" : contextBranch.remote ? "Remote branch cannot be deleted here" : "Delete local branch"}
|
||||
disabled={isBusy || contextBranch.current}
|
||||
title={contextBranch.current ? "Current branch cannot be deleted" : contextBranch.remote ? "Delete remote branch" : "Delete local branch"}
|
||||
>
|
||||
<Trash2 size={14} aria-hidden="true" />
|
||||
Delete
|
||||
{contextBranch.remote ? "Delete remote" : "Delete"}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
onToggleCommitFiles: (hash: string) => void;
|
||||
onCreateBranchFromCommit: (commit: GitCommit) => void;
|
||||
onCherryPickCommit: (commit: GitCommit) => void;
|
||||
onRevertCommit: (commit: GitCommit) => void;
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -58,6 +59,7 @@
|
||||
onToggleCommitFiles = () => {},
|
||||
onCreateBranchFromCommit = () => {},
|
||||
onCherryPickCommit = () => {},
|
||||
onRevertCommit = () => {},
|
||||
}: Props = $props();
|
||||
|
||||
let hiddenGraphBranches = $state<Set<string>>(new Set());
|
||||
@@ -406,6 +408,13 @@
|
||||
await onCherryPickCommit(commit);
|
||||
}
|
||||
|
||||
async function revertContextCommit() {
|
||||
const commit = contextCommit;
|
||||
if (!commit || isBusy) return;
|
||||
closeCommitContextMenu();
|
||||
await onRevertCommit(commit);
|
||||
}
|
||||
|
||||
function handleWindowKeydown(event: KeyboardEvent) {
|
||||
if (event.key !== "Escape") return;
|
||||
closeCommitContextMenu();
|
||||
@@ -718,6 +727,9 @@
|
||||
<Cherry size={14} aria-hidden="true" />
|
||||
Cherry-pick
|
||||
</button>
|
||||
<button type="button" role="menuitem" onclick={revertContextCommit} disabled={isBusy} title="Create a new commit that reverses this commit">
|
||||
<RotateCcw size={14} aria-hidden="true" /> Revert
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
<script lang="ts">
|
||||
import { Cloud, GitBranch, Plus, Save, Trash2, X } from "@lucide/svelte";
|
||||
import type { GitRemote, PullStrategy } from "../types";
|
||||
|
||||
export let remotes: GitRemote[] = [];
|
||||
export let remoteBranches: string[] = [];
|
||||
export let currentBranch = "";
|
||||
export let currentUpstream = "";
|
||||
export let strategy: PullStrategy = "merge";
|
||||
export let selectedRemote = "";
|
||||
export let isBusy = false;
|
||||
export let language: "en" | "de" = "en";
|
||||
export let onSaveSync: (strategy: PullStrategy, remote: string, upstream: string) => void | Promise<void> = () => {};
|
||||
export let onAddRemote: (name: string, url: string) => void | Promise<void> = () => {};
|
||||
export let onUpdateRemote: (name: string, url: string) => void | Promise<void> = () => {};
|
||||
export let onRemoveRemote: (name: string) => void | Promise<void> = () => {};
|
||||
export let onClose: () => void = () => {};
|
||||
|
||||
let draftStrategy = strategy;
|
||||
let draftRemote = selectedRemote;
|
||||
let draftUpstream = currentUpstream;
|
||||
let newName = "origin";
|
||||
let newUrl = "";
|
||||
let editingName = "";
|
||||
let editingUrl = "";
|
||||
let actionError = "";
|
||||
let actionStatus = "";
|
||||
$: de = language === "de";
|
||||
|
||||
function beginEdit(remote: GitRemote) { actionError = ""; editingName = remote.name; editingUrl = remote.fetch_url; }
|
||||
async function requestDelete(event: MouseEvent, name: string) {
|
||||
console.log(name)
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
editingName = "";
|
||||
actionError = "";
|
||||
actionStatus = de ? `Remote „${name}“ wird entfernt …` : `Removing remote “${name}” …`;
|
||||
console.info("[Gitty remote] remove button activated", { name });
|
||||
await remove(name);
|
||||
}
|
||||
function cancelEdit() { editingName = ""; editingUrl = ""; }
|
||||
async function add() { if (!newName.trim() || !newUrl.trim()) return; await onAddRemote(newName.trim(), newUrl.trim()); newName = "origin"; newUrl = ""; }
|
||||
async function update() { if (!editingName || !editingUrl.trim()) return; await onUpdateRemote(editingName, editingUrl.trim()); cancelEdit(); }
|
||||
async function remove(name: string) {
|
||||
actionError = "";
|
||||
console.log("remove")
|
||||
try {
|
||||
await onRemoveRemote(name);
|
||||
if (draftRemote === name) draftRemote = "";
|
||||
if (draftUpstream.startsWith(`${name}/`)) draftUpstream = "";
|
||||
} catch (error) {
|
||||
actionError = error instanceof Error ? error.message : String(error);
|
||||
} finally {
|
||||
actionStatus = "";
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="dialog-backdrop" role="presentation">
|
||||
<div class="sync-settings-dialog" role="dialog" aria-modal="true" aria-labelledby="sync-settings-title">
|
||||
<header class="sync-settings-head">
|
||||
<div class="sync-settings-title">
|
||||
<span class="sync-settings-icon"><Cloud size={18} aria-hidden="true" /></span>
|
||||
<div><span class="eyebrow">Git sync</span><h2 id="sync-settings-title">{de ? "Synchronisierung & Remotes" : "Sync & remotes"}</h2></div>
|
||||
</div>
|
||||
<button class="dialog-icon-button" type="button" onclick={onClose} disabled={isBusy} aria-label={de ? "Schließen" : "Close"}><X size={17} /></button>
|
||||
</header>
|
||||
|
||||
<div class="sync-settings-body">
|
||||
<section class="sync-settings-card">
|
||||
<div class="sync-card-heading"><div><h3>{de ? "Pull-Verhalten" : "Pull behavior"}</h3><p>{de ? "Legt fest, wie entfernte Änderungen in deinen aktuellen Branch übernommen werden." : "Controls how remote changes are integrated into your current branch."}</p></div></div>
|
||||
<div class="strategy-options">
|
||||
<label class:active={draftStrategy === "merge"}><input type="radio" bind:group={draftStrategy} value="merge" /><span><strong>Merge</strong><small>{de ? "Erstellt bei getrennten Verläufen einen Merge-Commit. Sicher und leicht nachvollziehbar." : "Creates a merge commit for diverged history. Safe and easy to follow."}</small></span></label>
|
||||
<label class:active={draftStrategy === "rebase"}><input type="radio" bind:group={draftStrategy} value="rebase" /><span><strong>Rebase</strong><small>{de ? "Setzt deine lokalen Commits auf die Remote-Änderungen. Ergibt eine lineare Historie." : "Replays your local commits on remote changes for a linear history."}</small></span></label>
|
||||
<label class:active={draftStrategy === "ff-only"}><input type="radio" bind:group={draftStrategy} value="ff-only" /><span><strong>Fast-forward only</strong><small>{de ? "Pull stoppt, sobald ein Merge nötig wäre. Verändert die Historie nie automatisch." : "Stops when a merge would be required. Never combines diverged history automatically."}</small></span></label>
|
||||
</div>
|
||||
|
||||
<div class="sync-fields">
|
||||
<label><span>{de ? "Remote für Sync" : "Remote used for sync"}</span><select bind:value={draftRemote}><option value="">{de ? "Automatisch wählen" : "Choose automatically"}</option>{#each remotes as remote}<option value={remote.name}>{remote.name}</option>{/each}</select><small>{de ? "Ein Remote ist die gespeicherte Verbindung zu einem Server-Repository." : "A remote is a saved connection to a repository on a server."}</small></label>
|
||||
<label><span>{de ? `Upstream für ${currentBranch || "aktuellen Branch"}` : `Upstream for ${currentBranch || "current branch"}`}</span><select bind:value={draftUpstream}><option value="">{de ? "Kein Upstream" : "No upstream"}</option>{#each remoteBranches as branch}<option value={branch}>{branch}</option>{/each}</select><small>{de ? "Der Upstream ist der Remote-Branch, mit dem Pull, Push und Ahead/Behind verglichen werden." : "The upstream is the remote branch used by Pull, Push, and Ahead/Behind."}</small></label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="sync-settings-card">
|
||||
<div class="sync-card-heading"><div><h3>Remotes</h3><p>{de ? "Server-Verbindungen dieses Repositorys verwalten." : "Manage this repository's server connections."}</p></div><span class="count-pill">{remotes.length}</span></div>
|
||||
<div class="remote-list">
|
||||
{#if actionError}<div class="sync-action-error" role="alert"><strong>{de ? "Remote konnte nicht entfernt werden" : "Remote could not be removed"}</strong><span>{actionError}</span></div>{/if}
|
||||
{#if actionStatus}<div class="sync-action-status" role="status">{actionStatus}</div>{/if}
|
||||
{#each remotes as remote (remote.name)}
|
||||
<div class="remote-row">
|
||||
<span class="remote-mark"><GitBranch size={15} /></span>
|
||||
{#if editingName === remote.name}
|
||||
<div class="remote-edit"><strong>{remote.name}</strong><input bind:value={editingUrl} aria-label={`URL for ${remote.name}`} /></div>
|
||||
<button class="btn-sm" type="button" onclick={update} disabled={isBusy || !editingUrl.trim()}><Save size={13} /> {de ? "Speichern" : "Save"}</button>
|
||||
<button class="btn-sm" type="button" onclick={cancelEdit} disabled={isBusy}>{de ? "Abbrechen" : "Cancel"}</button>
|
||||
{:else}
|
||||
<button class="remote-main" type="button" onclick={() => beginEdit(remote)} disabled={isBusy}><strong>{remote.name}</strong><span>{remote.fetch_url}</span></button>
|
||||
<button class="remote-delete" type="button" onclick={(event) => requestDelete(event, remote.name)} data-remote-name={remote.name} title={de ? "Remote-Verbindung sofort entfernen; lokale Daten bleiben erhalten" : "Remove remote connection now; local data is kept"}><Trash2 size={13} /><span>{de ? "Entfernen" : "Remove"}</span></button>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}<p class="remote-empty">{de ? "Noch kein Remote eingerichtet." : "No remote configured yet."}</p>{/each}
|
||||
</div>
|
||||
<div class="remote-add"><input bind:value={newName} placeholder={de ? "Name, z. B. origin" : "Name, e.g. origin"} aria-label="Remote name" /><input bind:value={newUrl} placeholder="https://… or git@…" aria-label="Remote URL" /><button class="btn-secondary" type="button" onclick={add} disabled={isBusy || !newName.trim() || !newUrl.trim()}><Plus size={14} /> {de ? "Hinzufügen" : "Add remote"}</button></div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<footer class="sync-settings-footer"><p>{de ? "Fetch + Prune entfernt veraltete Remote-Verweise. Force with lease ist ein geschütztes Force-Push nach einem Rebase." : "Fetch + Prune removes stale remote references. Force with lease is a protected force-push after a rebase."}</p><div><button class="btn-secondary" type="button" onclick={onClose} disabled={isBusy}>{de ? "Abbrechen" : "Cancel"}</button><button class="btn-primary" type="button" onclick={() => onSaveSync(draftStrategy, draftRemote, draftUpstream)} disabled={isBusy}><Save size={14} /> {de ? "Einstellungen speichern" : "Save settings"}</button></div></footer>
|
||||
</div>
|
||||
</div>
|
||||
+27
-8
@@ -11,6 +11,9 @@ import type {
|
||||
GitCommit,
|
||||
GitCommitComparison,
|
||||
GitRepositoryFile,
|
||||
GitRemote,
|
||||
MergeStrategy,
|
||||
PullStrategy,
|
||||
RebaseCommit,
|
||||
RebasePlanItem,
|
||||
ReflogEntry,
|
||||
@@ -28,6 +31,10 @@ export function openRepository(path: string): Promise<GitStatus> {
|
||||
return invoke<GitStatus>("open_repository", { path });
|
||||
}
|
||||
|
||||
export function initRepository(path: string, initialBranch = "main"): Promise<GitStatus> {
|
||||
return invoke<GitStatus>("init_repository", { path, initialBranch });
|
||||
}
|
||||
|
||||
export function openRepoInExplorer(path: string): Promise<void> {
|
||||
return invoke<void>("open_repo_in_explorer", { path });
|
||||
}
|
||||
@@ -72,6 +79,15 @@ export function listBranches(path: string): Promise<GitBranch[]> {
|
||||
return invoke<GitBranch[]>("list_branches", { path });
|
||||
}
|
||||
|
||||
export function listRemotes(path: string): Promise<GitRemote[]> { return invoke("list_remotes", { path }); }
|
||||
export function addRemote(path: string, name: string, url: string): Promise<GitRemote[]> { return invoke("add_remote", { path, name, url }); }
|
||||
export function updateRemote(path: string, name: string, url: string): Promise<GitRemote[]> { return invoke("update_remote", { path, name, url }); }
|
||||
export function removeRemote(path: string, name: string): Promise<GitRemote[]> {
|
||||
console.log("remove_remote")
|
||||
return invoke("remove_remote", { path, name }); }
|
||||
export function setBranchUpstream(path: string, branch: string, upstream?: string): Promise<GitStatus> { return invoke("set_branch_upstream", { path, branch, upstream: upstream || null }); }
|
||||
export function deleteRemoteBranch(path: string, remote: string, branch: string): Promise<GitStatus> { return invoke("delete_remote_branch", { path, remote, branch }); }
|
||||
|
||||
export function listStashes(path: string): Promise<GitStash[]> {
|
||||
return invoke<GitStash[]>("list_stashes", { path });
|
||||
}
|
||||
@@ -255,16 +271,16 @@ export function commitAiReview(path: string, options: CommitAiGenerateOptions):
|
||||
});
|
||||
}
|
||||
|
||||
export function pull(path: string, username?: string, password?: string): Promise<GitStatus> {
|
||||
return invoke<GitStatus>("pull", { path, username: username ?? null, password: password ?? null });
|
||||
export function pull(path: string, username?: string, password?: string, strategy: PullStrategy = "merge", remote?: string, branch?: string): Promise<GitStatus> {
|
||||
return invoke<GitStatus>("pull", { path, username: username ?? null, password: password ?? null, strategy, remote: remote || null, branch: branch || null });
|
||||
}
|
||||
|
||||
export function fetchRemote(path: string, username?: string, password?: string): Promise<GitStatus> {
|
||||
return invoke<GitStatus>("fetch", { path, username: username ?? null, password: password ?? null });
|
||||
export function fetchRemote(path: string, username?: string, password?: string, prune = false, remote?: string): Promise<GitStatus> {
|
||||
return invoke<GitStatus>("fetch", { path, username: username ?? null, password: password ?? null, prune, remote: remote || null });
|
||||
}
|
||||
|
||||
export function push(path: string, username?: string, password?: string): Promise<GitStatus> {
|
||||
return invoke<GitStatus>("push", { path, username: username ?? null, password: password ?? null });
|
||||
export function push(path: string, username?: string, password?: string, forceWithLease = false, remote?: string): Promise<GitStatus> {
|
||||
return invoke<GitStatus>("push", { path, username: username ?? null, password: password ?? null, forceWithLease, remote: remote || null });
|
||||
}
|
||||
|
||||
export function getRemoteUrl(path: string): Promise<string | null> {
|
||||
@@ -304,9 +320,12 @@ export function restoreFileFromCommit(
|
||||
return invoke<GitStatus>("restore_file_from_commit", { path, commit, file });
|
||||
}
|
||||
|
||||
export function mergeBranch(path: string, branch: string): Promise<GitStatus> {
|
||||
return invoke<GitStatus>("merge_branch", { path, branch });
|
||||
export function mergeBranch(path: string, branch: string, strategy: MergeStrategy = "default"): Promise<GitStatus> {
|
||||
return invoke<GitStatus>("merge_branch", { path, branch, strategy });
|
||||
}
|
||||
export function mergeContinue(path: string): Promise<GitStatus> { return invoke("merge_continue", { path }); }
|
||||
export function mergeAbort(path: string): Promise<GitStatus> { return invoke("merge_abort", { path }); }
|
||||
export function revertCommit(path: string, commit: string): Promise<GitStatus> { return invoke("revert_commit", { path, commit }); }
|
||||
|
||||
export function rebaseBranch(path: string, branch: string): Promise<GitStatus> {
|
||||
return invoke<GitStatus>("rebase_branch", { path, branch });
|
||||
|
||||
@@ -68,8 +68,13 @@ export interface GitStatus {
|
||||
clean: boolean;
|
||||
rebase_in_progress: boolean;
|
||||
cherry_pick_in_progress: boolean;
|
||||
merge_in_progress: boolean;
|
||||
}
|
||||
|
||||
export interface GitRemote { name: string; fetch_url: string; push_url: string; }
|
||||
export type PullStrategy = "merge" | "rebase" | "ff-only";
|
||||
export type MergeStrategy = "default" | "squash" | "ff-only" | "no-ff";
|
||||
|
||||
export interface GitFileStatus {
|
||||
path: string;
|
||||
old_path: string | null;
|
||||
|
||||
Reference in New Issue
Block a user