Merge branch 'new_featrues'

This commit is contained in:
Christoph Brandau
2026-07-20 23:05:00 +02:00
14 changed files with 882 additions and 80 deletions
+170 -7
View File
@@ -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