FN-5877: add sync-with-origin remotes action

Add a one-click Remotes sync flow that rebases on pull before pushing.

- add a Sync button in Git Manager Remotes that runs pull --rebase and then push
- stop before pushing when the rebase pull reports a conflict or fails, and surface toasts/status updates
- cover successful, conflicting, failing, and loading sync behavior in GitManagerModal tests
- document the new Sync action in the dashboard guide

Files changed:
 docs/dashboard-guide.md                            |   1 +
 .../dashboard/app/components/GitManagerModal.tsx   |  39 ++++++++
 .../components/__tests__/GitManagerModal.test.tsx  | 100 +++++++++++++++++++++
 3 files changed, 140 insertions(+)

Fusion-Task-Id: FN-5877

Fusion-Task-Lineage: 2e28c00d-b965-4f44-a6fa-26536d91d16d
This commit is contained in:
gsxdsm
2026-06-02 08:25:46 -07:00
parent 8b839d79e8
commit 419d6cda86
3 changed files with 140 additions and 0 deletions

View File

@@ -818,6 +818,28 @@ export function GitManagerModal({ isOpen, onClose, tasks: _tasks, addToast, proj
}
}, [addToast, projectId]);
const handleSyncWithOrigin = useCallback(async () => {
setRemoteLoading("sync");
try {
const pullResult = await pullBranch({ rebase: true }, projectId);
setLastRemoteResult(pullResult);
if (pullResult.conflict) {
addToast("Merge conflict detected. Resolve manually.", "error");
return;
}
const pushResult = await pushBranch(projectId);
setLastRemoteResult(pushResult);
addToast("Synced with origin (pull --rebase + push)", "success");
const statusData = await fetchGitStatus(projectId, { extended: true });
setStatus(statusData);
} catch (err) {
addToast(getErrorMessage(err) || "Sync with origin failed", "error");
} finally {
setRemoteLoading(null);
}
}, [addToast, projectId]);
// Fetch rootDir from config (used as worktreePath for the per-task sync
// button surfaced from RemotesPanel below).
useEffect(() => {
@@ -1039,6 +1061,7 @@ export function GitManagerModal({ isOpen, onClose, tasks: _tasks, addToast, proj
onFetch={handleFetch}
onPull={handlePull}
onPush={handlePush}
onSync={handleSyncWithOrigin}
onSyncIntegrationTip={handleSyncIntegrationTip}
syncIntegrationDisabled={
!status?.integrationBranch ||
@@ -2244,6 +2267,7 @@ function RemotesPanel({
onFetch,
onPull,
onPush,
onSync,
onSyncIntegrationTip,
syncIntegrationDisabled,
addToast,
@@ -2256,6 +2280,7 @@ function RemotesPanel({
onFetch: () => void;
onPull: (options?: { rebase?: boolean }) => void;
onPush: () => void;
onSync: () => void;
onSyncIntegrationTip: () => void;
syncIntegrationDisabled: boolean;
addToast: (message: string, type?: ToastType) => void;
@@ -2761,6 +2786,20 @@ function RemotesPanel({
)}
Push
</button>
<button
className="btn btn-primary"
onClick={onSync}
disabled={remoteLoading !== null || loading}
title="Pull --rebase from origin, then push current branch"
data-testid="remotes-sync-origin-btn"
>
{remoteLoading === "sync" ? (
<Loader2 size={14} className="spin" />
) : (
<GitMerge size={14} />
)}
Sync
</button>
{status?.integrationBranch && (
<button
className="btn gm-sync-integration-btn"