mirror of
https://github.com/desktop/desktop
synced 2024-08-28 12:40:55 +00:00
Merge branch 'development' into release/2.9.3
This commit is contained in:
commit
21965e1922
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
|
@ -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 }}
|
||||
|
|
|
@ -403,6 +403,12 @@ const extensionModes: ReadonlyArray<IModeDefinition> = [
|
|||
'.toml': 'text/x-toml',
|
||||
},
|
||||
},
|
||||
{
|
||||
install: () => import('codemirror/mode/dart/dart'),
|
||||
mappings: {
|
||||
'.dart': 'application/dart',
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
/**
|
||||
|
|
|
@ -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<string, Commit>
|
||||
|
||||
|
@ -535,39 +532,6 @@ export interface IBranchesState {
|
|||
readonly rebasedBranches: ReadonlyMap<string, string>
|
||||
}
|
||||
|
||||
/** 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<CommitOneLine> | 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<string>
|
||||
|
|
|
@ -131,15 +131,28 @@ async function findApplication(
|
|||
): Promise<string | null> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
55
app/src/lib/multi-commit-operation.ts
Normal file
55
app/src/lib/multi-commit-operation.ts
Normal file
|
@ -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)
|
||||
)
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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}`
|
||||
|
|
|
@ -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<IAppState> {
|
|||
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<IAppState> {
|
|||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
||||
if (conflictState === null || !isRebaseConflictState(conflictState)) {
|
||||
return
|
||||
}
|
||||
|
||||
const { step } = rebaseState
|
||||
if (step === null) {
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
step.kind === RebaseStep.ShowConflicts ||
|
||||
step.kind === RebaseStep.ConfirmAbort
|
||||
private async initializeMultiCommitOperationIfConflictsFound(
|
||||
repository: Repository,
|
||||
status: IStatusResult
|
||||
) {
|
||||
// merge in new conflicts with known branches so they are not forgotten
|
||||
const { baseBranch, targetBranch } = step.conflictState
|
||||
const newConflictsState = {
|
||||
...conflictState,
|
||||
baseBranch,
|
||||
const state = this.repositoryStateCache.get(repository)
|
||||
const {
|
||||
changesState: { conflictState },
|
||||
multiCommitOperationState,
|
||||
branchesState,
|
||||
} = state
|
||||
|
||||
if (conflictState === null) {
|
||||
this.clearConflictsFlowVisuals(state)
|
||||
return
|
||||
}
|
||||
|
||||
if (multiCommitOperationState !== null) {
|
||||
return
|
||||
}
|
||||
|
||||
let operationDetail: MultiCommitOperationDetail
|
||||
let targetBranch: Branch | null = null
|
||||
let commits: ReadonlyArray<Commit | CommitOneLine> = []
|
||||
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
|
||||
}
|
||||
|
||||
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.updateRebaseState(repository, () => ({
|
||||
step: { ...step, conflictState: newConflictsState },
|
||||
}))
|
||||
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<IAppState> {
|
|||
const { conflictState } = changesState
|
||||
|
||||
if (conflictState === null || multiCommitOperationState === null) {
|
||||
this.clearConflictsFlowVisuals(state)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -2105,7 +2185,6 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
const {
|
||||
changesState: { conflictState },
|
||||
multiCommitOperationState,
|
||||
branchesState,
|
||||
} = state
|
||||
|
||||
if (conflictState === null) {
|
||||
|
@ -2113,138 +2192,70 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
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<string | undefined> {
|
||||
let theirBranch: string | undefined
|
||||
if (
|
||||
multiCommitOperationState !== null &&
|
||||
|
@ -2271,22 +2282,26 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
? 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<IAppState> {
|
|||
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<CommitOneLine>
|
||||
) {
|
||||
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, () => ({
|
||||
this.repositoryStateCache.updateMultiCommitOperationState(
|
||||
repository,
|
||||
() => ({
|
||||
userHasResolvedConflicts: true,
|
||||
}))
|
||||
}
|
||||
|
||||
/** This shouldn't be called directly. See `Dispatcher`. */
|
||||
public async _setRebaseFlowStep(
|
||||
repository: Repository,
|
||||
step: RebaseFlowStep
|
||||
): Promise<void> {
|
||||
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 shouldn't be called directly. See `Dispatcher`. */
|
||||
|
@ -4718,14 +4661,9 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
baseBranch: Branch,
|
||||
targetBranch: Branch
|
||||
): Promise<RebaseResult> {
|
||||
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<IAppState> {
|
|||
workingDirectory: WorkingDirectoryStatus,
|
||||
manualResolutions: ReadonlyMap<string, ManualConflictResolution>
|
||||
): Promise<RebaseResult> {
|
||||
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<IAppState> {
|
|||
}
|
||||
})
|
||||
|
||||
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<IAppState> {
|
|||
)
|
||||
}
|
||||
|
||||
/** 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<IAppState> {
|
|||
operationDetail: MultiCommitOperationDetail,
|
||||
targetBranch: Branch | null,
|
||||
commits: ReadonlyArray<Commit | CommitOneLine>,
|
||||
originalBranchTip: string | null
|
||||
originalBranchTip: string | null,
|
||||
emitUpdate: boolean = true
|
||||
): void {
|
||||
this.repositoryStateCache.initializeMultiCommitOperationState(repository, {
|
||||
step: {
|
||||
|
@ -6882,6 +6694,10 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
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
|
||||
|
|
|
@ -15,7 +15,6 @@ import {
|
|||
IRepositoryState,
|
||||
RepositorySectionTab,
|
||||
ICommitSelection,
|
||||
IRebaseState,
|
||||
ChangesSelectionKind,
|
||||
IMultiCommitOperationUndoState,
|
||||
IMultiCommitOperationState,
|
||||
|
@ -109,17 +108,6 @@ export class RepositoryStateCache {
|
|||
})
|
||||
}
|
||||
|
||||
public updateRebaseState<K extends keyof IRebaseState>(
|
||||
repository: Repository,
|
||||
fn: (branchesState: IRebaseState) => Pick<IRebaseState, K>
|
||||
) {
|
||||
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<Branch>(),
|
||||
defaultBranch: null,
|
||||
},
|
||||
rebaseState: {
|
||||
step: null,
|
||||
progress: null,
|
||||
commits: null,
|
||||
userHasResolvedConflicts: false,
|
||||
},
|
||||
commitAuthor: null,
|
||||
commitLookup: new Map<string, Commit>(),
|
||||
localCommitSHAs: [],
|
||||
|
|
|
@ -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<CommitOneLine>
|
||||
/**
|
||||
* 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,
|
||||
]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<Branch>
|
||||
readonly recentBranches: ReadonlyArray<Branch>
|
||||
readonly initialBranch?: Branch
|
||||
}
|
||||
|
||||
export type WarnForcePushStep = {
|
||||
readonly kind: RebaseStep.WarnForcePush
|
||||
readonly baseBranch: Branch
|
||||
readonly targetBranch: Branch
|
||||
readonly commits: ReadonlyArray<CommitOneLine>
|
||||
}
|
||||
|
||||
/** 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<void>) | 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
|
||||
}
|
|
@ -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<IAppProps, IAppState> {
|
|||
if (!repository || repository instanceof CloningRepository) {
|
||||
return
|
||||
}
|
||||
|
||||
this.props.dispatcher.showRebaseDialog(repository)
|
||||
}
|
||||
|
||||
|
@ -1719,63 +1713,6 @@ export class App extends React.Component<IAppProps, IAppState> {
|
|||
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 (
|
||||
<RebaseFlow
|
||||
key="rebase-flow"
|
||||
repository={popup.repository}
|
||||
openFileInExternalEditor={this.openFileInExternalEditor}
|
||||
dispatcher={this.props.dispatcher}
|
||||
onFlowEnded={this.onRebaseFlowEnded}
|
||||
onDismissed={onPopupDismissedFn}
|
||||
workingDirectory={workingDirectory}
|
||||
progress={progress}
|
||||
step={step}
|
||||
userHasResolvedConflicts={userHasResolvedConflicts}
|
||||
askForConfirmationOnForcePush={
|
||||
this.state.askForConfirmationOnForcePush
|
||||
}
|
||||
resolvedExternalEditor={this.state.resolvedExternalEditor}
|
||||
openRepositoryInShell={this.openCurrentRepositoryInShell}
|
||||
onShowRebaseConflictsBanner={this.onShowRebaseConflictsBanner}
|
||||
emoji={emoji}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case PopupType.ConfirmForcePush: {
|
||||
const { askForConfirmationOnForcePush } = this.state
|
||||
|
||||
|
@ -2128,46 +2065,6 @@ export class App extends React.Component<IAppProps, IAppState> {
|
|||
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<IAppProps, IAppState> {
|
|||
) => {
|
||||
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<IAppProps, IAppState> {
|
|||
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()
|
||||
|
|
|
@ -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<CommitOneLine>
|
||||
) {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
// 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:
|
||||
|
|
|
@ -417,7 +417,7 @@ export async function rebaseConflictsHandler(
|
|||
|
||||
const { currentBranch } = gitContext
|
||||
|
||||
dispatcher.launchRebaseFlow(repository, currentBranch)
|
||||
dispatcher.launchRebaseOperation(repository, currentBranch)
|
||||
|
||||
return null
|
||||
}
|
||||
|
|
|
@ -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) => {
|
||||
|
|
78
app/src/ui/multi-commit-operation/base-rebase.tsx
Normal file
78
app/src/ui/multi-commit-operation/base-rebase.tsx
Normal file
|
@ -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<void> => {
|
||||
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<void> => {
|
||||
const { repository, dispatcher } = this.props
|
||||
this.onFlowEnded()
|
||||
return dispatcher.abortRebase(repository)
|
||||
}
|
||||
|
||||
protected onConflictsDialogDismissed = () => {
|
||||
this.onInvokeConflictsDialogDismissed(this.conflictDialogOperationPrefix)
|
||||
}
|
||||
}
|
|
@ -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:
|
||||
|
|
|
@ -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 {
|
||||
/**
|
|
@ -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
|
|
@ -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 {
|
||||
/**
|
|
@ -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 {
|
||||
/**
|
|
@ -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()
|
||||
|
|
|
@ -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 <CherryPick {...this.props} />
|
||||
case MultiCommitOperationKind.Rebase:
|
||||
return null
|
||||
return <Rebase {...this.props} />
|
||||
case MultiCommitOperationKind.Merge:
|
||||
return (
|
||||
<Merge
|
||||
|
|
70
app/src/ui/multi-commit-operation/rebase.tsx
Normal file
70
app/src/ui/multi-commit-operation/rebase.tsx
Normal file
|
@ -0,0 +1,70 @@
|
|||
import React from 'react'
|
||||
import {
|
||||
MultiCommitOperationKind,
|
||||
MultiCommitOperationStepKind,
|
||||
} from '../../models/multi-commit-operation'
|
||||
import { BaseRebase } from './base-rebase'
|
||||
import { RebaseChooseBranchDialog } from './choose-branch/rebase-choose-branch-dialog'
|
||||
|
||||
export abstract class Rebase extends BaseRebase {
|
||||
protected conflictDialogOperationPrefix = 'rebasing'
|
||||
protected rebaseKind = MultiCommitOperationKind.Rebase
|
||||
|
||||
protected onBeginOperation = () => {
|
||||
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 (
|
||||
<RebaseChooseBranchDialog
|
||||
key="choose-branch"
|
||||
dispatcher={dispatcher}
|
||||
repository={repository}
|
||||
allBranches={allBranches}
|
||||
defaultBranch={defaultBranch}
|
||||
recentBranches={recentBranches}
|
||||
currentBranch={currentBranch}
|
||||
initialBranch={initialBranch}
|
||||
operation={MultiCommitOperationKind.Rebase}
|
||||
onDismissed={this.onFlowEnded}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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<void> => {
|
||||
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<void> => {
|
||||
const { repository, dispatcher } = this.props
|
||||
this.onFlowEnded()
|
||||
return dispatcher.abortRebase(repository)
|
||||
}
|
||||
|
||||
protected onConflictsDialogDismissed = () => {
|
||||
this.onInvokeConflictsDialogDismissed('reordering commits on')
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<void> => {
|
||||
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<void> => {
|
||||
const { repository, dispatcher } = this.props
|
||||
this.onFlowEnded()
|
||||
return dispatcher.abortRebase(repository)
|
||||
}
|
||||
|
||||
protected onConflictsDialogDismissed = () => {
|
||||
this.onInvokeConflictsDialogDismissed('squashing commits on')
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<void>
|
||||
}
|
||||
|
||||
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 = (
|
||||
<p>
|
||||
{'Are you sure you want to abort rebasing '}
|
||||
<Ref>{baseBranch}</Ref>
|
||||
{' onto '}
|
||||
<Ref>{targetBranch}</Ref>?
|
||||
</p>
|
||||
)
|
||||
} else {
|
||||
firstParagraph = (
|
||||
<p>
|
||||
{'Are you sure you want to abort rebasing '}
|
||||
<Ref>{targetBranch}</Ref>?
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="column-left">
|
||||
{firstParagraph}
|
||||
<p>
|
||||
Aborting this rebase will take you back to the original branch state
|
||||
and and the conflicts you have already resolved will be discarded.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { targetBranch, baseBranch } = this.props.step.conflictState
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
id="abort-merge-warning"
|
||||
title={__DARWIN__ ? 'Confirm Abort Rebase' : 'Confirm abort rebase'}
|
||||
onDismissed={this.onCancel}
|
||||
onSubmit={this.onSubmit}
|
||||
disabled={this.state.isAborting}
|
||||
type="warning"
|
||||
>
|
||||
<DialogContent>
|
||||
{this.renderTextContent(targetBranch, baseBranch)}
|
||||
</DialogContent>
|
||||
<DialogFooter>
|
||||
<OkCancelButtonGroup
|
||||
destructive={true}
|
||||
okButtonText={__DARWIN__ ? 'Abort Rebase' : 'Abort rebase'}
|
||||
/>
|
||||
</DialogFooter>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
export { RebaseFlow } from './rebase-flow'
|
||||
export { ConfirmForcePush } from './confirm-force-push'
|
|
@ -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<string, string>
|
||||
}
|
||||
|
||||
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 (
|
||||
<Dialog
|
||||
dismissable={false}
|
||||
onDismissed={this.onDismissed}
|
||||
id="rebase-progress"
|
||||
title="Rebase in progress"
|
||||
>
|
||||
<DialogContent>
|
||||
<div>
|
||||
<progress value={progressValue} />
|
||||
|
||||
<div className="details">
|
||||
<div className="green-circle">
|
||||
<Octicon symbol={OcticonSymbol.check} />
|
||||
</div>
|
||||
<div className="summary">
|
||||
<div className="message">
|
||||
Commit {count} of {totalCommitCount}
|
||||
</div>
|
||||
<div className="detail">
|
||||
<RichText
|
||||
emoji={this.props.emoji}
|
||||
text={currentCommitSummary || ''}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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<string, string>
|
||||
|
||||
/** 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<IRebaseFlowProps> {
|
||||
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 '}
|
||||
<strong>{baseBranch}</strong>
|
||||
</>
|
||||
)
|
||||
|
||||
return (
|
||||
<span>
|
||||
{`Resolve conflicts before rebasing `}
|
||||
<strong>{targetBranch}</strong>
|
||||
{baseBranch !== undefined && baseBranchOutput}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<RebaseChooseBranchDialog
|
||||
key="choose-branch"
|
||||
dispatcher={dispatcher}
|
||||
repository={repository}
|
||||
allBranches={allBranches}
|
||||
defaultBranch={defaultBranch}
|
||||
recentBranches={recentBranches}
|
||||
currentBranch={currentBranch}
|
||||
initialBranch={initialBranch}
|
||||
operation={MultiCommitOperationKind.Rebase}
|
||||
onDismissed={this.onFlowEnded}
|
||||
/>
|
||||
)
|
||||
}
|
||||
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 <RebaseProgressDialog progress={progress} emoji={emoji} />
|
||||
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 (
|
||||
<ConflictsDialog
|
||||
dispatcher={dispatcher}
|
||||
repository={repository}
|
||||
workingDirectory={workingDirectory}
|
||||
userHasResolvedConflicts={userHasResolvedConflicts}
|
||||
resolvedExternalEditor={resolvedExternalEditor}
|
||||
ourBranch={baseBranch}
|
||||
theirBranch={targetBranch}
|
||||
manualResolutions={manualResolutions}
|
||||
headerTitle={this.renderConflictsHeaderTitle(
|
||||
targetBranch,
|
||||
baseBranch
|
||||
)}
|
||||
submitButton={submit}
|
||||
abortButton={abort}
|
||||
onSubmit={this.onContinueRebase}
|
||||
onAbort={this.onConfirmAbortRebase}
|
||||
onDismissed={this.onConflictsDialogDismissed}
|
||||
openFileInExternalEditor={openFileInExternalEditor}
|
||||
openRepositoryInShell={openRepositoryInShell}
|
||||
someConflictsHaveBeenResolved={this.setConflictsHaveBeenResolved}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
case RebaseStep.ConfirmAbort:
|
||||
return (
|
||||
<ConfirmAbortDialog
|
||||
step={step}
|
||||
onConfirmAbort={this.onAbortRebase}
|
||||
onReturnToConflicts={this.moveToShowConflictedFileState}
|
||||
/>
|
||||
)
|
||||
case RebaseStep.WarnForcePush:
|
||||
const {
|
||||
repository,
|
||||
dispatcher,
|
||||
askForConfirmationOnForcePush,
|
||||
} = this.props
|
||||
|
||||
return (
|
||||
<WarnForcePushDialog
|
||||
step={step}
|
||||
dispatcher={dispatcher}
|
||||
repository={repository}
|
||||
askForConfirmationOnForcePush={askForConfirmationOnForcePush}
|
||||
onDismissed={this.onFlowEnded}
|
||||
/>
|
||||
)
|
||||
|
||||
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')
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 (
|
||||
<Dialog
|
||||
title={title}
|
||||
onDismissed={this.props.onDismissed}
|
||||
onSubmit={this.onBeginRebase}
|
||||
dismissable={false}
|
||||
type="warning"
|
||||
>
|
||||
<DialogContent>
|
||||
<p>
|
||||
Are you sure you want to rebase <Ref>{targetBranch.name}</Ref> onto{' '}
|
||||
<Ref>{baseBranch.name}</Ref>?
|
||||
</p>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<div>
|
||||
<Checkbox
|
||||
label="Do not show this message again"
|
||||
value={
|
||||
this.state.askForConfirmationOnForcePush
|
||||
? CheckboxValue.Off
|
||||
: CheckboxValue.On
|
||||
}
|
||||
onChange={this.onAskForConfirmationOnForcePushChanged}
|
||||
/>
|
||||
</div>
|
||||
</DialogContent>
|
||||
<DialogFooter>
|
||||
<OkCancelButtonGroup
|
||||
okButtonText={__DARWIN__ ? 'Begin Rebase' : 'Begin rebase'}
|
||||
onCancelButtonClick={this.props.onDismissed}
|
||||
/>
|
||||
</DialogFooter>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
private onAskForConfirmationOnForcePushChanged = (
|
||||
event: React.FormEvent<HTMLInputElement>
|
||||
) => {
|
||||
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 }
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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!",
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
Loading…
Reference in a new issue