Skip to content

Commit e28b98e

Browse files
mshamis-metameta-codesync[bot]
authored andcommitted
Add "Move to workspace" commit context-menu item for Commit Cloud
Summary: Right-clicking a draft commit in the smartlog now offers a "Move to workspace" submenu listing the user's other Commit Cloud workspaces. Picking one dispatches `sl cloud move -d <workspace> -r <hash>` via the standard operation queue, with no confirmation modal — same UX as Hide / Rebase. The submenu only renders when commit cloud is enabled, the commit is a draft, the workspace's current name is known, and at least two workspaces exist (so there is somewhere to move to). The current workspace is excluded from the destination list. `sl cloud move` already walks `dag.descendants(removenodes)` and pulls attached bookmarks along (`commitcloud/move.py`), so the new operation passes one `-r <hash>` argument and lets sl handle stack semantics. No multi-select branching, no separate "move stack" menu item, no bookmark toggle — the simplest possible first cut. Two small supporting changes outside the new files: - `CommitCloud.tsx` exports `cloudSyncStateAtom` so the context-menu builder in `Commit.tsx` can read workspace state synchronously, and a one-line `registerDisposable` triggers `fetchCommitCloudState` on every `repoInfo` connect so the menu item is available without first requiring the user to open the Download Commits dropdown (the only other consumer of this state). - `isl-server/src/analytics/eventNames.ts` adds `CommitCloudMoveCommitsOperation` to the `TrackEventName` union. Reviewed By: evangrayk Differential Revision: D106259928 fbshipit-source-id: be42b44dfb70f024dd77dd4d87415628b070fa80
1 parent e4453e0 commit e28b98e

5 files changed

Lines changed: 421 additions & 2 deletions

File tree

addons/isl-server/src/analytics/eventNames.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export type TrackEventName =
4343
| 'CommitCloudSyncBackupStatusCommand'
4444
| 'CommitCloudChangeWorkspaceOperation'
4545
| 'CommitCloudCreateWorkspaceOperation'
46+
| 'CommitCloudMoveCommitsOperation'
4647
| 'CommitCloudSyncOperation'
4748
| 'CreateEmptyInitialCommit'
4849
| 'ClickGotoTimeButton'

