Skip to content

Commit 1fa93a2

Browse files
architmodifacebook-github-bot
authored andcommitted
Warn on rebase to an older commit
Summary: Warning for devs rebasing their diffs with a large time deviation (e.g. >12 hours jump) Indicates the slowness in making this jump and provides alternatives, which are to rebase the target diff against the nearest warm/stable and then make the jump ## Rollout plan Gk attached in the diff Reviewed By: evangrayk Differential Revision: D75969145 fbshipit-source-id: cf34d89f22a7910e7ac30570f1ef549c336bc103
1 parent fae02c7 commit 1fa93a2

File tree

7 files changed

+147
-39
lines changed

7 files changed

+147
-39
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ export type TrackEventName =
170170
| 'PendingSlocCommand'
171171
| 'SplitSuggestionsDismissedForSevenDays'
172172
| 'WarnAboutRebaseOffWarm'
173+
| 'WarnAboutDistantRebase'
173174
| 'SaplingISLUriHandlerHandle'
174175
| 'CommitInfoFieldEditFieldClick';
175176

addons/isl/src/Commit.tsx

Lines changed: 79 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,11 @@ export const rebaseOffWarmWarningEnabled = localStorageBackedAtom<boolean>(
8484
true,
8585
);
8686

87+
export const distantRebaseWarningEnabled = localStorageBackedAtom<boolean>(
88+
'isl.distant-rebase-warning-enabled',
89+
true,
90+
);
91+
8792
/**
8893
* Some preview types should not allow actions on top of them
8994
* For example, you can't click goto on the preview of dragging a rebase,
@@ -303,18 +308,7 @@ export const Commit = memo(
303308
<Button
304309
primary
305310
onClick={() => {
306-
handlePreviewedOperation(/* cancel */ false);
307-
308-
const dag = readAtom(dagWithPreviews);
309-
const onto = dag.get(commit.parents[0]);
310-
if (onto) {
311-
tracker.track('ConfirmDragAndDropRebase', {
312-
extras: {
313-
remoteBookmarks: onto.remoteBookmarks,
314-
locations: onto.stableCommitMetadata?.map(s => s.value),
315-
},
316-
});
317-
}
311+
return handleRebaseConfirmation(commit, handlePreviewedOperation);
318312
}}>
319313
<T>Run Rebase</T>
320314
</Button>
@@ -761,3 +755,76 @@ async function gotoAction(runOperation: ReturnType<typeof useRunOperation>, comm
761755
writeAtom(selectedCommits, new Set());
762756
}
763757
const ObsoleteTip = React.memo(ObsoleteTipInner);
758+
759+
function handleRebaseConfirmation(
760+
commit: CommitInfo,
761+
handlePreviewedOperation: (cancel: boolean) => void,
762+
): Promise<void> {
763+
return maybeWarnAboutDistantRebase(commit).then(shouldProceed => {
764+
if (shouldProceed) {
765+
handlePreviewedOperation(/* cancel */ false);
766+
767+
const dag = readAtom(dagWithPreviews);
768+
const onto = dag.get(commit.parents[0]);
769+
if (onto) {
770+
tracker.track('ConfirmDragAndDropRebase', {
771+
extras: {
772+
remoteBookmarks: onto.remoteBookmarks,
773+
locations: onto.stableCommitMetadata?.map(s => s.value),
774+
},
775+
});
776+
}
777+
}
778+
});
779+
}
780+
781+
async function maybeWarnAboutDistantRebase(commit: CommitInfo): Promise<boolean> {
782+
const isDistantRebaseWarningEnabled = readAtom(distantRebaseWarningEnabled);
783+
if (!isDistantRebaseWarningEnabled) {
784+
return true;
785+
}
786+
const dag = readAtom(dagWithPreviews);
787+
const onto = dag.get(commit.parents[0]);
788+
if (!onto) {
789+
return true; // If there's no target commit, proceed without warning
790+
}
791+
const currentBase = findPublicBaseAncestor(dag);
792+
const destBase = findPublicBaseAncestor(dag, onto.hash);
793+
if (!currentBase || !destBase) {
794+
// can't determine if we can show warning
795+
return Promise.resolve(true);
796+
}
797+
const warning = Promise.resolve(
798+
Internal.maybeWarnAboutDistantRebase?.(currentBase, destBase) ?? false,
799+
);
800+
801+
if (await warning) {
802+
tracker.track('WarnAboutDistantRebase');
803+
const buttons = [t('Opt Out of Future Warnings'), t('Cancel'), t('Rebase Anyway')];
804+
const answer = await showModal({
805+
type: 'confirm',
806+
buttons,
807+
title: <T>Distant Rebase Warning</T>,
808+
message: t(
809+
Internal.warnAboutDistantRebaseReason ??
810+
'The target commit is $age away from your current commit. ' +
811+
'Rebasing across a large time gap may cause slower builds and performance. ' +
812+
"It's recommended to rebase the destination commit(s) to the nearest stable or warm commit first and then attempt this rebase. " +
813+
'Do you want to `rebase` anyway?',
814+
{
815+
replace: {
816+
$age: relativeDate(onto.date, {reference: commit.date, useRelativeForm: true}),
817+
},
818+
},
819+
),
820+
});
821+
822+
if (answer === buttons[0]) {
823+
writeAtom(distantRebaseWarningEnabled, false);
824+
return true;
825+
}
826+
return answer === buttons[2];
827+
}
828+
829+
return true;
830+
}

