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
|
||||
|
||||
Reference in New Issue
Block a user