addons/isl/src/Commit.tsx

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {notEmpty, nullthrows} from 'shared/utils';
2626
import {AllBookmarksTruncated, Bookmark, Bookmarks, createBookmarkAtCommit} from './Bookmark';
2727
import {openBrowseUrlForHash, supportsBrowseUrlForHash} from './BrowseRepo';
2828
import css from './Commit.module.css';
29+
import {cloudSyncStateAtom} from './CommitCloud';
2930
import {hasUnsavedEditedCommitMessage} from './CommitInfoView/CommitInfoState';
3031
import {showComparison} from './ComparisonView/atoms';
3132
import {Row} from './ComponentUtils';
@@ -65,6 +66,7 @@ import {
6566
} from './jotaiUtils';
6667
import {CONFLICT_SIDE_LABELS} from './mergeConflicts/consts';
6768
import {getAmendToOperation, isAmendToAllowedForCommit} from './operationUtils';
69+
import {CommitCloudMoveCommitsOperation} from './operations/CommitCloudMoveCommitsOperation';
6870
import {GotoOperation} from './operations/GotoOperation';
6971
import {HideOperation} from './operations/HideOperation';
7072
import {RebaseOperation} from './operations/RebaseOperation';
@@ -479,6 +481,77 @@ export const Commit = memo(
479481
});
480482
}
481483
}
484+
if (!isPublic && !actionsPrevented && !inConflicts) {
485+
// Render "Move to workspace …" only when cloud is enabled, the
486+
// current workspace is known, and at least one other workspace
487+
// exists to target. `sl cloud move` itself follows descendants
488+
// and bookmarks, so each per-destination submenu item dispatches
489+
// exactly the hashes the user has selected — no separate
490+
// stack-aware branching is needed beyond multi-select.
491+
const cloudSyncState = readAtom(cloudSyncStateAtom);
492+
const cloud = cloudSyncState?.value;
493+
const currentWorkspace = cloud?.currentWorkspace;
494+
const workspaceChoices = cloud?.workspaceChoices;
495+
if (
496+
cloud != null &&
497+
cloud.isDisabled !== true &&
498+
currentWorkspace != null &&
499+
workspaceChoices != null &&
500+
workspaceChoices.length >= 2
501+
) {
502+
const destinations = workspaceChoices.filter(ws => ws !== currentWorkspace);
503+
if (destinations.length > 0) {
504+
// Multi-select: when the right-clicked commit is part of the
505+
// current smartlog selection (>1 commits), the operation
506+
// moves the whole selection in one `sl cloud move` invocation.
507+
// Mirrors the sibling Hide flow above (`isHideMultiSelect`)
508+
// so the two context-menu items behave consistently — without
509+
// this, the user would right-click on one of N selected
510+
// commits, see no visible cue that the menu only acts on
511+
// that single commit, and silently lose the other N-1
512+
// commits' moves (they'd stay in the source workspace).
513+
//
514+
// Filter to draft-phase only on the multi-select path. The
515+
// outer `!isPublic` gate at the submenu level already
516+
// protects the right-clicked commit; this filter extends
517+
// the same guarantee to the rest of the selection. `sl
518+
// cloud move` rejects public commits server-side, so
519+
// dispatching them in a mixed batch would either fail the
520+
// whole batch (no commits moved) or silently skip them
521+
// (depending on the sl version) — both bad UX. Filtering
522+
// client-side makes the behavior deterministic: only
523+
// drafts get moved, public commits stay put.
524+
const moveSelectedInfos = readAtom(selectedCommitInfos);
525+
const isMoveMultiSelect =
526+
moveSelectedInfos.length > 1 && moveSelectedInfos.some(c => c.hash === commit.hash);
527+
// Use `latestSuccessorUnlessExplicitlyObsolete` instead of raw
528+
// hashes so queued operations that mutate these commits before
529+
// `cloud move` runs are followed to their successor — except
530+
// when the user right-clicked a visibly-obsolete commit, in
531+
// which case we lock the action to the exact hash they saw.
532+
// Mirrors the sibling `HideOperation` callsite above.
533+
const sourcesToMove = isMoveMultiSelect
534+
? moveSelectedInfos
535+
.filter(c => c.phase !== 'public')
536+
.map(c => latestSuccessorUnlessExplicitlyObsolete(c))
537+
: [latestSuccessorUnlessExplicitlyObsolete(commit)];
538+
items.push({
539+
label: isMoveMultiSelect ? (
540+
<T replace={{$count: sourcesToMove.length}}>Move $count Commits to workspace</T>
541+
) : (
542+
<T>Move to workspace</T>
543+
),
544+
type: 'submenu',
545+
children: destinations.map(ws => ({
546+
label: ws,
547+
onClick: () => {
548+
runOperation(new CommitCloudMoveCommitsOperation(ws, sourcesToMove));
549+
},
550+
})),
551+
});
552+
}
553+
}
554+
}
482555
if (!actionsPrevented && !commit.isDot) {
483556
items.push({
484557
label: <T>Goto</T>,

addons/isl/src/CommitCloud.tsx

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import {CommitPreview, dagWithPreviews, useMostRecentPendingOperation} from './p
3131
import {RelativeDate} from './relativeDate';
3232
import {repoRootAndCwd} from './repositoryData';
3333
import {CommitCloudBackupStatus} from './types';
34-
import {registerDisposable} from './utils';
34+
import {registerCleanup, registerDisposable} from './utils';
3535

3636
import './CommitCloud.css';
3737

@@ -50,7 +50,7 @@ export const commitCloudEnabledAtom = atom(async (get): Promise<boolean> => {
5050
return enabled;
5151
});
5252

53-
const cloudSyncStateAtom = atom<Result<CommitCloudSyncState> | null>(null);
53+
export const cloudSyncStateAtom = atom<Result<CommitCloudSyncState> | null>(null);
5454

5555
registerDisposable(
5656
cloudSyncStateAtom,
@@ -60,6 +60,19 @@ registerDisposable(
6060
import.meta.hot,
6161
);
6262

63+
// Trigger an initial fetch on every repo connect (and reconnect, and cwd
64+
// change) so consumers outside the Download Commits dropdown — e.g. the
65+
// commit context-menu "Move to workspace" submenu — can read a populated
66+
// state without first requiring the user to open that dropdown.
67+
// `onSetup` fires exactly once per setup transition by design, matching
68+
// the precedent used elsewhere in this codebase for connect-time fetches
69+
// (serverAPIState.ts, CommitInfoState.tsx, codeReview/CodeReviewInfo.ts).
70+
registerCleanup(
71+
cloudSyncStateAtom,
72+
serverAPI.onSetup(() => serverAPI.postMessage({type: 'fetchCommitCloudState'})),
73+
import.meta.hot,
74+
);
75+
6376
const REFRESH_INTERVAL = 30 * 1000;
6477

6578
export function CommitCloudInfo() {

0 commit comments

Comments
 (0)