addons/isl/src/SettingsTooltip.tsx

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {Tooltip} from 'isl-components/Tooltip';
1919
import {useAtom, useAtomValue} from 'jotai';
2020
import {Suspense} from 'react';
2121
import {nullthrows, tryJsonParse} from 'shared/utils';
22-
import {rebaseOffWarmWarningEnabled} from './Commit';
22+
import {distantRebaseWarningEnabled, rebaseOffWarmWarningEnabled} from './Commit';
2323
import {condenseObsoleteStacks} from './CommitTreeList';
2424
import {Column, Row} from './ComponentUtils';
2525
import {confirmShouldSubmitEnabledAtom} from './ConfirmSubmitStack';
@@ -131,6 +131,7 @@ function SettingsDropdown({
131131
<RenderCompactSetting />
132132
<CondenseObsoleteSetting />
133133
<RebaseOffWarmWarningSetting />
134+
<DistantRebaseWarningSetting />
134135
<DeemphasizeIrrelevantCommitsSetting />
135136
</Column>
136137
</Setting>
@@ -320,6 +321,25 @@ function RebaseOffWarmWarningSetting() {
320321
);
321322
}
322323

324+
function DistantRebaseWarningSetting() {
325+
const [value, setValue] = useAtom(distantRebaseWarningEnabled);
326+
return (
327+
<Tooltip
328+
title={t(
329+
'Show a warning when rebasing onto a commit that is significantly older than the current commit.',
330+
)}>
331+
<Checkbox
332+
data-testid="distant-rebase-warning-enabled"
333+
checked={value}
334+
onChange={checked => {
335+
setValue(checked);
336+
}}>
337+
<T>Show Warning on Distant Rebase</T>
338+
</Checkbox>
339+
</Tooltip>
340+
);
341+
}
342+
323343
export const openFileCmdAtom = configBackedAtom<string | null>(
324344
'isl.open-file-cmd',
325345
null,

addons/isl/src/__tests__/operations.test.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8-
import {act, fireEvent, render, screen, within} from '@testing-library/react';
8+
import {act, fireEvent, render, screen, waitFor, within} from '@testing-library/react';
99
import * as utils from 'shared/utils';
1010
import App from '../App';
1111
import {Internal} from '../Internal';
@@ -501,6 +501,9 @@ describe('operations', () => {
501501

502502
dragAndDropCommits('c', 'a');
503503
fireEvent.click(screen.getByText('Run Rebase'));
504+
await waitFor(() => {
505+
expect(screen.getByText('rebasing...')).toBeInTheDocument();
506+
});
504507
await clickGoto('d'); // checkout d, which is now optimistic from the rebase, since it'll actually become d2.
505508

506509
act(() =>

addons/isl/src/__tests__/operations/rebaseOperation.test.tsx

Lines changed: 34 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -107,26 +107,27 @@ describe('rebase operation', () => {
107107
expect(screen.queryAllByTestId('commit-1')).toHaveLength(1);
108108
});
109109

110-
it('runs rebase operation', () => {
110+
it('runs rebase operation', async () => {
111111
dragAndDropCommits('d', '2');
112112

113113
const runRebaseButton = screen.getByText('Run Rebase');
114114
expect(runRebaseButton).toBeInTheDocument();
115115

116116
fireEvent.click(runRebaseButton);
117-
118-
expectMessageSentToServer({
119-
type: 'runOperation',
120-
operation: {
121-
args: ['rebase', '-s', succeedableRevset('d'), '-d', succeedableRevset('remote/master')],
122-
id: expect.anything(),
123-
runner: CommandRunner.Sapling,
124-
trackEventName: 'RebaseOperation',
125-
},
117+
await waitFor(() => {
118+
expectMessageSentToServer({
119+
type: 'runOperation',
120+
operation: {
121+
args: ['rebase', '-s', succeedableRevset('d'), '-d', succeedableRevset('remote/master')],
122+
id: expect.anything(),
123+
runner: CommandRunner.Sapling,
124+
trackEventName: 'RebaseOperation',
125+
},
126+
});
126127
});
127128
});
128129

129-
it('shows optimistic preview of rebase', () => {
130+
it('shows optimistic preview of rebase', async () => {
130131
dragAndDropCommits('d', '2');
131132

132133
fireEvent.click(screen.getByText('Run Rebase'));
@@ -136,14 +137,16 @@ describe('rebase operation', () => {
136137
// also includes descendants
137138
expect(screen.queryAllByTestId('commit-e')).toHaveLength(1);
138139

139-
expect(screen.getByText('rebasing...')).toBeInTheDocument();
140+
await waitFor(() => {
141+
expect(screen.getByText('rebasing...')).toBeInTheDocument();
142+
});
140143

141144
expect(
142145
screen.queryByTestId('commit-d')?.querySelector('.commit-preview-rebase-optimistic-root'),
143146
).toBeInTheDocument();
144147
});
145148

146-
it('allows re-dragging a previously dragged commit back onto the same parent', () => {
149+
it('allows re-dragging a previously dragged commit back onto the same parent', async () => {
147150
// Drag and drop 'e' normally onto 'c'
148151
dragCommits('e', 'd');
149152
expect(screen.queryByText('Run Rebase')).not.toBeInTheDocument(); // dragging on original parent is noop
@@ -158,15 +161,16 @@ describe('rebase operation', () => {
158161
dropCommits(getCommitWithPreview('e', CommitPreview.REBASE_ROOT), 'c');
159162

160163
fireEvent.click(screen.getByText('Run Rebase'));
161-
162-
expectMessageSentToServer({
163-
type: 'runOperation',
164-
operation: {
165-
args: ['rebase', '-s', succeedableRevset('e'), '-d', succeedableRevset('c')],
166-
id: expect.anything(),
167-
runner: CommandRunner.Sapling,
168-
trackEventName: 'RebaseOperation',
169-
},
164+
await waitFor(() => {
165+
expectMessageSentToServer({
166+
type: 'runOperation',
167+
operation: {
168+
args: ['rebase', '-s', succeedableRevset('e'), '-d', succeedableRevset('c')],
169+
id: expect.anything(),
170+
runner: CommandRunner.Sapling,
171+
trackEventName: 'RebaseOperation',
172+
},
173+
});
170174
});
171175
});
172176

@@ -276,7 +280,7 @@ describe('rebase operation', () => {
276280
expect(scanForkedBranchHashes('b')).toEqual(['d', 'e']);
277281
});
278282

279-
it('can preview drag drop while previous rebase running', () => {
283+
it('can preview drag drop while previous rebase running', async () => {
280284
// c
281285
// c | e
282286
// e b |/
@@ -286,6 +290,9 @@ describe('rebase operation', () => {
286290
// a a a
287291
dragAndDropCommits('d', 'a');
288292
fireEvent.click(screen.getByText('Run Rebase'));
293+
await waitFor(() => {
294+
expect(screen.getByText('rebasing...')).toBeInTheDocument();
295+
});
289296

290297
dragAndDropCommits(
291298
getCommitWithPreview('e', CommitPreview.REBASE_OPTIMISTIC_DESCENDANT),
@@ -298,7 +305,7 @@ describe('rebase operation', () => {
298305
expect(scanForkedBranchHashes('b')).toEqual(['e']);
299306
});
300307

301-
it('can see optimistic drag drop while previous rebase running', () => {
308+
it('can see optimistic drag drop while previous rebase running', async () => {
302309
// c
303310
// c | e
304311
// e b |/
@@ -308,7 +315,9 @@ describe('rebase operation', () => {
308315
// a a a
309316
dragAndDropCommits('d', 'a');
310317
fireEvent.click(screen.getByText('Run Rebase'));
311-
318+
await waitFor(() => {
319+
expect(screen.getByText('rebasing...')).toBeInTheDocument();
320+
});
312321
dragAndDropCommits(
313322
getCommitWithPreview('e', CommitPreview.REBASE_OPTIMISTIC_DESCENDANT),
314323
'b',

addons/isl/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -800,6 +800,7 @@ export type LocalStorageName =
800800
| 'isl.warn-about-diagnostics'
801801
| 'isl.hide-non-blocking-diagnostics'
802802
| 'isl.rebase-off-warm-warning-enabled'
803+
| 'isl.distant-rebase-warning-enabled'
803804
| 'isl.experimental-features-local-override'
804805
// These keys are prefixes, with further dynamic keys appended afterwards
805806
| 'isl.edited-commit-messages:'

addons/isl/src/utils.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,10 @@ export function registerDisposable(
8080
): void {
8181
registerCleanup(obj, () => disposable.dispose(), hot);
8282
}
83+
84+
/** Calculate hour difference between two dates (date1 - date2) */
85+
export function calculateHourDifference(date1: Date, date2: Date): number {
86+
const msDifference = date1.getTime() - date2.getTime();
87+
const hoursDifference = msDifference / (1000 * 60 * 60);
88+
return hoursDifference;
89+
}

0 commit comments

Comments
 (0)