diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4c1b4aca65..33271ded2d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -98,9 +98,6 @@ jobs: WINDOWS_CERT_PASSWORD: ${{ secrets.WINDOWS_CERT_PASSWORD }} KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }} DEPLOYMENT_SECRET: ${{ secrets.DEPLOYMENT_SECRET }} - S3_KEY: ${{ secrets.S3_KEY }} - S3_SECRET: ${{ secrets.S3_SECRET }} - S3_BUCKET: github-desktop AZURE_STORAGE_ACCOUNT: ${{ secrets.AZURE_STORAGE_ACCOUNT }} AZURE_STORAGE_ACCESS_KEY: ${{ secrets.AZURE_STORAGE_ACCESS_KEY }} AZURE_BLOB_CONTAINER: ${{ secrets.AZURE_BLOB_CONTAINER }} diff --git a/app/src/highlighter/index.ts b/app/src/highlighter/index.ts index 2529d3d045..8b8be402ca 100644 --- a/app/src/highlighter/index.ts +++ b/app/src/highlighter/index.ts @@ -403,6 +403,12 @@ const extensionModes: ReadonlyArray = [ '.toml': 'text/x-toml', }, }, + { + install: () => import('codemirror/mode/dart/dart'), + mappings: { + '.dart': 'application/dart', + }, + }, ] /** diff --git a/app/src/lib/app-state.ts b/app/src/lib/app-state.ts index ff7d200573..210cf6cd00 100644 --- a/app/src/lib/app-state.ts +++ b/app/src/lib/app-state.ts @@ -4,7 +4,7 @@ import { IDiff, ImageDiffType } from '../models/diff' import { Repository, ILocalRepositoryState } from '../models/repository' import { Branch, IAheadBehind } from '../models/branch' import { Tip } from '../models/tip' -import { Commit, CommitOneLine } from '../models/commit' +import { Commit } from '../models/commit' import { CommittedFileChange, WorkingDirectoryStatus } from '../models/status' import { CloningRepository } from '../models/cloning-repository' import { IMenu } from '../models/app-menu' @@ -33,7 +33,6 @@ import { ApplicableTheme, ApplicationTheme } from '../ui/lib/application-theme' import { IAccountRepositories } from './stores/api-repositories-store' import { ManualConflictResolution } from '../models/manual-conflict-resolution' import { Banner } from '../models/banner' -import { RebaseFlowStep } from '../models/rebase-flow-step' import { IStashEntry } from '../models/stash-entry' import { TutorialStep } from '../models/tutorial-step' import { UncommittedChangesStrategy } from '../models/uncommitted-changes-strategy' @@ -412,8 +411,6 @@ export interface IRepositoryState { readonly branchesState: IBranchesState - readonly rebaseState: IRebaseState - /** The commits loaded, keyed by their full SHA. */ readonly commitLookup: Map @@ -535,39 +532,6 @@ export interface IBranchesState { readonly rebasedBranches: ReadonlyMap } -/** State associated with a rebase being performed on a repository */ -export interface IRebaseState { - /** - * The current step of the flow the user should see. - * - * `null` indicates that there is no rebase underway. - */ - readonly step: RebaseFlowStep | null - - /** - * The underlying Git information associated with the current rebase - * - * This will be set to `null` when no base branch has been selected to - * initiate the rebase. - */ - readonly progress: IMultiCommitOperationProgress | null - - /** - * The known range of commits that will be applied to the repository - * - * This will be set to `null` when no base branch has been selected to - * initiate the rebase. - */ - readonly commits: ReadonlyArray | null - - /** - * Whether the user has done work to resolve any conflicts as part of this - * rebase, as the rebase flow should confirm the user wishes to abort the - * rebase and lose that work. - */ - readonly userHasResolvedConflicts: boolean -} - export interface ICommitSelection { /** The commits currently selected in the app */ readonly shas: ReadonlyArray diff --git a/app/src/lib/editors/darwin.ts b/app/src/lib/editors/darwin.ts index 24744981a4..3e5af72824 100644 --- a/app/src/lib/editors/darwin.ts +++ b/app/src/lib/editors/darwin.ts @@ -131,15 +131,28 @@ async function findApplication( ): Promise { for (const identifier of editor.bundleIdentifiers) { try { - const installPath = await appPath(identifier) - const exists = await pathExists(installPath) - if (exists) { + // app-path not finding the app isn't an error, it just means the + // bundle isn't registered on the machine. + // https://github.com/sindresorhus/app-path/blob/0e776d4e132676976b4a64e09b5e5a4c6e99fcba/index.js#L7-L13 + const installPath = await appPath(identifier).catch(e => + e.message === "Couldn't find the app" + ? Promise.resolve(null) + : Promise.reject(e) + ) + + if (installPath === null) { + return null + } + + if (await pathExists(installPath)) { return installPath } - log.debug(`App installation for ${editor} not found at '${installPath}'`) + log.debug( + `App installation for ${editor.name} not found at '${installPath}'` + ) } catch (error) { - log.debug(`Unable to locate ${editor} installation`, error) + log.debug(`Unable to locate ${editor.name} installation`, error) } } diff --git a/app/src/lib/multi-commit-operation.ts b/app/src/lib/multi-commit-operation.ts new file mode 100644 index 0000000000..93dbf6a09c --- /dev/null +++ b/app/src/lib/multi-commit-operation.ts @@ -0,0 +1,55 @@ +import { Branch } from '../models/branch' +import { + ChooseBranchStep, + conflictSteps, + MultiCommitOperationStepKind, +} from '../models/multi-commit-operation' +import { Popup, PopupType } from '../models/popup' +import { TipState } from '../models/tip' +import { IMultiCommitOperationState, IRepositoryState } from './app-state' + +/** + * Setup the multi commit operation state when the user needs to select a branch as the + * base for the operation. + */ +export function getMultiCommitOperationChooseBranchStep( + state: IRepositoryState, + initialBranch?: Branch | null +): ChooseBranchStep { + const { + defaultBranch, + allBranches, + recentBranches, + tip, + } = state.branchesState + let currentBranch: Branch | null = null + + if (tip.kind === TipState.Valid) { + currentBranch = tip.branch + } else { + throw new Error( + 'Tip is not in a valid state, which is required to start the multi commit operation' + ) + } + + return { + kind: MultiCommitOperationStepKind.ChooseBranch, + defaultBranch, + currentBranch, + allBranches, + recentBranches, + initialBranch: initialBranch !== null ? initialBranch : undefined, + } +} + +export function isConflictsFlow( + currentPopup: Popup | null, + multiCommitOperationState: IMultiCommitOperationState | null +): boolean { + return ( + currentPopup !== null && + currentPopup.type === PopupType.MultiCommitOperation && + multiCommitOperationState !== null && + conflictSteps.includes(multiCommitOperationState.step.kind) + ) +} diff --git a/app/src/lib/rebase.ts b/app/src/lib/rebase.ts index b16a55951a..a2ee339e83 100644 --- a/app/src/lib/rebase.ts +++ b/app/src/lib/rebase.ts @@ -1,71 +1,8 @@ -import { - IRepositoryState, - RebaseConflictState, - IBranchesState, -} from '../lib/app-state' -import { - ChooseBranchesStep, - RebaseStep, - ShowConflictsStep, -} from '../models/rebase-flow-step' -import { Branch, IAheadBehind } from '../models/branch' +import { IBranchesState } from '../lib/app-state' +import { IAheadBehind } from '../models/branch' import { TipState } from '../models/tip' import { clamp } from './clamp' -/** - * Setup the rebase flow state when the user needs to select a branch as the - * base for the operation. - */ -export function initializeNewRebaseFlow( - state: IRepositoryState, - initialBranch?: Branch | null -) { - const { - defaultBranch, - allBranches, - recentBranches, - tip, - } = state.branchesState - let currentBranch: Branch | null = null - - if (tip.kind === TipState.Valid) { - currentBranch = tip.branch - } else { - throw new Error( - 'Tip is not in a valid state, which is required to start the rebase flow' - ) - } - - const initialState: ChooseBranchesStep = { - kind: RebaseStep.ChooseBranch, - defaultBranch, - currentBranch, - allBranches, - recentBranches, - initialBranch: initialBranch !== null ? initialBranch : undefined, - } - - return initialState -} - -/** - * Setup the rebase flow when rebase conflicts are detected in the repository. - * - * This indicates a rebase is in progress, and the application needs to guide - * the user to resolve conflicts and complete the rebase. - * - * @param conflictState current set of conflicts - */ -export function initializeRebaseFlowForConflictedRepository( - conflictState: RebaseConflictState -): ShowConflictsStep { - const initialState: ShowConflictsStep = { - kind: RebaseStep.ShowConflicts, - conflictState, - } - return initialState -} - /** * Format rebase percentage to ensure it's a value between 0 and 1, but to also * constrain it to two significant figures, avoiding the remainder that comes diff --git a/app/src/lib/stats/stats-store.ts b/app/src/lib/stats/stats-store.ts index 70c06b8d0e..4102af24fc 100644 --- a/app/src/lib/stats/stats-store.ts +++ b/app/src/lib/stats/stats-store.ts @@ -1609,8 +1609,10 @@ export class StatsStore implements IStatsStore { return this.recordSquashConflictsEncountered() case MultiCommitOperationKind.Reorder: return this.recordReorderConflictsEncountered() - case MultiCommitOperationKind.CherryPick: case MultiCommitOperationKind.Rebase: + // ignored because rebase records different stats + return + case MultiCommitOperationKind.CherryPick: case MultiCommitOperationKind.Merge: log.error( `[recordOperationConflictsEncounteredCount] - Operation not supported: ${kind}` @@ -1632,6 +1634,8 @@ export class StatsStore implements IStatsStore { case MultiCommitOperationKind.CherryPick: return this.recordCherryPickSuccessful() case MultiCommitOperationKind.Rebase: + // ignored because rebase records different stats + return case MultiCommitOperationKind.Merge: log.error( `[recordOperationSuccessful] - Operation not supported: ${kind}` @@ -1650,8 +1654,9 @@ export class StatsStore implements IStatsStore { return this.recordSquashSuccessfulWithConflicts() case MultiCommitOperationKind.Reorder: return this.recordReorderSuccessfulWithConflicts() - case MultiCommitOperationKind.CherryPick: case MultiCommitOperationKind.Rebase: + return this.recordRebaseSuccessAfterConflicts() + case MultiCommitOperationKind.CherryPick: case MultiCommitOperationKind.Merge: log.error( `[recordOperationSuccessfulWithConflicts] - Operation not supported: ${kind}` diff --git a/app/src/lib/stores/app-store.ts b/app/src/lib/stores/app-store.ts index d9c99a5a03..954f39c09d 100644 --- a/app/src/lib/stores/app-store.ts +++ b/app/src/lib/stores/app-store.ts @@ -52,7 +52,7 @@ import { WorkingDirectoryStatus, AppFileStatusKind, } from '../../models/status' -import { TipState, tipEquals, IValidBranch, Tip } from '../../models/tip' +import { TipState, tipEquals, IValidBranch } from '../../models/tip' import { ICommitMessage } from '../../models/commit-message' import { Progress, @@ -98,16 +98,12 @@ import { PossibleSelections, RepositorySectionTab, SelectionType, - MergeConflictState, - RebaseConflictState, - IRebaseState, IRepositoryState, ChangesSelectionKind, ChangesWorkingDirectorySelection, isRebaseConflictState, isCherryPickConflictState, isMergeConflictState, - CherryPickConflictState, IMultiCommitOperationState, } from '../app-state' import { @@ -157,6 +153,8 @@ import { GitResetMode, reset, getBranchAheadBehind, + getRebaseInternalState, + getCommit, } from '../git' import { installGlobalLFSFilters, @@ -172,11 +170,7 @@ import { matchExistingRepository, urlMatchesRemote, } from '../repository-matching' -import { - initializeRebaseFlowForConflictedRepository, - formatRebaseValue, - isCurrentBranchForcePush, -} from '../rebase' +import { isCurrentBranchForcePush } from '../rebase' import { RetryAction, RetryActionType } from '../../models/retry-actions' import { Default as DefaultShell, @@ -194,7 +188,7 @@ import { } from '../window-state' import { TypedBaseStore } from './base-store' import { MergeTreeResult } from '../../models/merge' -import { promiseWithMinimumTimeout, sleep } from '../promise' +import { promiseWithMinimumTimeout } from '../promise' import { BackgroundFetcher } from './helpers/background-fetcher' import { validatedRepositoryPath } from './helpers/validated-repository-path' import { RepositoryStateCache } from './repository-state-cache' @@ -236,7 +230,6 @@ import { defaultUncommittedChangesStrategy, } from '../../models/uncommitted-changes-strategy' import { IStashEntry, StashedChangesLoadStates } from '../../models/stash-entry' -import { RebaseFlowStep, RebaseStep } from '../../models/rebase-flow-step' import { arrayEquals } from '../equality' import { MenuLabelsEvent } from '../../models/menu-labels' import { findRemoteBranchName } from './helpers/find-branch-name' @@ -289,6 +282,7 @@ import { import { reorder } from '../git/reorder' import { DragAndDropIntroType } from '../../ui/history/drag-and-drop-intro' import { UseWindowsOpenSSHKey } from '../ssh/ssh' +import { isConflictsFlow } from '../multi-commit-operation' const LastSelectedRepositoryIDKey = 'last-selected-repository-id' @@ -2009,8 +2003,11 @@ export class AppStore extends TypedBaseStore { conflictState: updateConflictState(state, status, this.statsStore), })) - this.updateRebaseFlowConflictsIfFound(repository) this.updateMultiCommitOperationConflictsIfFound(repository) + await this.initializeMultiCommitOperationIfConflictsFound( + repository, + status + ) if (this.selectedRepository === repository) { this._triggerConflictsFlow(repository, status) @@ -2024,46 +2021,128 @@ export class AppStore extends TypedBaseStore { } /** - * Push changes from latest conflicts into current rebase flow step, if needed + * This method is to initialize a multi commit operation state on app load + * if conflicts are found but not multi commmit operation exists. */ - private updateRebaseFlowConflictsIfFound(repository: Repository) { - const { changesState, rebaseState } = this.repositoryStateCache.get( - repository - ) - const { conflictState } = changesState + private async initializeMultiCommitOperationIfConflictsFound( + repository: Repository, + status: IStatusResult + ) { + const state = this.repositoryStateCache.get(repository) + const { + changesState: { conflictState }, + multiCommitOperationState, + branchesState, + } = state - if (conflictState === null || !isRebaseConflictState(conflictState)) { + if (conflictState === null) { + this.clearConflictsFlowVisuals(state) return } - const { step } = rebaseState - if (step === null) { + if (multiCommitOperationState !== null) { return } - if ( - step.kind === RebaseStep.ShowConflicts || - step.kind === RebaseStep.ConfirmAbort - ) { - // merge in new conflicts with known branches so they are not forgotten - const { baseBranch, targetBranch } = step.conflictState - const newConflictsState = { - ...conflictState, - baseBranch, - targetBranch, + let operationDetail: MultiCommitOperationDetail + let targetBranch: Branch | null = null + let commits: ReadonlyArray = [] + let originalBranchTip: string | null = '' + let progress: IMultiCommitOperationProgress | undefined = undefined + + if (branchesState.tip.kind === TipState.Valid) { + targetBranch = branchesState.tip.branch + originalBranchTip = targetBranch.tip.sha + } + + if (isMergeConflictState(conflictState)) { + operationDetail = { + kind: MultiCommitOperationKind.Merge, + isSquash: status.squashMsgFound, + sourceBranch: null, + } + originalBranchTip = targetBranch !== null ? targetBranch.tip.sha : null + } else if (isRebaseConflictState(conflictState)) { + const snapshot = await getRebaseSnapshot(repository) + const rebaseState = await getRebaseInternalState(repository) + if (snapshot === null || rebaseState === null) { + return } - this.repositoryStateCache.updateRebaseState(repository, () => ({ - step: { ...step, conflictState: newConflictsState }, - })) - } - } + originalBranchTip = rebaseState.originalBranchTip + commits = snapshot.commits + progress = snapshot.progress + operationDetail = { + kind: MultiCommitOperationKind.Rebase, + sourceBranch: null, + commits, + currentTip: rebaseState.baseBranchTip, + } + const commit = await getCommit(repository, rebaseState.originalBranchTip) + + if (commit !== null) { + targetBranch = new Branch( + rebaseState.targetBranch, + null, + commit, + BranchType.Local, + `refs/heads/${rebaseState.targetBranch}` + ) + } + } else if (isCherryPickConflictState(conflictState)) { + const snapshot = await getCherryPickSnapshot(repository) + if (snapshot === null) { + return + } + + originalBranchTip = null + commits = snapshot.commits + progress = snapshot.progress + operationDetail = { + kind: MultiCommitOperationKind.CherryPick, + sourceBranch: null, + branchCreated: false, + commits, + } + + this.repositoryStateCache.updateMultiCommitOperationUndoState( + repository, + () => ({ + undoSha: snapshot.targetBranchUndoSha, + branchName: '', + }) + ) + } else { + assertNever(conflictState, `Unsupported conflict kind`) + } + + this._initializeMultiCommitOperation( + repository, + operationDetail, + targetBranch, + commits, + originalBranchTip, + false + ) + + if (progress === undefined) { + return + } + + this.repositoryStateCache.updateMultiCommitOperationState( + repository, + () => ({ + progress: progress as IMultiCommitOperationProgress, + }) + ) + } /** * Push changes from latest conflicts into current multi step operation step, if needed * - i.e. - multiple instance of running in to conflicts */ private updateMultiCommitOperationConflictsIfFound(repository: Repository) { + const state = this.repositoryStateCache.get(repository) const { changesState, multiCommitOperationState, @@ -2071,6 +2150,7 @@ export class AppStore extends TypedBaseStore { const { conflictState } = changesState if (conflictState === null || multiCommitOperationState === null) { + this.clearConflictsFlowVisuals(state) return } @@ -2105,7 +2185,6 @@ export class AppStore extends TypedBaseStore { const { changesState: { conflictState }, multiCommitOperationState, - branchesState, } = state if (conflictState === null) { @@ -2113,138 +2192,70 @@ export class AppStore extends TypedBaseStore { return } - if (isMergeConflictState(conflictState)) { - await this.showMergeConflictsDialog( - repository, - conflictState, - multiCommitOperationState, - branchesState.tip, - status.squashMsgFound - ) - } else if (isRebaseConflictState(conflictState)) { - // TODO: This will likely get refactored to a showConflictsDialog method - const invalidOperationKinds = new Set([ - MultiCommitOperationKind.Squash, - MultiCommitOperationKind.Reorder, - ]) - if ( - multiCommitOperationState === null || - !invalidOperationKinds.has( - multiCommitOperationState.operationDetail.kind - ) - ) { - await this.showRebaseConflictsDialog(repository, conflictState) - } - } else if (isCherryPickConflictState(conflictState)) { - await this.showCherryPickConflictsDialog( - repository, - conflictState, - multiCommitOperationState, - branchesState.tip - ) - } else { - assertNever(conflictState, `Unsupported conflict kind`) - } - } - - /** - * Cleanup any related UI related to conflicts if still in use. - */ - private clearConflictsFlowVisuals(state: IRepositoryState) { - const { multiCommitOperationState, rebaseState } = state - if ( - userIsStartingRebaseFlow(this.currentPopup, rebaseState) || - userIsStartingMultiCommitOperation( - this.currentPopup, - multiCommitOperationState - ) - ) { - return - } - - this._closePopup(PopupType.MultiCommitOperation) - this._clearBanner(BannerType.MergeConflictsFound) - - this._closePopup(PopupType.RebaseFlow) - this._clearBanner(BannerType.RebaseConflictsFound) - } - - /** display the rebase flow, if not already in this flow */ - private async showRebaseConflictsDialog( - repository: Repository, - conflictState: RebaseConflictState - ) { - const alreadyInFlow = - this.currentPopup !== null && - this.currentPopup.type === PopupType.RebaseFlow - - if (alreadyInFlow) { + if (multiCommitOperationState === null) { return } const displayingBanner = this.currentBanner !== null && - this.currentBanner.type === BannerType.RebaseConflictsFound + this.currentBanner.type === BannerType.ConflictsFound - if (displayingBanner) { + if ( + displayingBanner || + isConflictsFlow(this.currentPopup, multiCommitOperationState) + ) { return } - await this._setRebaseProgressFromState(repository) + const { manualResolutions } = conflictState + let ourBranch, theirBranch - const step = initializeRebaseFlowForConflictedRepository(conflictState) + if (isMergeConflictState(conflictState)) { + theirBranch = await this.getMergeConflictsTheirBranch( + repository, + status.squashMsgFound, + multiCommitOperationState + ) + ourBranch = conflictState.currentBranch + } else if (isRebaseConflictState(conflictState)) { + theirBranch = conflictState.targetBranch + ourBranch = conflictState.baseBranch + } else if (isCherryPickConflictState(conflictState)) { + if ( + multiCommitOperationState !== null && + multiCommitOperationState.operationDetail.kind === + MultiCommitOperationKind.CherryPick && + multiCommitOperationState.operationDetail.sourceBranch !== null + ) { + theirBranch = + multiCommitOperationState.operationDetail.sourceBranch.name + } + ourBranch = conflictState.targetBranchName + } else { + assertNever(conflictState, `Unsupported conflict kind`) + } - this.repositoryStateCache.updateRebaseState(repository, () => ({ - step, - })) + this._setMultiCommitOperationStep(repository, { + kind: MultiCommitOperationStepKind.ShowConflicts, + conflictState: { + kind: 'multiCommitOperation', + manualResolutions, + ourBranch, + theirBranch, + }, + }) this._showPopup({ - type: PopupType.RebaseFlow, + type: PopupType.MultiCommitOperation, repository, }) } - /** starts the conflict resolution flow, if appropriate */ - private async showMergeConflictsDialog( + private async getMergeConflictsTheirBranch( repository: Repository, - conflictState: MergeConflictState, - multiCommitOperationState: IMultiCommitOperationState | null, - tip: Tip, - isSquash: boolean - ) { - if (multiCommitOperationState === null && tip.kind === TipState.Valid) { - this._initializeMultiCommitOperation( - repository, - { - kind: MultiCommitOperationKind.Merge, - isSquash, - sourceBranch: null, - }, - tip.branch, - [], - tip.branch.tip.sha - ) - } - - // are we already in the merge conflicts flow? - const alreadyInFlow = - this.currentPopup !== null && - this.currentPopup.type === PopupType.MultiCommitOperation && - multiCommitOperationState !== null && - (multiCommitOperationState.step.kind === - MultiCommitOperationStepKind.ShowConflicts || - multiCommitOperationState.step.kind === - MultiCommitOperationStepKind.ConfirmAbort) - - // have we already been shown the merge conflicts flow *and closed it*? - const alreadyExitedFlow = - this.currentBanner !== null && - this.currentBanner.type === BannerType.ConflictsFound - - if (alreadyInFlow || alreadyExitedFlow) { - return - } - + isSquash: boolean, + multiCommitOperationState: IMultiCommitOperationState | null + ): Promise { let theirBranch: string | undefined if ( multiCommitOperationState !== null && @@ -2271,22 +2282,26 @@ export class AppStore extends TypedBaseStore { ? possibleTheirsBranches[0] : undefined } + return theirBranch + } - const { manualResolutions, currentBranch: ourBranch } = conflictState - this._setMultiCommitOperationStep(repository, { - kind: MultiCommitOperationStepKind.ShowConflicts, - conflictState: { - kind: 'multiCommitOperation', - manualResolutions, - ourBranch, - theirBranch, - }, - }) + /** + * Cleanup any related UI related to conflicts if still in use. + */ + private clearConflictsFlowVisuals(state: IRepositoryState) { + const { multiCommitOperationState } = state + if ( + userIsStartingMultiCommitOperation( + this.currentPopup, + multiCommitOperationState + ) + ) { + return + } - this._showPopup({ - type: PopupType.MultiCommitOperation, - repository, - }) + this._closePopup(PopupType.MultiCommitOperation) + this._clearBanner(BannerType.ConflictsFound) + this._clearBanner(BannerType.MergeConflictsFound) } /** This shouldn't be called directly. See `Dispatcher`. */ @@ -4627,89 +4642,17 @@ export class AppStore extends TypedBaseStore { return this._refreshRepository(repository) } - /** This shouldn't be called directly. See `Dispatcher`. */ - public async _setRebaseProgressFromState(repository: Repository) { - const snapshot = await getRebaseSnapshot(repository) - if (snapshot === null) { - return - } - - const { progress, commits } = snapshot - - this.repositoryStateCache.updateRebaseState(repository, () => { - return { - progress, - commits, - } - }) - } - - /** This shouldn't be called directly. See `Dispatcher`. */ - public _initializeRebaseProgress( - repository: Repository, - commits: ReadonlyArray - ) { - this.repositoryStateCache.updateRebaseState(repository, () => { - const hasCommits = commits.length > 0 - const firstCommitSummary = hasCommits ? commits[0].summary : null - - return { - progress: { - kind: 'multiCommitOperation', - value: formatRebaseValue(0), - position: 0, - currentCommitSummary: - firstCommitSummary !== null ? firstCommitSummary : '', - totalCommitCount: commits.length, - }, - commits, - } - }) - - this.emitUpdate() - } - /** This shouldn't be called directly. See `Dispatcher`. */ public _setConflictsResolved(repository: Repository) { // an update is not emitted here because there is no need // to trigger a re-render at this point - this.repositoryStateCache.updateRebaseState(repository, () => ({ - userHasResolvedConflicts: true, - })) - } - - /** This shouldn't be called directly. See `Dispatcher`. */ - public async _setRebaseFlowStep( - repository: Repository, - step: RebaseFlowStep - ): Promise { - this.repositoryStateCache.updateRebaseState(repository, () => ({ - step, - })) - - this.emitUpdate() - - if (step.kind === RebaseStep.ShowProgress && step.rebaseAction !== null) { - // this timeout is intended to defer the action from running immediately - // after the progress UI is shown, to better show that rebase is - // progressing rather than suddenly appearing and disappearing again - await sleep(500) - await step.rebaseAction() - } - } - - /** This shouldn't be called directly. See `Dispatcher`. */ - public _endRebaseFlow(repository: Repository) { - this.repositoryStateCache.updateRebaseState(repository, () => ({ - step: null, - progress: null, - commits: null, - preview: null, - userHasResolvedConflicts: false, - })) - - this.emitUpdate() + this.repositoryStateCache.updateMultiCommitOperationState( + repository, + () => ({ + userHasResolvedConflicts: true, + }) + ) } /** This shouldn't be called directly. See `Dispatcher`. */ @@ -4718,14 +4661,9 @@ export class AppStore extends TypedBaseStore { baseBranch: Branch, targetBranch: Branch ): Promise { - const progressCallback = (progress: IMultiCommitOperationProgress) => { - this.repositoryStateCache.updateRebaseState(repository, () => ({ - progress, - })) - - this.emitUpdate() - } - + const progressCallback = this.getMultiCommitOperationProgressCallBack( + repository + ) const gitStore = this.gitStoreCache.get(repository) const result = await gitStore.performFailableOperation( () => rebase(repository, baseBranch, targetBranch, progressCallback), @@ -4756,13 +4694,9 @@ export class AppStore extends TypedBaseStore { workingDirectory: WorkingDirectoryStatus, manualResolutions: ReadonlyMap ): Promise { - const progressCallback = (progress: IMultiCommitOperationProgress) => { - this.repositoryStateCache.updateRebaseState(repository, () => ({ - progress, - })) - - this.emitUpdate() - } + const progressCallback = this.getMultiCommitOperationProgressCallBack( + repository + ) const gitStore = this.gitStoreCache.get(repository) const result = await gitStore.performFailableOperation(() => @@ -5921,35 +5855,11 @@ export class AppStore extends TypedBaseStore { } }) - this.updateRebaseStateAfterManualResolution(repository) this.updateMultiCommitOperationStateAfterManualResolution(repository) this.emitUpdate() } - /** - * Updates the rebase flow conflict step state as the manual resolutions - * have been changed. - */ - private updateRebaseStateAfterManualResolution(repository: Repository) { - const currentState = this.repositoryStateCache.get(repository) - - const { changesState, rebaseState } = currentState - const { conflictState } = changesState - const { step } = rebaseState - - if ( - conflictState !== null && - conflictState.kind === 'rebase' && - step !== null && - step.kind === RebaseStep.ShowConflicts - ) { - this.repositoryStateCache.updateRebaseState(repository, () => ({ - step: { ...step, conflictState }, - })) - } - } - /** * Updates the multi commit operation conflict step state as the manual * resolutions have been changed. @@ -6362,105 +6272,6 @@ export class AppStore extends TypedBaseStore { ) } - /** display the cherry pick flow, if not already in this flow */ - private async showCherryPickConflictsDialog( - repository: Repository, - conflictState: CherryPickConflictState, - multiCommitOperationState: IMultiCommitOperationState | null, - tip: Tip - ) { - const snapshot = await getCherryPickSnapshot(repository) - - if (snapshot === null) { - log.error( - `[showCherryPickConflictsDialog] unable to get cherry-pick status from git, unable to continue` - ) - return - } - - if (multiCommitOperationState === null && tip.kind === TipState.Valid) { - // This is only true is we get here when opening the app and therefore we - // don't know some of this data - this._initializeMultiCommitOperation( - repository, - { - kind: MultiCommitOperationKind.CherryPick, - sourceBranch: null, - branchCreated: false, - commits: snapshot.commits, - }, - tip.branch, - snapshot.commits, - null - ) - - this.repositoryStateCache.updateMultiCommitOperationUndoState( - repository, - () => ({ - undoSha: snapshot.targetBranchUndoSha, - branchName: '', - }) - ) - - this.repositoryStateCache.updateMultiCommitOperationState( - repository, - () => ({ - progress: snapshot.progress, - }) - ) - } - - // are we already in the conflicts flow? - const alreadyInFlow = - this.currentPopup !== null && - this.currentPopup.type === PopupType.MultiCommitOperation && - multiCommitOperationState !== null && - (multiCommitOperationState.step.kind === - MultiCommitOperationStepKind.ShowConflicts || - multiCommitOperationState.step.kind === - MultiCommitOperationStepKind.ConfirmAbort) - - // have we already been shown the merge conflicts flow *and closed it*? - const alreadyExitedFlow = - this.currentBanner !== null && - this.currentBanner.type === BannerType.ConflictsFound - - const displayingBanner = - this.currentBanner !== null && - this.currentBanner.type === BannerType.CherryPickConflictsFound - - if (alreadyInFlow || alreadyExitedFlow || displayingBanner) { - return - } - - let theirBranch = undefined - - if ( - multiCommitOperationState !== null && - multiCommitOperationState.operationDetail.kind === - MultiCommitOperationKind.CherryPick && - multiCommitOperationState.operationDetail.sourceBranch !== null - ) { - theirBranch = multiCommitOperationState.operationDetail.sourceBranch.name - } - - const { manualResolutions, targetBranchName: ourBranch } = conflictState - this._setMultiCommitOperationStep(repository, { - kind: MultiCommitOperationStepKind.ShowConflicts, - conflictState: { - kind: 'multiCommitOperation', - manualResolutions, - ourBranch, - theirBranch, - }, - }) - - this._showPopup({ - type: PopupType.MultiCommitOperation, - repository, - }) - } - /** This shouldn't be called directly. See `Dispatcher`. */ public _markDragAndDropIntroAsSeen(intro: DragAndDropIntroType) { if (this.dragAndDropIntroTypesShown.has(intro)) { @@ -6863,7 +6674,8 @@ export class AppStore extends TypedBaseStore { operationDetail: MultiCommitOperationDetail, targetBranch: Branch | null, commits: ReadonlyArray, - originalBranchTip: string | null + originalBranchTip: string | null, + emitUpdate: boolean = true ): void { this.repositoryStateCache.initializeMultiCommitOperationState(repository, { step: { @@ -6882,6 +6694,10 @@ export class AppStore extends TypedBaseStore { targetBranch, }) + if (!emitUpdate) { + return + } + this.emitUpdate() } } @@ -6909,38 +6725,6 @@ function getInitialAction( } } -/** - * Check if the user is in a rebase flow step that doesn't depend on conflicted - * state, as the app should not attempt to clean up any popups or banners while - * this is occurring. - */ -function userIsStartingRebaseFlow( - currentPopup: Popup | null, - state: IRebaseState -) { - if (currentPopup === null) { - return false - } - - if (currentPopup.type !== PopupType.RebaseFlow) { - return false - } - - if (state.step === null) { - return false - } - - if ( - state.step.kind === RebaseStep.ChooseBranch || - state.step.kind === RebaseStep.WarnForcePush || - state.step.kind === RebaseStep.ShowProgress - ) { - return true - } - - return false -} - function userIsStartingMultiCommitOperation( currentPopup: Popup | null, state: IMultiCommitOperationState | null diff --git a/app/src/lib/stores/repository-state-cache.ts b/app/src/lib/stores/repository-state-cache.ts index ecb808d7c6..a52d8757cb 100644 --- a/app/src/lib/stores/repository-state-cache.ts +++ b/app/src/lib/stores/repository-state-cache.ts @@ -15,7 +15,6 @@ import { IRepositoryState, RepositorySectionTab, ICommitSelection, - IRebaseState, ChangesSelectionKind, IMultiCommitOperationUndoState, IMultiCommitOperationState, @@ -109,17 +108,6 @@ export class RepositoryStateCache { }) } - public updateRebaseState( - repository: Repository, - fn: (branchesState: IRebaseState) => Pick - ) { - this.update(repository, state => { - const { rebaseState } = state - const newState = merge(rebaseState, fn(rebaseState)) - return { rebaseState: newState } - }) - } - public updateMultiCommitOperationUndoState< K extends keyof IMultiCommitOperationUndoState >( @@ -225,12 +213,6 @@ function getInitialRepositoryState(): IRepositoryState { recentBranches: new Array(), defaultBranch: null, }, - rebaseState: { - step: null, - progress: null, - commits: null, - userHasResolvedConflicts: false, - }, commitAuthor: null, commitLookup: new Map(), localCommitSHAs: [], diff --git a/app/src/models/multi-commit-operation.ts b/app/src/models/multi-commit-operation.ts index cf9f874696..d1c565fa4f 100644 --- a/app/src/models/multi-commit-operation.ts +++ b/app/src/models/multi-commit-operation.ts @@ -134,13 +134,7 @@ export type CreateBranchStep = { targetBranchName: string } -interface IInteractiveRebaseDetails { - /** - * The reference to the last retained commit on the branch during an - * interactive rebase or null if rebasing to the root. - */ - readonly lastRetainedCommitRef: string | null - +interface IBaseInteractiveRebaseDetails { /** * Array of commits used during the operation. */ @@ -153,6 +147,14 @@ interface IInteractiveRebaseDetails { readonly currentTip: string } +interface IInteractiveRebaseDetails extends IBaseInteractiveRebaseDetails { + /** + * The reference to the last retained commit on the branch during an + * interactive rebase or null if rebasing to the root. + */ + readonly lastRetainedCommitRef: string | null +} + interface ISourceBranchDetails { /** * The branch that are the source of the commits for the operation. @@ -203,6 +205,12 @@ interface ICherryPickDetails extends ISourceBranchDetails { interface IRebaseDetails extends ISourceBranchDetails { readonly kind: MultiCommitOperationKind.Rebase + readonly commits: ReadonlyArray + /** + * This is the commit sha of the HEAD of the in-flight operation used to compare + * the state of the after an operation to a previous state. + */ + readonly currentTip: string } interface IMergeDetails extends ISourceBranchDetails { @@ -216,3 +224,19 @@ export type MultiCommitOperationDetail = | ICherryPickDetails | IRebaseDetails | IMergeDetails + +export function instanceOfIBaseRebaseDetails( + object: any +): object is IBaseInteractiveRebaseDetails { + const objectWithRequiredFields: IBaseInteractiveRebaseDetails = { + commits: [], + currentTip: '', + } + + return Object.keys(objectWithRequiredFields).every(key => key in object) +} + +export const conflictSteps = [ + MultiCommitOperationStepKind.ShowConflicts, + MultiCommitOperationStepKind.ConfirmAbort, +] diff --git a/app/src/models/popup.ts b/app/src/models/popup.ts index 77474f4211..547363945c 100644 --- a/app/src/models/popup.ts +++ b/app/src/models/popup.ts @@ -51,7 +51,6 @@ export enum PopupType { OversizedFiles, CommitConflictsWarning, PushNeedsPull, - RebaseFlow, ConfirmForcePush, StashAndSwitchBranch, ConfirmOverwriteStash, @@ -195,10 +194,6 @@ export type Popup = repository: Repository upstreamBranch: string } - | { - type: PopupType.RebaseFlow - repository: Repository - } | { type: PopupType.StashAndSwitchBranch repository: Repository diff --git a/app/src/models/rebase-flow-step.ts b/app/src/models/rebase-flow-step.ts deleted file mode 100644 index 6af524fba5..0000000000 --- a/app/src/models/rebase-flow-step.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { Branch } from './branch' -import { RebaseConflictState } from '../lib/app-state' -import { CommitOneLine } from './commit' - -/** Union type representing the possible states of the rebase flow */ -export type RebaseFlowStep = - | ChooseBranchesStep - | WarnForcePushStep - | ShowProgressStep - | ShowConflictsStep - | HideConflictsStep - | ConfirmAbortStep - | CompletedStep - -export const enum RebaseStep { - /** - * The initial state of a rebase - the user choosing the start point. - * - * This is not encountered if the user tries to 'pull with rebase' and - * encounters conflicts, because the rebase happens as part of the pull - * operation and the only remaining work for the user is to resolve any - * conflicts. - */ - ChooseBranch = 'ChooseBranch', - /** - * The initial state of a rebase - the user choosing the start point. - * - * This is not encountered if the user tries to 'pull with rebase' and - * encounters conflicts, because the rebase happens as part of the pull - * operation and the only remaining work for the user is to resolve any - * conflicts. - */ - WarnForcePush = 'WarnForcePush', - /** - * After the user chooses which branch to use as the base branch for the - * rebase, the progress view is shown indicating how the rebase work is - * progressing. - * - * This should be the default view when there are no conflicts to address. - */ - ShowProgress = 'ShowProgress', - /** - * The rebase has encountered a problem requiring the user to intervene and - * resolve conflicts. This will be shown as a list of files and the conflict - * state. - * - * Once the conflicts are resolved, the user can continue the rebase and the - * view will switch back to `ShowProgress`. - */ - ShowConflicts = 'ShowConflicts', - /** - * The user may wish to leave the conflict dialog and view the files in - * the Changes tab to get a better context. In this situation, the application - * will show a banner to indicate this context and help the user return to the - * conflicted list. - */ - HideConflicts = 'HideConflicts', - /** - * If the user wishes to abort the in-progress rebase, and the user has - * resolved conflicts at any point of the rebase, the application should ask - * the user to confirm that they wish to abort. - * - * This is to ensure the user doesn't throw away their work attempting to - * rebase the current branch. - */ - ConfirmAbort = 'ConfirmAbort', - /** - * When the rebase is completed, the dialog should be closed and a success - * banner shown to the user. - */ - Completed = 'Completed', -} - -/** Shape of data needed to choose the base branch for a rebase */ -export type ChooseBranchesStep = { - readonly kind: RebaseStep.ChooseBranch - readonly defaultBranch: Branch | null - readonly currentBranch: Branch - readonly allBranches: ReadonlyArray - readonly recentBranches: ReadonlyArray - readonly initialBranch?: Branch -} - -export type WarnForcePushStep = { - readonly kind: RebaseStep.WarnForcePush - readonly baseBranch: Branch - readonly targetBranch: Branch - readonly commits: ReadonlyArray -} - -/** Shape of data to show progress of the current rebase */ -export type ShowProgressStep = { - readonly kind: RebaseStep.ShowProgress - - /** - * An optional action to run when the component is mounted. - * - * This is provided to the component because a rebase can be very fast, and we - * want to defer the rebase action until after _something_ is shown to the - * user. - */ - readonly rebaseAction: (() => Promise) | null -} - -/** Shape of data to show conflicts that need to be resolved by the user */ -export type ShowConflictsStep = { - readonly kind: RebaseStep.ShowConflicts - readonly conflictState: RebaseConflictState -} - -/** Shape of data to track when user hides conflicts dialog */ -export type HideConflictsStep = { - readonly kind: RebaseStep.HideConflicts -} - -/** Shape of data to use when confirming user should abort rebase */ -export type ConfirmAbortStep = { - readonly kind: RebaseStep.ConfirmAbort - readonly conflictState: RebaseConflictState -} - -/** Shape of data to track when rebase has completed successfully */ -export type CompletedStep = { - readonly kind: RebaseStep.Completed -} diff --git a/app/src/ui/app.tsx b/app/src/ui/app.tsx index ca2f114287..72cca97ee3 100644 --- a/app/src/ui/app.tsx +++ b/app/src/ui/app.tsx @@ -8,7 +8,6 @@ import { FoldoutType, SelectionType, HistoryTabMode, - isRebaseConflictState, } from '../lib/app-state' import { Dispatcher } from './dispatcher' import { AppStore, GitHubUserStore, IssuesStore } from '../lib/stores' @@ -92,11 +91,7 @@ import { RepositoryStateCache } from '../lib/stores/repository-state-cache' import { PopupType, Popup } from '../models/popup' import { OversizedFiles } from './changes/oversized-files-warning' import { PushNeedsPullWarning } from './push-needs-pull' -import { RebaseFlow, ConfirmForcePush } from './rebase' -import { - initializeRebaseFlowForConflictedRepository, - isCurrentBranchForcePush, -} from '../lib/rebase' +import { isCurrentBranchForcePush } from '../lib/rebase' import { Banner, BannerType } from '../models/banner' import { StashAndSwitchBranch } from './stash-changes/stash-and-switch-branch-dialog' import { OverwriteStash } from './stash-changes/overwrite-stashed-changes-dialog' @@ -141,13 +136,11 @@ import { MultiCommitOperation } from './multi-commit-operation/multi-commit-oper import { WarnLocalChangesBeforeUndo } from './undo/warn-local-changes-before-undo' import { WarningBeforeReset } from './reset/warning-before-reset' import { InvalidatedToken } from './invalidated-token/invalidated-token' -import { - ChooseBranchStep, - MultiCommitOperationKind, - MultiCommitOperationStepKind, -} from '../models/multi-commit-operation' +import { MultiCommitOperationKind } from '../models/multi-commit-operation' import { AddSSHHost } from './ssh/add-ssh-host' import { SSHKeyPassphrase } from './ssh/ssh-key-passphrase' +import { getMultiCommitOperationChooseBranchStep } from '../lib/multi-commit-operation' +import { ConfirmForcePush } from './rebase/confirm-force-push' import { setAlmostImmediate } from '../lib/set-almost-immediate' const MinuteInMilliseconds = 1000 * 60 @@ -1119,6 +1112,7 @@ export class App extends React.Component { if (!repository || repository instanceof CloningRepository) { return } + this.props.dispatcher.showRebaseDialog(repository) } @@ -1719,63 +1713,6 @@ export class App extends React.Component { onDismissed={onPopupDismissedFn} /> ) - case PopupType.RebaseFlow: { - const { selectedState, emoji } = this.state - - if ( - selectedState === null || - selectedState.type !== SelectionType.Repository - ) { - return null - } - - const { - changesState, - rebaseState, - multiCommitOperationState, - } = selectedState.state - const { workingDirectory, conflictState } = changesState - const { progress, step, userHasResolvedConflicts } = rebaseState - - if ( - (conflictState !== null && conflictState.kind !== 'rebase') || - multiCommitOperationState !== null - ) { - log.warn( - '[App] invalid state encountered - rebase flow should not be used when merge conflicts found' - ) - return null - } - - if (step === null) { - log.warn( - '[App] invalid state encountered - rebase flow should not be active when step is null' - ) - return null - } - - return ( - - ) - } case PopupType.ConfirmForcePush: { const { askForConfirmationOnForcePush } = this.state @@ -2128,46 +2065,6 @@ export class App extends React.Component { this.props.dispatcher.createTutorialRepository(account) } - private onShowRebaseConflictsBanner = ( - repository: Repository, - targetBranch: string - ) => { - this.props.dispatcher.setBanner({ - type: BannerType.RebaseConflictsFound, - targetBranch, - onOpenDialog: async () => { - const { changesState } = this.props.repositoryStateManager.get( - repository - ) - const { conflictState } = changesState - - if (conflictState === null || !isRebaseConflictState(conflictState)) { - log.debug( - `[App.onShowRebaseConflictsBanner] no rebase conflict state found, ignoring...` - ) - return - } - - await this.props.dispatcher.setRebaseProgressFromState(repository) - - const initialStep = initializeRebaseFlowForConflictedRepository( - conflictState - ) - - this.props.dispatcher.setRebaseFlowStep(repository, initialStep) - - this.props.dispatcher.showPopup({ - type: PopupType.RebaseFlow, - repository, - }) - }, - }) - } - - private onRebaseFlowEnded = (repository: Repository) => { - this.props.dispatcher.endRebaseFlow(repository) - } - private onUpdateExistingUpstreamRemote = (repository: Repository) => { this.props.dispatcher.updateExistingUpstreamRemote(repository) } @@ -2870,12 +2767,7 @@ export class App extends React.Component { ) => { const repositoryState = this.props.repositoryStateManager.get(repository) - const { - defaultBranch, - allBranches, - recentBranches, - tip, - } = repositoryState.branchesState + const { tip } = repositoryState.branchesState let currentBranch: Branch | null = null if (tip.kind === TipState.Valid) { @@ -2899,13 +2791,7 @@ export class App extends React.Component { tip.branch.tip.sha ) - const initialStep: ChooseBranchStep = { - kind: MultiCommitOperationStepKind.ChooseBranch, - defaultBranch, - currentBranch, - allBranches, - recentBranches, - } + const initialStep = getMultiCommitOperationChooseBranchStep(repositoryState) this.props.dispatcher.setMultiCommitOperationStep(repository, initialStep) this.props.dispatcher.recordCherryPickViaContextMenu() diff --git a/app/src/ui/dispatcher/dispatcher.ts b/app/src/ui/dispatcher/dispatcher.ts index b96bf0a671..5348b11854 100644 --- a/app/src/ui/dispatcher/dispatcher.ts +++ b/app/src/ui/dispatcher/dispatcher.ts @@ -31,6 +31,7 @@ import { PushOptions, getCommitsBetweenCommits, getBranches, + getRebaseSnapshot, } from '../../lib/git' import { isGitOnPath } from '../../lib/is-git-on-path' import { @@ -53,10 +54,6 @@ import { AppStore } from '../../lib/stores/app-store' import { validatedRepositoryPath } from '../../lib/stores/helpers/validated-repository-path' import { RepositoryStateCache } from '../../lib/stores/repository-state-cache' import { getTipSha } from '../../lib/tip' -import { - initializeNewRebaseFlow, - initializeRebaseFlowForConflictedRepository, -} from '../../lib/rebase' import { Account } from '../../models/account' import { AppMenu, ExecutableMenuItem } from '../../models/app-menu' @@ -100,7 +97,6 @@ import { } from '../../lib/stores/commit-status-store' import { MergeTreeResult } from '../../models/merge' import { UncommittedChangesStrategy } from '../../models/uncommitted-changes-strategy' -import { RebaseFlowStep, RebaseStep } from '../../models/rebase-flow-step' import { IStashEntry } from '../../models/stash-entry' import { WorkflowPreferences } from '../../models/workflow-preferences' import { resolveWithin } from '../../lib/path' @@ -118,6 +114,7 @@ import { MultiCommitOperationStepKind, } from '../../models/multi-commit-operation' import { DragAndDropIntroType } from '../history/drag-and-drop-intro' +import { getMultiCommitOperationChooseBranchStep } from '../../lib/multi-commit-operation' /** * An error handler function. @@ -440,12 +437,39 @@ export class Dispatcher { initialBranch?: Branch | null ) { const repositoryState = this.repositoryStateManager.get(repository) - const initialStep = initializeNewRebaseFlow(repositoryState, initialBranch) + const initialStep = getMultiCommitOperationChooseBranchStep( + repositoryState, + initialBranch + ) - this.setRebaseFlowStep(repository, initialStep) + const { tip } = repositoryState.branchesState + let currentBranch: Branch | null = null + + if (tip.kind === TipState.Valid) { + currentBranch = tip.branch + } else { + throw new Error( + 'Tip is not in a valid state, which is required to start the rebase flow' + ) + } + + this.initializeMultiCommitOperation( + repository, + { + kind: MultiCommitOperationKind.Rebase, + sourceBranch: null, + commits: [], + currentTip: tip.branch.tip.sha, + }, + currentBranch, + [], + currentBranch.tip.sha + ) + + this.setMultiCommitOperationStep(repository, initialStep) this.showPopup({ - type: PopupType.RebaseFlow, + type: PopupType.MultiCommitOperation, repository, }) } @@ -463,6 +487,22 @@ export class Dispatcher { const hasOverriddenForcePushCheck = options !== undefined && options.continueWithForcePush + const { branchesState } = this.repositoryStateManager.get(repository) + const originalBranchTip = getTipSha(branchesState.tip) + + this.appStore._initializeMultiCommitOperation( + repository, + { + kind: MultiCommitOperationKind.Rebase, + commits, + currentTip: baseBranch.tip.sha, + sourceBranch: baseBranch, + }, + targetBranch, + commits, + originalBranchTip + ) + if (askForConfirmationOnForcePush && !hasOverriddenForcePushCheck) { const showWarning = await this.warnAboutRemoteCommits( repository, @@ -471,32 +511,26 @@ export class Dispatcher { ) if (showWarning) { - this.setRebaseFlowStep(repository, { - kind: RebaseStep.WarnForcePush, - baseBranch, + this.setMultiCommitOperationStep(repository, { + kind: MultiCommitOperationStepKind.WarnForcePush, targetBranch, + baseBranch, commits, }) return } } - this.initializeRebaseProgress(repository, commits) - - const startRebaseAction = () => { - return this.rebase(repository, baseBranch, targetBranch) - } - - this.setRebaseFlowStep(repository, { - kind: RebaseStep.ShowProgress, - rebaseAction: startRebaseAction, - }) + await this.rebase(repository, baseBranch, targetBranch) } /** * Initialize and launch the rebase flow for a conflicted repository */ - public async launchRebaseFlow(repository: Repository, targetBranch: string) { + public async launchRebaseOperation( + repository: Repository, + targetBranch: string + ) { await this.appStore._loadStatus(repository) const repositoryState = this.repositoryStateManager.get(repository) @@ -515,16 +549,45 @@ export class Dispatcher { conflictState: updatedConflictState, })) - await this.setRebaseProgressFromState(repository) + const snapshot = await getRebaseSnapshot(repository) + if (snapshot === null) { + return + } - const initialStep = initializeRebaseFlowForConflictedRepository( - updatedConflictState + const { progress, commits } = snapshot + this.initializeMultiCommitOperation( + repository, + { + kind: MultiCommitOperationKind.Rebase, + sourceBranch: null, + commits, + currentTip: '', + }, + null, + commits, + null ) - this.setRebaseFlowStep(repository, initialStep) + this.repositoryStateManager.updateMultiCommitOperationState( + repository, + () => ({ + progress, + }) + ) + + const { manualResolutions } = conflictState + this.setMultiCommitOperationStep(repository, { + kind: MultiCommitOperationStepKind.ShowConflicts, + conflictState: { + kind: 'multiCommitOperation', + manualResolutions, + ourBranch: targetBranch, + theirBranch: undefined, + }, + }) this.showPopup({ - type: PopupType.RebaseFlow, + type: PopupType.MultiCommitOperation, repository, }) } @@ -1016,52 +1079,27 @@ export class Dispatcher { return this.appStore._setConflictsResolved(repository) } - /** - * Initialize the progress in application state based on the known commits - * that will be applied in the rebase. - * - * @param commits the list of commits that exist on the target branch which do - * not exist on the base branch - */ - public initializeRebaseProgress( - repository: Repository, - commits: ReadonlyArray - ) { - return this.appStore._initializeRebaseProgress(repository, commits) - } - - /** - * Update the rebase progress in application state by querying the Git - * repository state. - */ - public setRebaseProgressFromState(repository: Repository) { - return this.appStore._setRebaseProgressFromState(repository) - } - - /** - * Move the rebase flow to a new state. - */ - public setRebaseFlowStep( - repository: Repository, - step: RebaseFlowStep - ): Promise { - return this.appStore._setRebaseFlowStep(repository, step) - } - - /** End the rebase flow and cleanup any related app state */ - public endRebaseFlow(repository: Repository) { - return this.appStore._endRebaseFlow(repository) - } - /** Starts a rebase for the given base and target branch */ public async rebase( repository: Repository, baseBranch: Branch, targetBranch: Branch ): Promise { - const stateBefore = this.repositoryStateManager.get(repository) + const { + branchesState, + multiCommitOperationState, + } = this.repositoryStateManager.get(repository) - const beforeSha = getTipSha(stateBefore.branchesState.tip) + if ( + multiCommitOperationState == null || + multiCommitOperationState.operationDetail.kind !== + MultiCommitOperationKind.Rebase + ) { + return + } + const { commits } = multiCommitOperationState.operationDetail + + const beforeSha = getTipSha(branchesState.tip) log.info( `[rebase] starting rebase for ${targetBranch.name} at ${beforeSha}` @@ -1104,13 +1142,12 @@ export class Dispatcher { return } - const conflictsWithBranches: RebaseConflictState = { - ...conflictState, - baseBranch: baseBranch.name, - targetBranch: targetBranch.name, - } - - this.switchToConflicts(repository, conflictsWithBranches) + return this.startMultiCommitOperationConflictFlow( + MultiCommitOperationKind.Rebase, + repository, + baseBranch.name, + targetBranch.name + ) } else if (result === RebaseResult.CompletedWithoutError) { if (tip.kind !== TipState.Valid) { log.warn( @@ -1120,22 +1157,12 @@ export class Dispatcher { } this.statsStore.recordRebaseSuccessWithoutConflicts() - - await this.completeRebase( - repository, - { - type: BannerType.SuccessfulRebase, - targetBranch: targetBranch.name, - baseBranch: baseBranch.name, - }, - tip, - beforeSha - ) + await this.completeMultiCommitOperation(repository, commits.length) } else if (result === RebaseResult.Error) { // we were unable to successfully start the rebase, and an error should // be shown through the default error handling infrastructure, so we can // just abandon the rebase for now - this.endRebaseFlow(repository) + this.endMultiCommitOperation(repository) } } @@ -1186,93 +1213,6 @@ export class Dispatcher { return result } - public processContinueRebaseResult( - result: RebaseResult, - conflictsState: RebaseConflictState, - repository: Repository - ) { - const stateAfter = this.repositoryStateManager.get(repository) - const { tip } = stateAfter.branchesState - const { targetBranch, baseBranch, originalBranchTip } = conflictsState - - if (result === RebaseResult.ConflictsEncountered) { - const { conflictState } = stateAfter.changesState - if (conflictState === null) { - log.warn( - `[continueRebase] conflict state after rebase is null - unable to continue` - ) - return - } - - if (!isRebaseConflictState(conflictState)) { - log.warn( - `[continueRebase] conflict state after rebase is not rebase conflicts - unable to continue` - ) - return - } - - // ensure branches are persisted when transitioning back to conflicts - const conflictsWithBranches: RebaseConflictState = { - ...conflictState, - baseBranch, - targetBranch, - } - - return this.switchToConflicts(repository, conflictsWithBranches) - } else if (result === RebaseResult.CompletedWithoutError) { - if (tip.kind !== TipState.Valid) { - log.warn( - `[continueRebase] tip after completing rebase is ${tip.kind} but this should be a valid tip if the rebase completed without error` - ) - return - } - - this.statsStore.recordRebaseSuccessAfterConflicts() - - return this.completeRebase( - repository, - { - type: BannerType.SuccessfulRebase, - targetBranch: targetBranch, - baseBranch: baseBranch, - }, - tip, - originalBranchTip - ) - } - } - - /** Switch the rebase flow to show the latest conflicts */ - private switchToConflicts = ( - repository: Repository, - conflictState: RebaseConflictState - ) => { - this.setRebaseFlowStep(repository, { - kind: RebaseStep.ShowConflicts, - conflictState, - }) - } - - /** Tidy up the rebase flow after reaching the end */ - private async completeRebase( - repository: Repository, - banner: Banner, - tip: IValidBranch, - originalBranchTip: string - ): Promise { - this.closePopup() - - this.setBanner(banner) - - if (tip.kind === TipState.Valid) { - this.addRebasedBranchToForcePushList(repository, tip, originalBranchTip) - } - - this.endRebaseFlow(repository) - - await this.refreshRepository(repository) - } - /** aborts an in-flight merge and refreshes the repository's status */ public async abortMerge(repository: Repository) { await this.appStore._abortMerge(repository) @@ -3217,7 +3157,8 @@ export class Dispatcher { repository, result, commitsToReorder.length, - tip.branch.name + tip.branch.name, + `${MultiCommitOperationKind.Reorder.toLowerCase()} commit` ) } @@ -3326,7 +3267,8 @@ export class Dispatcher { repository, result, toSquash.length + 1, - tip.branch.name + tip.branch.name, + `${MultiCommitOperationKind.Squash.toLowerCase()} commit` ) } @@ -3378,7 +3320,8 @@ export class Dispatcher { repository: Repository, result: RebaseResult, totalNumberOfCommits: number, - targetBranchName: string + ourBranch: string, + theirBranch: string ): Promise { // This will update the conflict state of the app. This is needed to start // conflict flow if squash results in conflict. @@ -3403,8 +3346,8 @@ export class Dispatcher { this.startMultiCommitOperationConflictFlow( kind, repository, - targetBranchName, - `${kind.toLowerCase()} commit` + ourBranch, + theirBranch ) break default: @@ -3508,10 +3451,8 @@ export class Dispatcher { count: number, mcos: IMultiCommitOperationState ): Banner { - const { - operationDetail: { kind }, - targetBranch, - } = mcos + const { operationDetail, targetBranch } = mcos + const { kind } = operationDetail const bannerBase = { count, @@ -3536,6 +3477,16 @@ export class Dispatcher { } break case MultiCommitOperationKind.Rebase: + const sourceBranch = + operationDetail.kind === MultiCommitOperationKind.Rebase + ? operationDetail.sourceBranch + : null + banner = { + type: BannerType.SuccessfulRebase, + targetBranch: targetBranch !== null ? targetBranch.name : '', + baseBranch: sourceBranch !== null ? sourceBranch.name : undefined, + } + break case MultiCommitOperationKind.Merge: throw new Error(`Unexpected multi commit operation kind ${kind}`) default: diff --git a/app/src/ui/dispatcher/error-handlers.ts b/app/src/ui/dispatcher/error-handlers.ts index 3cbcb8b3a1..bef4ae7265 100644 --- a/app/src/ui/dispatcher/error-handlers.ts +++ b/app/src/ui/dispatcher/error-handlers.ts @@ -417,7 +417,7 @@ export async function rebaseConflictsHandler( const { currentBranch } = gitContext - dispatcher.launchRebaseFlow(repository, currentBranch) + dispatcher.launchRebaseOperation(repository, currentBranch) return null } diff --git a/app/src/ui/multi-commit-operation/base-multi-commit-operation.tsx b/app/src/ui/multi-commit-operation/base-multi-commit-operation.tsx index 6dd43886bc..8e04f114fd 100644 --- a/app/src/ui/multi-commit-operation/base-multi-commit-operation.tsx +++ b/app/src/ui/multi-commit-operation/base-multi-commit-operation.tsx @@ -7,10 +7,10 @@ import { getResolvedFiles } from '../../lib/status' import { ConflictState, IMultiCommitOperationState } from '../../lib/app-state' import { Branch } from '../../models/branch' import { MultiCommitOperationStepKind } from '../../models/multi-commit-operation' -import { ConflictsDialog } from './conflicts-dialog' -import { ConfirmAbortDialog } from './confirm-abort-dialog' -import { ProgressDialog } from './progress-dialog' -import { WarnForcePushDialog } from './warn-force-push-dialog' +import { ConflictsDialog } from './dialog/conflicts-dialog' +import { ConfirmAbortDialog } from './dialog/confirm-abort-dialog' +import { ProgressDialog } from './dialog/progress-dialog' +import { WarnForcePushDialog } from './dialog/warn-force-push-dialog' import { PopupType } from '../../models/popup' export interface IMultiCommitOperationProps { @@ -73,11 +73,15 @@ export abstract class BaseMultiCommitOperation extends React.Component< * needed for typing purposes. Thus it should never happen, so throw error if * does. */ - protected endFlowInvalidState(): void { + protected endFlowInvalidState(isSilent: boolean = false): void { const { step, operationDetail } = this.props.state - throw new Error( - `[${operationDetail.kind}] - Invalid state - ${operationDetail.kind} ended during ${step.kind}.` - ) + const errorMessage = `[${operationDetail.kind}] - Invalid state - ${operationDetail.kind} ended during ${step.kind}.` + if (isSilent) { + this.onFlowEnded() + log.error(errorMessage) + return + } + throw new Error(errorMessage) } protected onInvokeConflictsDialogDismissed = (operationPrefix: string) => { diff --git a/app/src/ui/multi-commit-operation/base-rebase.tsx b/app/src/ui/multi-commit-operation/base-rebase.tsx new file mode 100644 index 0000000000..f847be5cf8 --- /dev/null +++ b/app/src/ui/multi-commit-operation/base-rebase.tsx @@ -0,0 +1,78 @@ +import { isRebaseConflictState, RebaseConflictState } from '../../lib/app-state' +import { + instanceOfIBaseRebaseDetails, + MultiCommitOperationKind, +} from '../../models/multi-commit-operation' +import { BaseMultiCommitOperation } from './base-multi-commit-operation' + +export abstract class BaseRebase extends BaseMultiCommitOperation { + protected abstract conflictDialogOperationPrefix: string + protected abstract rebaseKind: MultiCommitOperationKind + + protected onContinueAfterConflicts = async (): Promise => { + const { + repository, + dispatcher, + workingDirectory, + state, + conflictState, + } = this.props + const { operationDetail, originalBranchTip } = state + + if ( + conflictState === null || + originalBranchTip === null || + !isRebaseConflictState(conflictState) || + !instanceOfIBaseRebaseDetails(operationDetail) + ) { + this.endFlowInvalidState() + return + } + + const { targetBranch, baseBranch } = conflictState + const { commits, currentTip } = operationDetail + + await dispatcher.switchMultiCommitOperationToShowProgress(repository) + + const rebaseConflictState: RebaseConflictState = { + kind: 'rebase', + currentTip, + targetBranch, + baseBranch, + originalBranchTip, + baseBranchTip: currentTip, + manualResolutions: conflictState.manualResolutions, + } + + const rebaseResult = await dispatcher.continueRebase( + this.rebaseKind, + repository, + workingDirectory, + rebaseConflictState + ) + + const thierBranch = + this.rebaseKind === MultiCommitOperationKind.Rebase + ? baseBranch || '' + : `${this.rebaseKind.toLowerCase()} commit` + + await dispatcher.processMultiCommitOperationRebaseResult( + this.rebaseKind, + repository, + rebaseResult, + commits.length + 1, + targetBranch, + thierBranch + ) + } + + protected onAbort = async (): Promise => { + const { repository, dispatcher } = this.props + this.onFlowEnded() + return dispatcher.abortRebase(repository) + } + + protected onConflictsDialogDismissed = () => { + this.onInvokeConflictsDialogDismissed(this.conflictDialogOperationPrefix) + } +} diff --git a/app/src/ui/multi-commit-operation/choose-branch/base-choose-branch-dialog.tsx b/app/src/ui/multi-commit-operation/choose-branch/base-choose-branch-dialog.tsx index e826b07dbf..d31d67aa1f 100644 --- a/app/src/ui/multi-commit-operation/choose-branch/base-choose-branch-dialog.tsx +++ b/app/src/ui/multi-commit-operation/choose-branch/base-choose-branch-dialog.tsx @@ -173,15 +173,12 @@ export abstract class BaseChooseBranchDialog extends React.Component< const { selectedBranch } = this.state switch (value) { case MultiCommitOperationKind.Merge: - dispatcher.endRebaseFlow(repository) dispatcher.startMergeBranchOperation(repository, false, selectedBranch) break case MultiCommitOperationKind.Squash: - dispatcher.endRebaseFlow(repository) dispatcher.startMergeBranchOperation(repository, true, selectedBranch) break case MultiCommitOperationKind.Rebase: - dispatcher.endMultiCommitOperation(repository) dispatcher.showRebaseDialog(repository, selectedBranch) break case MultiCommitOperationKind.CherryPick: diff --git a/app/src/ui/multi-commit-operation/confirm-abort-dialog.tsx b/app/src/ui/multi-commit-operation/dialog/confirm-abort-dialog.tsx similarity index 93% rename from app/src/ui/multi-commit-operation/confirm-abort-dialog.tsx rename to app/src/ui/multi-commit-operation/dialog/confirm-abort-dialog.tsx index 69d584e68b..6bcaee3c10 100644 --- a/app/src/ui/multi-commit-operation/confirm-abort-dialog.tsx +++ b/app/src/ui/multi-commit-operation/dialog/confirm-abort-dialog.tsx @@ -1,6 +1,6 @@ import * as React from 'react' -import { Dialog, DialogContent, DialogFooter } from '../dialog' -import { OkCancelButtonGroup } from '../dialog/ok-cancel-button-group' +import { Dialog, DialogContent, DialogFooter } from '../../dialog' +import { OkCancelButtonGroup } from '../../dialog/ok-cancel-button-group' interface IConfirmAbortDialogProps { /** diff --git a/app/src/ui/multi-commit-operation/conflicts-dialog.tsx b/app/src/ui/multi-commit-operation/dialog/conflicts-dialog.tsx similarity index 92% rename from app/src/ui/multi-commit-operation/conflicts-dialog.tsx rename to app/src/ui/multi-commit-operation/dialog/conflicts-dialog.tsx index 8c534acb4c..e07d996601 100644 --- a/app/src/ui/multi-commit-operation/conflicts-dialog.tsx +++ b/app/src/ui/multi-commit-operation/dialog/conflicts-dialog.tsx @@ -1,25 +1,25 @@ import * as React from 'react' -import { Dialog, DialogContent, DialogFooter } from '../dialog' -import { Dispatcher } from '../dispatcher' -import { Repository } from '../../models/repository' +import { Dialog, DialogContent, DialogFooter } from '../../dialog' +import { Dispatcher } from '../../dispatcher' +import { Repository } from '../../../models/repository' import { WorkingDirectoryStatus, WorkingDirectoryFileChange, -} from '../../models/status' +} from '../../../models/status' import { isConflictedFile, getResolvedFiles, getConflictedFiles, getUnmergedFiles, -} from '../../lib/status' +} from '../../../lib/status' import { renderUnmergedFile, renderUnmergedFilesSummary, renderShellLink, renderAllResolved, -} from '../lib/conflicts' -import { ManualConflictResolution } from '../../models/manual-conflict-resolution' -import { OkCancelButtonGroup } from '../dialog/ok-cancel-button-group' +} from '../../lib/conflicts' +import { ManualConflictResolution } from '../../../models/manual-conflict-resolution' +import { OkCancelButtonGroup } from '../../dialog/ok-cancel-button-group' interface IConflictsDialogProps { readonly dispatcher: Dispatcher diff --git a/app/src/ui/multi-commit-operation/progress-dialog.tsx b/app/src/ui/multi-commit-operation/dialog/progress-dialog.tsx similarity index 80% rename from app/src/ui/multi-commit-operation/progress-dialog.tsx rename to app/src/ui/multi-commit-operation/dialog/progress-dialog.tsx index e3a89c4e75..3ee25432dd 100644 --- a/app/src/ui/multi-commit-operation/progress-dialog.tsx +++ b/app/src/ui/multi-commit-operation/dialog/progress-dialog.tsx @@ -1,10 +1,10 @@ import * as React from 'react' -import { formatRebaseValue } from '../../lib/rebase' -import { RichText } from '../lib/rich-text' -import { Dialog, DialogContent } from '../dialog' -import { Octicon } from '../octicons' -import * as OcticonSymbol from '../octicons/octicons.generated' -import { IMultiCommitOperationProgress } from '../../models/progress' +import { formatRebaseValue } from '../../../lib/rebase' +import { RichText } from '../../lib/rich-text' +import { Dialog, DialogContent } from '../../dialog' +import { Octicon } from '../../octicons' +import * as OcticonSymbol from '../../octicons/octicons.generated' +import { IMultiCommitOperationProgress } from '../../../models/progress' interface IProgressDialogProps { /** diff --git a/app/src/ui/multi-commit-operation/warn-force-push-dialog.tsx b/app/src/ui/multi-commit-operation/dialog/warn-force-push-dialog.tsx similarity index 91% rename from app/src/ui/multi-commit-operation/warn-force-push-dialog.tsx rename to app/src/ui/multi-commit-operation/dialog/warn-force-push-dialog.tsx index 203583db5c..94dd8343a9 100644 --- a/app/src/ui/multi-commit-operation/warn-force-push-dialog.tsx +++ b/app/src/ui/multi-commit-operation/dialog/warn-force-push-dialog.tsx @@ -1,8 +1,8 @@ import * as React from 'react' -import { Checkbox, CheckboxValue } from '../lib/checkbox' -import { Dispatcher } from '../dispatcher' -import { DialogFooter, DialogContent, Dialog } from '../dialog' -import { OkCancelButtonGroup } from '../dialog/ok-cancel-button-group' +import { Checkbox, CheckboxValue } from '../../lib/checkbox' +import { Dispatcher } from '../../dispatcher' +import { DialogFooter, DialogContent, Dialog } from '../../dialog' +import { OkCancelButtonGroup } from '../../dialog/ok-cancel-button-group' interface IWarnForcePushProps { /** diff --git a/app/src/ui/multi-commit-operation/merge.tsx b/app/src/ui/multi-commit-operation/merge.tsx index 763907a311..719dec7d65 100644 --- a/app/src/ui/multi-commit-operation/merge.tsx +++ b/app/src/ui/multi-commit-operation/merge.tsx @@ -72,7 +72,7 @@ export abstract class Merge extends BaseMultiCommitOperation { protected onConflictsDialogDismissed = () => { const { dispatcher, workingDirectory, conflictState } = this.props if (conflictState === null || !isMergeConflictState(conflictState)) { - this.endFlowInvalidState() + this.endFlowInvalidState(true) return } dispatcher.recordMergeConflictsDialogDismissal() diff --git a/app/src/ui/multi-commit-operation/multi-commit-operation.tsx b/app/src/ui/multi-commit-operation/multi-commit-operation.tsx index 79bf4410a9..9d94f03908 100644 --- a/app/src/ui/multi-commit-operation/multi-commit-operation.tsx +++ b/app/src/ui/multi-commit-operation/multi-commit-operation.tsx @@ -6,6 +6,7 @@ import { IMultiCommitOperationProps } from './base-multi-commit-operation' import { Merge } from './merge' import { Reorder } from './reorder' import { CherryPick } from './cherry-pick' +import { Rebase } from './rebase' /** A component for managing the views of a multi commit operation. */ export class MultiCommitOperation extends React.Component< @@ -17,7 +18,7 @@ export class MultiCommitOperation extends React.Component< case MultiCommitOperationKind.CherryPick: return case MultiCommitOperationKind.Rebase: - return null + return case MultiCommitOperationKind.Merge: return ( { + const { repository, dispatcher, state } = this.props + const { operationDetail, targetBranch } = state + + if (operationDetail.kind !== MultiCommitOperationKind.Rebase) { + this.endFlowInvalidState() + return + } + + const { commits, sourceBranch } = operationDetail + + if (sourceBranch === null || targetBranch === null) { + this.endFlowInvalidState() + return + } + + return dispatcher.startRebase( + repository, + sourceBranch, + targetBranch, + commits, + { continueWithForcePush: true } + ) + } + + protected renderChooseBranch = (): JSX.Element | null => { + const { repository, dispatcher, state } = this.props + const { step } = state + + if (step.kind !== MultiCommitOperationStepKind.ChooseBranch) { + this.endFlowInvalidState() + return null + } + + const { + defaultBranch, + currentBranch, + allBranches, + recentBranches, + initialBranch, + } = step + + return ( + + ) + } +} diff --git a/app/src/ui/multi-commit-operation/reorder.tsx b/app/src/ui/multi-commit-operation/reorder.tsx index 448ebbed36..8f9512fcec 100644 --- a/app/src/ui/multi-commit-operation/reorder.tsx +++ b/app/src/ui/multi-commit-operation/reorder.tsx @@ -1,8 +1,10 @@ -import { RebaseConflictState } from '../../lib/app-state' import { MultiCommitOperationKind } from '../../models/multi-commit-operation' -import { BaseMultiCommitOperation } from './base-multi-commit-operation' +import { BaseRebase } from './base-rebase' + +export abstract class Reorder extends BaseRebase { + protected conflictDialogOperationPrefix = 'reordering commits on' + protected rebaseKind = MultiCommitOperationKind.Reorder -export abstract class Reorder extends BaseMultiCommitOperation { protected onBeginOperation = () => { const { repository, dispatcher, state } = this.props const { operationDetail } = state @@ -22,64 +24,4 @@ export abstract class Reorder extends BaseMultiCommitOperation { true ) } - - protected onContinueAfterConflicts = async (): Promise => { - const { - repository, - dispatcher, - workingDirectory, - state, - conflictState, - } = this.props - const { operationDetail, targetBranch, originalBranchTip } = state - - if ( - conflictState === null || - targetBranch === null || - originalBranchTip === null || - operationDetail.kind !== MultiCommitOperationKind.Reorder - ) { - this.endFlowInvalidState() - return - } - - const { commits, currentTip } = operationDetail - - await dispatcher.switchMultiCommitOperationToShowProgress(repository) - - const rebaseConflictState: RebaseConflictState = { - kind: 'rebase', - currentTip, - targetBranch: targetBranch.name, - baseBranch: undefined, - originalBranchTip, - baseBranchTip: currentTip, - manualResolutions: conflictState.manualResolutions, - } - - const rebaseResult = await dispatcher.continueRebase( - MultiCommitOperationKind.Reorder, - repository, - workingDirectory, - rebaseConflictState - ) - - return dispatcher.processMultiCommitOperationRebaseResult( - MultiCommitOperationKind.Reorder, - repository, - rebaseResult, - commits.length, - targetBranch.name - ) - } - - protected onAbort = async (): Promise => { - const { repository, dispatcher } = this.props - this.onFlowEnded() - return dispatcher.abortRebase(repository) - } - - protected onConflictsDialogDismissed = () => { - this.onInvokeConflictsDialogDismissed('reordering commits on') - } } diff --git a/app/src/ui/multi-commit-operation/squash.tsx b/app/src/ui/multi-commit-operation/squash.tsx index 518dd6bade..5de8fc9274 100644 --- a/app/src/ui/multi-commit-operation/squash.tsx +++ b/app/src/ui/multi-commit-operation/squash.tsx @@ -1,8 +1,10 @@ -import { RebaseConflictState } from '../../lib/app-state' import { MultiCommitOperationKind } from '../../models/multi-commit-operation' -import { BaseMultiCommitOperation } from './base-multi-commit-operation' +import { BaseRebase } from './base-rebase' + +export abstract class Squash extends BaseRebase { + protected conflictDialogOperationPrefix = 'squashing commits on' + protected rebaseKind = MultiCommitOperationKind.Squash -export abstract class Squash extends BaseMultiCommitOperation { protected onBeginOperation = () => { const { repository, dispatcher, state } = this.props const { operationDetail } = state @@ -28,64 +30,4 @@ export abstract class Squash extends BaseMultiCommitOperation { true ) } - - protected onContinueAfterConflicts = async (): Promise => { - const { - repository, - dispatcher, - workingDirectory, - state, - conflictState, - } = this.props - const { operationDetail, targetBranch, originalBranchTip } = state - - if ( - conflictState === null || - targetBranch === null || - originalBranchTip === null || - operationDetail.kind !== MultiCommitOperationKind.Squash - ) { - this.endFlowInvalidState() - return - } - - const { commits, currentTip } = operationDetail - - await dispatcher.switchMultiCommitOperationToShowProgress(repository) - - const rebaseConflictState: RebaseConflictState = { - kind: 'rebase', - currentTip, - targetBranch: targetBranch.name, - baseBranch: undefined, - originalBranchTip, - baseBranchTip: currentTip, - manualResolutions: conflictState.manualResolutions, - } - - const rebaseResult = await dispatcher.continueRebase( - MultiCommitOperationKind.Squash, - repository, - workingDirectory, - rebaseConflictState - ) - - return dispatcher.processMultiCommitOperationRebaseResult( - MultiCommitOperationKind.Squash, - repository, - rebaseResult, - commits.length + 1, - targetBranch.name - ) - } - - protected onAbort = async (): Promise => { - const { repository, dispatcher } = this.props - this.onFlowEnded() - return dispatcher.abortRebase(repository) - } - - protected onConflictsDialogDismissed = () => { - this.onInvokeConflictsDialogDismissed('squashing commits on') - } } diff --git a/app/src/ui/rebase/confirm-abort-dialog.tsx b/app/src/ui/rebase/confirm-abort-dialog.tsx deleted file mode 100644 index 04ecc0255c..0000000000 --- a/app/src/ui/rebase/confirm-abort-dialog.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import * as React from 'react' - -import { Dialog, DialogContent, DialogFooter } from '../dialog' -import { ConfirmAbortStep } from '../../models/rebase-flow-step' -import { Ref } from '../lib/ref' -import { OkCancelButtonGroup } from '../dialog/ok-cancel-button-group' - -interface IConfirmAbortDialogProps { - readonly step: ConfirmAbortStep - - readonly onReturnToConflicts: (step: ConfirmAbortStep) => void - readonly onConfirmAbort: () => Promise -} - -interface IConfirmAbortDialogState { - readonly isAborting: boolean -} - -export class ConfirmAbortDialog extends React.Component< - IConfirmAbortDialogProps, - IConfirmAbortDialogState -> { - public constructor(props: IConfirmAbortDialogProps) { - super(props) - this.state = { - isAborting: false, - } - } - - private onSubmit = async () => { - this.setState({ - isAborting: true, - }) - - await this.props.onConfirmAbort() - - this.setState({ - isAborting: false, - }) - } - - /** - * Dismisses the modal and shows the rebase conflicts modal - */ - private onCancel = async () => { - await this.props.onReturnToConflicts(this.props.step) - } - - private renderTextContent(targetBranch: string, baseBranch?: string) { - let firstParagraph - - if (baseBranch !== undefined) { - firstParagraph = ( -

- {'Are you sure you want to abort rebasing '} - {baseBranch} - {' onto '} - {targetBranch}? -

- ) - } else { - firstParagraph = ( -

- {'Are you sure you want to abort rebasing '} - {targetBranch}? -

- ) - } - - return ( -
- {firstParagraph} -

- Aborting this rebase will take you back to the original branch state - and and the conflicts you have already resolved will be discarded. -

-
- ) - } - - public render() { - const { targetBranch, baseBranch } = this.props.step.conflictState - - return ( - - - {this.renderTextContent(targetBranch, baseBranch)} - - - - - - ) - } -} diff --git a/app/src/ui/rebase/index.ts b/app/src/ui/rebase/index.ts deleted file mode 100644 index 4794798092..0000000000 --- a/app/src/ui/rebase/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { RebaseFlow } from './rebase-flow' -export { ConfirmForcePush } from './confirm-force-push' diff --git a/app/src/ui/rebase/progress-dialog.tsx b/app/src/ui/rebase/progress-dialog.tsx deleted file mode 100644 index 671f3a4a66..0000000000 --- a/app/src/ui/rebase/progress-dialog.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import * as React from 'react' - -import { formatRebaseValue } from '../../lib/rebase' - -import { RichText } from '../lib/rich-text' - -import { Dialog, DialogContent } from '../dialog' -import { Octicon } from '../octicons' -import * as OcticonSymbol from '../octicons/octicons.generated' -import { IMultiCommitOperationProgress } from '../../models/progress' - -interface IRebaseProgressDialogProps { - /** Progress information about the current rebase */ - readonly progress: IMultiCommitOperationProgress - - readonly emoji: Map -} - -export class RebaseProgressDialog extends React.Component< - IRebaseProgressDialogProps -> { - private onDismissed = () => { - // this dialog is undismissable, but I need to handle the event - } - - public render() { - const { - position, - totalCommitCount, - value, - currentCommitSummary, - } = this.props.progress - - // ensure progress always starts from 1 - const count = position <= 1 ? 1 : position - - const progressValue = formatRebaseValue(value) - return ( - - -
- - -
-
- -
-
-
- Commit {count} of {totalCommitCount} -
-
- -
-
-
-
-
-
- ) - } -} diff --git a/app/src/ui/rebase/rebase-flow.tsx b/app/src/ui/rebase/rebase-flow.tsx deleted file mode 100644 index 207cab6d36..0000000000 --- a/app/src/ui/rebase/rebase-flow.tsx +++ /dev/null @@ -1,317 +0,0 @@ -import * as React from 'react' - -import { assertNever } from '../../lib/fatal-error' - -import { Repository } from '../../models/repository' -import { - RebaseStep, - RebaseFlowStep, - ConfirmAbortStep, -} from '../../models/rebase-flow-step' -import { WorkingDirectoryStatus } from '../../models/status' - -import { Dispatcher } from '../dispatcher' - -import { RebaseProgressDialog } from './progress-dialog' -import { ConfirmAbortDialog } from './confirm-abort-dialog' -import { getResolvedFiles } from '../../lib/status' -import { WarnForcePushDialog } from './warn-force-push-dialog' -import { ConflictsDialog } from '../multi-commit-operation/conflicts-dialog' -import { IMultiCommitOperationProgress } from '../../models/progress' -import { RebaseChooseBranchDialog } from '../multi-commit-operation/choose-branch/rebase-choose-branch-dialog' -import { MultiCommitOperationKind } from '../../models/multi-commit-operation' - -interface IRebaseFlowProps { - readonly repository: Repository - readonly dispatcher: Dispatcher - readonly emoji: Map - - /** The current state of the working directory */ - readonly workingDirectory: WorkingDirectoryStatus - - /** - * The current step in the rebase flow, containing application-specific - * state needed for the UI components. - */ - readonly step: RebaseFlowStep - - /** Git progress information about the current rebase */ - readonly progress: IMultiCommitOperationProgress | null - - /** - * Track whether the user has done work to resolve conflicts as part of this - * rebase, as the component should confirm with the user that they wish to - * abort the rebase and lose that work. - */ - readonly userHasResolvedConflicts: boolean - - readonly askForConfirmationOnForcePush: boolean - - /** - * Callback to hide the rebase flow and show a banner about the current state - * of conflicts, because this component will be unmounted by the runtime. - */ - readonly onShowRebaseConflictsBanner: ( - repository: Repository, - targetBranch: string - ) => void - - /** - * Callback to fire to signal to the application that the rebase flow has - * either ended in success or has been aborted and the flow can be closed. - */ - readonly onFlowEnded: (repository: Repository) => void - - /** - * Callbacks for the conflict selection components to let the user jump out - * to their preferred editor. - */ - readonly openFileInExternalEditor: (path: string) => void - readonly resolvedExternalEditor: string | null - readonly openRepositoryInShell: (repository: Repository) => void - readonly onDismissed: () => void -} - -/** A component for initiating and performing a rebase of the current branch. */ -export class RebaseFlow extends React.Component { - private moveToShowConflictedFileState = (step: ConfirmAbortStep) => { - const { conflictState } = step - this.props.dispatcher.setRebaseFlowStep(this.props.repository, { - kind: RebaseStep.ShowConflicts, - conflictState, - }) - } - - private onContinueRebase = async () => { - const { dispatcher, repository, workingDirectory, step } = this.props - if (step.kind !== RebaseStep.ShowConflicts) { - // This shouldn't happen, but needed the type checking. - log.error( - '[Rebase] Invoked continue of rebase without being in a conflict step.' - ) - this.onFlowEnded() - return - } - const { conflictState } = step - - const continueRebaseAction = async () => { - const rebaseResult = await dispatcher.continueRebase( - MultiCommitOperationKind.Rebase, - repository, - workingDirectory, - conflictState - ) - return dispatcher.processContinueRebaseResult( - rebaseResult, - conflictState, - repository - ) - } - - return dispatcher.setRebaseFlowStep(repository, { - kind: RebaseStep.ShowProgress, - rebaseAction: continueRebaseAction, - }) - } - - private onConflictsDialogDismissed = () => { - const { dispatcher, repository, step } = this.props - if (step.kind !== RebaseStep.ShowConflicts) { - // This shouldn't happen, but needed the type checking. - log.error( - '[Rebase] Cannot show rebase conflict banner without being in a conflict step.' - ) - this.onFlowEnded() - return - } - - dispatcher.setRebaseFlowStep(repository, { - kind: RebaseStep.HideConflicts, - }) - - const { targetBranch } = step.conflictState - - this.props.onShowRebaseConflictsBanner(repository, targetBranch) - } - - private onConfirmAbortRebase = async () => { - const { workingDirectory, userHasResolvedConflicts, step } = this.props - if (step.kind !== RebaseStep.ShowConflicts) { - // This shouldn't happen, but needed the type checking. - log.error( - '[Rebase] Invoked abort of rebase without being in a conflict step.' - ) - this.onFlowEnded() - return - } - - const { conflictState } = step - const { manualResolutions } = conflictState - - const resolvedConflicts = getResolvedFiles( - workingDirectory, - manualResolutions - ) - - if (userHasResolvedConflicts || resolvedConflicts.length > 0) { - // a previous commit was resolved by the user - this.props.dispatcher.setRebaseFlowStep(this.props.repository, { - kind: RebaseStep.ConfirmAbort, - conflictState, - }) - return - } - - return this.onAbortRebase() - } - - private onAbortRebase = async () => { - await this.props.dispatcher.abortRebase(this.props.repository) - this.onFlowEnded() - } - - private onFlowEnded = () => { - this.props.onDismissed() - this.props.onFlowEnded(this.props.repository) - } - - private setConflictsHaveBeenResolved = () => { - this.props.dispatcher.setConflictsResolved(this.props.repository) - } - - private renderConflictsHeaderTitle( - targetBranch: string, - baseBranch?: string - ) { - const baseBranchOutput = ( - <> - {' on '} - {baseBranch} - - ) - - return ( - - {`Resolve conflicts before rebasing `} - {targetBranch} - {baseBranch !== undefined && baseBranchOutput} - - ) - } - - public render() { - const { step } = this.props - - switch (step.kind) { - case RebaseStep.ChooseBranch: { - const { repository, dispatcher } = this.props - const { - allBranches, - defaultBranch, - currentBranch, - recentBranches, - initialBranch, - } = step - return ( - - ) - } - case RebaseStep.ShowProgress: - const { progress, emoji } = this.props - - if (progress === null) { - log.error( - '[RebaseFlow] progress is null despite trying to show the progress view. Skipping rendering...' - ) - return null - } - - return - case RebaseStep.ShowConflicts: { - const { - repository, - resolvedExternalEditor, - openFileInExternalEditor, - openRepositoryInShell, - dispatcher, - workingDirectory, - userHasResolvedConflicts, - } = this.props - - const { conflictState } = step - const { manualResolutions, targetBranch, baseBranch } = conflictState - - const submit = __DARWIN__ ? 'Continue Rebase' : 'Continue rebase' - const abort = __DARWIN__ ? 'Abort Rebase' : 'Abort rebase' - - return ( - - ) - } - - case RebaseStep.ConfirmAbort: - return ( - - ) - case RebaseStep.WarnForcePush: - const { - repository, - dispatcher, - askForConfirmationOnForcePush, - } = this.props - - return ( - - ) - - case RebaseStep.HideConflicts: - case RebaseStep.Completed: - // there is no UI to display at this point in the flow - return null - default: - return assertNever(step, 'Unknown rebase step found') - } - } -} diff --git a/app/src/ui/rebase/warn-force-push-dialog.tsx b/app/src/ui/rebase/warn-force-push-dialog.tsx deleted file mode 100644 index 09d990dc5d..0000000000 --- a/app/src/ui/rebase/warn-force-push-dialog.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import * as React from 'react' - -import { Repository } from '../../models/repository' -import { WarnForcePushStep } from '../../models/rebase-flow-step' -import { Checkbox, CheckboxValue } from '../lib/checkbox' -import { Dispatcher } from '../dispatcher' -import { DialogFooter, DialogContent, Dialog } from '../dialog' -import { Ref } from '../lib/ref' -import { OkCancelButtonGroup } from '../dialog/ok-cancel-button-group' - -interface IWarnForcePushProps { - readonly dispatcher: Dispatcher - readonly repository: Repository - readonly step: WarnForcePushStep - readonly askForConfirmationOnForcePush: boolean - readonly onDismissed: () => void -} - -interface IWarnForcePushState { - readonly askForConfirmationOnForcePush: boolean -} - -export class WarnForcePushDialog extends React.Component< - IWarnForcePushProps, - IWarnForcePushState -> { - public constructor(props: IWarnForcePushProps) { - super(props) - - this.state = { - askForConfirmationOnForcePush: props.askForConfirmationOnForcePush, - } - } - - public render() { - const { baseBranch, targetBranch } = this.props.step - - const title = __DARWIN__ - ? 'Rebase Will Require Force Push' - : 'Rebase will require force push' - - return ( - - -

- Are you sure you want to rebase {targetBranch.name} onto{' '} - {baseBranch.name}? -

-

- At the end of the rebase flow, GitHub Desktop will enable you to - force push the branch to update the upstream branch. Force pushing - will alter the history on the remote and potentially cause problems - for others collaborating on this branch. -

-
- -
-
- - - -
- ) - } - - private onAskForConfirmationOnForcePushChanged = ( - event: React.FormEvent - ) => { - const value = !event.currentTarget.checked - - this.setState({ askForConfirmationOnForcePush: value }) - } - - private onBeginRebase = async () => { - this.props.dispatcher.setConfirmForcePushSetting( - this.state.askForConfirmationOnForcePush - ) - - const { baseBranch, targetBranch, commits } = this.props.step - - await this.props.dispatcher.startRebase( - this.props.repository, - baseBranch, - targetBranch, - commits, - { continueWithForcePush: true } - ) - } -} diff --git a/changelog.json b/changelog.json index 25ccf16507..5747b19d7c 100644 --- a/changelog.json +++ b/changelog.json @@ -4,6 +4,14 @@ "[Fixed] Fix Notepad++ and RStudio integration on Windows - #12841", "[Fixed] Add minor version support for JetBrains IDEs on Windows - #12847. Thanks @tsvetilian-ty!" ], + "2.9.3-beta2": [ + "[Fixed] Fix Notepad++ and RStudio integration on Windows - #12841", + "[Fixed] Add minor version support for JetBrains IDEs on Windows - #12847. Thanks @tsvetilian-ty!" + ], + "2.9.3-beta1": [ + "[Added] Add syntax highlighting for dart - #12827. Thanks @say25!", + "[Fixed] Fix scrolling performance issue for large diffs." + ], "2.9.2": ["[Fixed] Fix scrolling performance issue for large diffs."], "2.9.1": [ "[Added] Add Fluent Terminal shell support - #12305. Thanks @Idered!", diff --git a/docs/technical/syntax-highlighting.md b/docs/technical/syntax-highlighting.md index bc3d858206..aaaae9df2f 100644 --- a/docs/technical/syntax-highlighting.md +++ b/docs/technical/syntax-highlighting.md @@ -8,7 +8,7 @@ We introduced syntax highlighted diffs in [#3101](https://github.com/desktop/des We currently support syntax highlighting for the following languages and file types. -JavaScript, JSON, TypeScript, Coffeescript, HTML, Asp, JavaServer Pages, CSS, SCSS, LESS, VUE, Markdown, Toml, Yaml, XML, Diff, Objective-C, Scala, C#, Java, C, C++, Kotlin, Ocaml, F#, Swift, sh/bash, SQL, CYPHER, Go, Perl, PHP, Python, Ruby, Clojure, Rust, Elixir, Haxe, R, PowerShell, Visual Basic, Fortran, Lua, Crystal, Julia, sTex, SPARQL, Stylus, Soy, Smalltalk, Slim, Sieve, Scheme, ReStructuredText, RPM, Q, Puppet, Pug, Protobuf, Properties, Apache Pig, ASCII Armor (PGP), Oz, Pascal and Docker. +JavaScript, JSON, TypeScript, Coffeescript, HTML, Asp, JavaServer Pages, CSS, SCSS, LESS, VUE, Markdown, Yaml, XML, Diff, Objective-C, Scala, C#, Java, C, C++, Kotlin, Ocaml, F#, Swift, sh/bash, SQL, CYPHER, Go, Perl, PHP, Python, Ruby, Clojure, Rust, Elixir, Haxe, R, PowerShell, Visual Basic, Fortran, Lua, Crystal, Julia, sTex, SPARQL, Stylus, Soy, Smalltalk, Slim, Sieve, Scheme, ReStructuredText, RPM, Q, Puppet, Pug, Protobuf, Properties, Apache Pig, ASCII Armor (PGP), Oz, Pascal, Toml, Dart and Docker. This list was never meant to be exhaustive, we expect to add more languages going forward but this seemed like a good first step.