mirror of
https://github.com/desktop/desktop
synced 2024-08-28 12:40:55 +00:00
Merge branch 'development' into releases/3.1.2
This commit is contained in:
commit
9a89aa1eed
2
.github/workflows/release-pr.yml
vendored
2
.github/workflows/release-pr.yml
vendored
|
@ -37,7 +37,7 @@ jobs:
|
||||||
private_key: ${{ secrets.DESKTOP_RELEASES_APP_PRIVATE_KEY }}
|
private_key: ${{ secrets.DESKTOP_RELEASES_APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
- name: Create Release Pull Request
|
- name: Create Release Pull Request
|
||||||
uses: peter-evans/create-pull-request@v4.1.1
|
uses: peter-evans/create-pull-request@v4.1.4
|
||||||
if: |
|
if: |
|
||||||
startsWith(github.ref, 'refs/heads/releases/') && !contains(github.ref, 'test')
|
startsWith(github.ref, 'refs/heads/releases/') && !contains(github.ref, 'test')
|
||||||
with:
|
with:
|
||||||
|
|
|
@ -41,8 +41,7 @@ The release notes for the latest beta versions are available [here](https://desk
|
||||||
|
|
||||||
There are several community-supported package managers that can be used to
|
There are several community-supported package managers that can be used to
|
||||||
install GitHub Desktop:
|
install GitHub Desktop:
|
||||||
- Windows users can install using [Chocolatey](https://chocolatey.org/) package manager:
|
- Windows users can install using [winget](https://docs.microsoft.com/en-us/windows/package-manager/winget/) `c:/> winget install github-desktop` or [Chocolatey](https://chocolatey.org/) `c:\> choco install github-desktop`
|
||||||
`c:\> choco install github-desktop`
|
|
||||||
- macOS users can install using [Homebrew](https://brew.sh/) package manager:
|
- macOS users can install using [Homebrew](https://brew.sh/) package manager:
|
||||||
`$ brew install --cask github`
|
`$ brew install --cask github`
|
||||||
|
|
||||||
|
@ -85,6 +84,10 @@ resources relevant to the project.
|
||||||
|
|
||||||
If you're looking for something to work on, check out the [help wanted](https://github.com/desktop/desktop/issues?q=is%3Aissue+is%3Aopen+label%3A%22help%20wanted%22) label.
|
If you're looking for something to work on, check out the [help wanted](https://github.com/desktop/desktop/issues?q=is%3Aissue+is%3Aopen+label%3A%22help%20wanted%22) label.
|
||||||
|
|
||||||
|
## Building Desktop
|
||||||
|
|
||||||
|
To get your development environment set up for building Desktop, see [setup.md](./docs/contributing/setup.md).
|
||||||
|
|
||||||
## More Resources
|
## More Resources
|
||||||
|
|
||||||
See [desktop.github.com](https://desktop.github.com) for more product-oriented
|
See [desktop.github.com](https://desktop.github.com) for more product-oriented
|
||||||
|
|
|
@ -170,6 +170,9 @@ export interface IAppState {
|
||||||
/** The width of the files list in the stash view */
|
/** The width of the files list in the stash view */
|
||||||
readonly stashedFilesWidth: IConstrainedValue
|
readonly stashedFilesWidth: IConstrainedValue
|
||||||
|
|
||||||
|
/** The width of the files list in the pull request files changed view */
|
||||||
|
readonly pullRequestFilesListWidth: IConstrainedValue
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used to highlight access keys throughout the app when the
|
* Used to highlight access keys throughout the app when the
|
||||||
* Alt key is pressed. Only applicable on non-macOS platforms.
|
* Alt key is pressed. Only applicable on non-macOS platforms.
|
||||||
|
@ -194,6 +197,9 @@ export interface IAppState {
|
||||||
/** Whether we should show a confirmation dialog */
|
/** Whether we should show a confirmation dialog */
|
||||||
readonly askForConfirmationOnDiscardChangesPermanently: boolean
|
readonly askForConfirmationOnDiscardChangesPermanently: boolean
|
||||||
|
|
||||||
|
/** Should the app prompt the user to confirm a discard stash */
|
||||||
|
readonly askForConfirmationOnDiscardStash: boolean
|
||||||
|
|
||||||
/** Should the app prompt the user to confirm a force push? */
|
/** Should the app prompt the user to confirm a force push? */
|
||||||
readonly askForConfirmationOnForcePush: boolean
|
readonly askForConfirmationOnForcePush: boolean
|
||||||
|
|
||||||
|
@ -230,6 +236,9 @@ export interface IAppState {
|
||||||
/** Whether we should hide white space changes in history diff */
|
/** Whether we should hide white space changes in history diff */
|
||||||
readonly hideWhitespaceInHistoryDiff: boolean
|
readonly hideWhitespaceInHistoryDiff: boolean
|
||||||
|
|
||||||
|
/** Whether we should hide white space changes in the pull request diff */
|
||||||
|
readonly hideWhitespaceInPullRequestDiff: boolean
|
||||||
|
|
||||||
/** Whether we should show side by side diffs */
|
/** Whether we should show side by side diffs */
|
||||||
readonly showSideBySideDiff: boolean
|
readonly showSideBySideDiff: boolean
|
||||||
|
|
||||||
|
@ -965,4 +974,7 @@ export interface IPullRequestState {
|
||||||
* diff between the latest commit and the earliest commits parent.
|
* diff between the latest commit and the earliest commits parent.
|
||||||
*/
|
*/
|
||||||
readonly commitSelection: ICommitSelection
|
readonly commitSelection: ICommitSelection
|
||||||
|
|
||||||
|
/** The result of merging the pull request branch into the base branch */
|
||||||
|
readonly mergeStatus: MergeTreeResult | null
|
||||||
}
|
}
|
||||||
|
|
24
app/src/lib/commit-url.ts
Normal file
24
app/src/lib/commit-url.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import * as crypto from 'crypto'
|
||||||
|
import { GitHubRepository } from '../models/github-repository'
|
||||||
|
|
||||||
|
/** Method to create the url for viewing a commit on dotcom */
|
||||||
|
export function createCommitURL(
|
||||||
|
gitHubRepository: GitHubRepository,
|
||||||
|
SHA: string,
|
||||||
|
filePath?: string
|
||||||
|
): string | null {
|
||||||
|
const baseURL = gitHubRepository.htmlURL
|
||||||
|
|
||||||
|
if (baseURL === null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filePath === undefined) {
|
||||||
|
return `${baseURL}/commit/${SHA}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileHash = crypto.createHash('sha256').update(filePath).digest('hex')
|
||||||
|
const fileSuffix = '#diff-' + fileHash
|
||||||
|
|
||||||
|
return `${baseURL}/commit/${SHA}${fileSuffix}`
|
||||||
|
}
|
|
@ -62,6 +62,14 @@ const editors: ILinuxExternalEditor[] = [
|
||||||
name: 'Lite XL',
|
name: 'Lite XL',
|
||||||
paths: ['/usr/bin/lite-xl'],
|
paths: ['/usr/bin/lite-xl'],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Jetbrains PhpStorm',
|
||||||
|
paths: ['/snap/bin/phpstorm'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Jetbrains WebStorm',
|
||||||
|
paths: ['/snap/bin/webstorm'],
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
async function getAvailablePath(paths: string[]): Promise<string | null> {
|
async function getAvailablePath(paths: string[]): Promise<string | null> {
|
||||||
|
|
|
@ -110,5 +110,5 @@ export function enableSubmoduleDiff(): boolean {
|
||||||
|
|
||||||
/** Should we enable starting pull requests? */
|
/** Should we enable starting pull requests? */
|
||||||
export function enableStartingPullRequests(): boolean {
|
export function enableStartingPullRequests(): boolean {
|
||||||
return enableDevelopmentFeatures()
|
return enableBetaFeatures()
|
||||||
}
|
}
|
||||||
|
|
|
@ -254,7 +254,7 @@ export async function getBranchMergeBaseChangedFiles(
|
||||||
baseBranchName: string,
|
baseBranchName: string,
|
||||||
comparisonBranchName: string,
|
comparisonBranchName: string,
|
||||||
latestComparisonBranchCommitRef: string
|
latestComparisonBranchCommitRef: string
|
||||||
): Promise<IChangesetData> {
|
): Promise<IChangesetData | null> {
|
||||||
const baseArgs = [
|
const baseArgs = [
|
||||||
'diff',
|
'diff',
|
||||||
'--merge-base',
|
'--merge-base',
|
||||||
|
@ -268,22 +268,26 @@ export async function getBranchMergeBaseChangedFiles(
|
||||||
'--',
|
'--',
|
||||||
]
|
]
|
||||||
|
|
||||||
const result = await git(
|
|
||||||
baseArgs,
|
|
||||||
repository.path,
|
|
||||||
'getBranchMergeBaseChangedFiles'
|
|
||||||
)
|
|
||||||
|
|
||||||
const mergeBaseCommit = await getMergeBase(
|
const mergeBaseCommit = await getMergeBase(
|
||||||
repository,
|
repository,
|
||||||
baseBranchName,
|
baseBranchName,
|
||||||
comparisonBranchName
|
comparisonBranchName
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (mergeBaseCommit === null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await git(
|
||||||
|
baseArgs,
|
||||||
|
repository.path,
|
||||||
|
'getBranchMergeBaseChangedFiles'
|
||||||
|
)
|
||||||
|
|
||||||
return parseRawLogWithNumstat(
|
return parseRawLogWithNumstat(
|
||||||
result.combinedOutput,
|
result.combinedOutput,
|
||||||
`${latestComparisonBranchCommitRef}`,
|
`${latestComparisonBranchCommitRef}`,
|
||||||
mergeBaseCommit ?? NullTreeSHA
|
mergeBaseCommit
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -323,15 +323,20 @@ const commitSummaryWidthConfigKey: string = 'commit-summary-width'
|
||||||
const defaultStashedFilesWidth: number = 250
|
const defaultStashedFilesWidth: number = 250
|
||||||
const stashedFilesWidthConfigKey: string = 'stashed-files-width'
|
const stashedFilesWidthConfigKey: string = 'stashed-files-width'
|
||||||
|
|
||||||
|
const defaultPullRequestFileListWidth: number = 250
|
||||||
|
const pullRequestFileListConfigKey: string = 'pull-request-files-width'
|
||||||
|
|
||||||
const askToMoveToApplicationsFolderDefault: boolean = true
|
const askToMoveToApplicationsFolderDefault: boolean = true
|
||||||
const confirmRepoRemovalDefault: boolean = true
|
const confirmRepoRemovalDefault: boolean = true
|
||||||
const confirmDiscardChangesDefault: boolean = true
|
const confirmDiscardChangesDefault: boolean = true
|
||||||
const confirmDiscardChangesPermanentlyDefault: boolean = true
|
const confirmDiscardChangesPermanentlyDefault: boolean = true
|
||||||
|
const confirmDiscardStashDefault: boolean = true
|
||||||
const askForConfirmationOnForcePushDefault = true
|
const askForConfirmationOnForcePushDefault = true
|
||||||
const confirmUndoCommitDefault: boolean = true
|
const confirmUndoCommitDefault: boolean = true
|
||||||
const askToMoveToApplicationsFolderKey: string = 'askToMoveToApplicationsFolder'
|
const askToMoveToApplicationsFolderKey: string = 'askToMoveToApplicationsFolder'
|
||||||
const confirmRepoRemovalKey: string = 'confirmRepoRemoval'
|
const confirmRepoRemovalKey: string = 'confirmRepoRemoval'
|
||||||
const confirmDiscardChangesKey: string = 'confirmDiscardChanges'
|
const confirmDiscardChangesKey: string = 'confirmDiscardChanges'
|
||||||
|
const confirmDiscardStashKey: string = 'confirmDiscardStash'
|
||||||
const confirmDiscardChangesPermanentlyKey: string =
|
const confirmDiscardChangesPermanentlyKey: string =
|
||||||
'confirmDiscardChangesPermanentlyKey'
|
'confirmDiscardChangesPermanentlyKey'
|
||||||
const confirmForcePushKey: string = 'confirmForcePush'
|
const confirmForcePushKey: string = 'confirmForcePush'
|
||||||
|
@ -348,6 +353,9 @@ const hideWhitespaceInChangesDiffDefault = false
|
||||||
const hideWhitespaceInChangesDiffKey = 'hide-whitespace-in-changes-diff'
|
const hideWhitespaceInChangesDiffKey = 'hide-whitespace-in-changes-diff'
|
||||||
const hideWhitespaceInHistoryDiffDefault = false
|
const hideWhitespaceInHistoryDiffDefault = false
|
||||||
const hideWhitespaceInHistoryDiffKey = 'hide-whitespace-in-diff'
|
const hideWhitespaceInHistoryDiffKey = 'hide-whitespace-in-diff'
|
||||||
|
const hideWhitespaceInPullRequestDiffDefault = false
|
||||||
|
const hideWhitespaceInPullRequestDiffKey =
|
||||||
|
'hide-whitespace-in-pull-request-diff'
|
||||||
|
|
||||||
const commitSpellcheckEnabledDefault = true
|
const commitSpellcheckEnabledDefault = true
|
||||||
const commitSpellcheckEnabledKey = 'commit-spellcheck-enabled'
|
const commitSpellcheckEnabledKey = 'commit-spellcheck-enabled'
|
||||||
|
@ -424,6 +432,7 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
||||||
private sidebarWidth = constrain(defaultSidebarWidth)
|
private sidebarWidth = constrain(defaultSidebarWidth)
|
||||||
private commitSummaryWidth = constrain(defaultCommitSummaryWidth)
|
private commitSummaryWidth = constrain(defaultCommitSummaryWidth)
|
||||||
private stashedFilesWidth = constrain(defaultStashedFilesWidth)
|
private stashedFilesWidth = constrain(defaultStashedFilesWidth)
|
||||||
|
private pullRequestFileListWidth = constrain(defaultPullRequestFileListWidth)
|
||||||
|
|
||||||
private windowState: WindowState | null = null
|
private windowState: WindowState | null = null
|
||||||
private windowZoomFactor: number = 1
|
private windowZoomFactor: number = 1
|
||||||
|
@ -437,6 +446,7 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
||||||
private confirmDiscardChanges: boolean = confirmDiscardChangesDefault
|
private confirmDiscardChanges: boolean = confirmDiscardChangesDefault
|
||||||
private confirmDiscardChangesPermanently: boolean =
|
private confirmDiscardChangesPermanently: boolean =
|
||||||
confirmDiscardChangesPermanentlyDefault
|
confirmDiscardChangesPermanentlyDefault
|
||||||
|
private confirmDiscardStash: boolean = confirmDiscardStashDefault
|
||||||
private askForConfirmationOnForcePush = askForConfirmationOnForcePushDefault
|
private askForConfirmationOnForcePush = askForConfirmationOnForcePushDefault
|
||||||
private confirmUndoCommit: boolean = confirmUndoCommitDefault
|
private confirmUndoCommit: boolean = confirmUndoCommitDefault
|
||||||
private imageDiffType: ImageDiffType = imageDiffTypeDefault
|
private imageDiffType: ImageDiffType = imageDiffTypeDefault
|
||||||
|
@ -444,6 +454,8 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
||||||
hideWhitespaceInChangesDiffDefault
|
hideWhitespaceInChangesDiffDefault
|
||||||
private hideWhitespaceInHistoryDiff: boolean =
|
private hideWhitespaceInHistoryDiff: boolean =
|
||||||
hideWhitespaceInHistoryDiffDefault
|
hideWhitespaceInHistoryDiffDefault
|
||||||
|
private hideWhitespaceInPullRequestDiff: boolean =
|
||||||
|
hideWhitespaceInPullRequestDiffDefault
|
||||||
/** Whether or not the spellchecker is enabled for commit summary and description */
|
/** Whether or not the spellchecker is enabled for commit summary and description */
|
||||||
private commitSpellcheckEnabled: boolean = commitSpellcheckEnabledDefault
|
private commitSpellcheckEnabled: boolean = commitSpellcheckEnabledDefault
|
||||||
private showSideBySideDiff: boolean = ShowSideBySideDiffDefault
|
private showSideBySideDiff: boolean = ShowSideBySideDiffDefault
|
||||||
|
@ -901,6 +913,7 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
||||||
sidebarWidth: this.sidebarWidth,
|
sidebarWidth: this.sidebarWidth,
|
||||||
commitSummaryWidth: this.commitSummaryWidth,
|
commitSummaryWidth: this.commitSummaryWidth,
|
||||||
stashedFilesWidth: this.stashedFilesWidth,
|
stashedFilesWidth: this.stashedFilesWidth,
|
||||||
|
pullRequestFilesListWidth: this.pullRequestFileListWidth,
|
||||||
appMenuState: this.appMenu ? this.appMenu.openMenus : [],
|
appMenuState: this.appMenu ? this.appMenu.openMenus : [],
|
||||||
highlightAccessKeys: this.highlightAccessKeys,
|
highlightAccessKeys: this.highlightAccessKeys,
|
||||||
isUpdateAvailableBannerVisible: this.isUpdateAvailableBannerVisible,
|
isUpdateAvailableBannerVisible: this.isUpdateAvailableBannerVisible,
|
||||||
|
@ -913,6 +926,7 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
||||||
askForConfirmationOnDiscardChanges: this.confirmDiscardChanges,
|
askForConfirmationOnDiscardChanges: this.confirmDiscardChanges,
|
||||||
askForConfirmationOnDiscardChangesPermanently:
|
askForConfirmationOnDiscardChangesPermanently:
|
||||||
this.confirmDiscardChangesPermanently,
|
this.confirmDiscardChangesPermanently,
|
||||||
|
askForConfirmationOnDiscardStash: this.confirmDiscardStash,
|
||||||
askForConfirmationOnForcePush: this.askForConfirmationOnForcePush,
|
askForConfirmationOnForcePush: this.askForConfirmationOnForcePush,
|
||||||
askForConfirmationOnUndoCommit: this.confirmUndoCommit,
|
askForConfirmationOnUndoCommit: this.confirmUndoCommit,
|
||||||
uncommittedChangesStrategy: this.uncommittedChangesStrategy,
|
uncommittedChangesStrategy: this.uncommittedChangesStrategy,
|
||||||
|
@ -920,6 +934,7 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
||||||
imageDiffType: this.imageDiffType,
|
imageDiffType: this.imageDiffType,
|
||||||
hideWhitespaceInChangesDiff: this.hideWhitespaceInChangesDiff,
|
hideWhitespaceInChangesDiff: this.hideWhitespaceInChangesDiff,
|
||||||
hideWhitespaceInHistoryDiff: this.hideWhitespaceInHistoryDiff,
|
hideWhitespaceInHistoryDiff: this.hideWhitespaceInHistoryDiff,
|
||||||
|
hideWhitespaceInPullRequestDiff: this.hideWhitespaceInPullRequestDiff,
|
||||||
showSideBySideDiff: this.showSideBySideDiff,
|
showSideBySideDiff: this.showSideBySideDiff,
|
||||||
selectedShell: this.selectedShell,
|
selectedShell: this.selectedShell,
|
||||||
repositoryFilterText: this.repositoryFilterText,
|
repositoryFilterText: this.repositoryFilterText,
|
||||||
|
@ -1426,17 +1441,11 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tip.kind === TipState.Valid && aheadBehind.behind > 0) {
|
if (tip.kind === TipState.Valid && aheadBehind.behind > 0) {
|
||||||
const mergeTreePromise = promiseWithMinimumTimeout(
|
this.currentMergeTreePromise = this.setupMergabilityPromise(
|
||||||
() => determineMergeability(repository, tip.branch, action.branch),
|
repository,
|
||||||
500
|
tip.branch,
|
||||||
|
action.branch
|
||||||
)
|
)
|
||||||
.catch(err => {
|
|
||||||
log.warn(
|
|
||||||
`Error occurred while trying to merge ${tip.branch.name} (${tip.branch.tip.sha}) and ${action.branch.name} (${action.branch.tip.sha})`,
|
|
||||||
err
|
|
||||||
)
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
.then(mergeStatus => {
|
.then(mergeStatus => {
|
||||||
this.repositoryStateCache.updateCompareState(repository, () => ({
|
this.repositoryStateCache.updateCompareState(repository, () => ({
|
||||||
mergeStatus,
|
mergeStatus,
|
||||||
|
@ -1444,16 +1453,9 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
||||||
|
|
||||||
this.emitUpdate()
|
this.emitUpdate()
|
||||||
})
|
})
|
||||||
|
.finally(() => {
|
||||||
const cleanup = () => {
|
this.currentMergeTreePromise = null
|
||||||
this.currentMergeTreePromise = null
|
})
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: when we have Promise.prototype.finally available we
|
|
||||||
// should use that here to make this intent clearer
|
|
||||||
mergeTreePromise.then(cleanup, cleanup)
|
|
||||||
|
|
||||||
this.currentMergeTreePromise = mergeTreePromise
|
|
||||||
|
|
||||||
return this.currentMergeTreePromise
|
return this.currentMergeTreePromise
|
||||||
} else {
|
} else {
|
||||||
|
@ -1465,6 +1467,23 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private setupMergabilityPromise(
|
||||||
|
repository: Repository,
|
||||||
|
baseBranch: Branch,
|
||||||
|
compareBranch: Branch
|
||||||
|
) {
|
||||||
|
return promiseWithMinimumTimeout(
|
||||||
|
() => determineMergeability(repository, baseBranch, compareBranch),
|
||||||
|
500
|
||||||
|
).catch(err => {
|
||||||
|
log.warn(
|
||||||
|
`Error occurred while trying to merge ${baseBranch.name} (${baseBranch.tip.sha}) and ${compareBranch.name} (${compareBranch.tip.sha})`,
|
||||||
|
err
|
||||||
|
)
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/** This shouldn't be called directly. See `Dispatcher`. */
|
/** This shouldn't be called directly. See `Dispatcher`. */
|
||||||
public _updateCompareForm<K extends keyof ICompareFormUpdate>(
|
public _updateCompareForm<K extends keyof ICompareFormUpdate>(
|
||||||
repository: Repository,
|
repository: Repository,
|
||||||
|
@ -1951,8 +1970,13 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
||||||
this.stashedFilesWidth = constrain(
|
this.stashedFilesWidth = constrain(
|
||||||
getNumber(stashedFilesWidthConfigKey, defaultStashedFilesWidth)
|
getNumber(stashedFilesWidthConfigKey, defaultStashedFilesWidth)
|
||||||
)
|
)
|
||||||
|
this.pullRequestFileListWidth = constrain(
|
||||||
|
getNumber(pullRequestFileListConfigKey, defaultPullRequestFileListWidth)
|
||||||
|
)
|
||||||
|
|
||||||
this.updateResizableConstraints()
|
this.updateResizableConstraints()
|
||||||
|
// TODO: Initiliaze here for now... maybe move to dialog mounting
|
||||||
|
this.updatePullRequestResizableConstraints()
|
||||||
|
|
||||||
this.askToMoveToApplicationsFolderSetting = getBoolean(
|
this.askToMoveToApplicationsFolderSetting = getBoolean(
|
||||||
askToMoveToApplicationsFolderKey,
|
askToMoveToApplicationsFolderKey,
|
||||||
|
@ -1974,6 +1998,11 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
||||||
confirmDiscardChangesPermanentlyDefault
|
confirmDiscardChangesPermanentlyDefault
|
||||||
)
|
)
|
||||||
|
|
||||||
|
this.confirmDiscardStash = getBoolean(
|
||||||
|
confirmDiscardStashKey,
|
||||||
|
confirmDiscardStashDefault
|
||||||
|
)
|
||||||
|
|
||||||
this.askForConfirmationOnForcePush = getBoolean(
|
this.askForConfirmationOnForcePush = getBoolean(
|
||||||
confirmForcePushKey,
|
confirmForcePushKey,
|
||||||
askForConfirmationOnForcePushDefault
|
askForConfirmationOnForcePushDefault
|
||||||
|
@ -2011,6 +2040,10 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
||||||
hideWhitespaceInHistoryDiffKey,
|
hideWhitespaceInHistoryDiffKey,
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
|
this.hideWhitespaceInPullRequestDiff = getBoolean(
|
||||||
|
hideWhitespaceInPullRequestDiffKey,
|
||||||
|
false
|
||||||
|
)
|
||||||
this.commitSpellcheckEnabled = getBoolean(
|
this.commitSpellcheckEnabled = getBoolean(
|
||||||
commitSpellcheckEnabledKey,
|
commitSpellcheckEnabledKey,
|
||||||
commitSpellcheckEnabledDefault
|
commitSpellcheckEnabledDefault
|
||||||
|
@ -2077,6 +2110,41 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
||||||
this.stashedFilesWidth = constrain(this.stashedFilesWidth, 100, filesMax)
|
this.stashedFilesWidth = constrain(this.stashedFilesWidth, 100, filesMax)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the constraints of the resizable pane in the pull request dialog
|
||||||
|
* whenever the window dimensions change.
|
||||||
|
*/
|
||||||
|
private updatePullRequestResizableConstraints() {
|
||||||
|
// TODO: Get width of PR dialog -> determine if we will have default width
|
||||||
|
// for pr dialog. The goal is for it expand to fill some percent of
|
||||||
|
// available window so it will change on window resize. We may have some max
|
||||||
|
// value and min value of where to derive a default is we cannot obtain the
|
||||||
|
// width for some reason (like initialization nad no pr dialog is open)
|
||||||
|
// Thoughts -> ß
|
||||||
|
// 1. Use dialog id to grab dialog if exists, else use default
|
||||||
|
// 2. Pass dialog width up when and call this contrainst on dialog mounting
|
||||||
|
// to initialize and subscribe to window resize inside dialog to be able
|
||||||
|
// to pass up dialog width on window resize.
|
||||||
|
|
||||||
|
// Get the width of the dialog
|
||||||
|
const available = 850
|
||||||
|
const dialogPadding = 20
|
||||||
|
|
||||||
|
// This is a pretty silly width for a diff but it will fit ~9 chars per line
|
||||||
|
// in unified mode after subtracting the width of the unified gutter and ~4
|
||||||
|
// chars per side in split diff mode. No one would want to use it this way
|
||||||
|
// but it doesn't break the layout and it allows users to temporarily
|
||||||
|
// maximize the width of the file list to see long path names.
|
||||||
|
const diffPaneMinWidth = 150
|
||||||
|
const filesListMax = available - dialogPadding - diffPaneMinWidth
|
||||||
|
|
||||||
|
this.pullRequestFileListWidth = constrain(
|
||||||
|
this.pullRequestFileListWidth,
|
||||||
|
100,
|
||||||
|
filesListMax
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private updateSelectedExternalEditor(
|
private updateSelectedExternalEditor(
|
||||||
selectedEditor: string | null
|
selectedEditor: string | null
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
@ -5193,6 +5261,15 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public _setConfirmDiscardStashSetting(value: boolean): Promise<void> {
|
||||||
|
this.confirmDiscardStash = value
|
||||||
|
|
||||||
|
setBoolean(confirmDiscardStashKey, value)
|
||||||
|
this.emitUpdate()
|
||||||
|
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
public _setConfirmForcePushSetting(value: boolean): Promise<void> {
|
public _setConfirmForcePushSetting(value: boolean): Promise<void> {
|
||||||
this.askForConfirmationOnForcePush = value
|
this.askForConfirmationOnForcePush = value
|
||||||
setBoolean(confirmForcePushKey, value)
|
setBoolean(confirmForcePushKey, value)
|
||||||
|
@ -5279,6 +5356,19 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public _setHideWhitespaceInPullRequestDiff(
|
||||||
|
hideWhitespaceInDiff: boolean,
|
||||||
|
repository: Repository,
|
||||||
|
file: CommittedFileChange | null
|
||||||
|
) {
|
||||||
|
setBoolean(hideWhitespaceInPullRequestDiffKey, hideWhitespaceInDiff)
|
||||||
|
this.hideWhitespaceInPullRequestDiff = hideWhitespaceInDiff
|
||||||
|
|
||||||
|
if (file !== null) {
|
||||||
|
this._changePullRequestFileSelection(repository, file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public _setShowSideBySideDiff(showSideBySideDiff: boolean) {
|
public _setShowSideBySideDiff(showSideBySideDiff: boolean) {
|
||||||
if (showSideBySideDiff !== this.showSideBySideDiff) {
|
if (showSideBySideDiff !== this.showSideBySideDiff) {
|
||||||
setShowSideBySideDiff(showSideBySideDiff)
|
setShowSideBySideDiff(showSideBySideDiff)
|
||||||
|
@ -7146,27 +7236,36 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
||||||
if (defaultBranch === null || tip.kind !== TipState.Valid) {
|
if (defaultBranch === null || tip.kind !== TipState.Valid) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentBranch = tip.branch
|
const currentBranch = tip.branch
|
||||||
|
this._initializePullRequestPreview(repository, defaultBranch, currentBranch)
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _initializePullRequestPreview(
|
||||||
|
repository: Repository,
|
||||||
|
baseBranch: Branch,
|
||||||
|
currentBranch: Branch
|
||||||
|
) {
|
||||||
|
const { branchesState, localCommitSHAs } =
|
||||||
|
this.repositoryStateCache.get(repository)
|
||||||
const gitStore = this.gitStoreCache.get(repository)
|
const gitStore = this.gitStoreCache.get(repository)
|
||||||
|
|
||||||
const pullRequestCommits = await gitStore.getCommitsBetweenBranches(
|
const pullRequestCommits = await gitStore.getCommitsBetweenBranches(
|
||||||
defaultBranch,
|
baseBranch,
|
||||||
currentBranch
|
currentBranch
|
||||||
)
|
)
|
||||||
|
|
||||||
const commitSHAs = pullRequestCommits.map(c => c.sha)
|
const commitsBetweenBranches = pullRequestCommits.map(c => c.sha)
|
||||||
|
|
||||||
// A user may compare two branches with no changes between them.
|
// A user may compare two branches with no changes between them.
|
||||||
const emptyChangeSet = { files: [], linesAdded: 0, linesDeleted: 0 }
|
const emptyChangeSet = { files: [], linesAdded: 0, linesDeleted: 0 }
|
||||||
const changesetData =
|
const changesetData =
|
||||||
commitSHAs.length > 0
|
commitsBetweenBranches.length > 0
|
||||||
? await gitStore.performFailableOperation(() =>
|
? await gitStore.performFailableOperation(() =>
|
||||||
getBranchMergeBaseChangedFiles(
|
getBranchMergeBaseChangedFiles(
|
||||||
repository,
|
repository,
|
||||||
defaultBranch.name,
|
baseBranch.name,
|
||||||
currentBranch.name,
|
currentBranch.name,
|
||||||
commitSHAs[0]
|
commitsBetweenBranches[0]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
: emptyChangeSet
|
: emptyChangeSet
|
||||||
|
@ -7175,25 +7274,64 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasMergeBase = changesetData !== null
|
||||||
|
// We don't care how many commits exist on the unrelated history that
|
||||||
|
// can't be merged.
|
||||||
|
const commitSHAs = hasMergeBase ? commitsBetweenBranches : []
|
||||||
|
|
||||||
this.repositoryStateCache.initializePullRequestState(repository, {
|
this.repositoryStateCache.initializePullRequestState(repository, {
|
||||||
baseBranch: defaultBranch,
|
baseBranch,
|
||||||
commitSHAs,
|
commitSHAs,
|
||||||
commitSelection: {
|
commitSelection: {
|
||||||
shas: commitSHAs,
|
shas: commitSHAs,
|
||||||
shasInDiff: commitSHAs,
|
shasInDiff: commitSHAs,
|
||||||
isContiguous: true,
|
isContiguous: true,
|
||||||
changesetData,
|
changesetData: changesetData ?? emptyChangeSet,
|
||||||
file: null,
|
file: null,
|
||||||
diff: null,
|
diff: null,
|
||||||
},
|
},
|
||||||
|
mergeStatus:
|
||||||
|
commitSHAs.length > 0 || !hasMergeBase
|
||||||
|
? {
|
||||||
|
kind: hasMergeBase
|
||||||
|
? ComputedAction.Loading
|
||||||
|
: ComputedAction.Invalid,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (changesetData.files.length > 0) {
|
this.emitUpdate()
|
||||||
|
|
||||||
|
if (commitSHAs.length > 0) {
|
||||||
|
this.setupPRMergeTreePromise(repository, baseBranch, currentBranch)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changesetData !== null && changesetData.files.length > 0) {
|
||||||
await this._changePullRequestFileSelection(
|
await this._changePullRequestFileSelection(
|
||||||
repository,
|
repository,
|
||||||
changesetData.files[0]
|
changesetData.files[0]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { allBranches, recentBranches, defaultBranch } = branchesState
|
||||||
|
const { imageDiffType, selectedExternalEditor, showSideBySideDiff } =
|
||||||
|
this.getState()
|
||||||
|
|
||||||
|
this._showPopup({
|
||||||
|
type: PopupType.StartPullRequest,
|
||||||
|
allBranches,
|
||||||
|
currentBranch,
|
||||||
|
defaultBranch,
|
||||||
|
imageDiffType,
|
||||||
|
recentBranches,
|
||||||
|
repository,
|
||||||
|
externalEditorLabel: selectedExternalEditor ?? undefined,
|
||||||
|
nonLocalCommitSHA:
|
||||||
|
commitSHAs.length > 0 && !localCommitSHAs.includes(commitSHAs[0])
|
||||||
|
? commitSHAs[0]
|
||||||
|
: null,
|
||||||
|
showSideBySideDiff,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
public async _changePullRequestFileSelection(
|
public async _changePullRequestFileSelection(
|
||||||
|
@ -7223,6 +7361,7 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
||||||
diff: null,
|
diff: null,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
this.emitUpdate()
|
this.emitUpdate()
|
||||||
|
|
||||||
if (commitSHAs.length === 0) {
|
if (commitSHAs.length === 0) {
|
||||||
|
@ -7240,7 +7379,7 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
||||||
file,
|
file,
|
||||||
baseBranch.name,
|
baseBranch.name,
|
||||||
currentBranch.name,
|
currentBranch.name,
|
||||||
this.hideWhitespaceInHistoryDiff,
|
this.hideWhitespaceInPullRequestDiff,
|
||||||
commitSHAs[0]
|
commitSHAs[0]
|
||||||
)
|
)
|
||||||
)) ?? null
|
)) ?? null
|
||||||
|
@ -7263,6 +7402,66 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
||||||
|
|
||||||
this.emitUpdate()
|
this.emitUpdate()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public _setPullRequestFileListWidth(width: number): Promise<void> {
|
||||||
|
this.pullRequestFileListWidth = {
|
||||||
|
...this.pullRequestFileListWidth,
|
||||||
|
value: width,
|
||||||
|
}
|
||||||
|
setNumber(pullRequestFileListConfigKey, width)
|
||||||
|
this.updatePullRequestResizableConstraints()
|
||||||
|
this.emitUpdate()
|
||||||
|
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
public _resetPullRequestFileListWidth(): Promise<void> {
|
||||||
|
this.pullRequestFileListWidth = {
|
||||||
|
...this.pullRequestFileListWidth,
|
||||||
|
value: defaultPullRequestFileListWidth,
|
||||||
|
}
|
||||||
|
localStorage.removeItem(pullRequestFileListConfigKey)
|
||||||
|
this.updatePullRequestResizableConstraints()
|
||||||
|
this.emitUpdate()
|
||||||
|
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
public _updatePullRequestBaseBranch(
|
||||||
|
repository: Repository,
|
||||||
|
baseBranch: Branch
|
||||||
|
) {
|
||||||
|
const { branchesState, pullRequestState } =
|
||||||
|
this.repositoryStateCache.get(repository)
|
||||||
|
const { tip } = branchesState
|
||||||
|
|
||||||
|
if (tip.kind !== TipState.Valid) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pullRequestState === null) {
|
||||||
|
// This would mean the user submitted PR after requesting base branch
|
||||||
|
// update.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this._initializePullRequestPreview(repository, baseBranch, tip.branch)
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupPRMergeTreePromise(
|
||||||
|
repository: Repository,
|
||||||
|
baseBranch: Branch,
|
||||||
|
compareBranch: Branch
|
||||||
|
) {
|
||||||
|
this.setupMergabilityPromise(repository, baseBranch, compareBranch).then(
|
||||||
|
(mergeStatus: MergeTreeResult | null) => {
|
||||||
|
this.repositoryStateCache.updatePullRequestState(repository, () => ({
|
||||||
|
mergeStatus,
|
||||||
|
}))
|
||||||
|
this.emitUpdate()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -640,7 +640,7 @@ function emit(name: MenuEvent): ClickHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The zoom steps that we support, these factors must sorted */
|
/** The zoom steps that we support, these factors must sorted */
|
||||||
const ZoomInFactors = [1, 1.1, 1.25, 1.5, 1.75, 2]
|
const ZoomInFactors = [0.67, 0.75, 0.8, 0.9, 1, 1.1, 1.25, 1.5, 1.75, 2]
|
||||||
const ZoomOutFactors = ZoomInFactors.slice().reverse()
|
const ZoomOutFactors = ZoomInFactors.slice().reverse()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -14,7 +14,7 @@ import { Commit, CommitOneLine, ICommitContext } from './commit'
|
||||||
import { IStashEntry } from './stash-entry'
|
import { IStashEntry } from './stash-entry'
|
||||||
import { Account } from '../models/account'
|
import { Account } from '../models/account'
|
||||||
import { Progress } from './progress'
|
import { Progress } from './progress'
|
||||||
import { ITextDiff, DiffSelection } from './diff'
|
import { ITextDiff, DiffSelection, ImageDiffType } from './diff'
|
||||||
import { RepositorySettingsTab } from '../ui/repository-settings/repository-settings'
|
import { RepositorySettingsTab } from '../ui/repository-settings/repository-settings'
|
||||||
import { ICommitMessage } from './commit-message'
|
import { ICommitMessage } from './commit-message'
|
||||||
import { IAuthor } from './author'
|
import { IAuthor } from './author'
|
||||||
|
@ -362,4 +362,13 @@ export type Popup =
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: PopupType.StartPullRequest
|
type: PopupType.StartPullRequest
|
||||||
|
allBranches: ReadonlyArray<Branch>
|
||||||
|
currentBranch: Branch
|
||||||
|
defaultBranch: Branch | null
|
||||||
|
externalEditorLabel?: string
|
||||||
|
imageDiffType: ImageDiffType
|
||||||
|
recentBranches: ReadonlyArray<Branch>
|
||||||
|
repository: Repository
|
||||||
|
nonLocalCommitSHA: string | null
|
||||||
|
showSideBySideDiff: boolean
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import * as crypto from 'crypto'
|
|
||||||
import { TransitionGroup, CSSTransition } from 'react-transition-group'
|
import { TransitionGroup, CSSTransition } from 'react-transition-group'
|
||||||
import {
|
import {
|
||||||
IAppState,
|
IAppState,
|
||||||
|
@ -158,6 +157,8 @@ import { SSHUserPassword } from './ssh/ssh-user-password'
|
||||||
import { showContextualMenu } from '../lib/menu-item'
|
import { showContextualMenu } from '../lib/menu-item'
|
||||||
import { UnreachableCommitsDialog } from './history/unreachable-commits-dialog'
|
import { UnreachableCommitsDialog } from './history/unreachable-commits-dialog'
|
||||||
import { OpenPullRequestDialog } from './open-pull-request/open-pull-request-dialog'
|
import { OpenPullRequestDialog } from './open-pull-request/open-pull-request-dialog'
|
||||||
|
import { sendNonFatalException } from '../lib/helpers/non-fatal-exception'
|
||||||
|
import { createCommitURL } from '../lib/commit-url'
|
||||||
|
|
||||||
const MinuteInMilliseconds = 1000 * 60
|
const MinuteInMilliseconds = 1000 * 60
|
||||||
const HourInMilliseconds = MinuteInMilliseconds * 60
|
const HourInMilliseconds = MinuteInMilliseconds * 60
|
||||||
|
@ -1481,6 +1482,7 @@ export class App extends React.Component<IAppProps, IAppState> {
|
||||||
confirmDiscardChangesPermanently={
|
confirmDiscardChangesPermanently={
|
||||||
this.state.askForConfirmationOnDiscardChangesPermanently
|
this.state.askForConfirmationOnDiscardChangesPermanently
|
||||||
}
|
}
|
||||||
|
confirmDiscardStash={this.state.askForConfirmationOnDiscardStash}
|
||||||
confirmForcePush={this.state.askForConfirmationOnForcePush}
|
confirmForcePush={this.state.askForConfirmationOnForcePush}
|
||||||
confirmUndoCommit={this.state.askForConfirmationOnUndoCommit}
|
confirmUndoCommit={this.state.askForConfirmationOnUndoCommit}
|
||||||
uncommittedChangesStrategy={this.state.uncommittedChangesStrategy}
|
uncommittedChangesStrategy={this.state.uncommittedChangesStrategy}
|
||||||
|
@ -1849,6 +1851,9 @@ export class App extends React.Component<IAppProps, IAppState> {
|
||||||
<ConfirmDiscardStashDialog
|
<ConfirmDiscardStashDialog
|
||||||
key="confirm-discard-stash-dialog"
|
key="confirm-discard-stash-dialog"
|
||||||
dispatcher={this.props.dispatcher}
|
dispatcher={this.props.dispatcher}
|
||||||
|
askForConfirmationOnDiscardStash={
|
||||||
|
this.state.askForConfirmationOnDiscardStash
|
||||||
|
}
|
||||||
repository={repository}
|
repository={repository}
|
||||||
stash={stash}
|
stash={stash}
|
||||||
onDismissed={onPopupDismissedFn}
|
onDismissed={onPopupDismissedFn}
|
||||||
|
@ -2243,25 +2248,35 @@ export class App extends React.Component<IAppProps, IAppState> {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
case PopupType.StartPullRequest: {
|
case PopupType.StartPullRequest: {
|
||||||
const { selectedState } = this.state
|
// Intentionally chose to get the current pull request state on
|
||||||
if (
|
// rerender because state variables such as file selection change
|
||||||
selectedState == null ||
|
// via the dispatcher.
|
||||||
selectedState.type !== SelectionType.Repository
|
const pullRequestState = this.getPullRequestState()
|
||||||
) {
|
if (pullRequestState === null) {
|
||||||
|
// This shouldn't happen..
|
||||||
|
sendNonFatalException(
|
||||||
|
'FailedToStartPullRequest',
|
||||||
|
new Error(
|
||||||
|
'Failed to start pull request because pull request state was null'
|
||||||
|
)
|
||||||
|
)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const { state: repoState, repository } = selectedState
|
const { pullRequestFilesListWidth, hideWhitespaceInPullRequestDiff } =
|
||||||
const { pullRequestState, branchesState } = repoState
|
this.state
|
||||||
if (
|
|
||||||
pullRequestState === null ||
|
const {
|
||||||
branchesState.tip.kind !== TipState.Valid
|
allBranches,
|
||||||
) {
|
currentBranch,
|
||||||
return null
|
defaultBranch,
|
||||||
}
|
imageDiffType,
|
||||||
const { allBranches, recentBranches, defaultBranch, tip } =
|
externalEditorLabel,
|
||||||
branchesState
|
nonLocalCommitSHA,
|
||||||
const currentBranch = tip.branch
|
recentBranches,
|
||||||
|
repository,
|
||||||
|
showSideBySideDiff,
|
||||||
|
} = popup
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OpenPullRequestDialog
|
<OpenPullRequestDialog
|
||||||
|
@ -2270,9 +2285,15 @@ export class App extends React.Component<IAppProps, IAppState> {
|
||||||
currentBranch={currentBranch}
|
currentBranch={currentBranch}
|
||||||
defaultBranch={defaultBranch}
|
defaultBranch={defaultBranch}
|
||||||
dispatcher={this.props.dispatcher}
|
dispatcher={this.props.dispatcher}
|
||||||
|
fileListWidth={pullRequestFilesListWidth}
|
||||||
|
hideWhitespaceInDiff={hideWhitespaceInPullRequestDiff}
|
||||||
|
imageDiffType={imageDiffType}
|
||||||
|
nonLocalCommitSHA={nonLocalCommitSHA}
|
||||||
pullRequestState={pullRequestState}
|
pullRequestState={pullRequestState}
|
||||||
recentBranches={recentBranches}
|
recentBranches={recentBranches}
|
||||||
repository={repository}
|
repository={repository}
|
||||||
|
externalEditorLabel={externalEditorLabel}
|
||||||
|
showSideBySideDiff={showSideBySideDiff}
|
||||||
onDismissed={onPopupDismissedFn}
|
onDismissed={onPopupDismissedFn}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
@ -2282,6 +2303,18 @@ export class App extends React.Component<IAppProps, IAppState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getPullRequestState() {
|
||||||
|
const { selectedState } = this.state
|
||||||
|
if (
|
||||||
|
selectedState == null ||
|
||||||
|
selectedState.type !== SelectionType.Repository
|
||||||
|
) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return selectedState.state.pullRequestState
|
||||||
|
}
|
||||||
|
|
||||||
private getWarnForcePushDialogOnBegin(
|
private getWarnForcePushDialogOnBegin(
|
||||||
onBegin: () => void,
|
onBegin: () => void,
|
||||||
onPopupDismissedFn: () => void
|
onPopupDismissedFn: () => void
|
||||||
|
@ -2955,6 +2988,9 @@ export class App extends React.Component<IAppProps, IAppState> {
|
||||||
askForConfirmationOnDiscardChanges={
|
askForConfirmationOnDiscardChanges={
|
||||||
state.askForConfirmationOnDiscardChanges
|
state.askForConfirmationOnDiscardChanges
|
||||||
}
|
}
|
||||||
|
askForConfirmationOnDiscardStash={
|
||||||
|
state.askForConfirmationOnDiscardStash
|
||||||
|
}
|
||||||
accounts={state.accounts}
|
accounts={state.accounts}
|
||||||
externalEditorLabel={externalEditorLabel}
|
externalEditorLabel={externalEditorLabel}
|
||||||
resolvedExternalEditor={state.resolvedExternalEditor}
|
resolvedExternalEditor={state.resolvedExternalEditor}
|
||||||
|
@ -3049,22 +3085,17 @@ export class App extends React.Component<IAppProps, IAppState> {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const baseURL = repository.gitHubRepository.htmlURL
|
const commitURL = createCommitURL(
|
||||||
|
repository.gitHubRepository,
|
||||||
|
SHA,
|
||||||
|
filePath
|
||||||
|
)
|
||||||
|
|
||||||
let fileSuffix = ''
|
if (commitURL === null) {
|
||||||
if (filePath != null) {
|
return
|
||||||
const fileHash = crypto
|
|
||||||
.createHash('sha256')
|
|
||||||
.update(filePath)
|
|
||||||
.digest('hex')
|
|
||||||
fileSuffix = '#diff-' + fileHash
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (baseURL) {
|
this.props.dispatcher.openInBrowser(commitURL)
|
||||||
this.props.dispatcher.openInBrowser(
|
|
||||||
`${baseURL}/commit/${SHA}${fileSuffix}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private onBranchDeleted = (repository: Repository) => {
|
private onBranchDeleted = (repository: Repository) => {
|
||||||
|
|
|
@ -1,19 +1,12 @@
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { IMatches } from '../../lib/fuzzy-find'
|
import { IMatches } from '../../lib/fuzzy-find'
|
||||||
import { Branch } from '../../models/branch'
|
import { Branch } from '../../models/branch'
|
||||||
import { Button } from '../lib/button'
|
|
||||||
import { ClickSource } from '../lib/list'
|
import { ClickSource } from '../lib/list'
|
||||||
import { Popover } from '../lib/popover'
|
import { PopoverDropdown } from '../lib/popover-dropdown'
|
||||||
import { Ref } from '../lib/ref'
|
|
||||||
import { Octicon } from '../octicons'
|
|
||||||
import * as OcticonSymbol from '../octicons/octicons.generated'
|
|
||||||
import { BranchList } from './branch-list'
|
import { BranchList } from './branch-list'
|
||||||
import { renderDefaultBranch } from './branch-renderer'
|
import { renderDefaultBranch } from './branch-renderer'
|
||||||
import { IBranchListItem } from './group-branches'
|
import { IBranchListItem } from './group-branches'
|
||||||
|
|
||||||
const defaultDropdownListHeight = 300
|
|
||||||
const maxDropdownListHeight = 500
|
|
||||||
|
|
||||||
interface IBranchSelectProps {
|
interface IBranchSelectProps {
|
||||||
/** The initially selected branch. */
|
/** The initially selected branch. */
|
||||||
readonly branch: Branch
|
readonly branch: Branch
|
||||||
|
@ -43,10 +36,8 @@ interface IBranchSelectProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IBranchSelectState {
|
interface IBranchSelectState {
|
||||||
readonly showBranchDropdown: boolean
|
|
||||||
readonly selectedBranch: Branch | null
|
readonly selectedBranch: Branch | null
|
||||||
readonly filterText: string
|
readonly filterText: string
|
||||||
readonly dropdownListHeight: number
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -56,67 +47,25 @@ export class BranchSelect extends React.Component<
|
||||||
IBranchSelectProps,
|
IBranchSelectProps,
|
||||||
IBranchSelectState
|
IBranchSelectState
|
||||||
> {
|
> {
|
||||||
private invokeButtonRef: HTMLButtonElement | null = null
|
private popoverRef = React.createRef<PopoverDropdown>()
|
||||||
|
|
||||||
public constructor(props: IBranchSelectProps) {
|
public constructor(props: IBranchSelectProps) {
|
||||||
super(props)
|
super(props)
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
showBranchDropdown: false,
|
|
||||||
selectedBranch: props.branch,
|
selectedBranch: props.branch,
|
||||||
filterText: '',
|
filterText: '',
|
||||||
dropdownListHeight: defaultDropdownListHeight,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidMount() {
|
|
||||||
this.calculateDropdownListHeight()
|
|
||||||
}
|
|
||||||
|
|
||||||
public componentDidUpdate() {
|
|
||||||
this.calculateDropdownListHeight()
|
|
||||||
}
|
|
||||||
|
|
||||||
private calculateDropdownListHeight = () => {
|
|
||||||
if (this.invokeButtonRef === null) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const windowHeight = window.innerHeight
|
|
||||||
const bottomOfButton = this.invokeButtonRef.getBoundingClientRect().bottom
|
|
||||||
const listHeaderHeight = 75
|
|
||||||
const calcMaxHeight = Math.round(
|
|
||||||
windowHeight - bottomOfButton - listHeaderHeight
|
|
||||||
)
|
|
||||||
|
|
||||||
const dropdownListHeight =
|
|
||||||
calcMaxHeight > maxDropdownListHeight
|
|
||||||
? maxDropdownListHeight
|
|
||||||
: calcMaxHeight
|
|
||||||
if (dropdownListHeight !== this.state.dropdownListHeight) {
|
|
||||||
this.setState({ dropdownListHeight })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private onInvokeButtonRef = (buttonRef: HTMLButtonElement | null) => {
|
|
||||||
this.invokeButtonRef = buttonRef
|
|
||||||
}
|
|
||||||
|
|
||||||
private toggleBranchDropdown = () => {
|
|
||||||
this.setState({ showBranchDropdown: !this.state.showBranchDropdown })
|
|
||||||
}
|
|
||||||
|
|
||||||
private closeBranchDropdown = () => {
|
|
||||||
this.setState({ showBranchDropdown: false })
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderBranch = (item: IBranchListItem, matches: IMatches) => {
|
private renderBranch = (item: IBranchListItem, matches: IMatches) => {
|
||||||
return renderDefaultBranch(item, matches, this.props.currentBranch)
|
return renderDefaultBranch(item, matches, this.props.currentBranch)
|
||||||
}
|
}
|
||||||
|
|
||||||
private onItemClick = (branch: Branch, source: ClickSource) => {
|
private onItemClick = (branch: Branch, source: ClickSource) => {
|
||||||
source.event.preventDefault()
|
source.event.preventDefault()
|
||||||
this.setState({ showBranchDropdown: false, selectedBranch: branch })
|
this.popoverRef.current?.closePopover()
|
||||||
|
this.setState({ selectedBranch: branch })
|
||||||
this.props.onChange?.(branch)
|
this.props.onChange?.(branch)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -124,67 +73,32 @@ export class BranchSelect extends React.Component<
|
||||||
this.setState({ filterText })
|
this.setState({ filterText })
|
||||||
}
|
}
|
||||||
|
|
||||||
public renderBranchDropdown() {
|
public render() {
|
||||||
if (!this.state.showBranchDropdown) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const { currentBranch, defaultBranch, recentBranches, allBranches } =
|
const { currentBranch, defaultBranch, recentBranches, allBranches } =
|
||||||
this.props
|
this.props
|
||||||
|
|
||||||
const { filterText, selectedBranch, dropdownListHeight } = this.state
|
const { filterText, selectedBranch } = this.state
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover
|
<PopoverDropdown
|
||||||
className="branch-select-dropdown"
|
contentTitle="Choose a base branch"
|
||||||
onClickOutside={this.closeBranchDropdown}
|
buttonContent={selectedBranch?.name ?? ''}
|
||||||
|
label="base:"
|
||||||
|
ref={this.popoverRef}
|
||||||
>
|
>
|
||||||
<div className="branch-select-dropdown-header">
|
<BranchList
|
||||||
Choose a base branch
|
allBranches={allBranches}
|
||||||
<button
|
currentBranch={currentBranch}
|
||||||
className="close"
|
defaultBranch={defaultBranch}
|
||||||
onClick={this.closeBranchDropdown}
|
recentBranches={recentBranches}
|
||||||
aria-label="close"
|
filterText={filterText}
|
||||||
>
|
onFilterTextChanged={this.onFilterTextChanged}
|
||||||
<Octicon symbol={OcticonSymbol.x} />
|
selectedBranch={selectedBranch}
|
||||||
</button>
|
canCreateNewBranch={false}
|
||||||
</div>
|
renderBranch={this.renderBranch}
|
||||||
<div
|
onItemClick={this.onItemClick}
|
||||||
className="branch-select-dropdown-list"
|
/>
|
||||||
style={{ height: `${dropdownListHeight}px` }}
|
</PopoverDropdown>
|
||||||
>
|
|
||||||
<BranchList
|
|
||||||
allBranches={allBranches}
|
|
||||||
currentBranch={currentBranch}
|
|
||||||
defaultBranch={defaultBranch}
|
|
||||||
recentBranches={recentBranches}
|
|
||||||
filterText={filterText}
|
|
||||||
onFilterTextChanged={this.onFilterTextChanged}
|
|
||||||
selectedBranch={selectedBranch}
|
|
||||||
canCreateNewBranch={false}
|
|
||||||
renderBranch={this.renderBranch}
|
|
||||||
onItemClick={this.onItemClick}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Popover>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
public render() {
|
|
||||||
return (
|
|
||||||
<div className="branch-select-component">
|
|
||||||
<Button
|
|
||||||
onClick={this.toggleBranchDropdown}
|
|
||||||
onButtonRef={this.onInvokeButtonRef}
|
|
||||||
>
|
|
||||||
<Ref>
|
|
||||||
<span className="base-label">base:</span>
|
|
||||||
{this.state.selectedBranch?.name}
|
|
||||||
<Octicon symbol={OcticonSymbol.triangleDown} />
|
|
||||||
</Ref>
|
|
||||||
</Button>
|
|
||||||
{this.renderBranchDropdown()}
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,6 @@ import { Octicon, iconForStatus } from '../octicons'
|
||||||
import * as OcticonSymbol from '../octicons/octicons.generated'
|
import * as OcticonSymbol from '../octicons/octicons.generated'
|
||||||
import { mapStatus } from '../../lib/status'
|
import { mapStatus } from '../../lib/status'
|
||||||
import { DiffOptions } from '../diff/diff-options'
|
import { DiffOptions } from '../diff/diff-options'
|
||||||
import { RepositorySectionTab } from '../../lib/app-state'
|
|
||||||
|
|
||||||
interface IChangedFileDetailsProps {
|
interface IChangedFileDetailsProps {
|
||||||
readonly path: string
|
readonly path: string
|
||||||
|
@ -61,7 +60,7 @@ export class ChangedFileDetails extends React.Component<
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DiffOptions
|
<DiffOptions
|
||||||
sourceTab={RepositorySectionTab.Changes}
|
isInteractiveDiff={true}
|
||||||
onHideWhitespaceChangesChanged={
|
onHideWhitespaceChangesChanged={
|
||||||
this.props.onHideWhitespaceInDiffChanged
|
this.props.onHideWhitespaceInDiffChanged
|
||||||
}
|
}
|
||||||
|
|
|
@ -351,7 +351,7 @@ export class CommitMessage extends React.Component<
|
||||||
if (
|
if (
|
||||||
isShortcutKey &&
|
isShortcutKey &&
|
||||||
event.key === 'Enter' &&
|
event.key === 'Enter' &&
|
||||||
this.canCommit() &&
|
(this.canCommit() || this.canAmend()) &&
|
||||||
this.canExcecuteCommitShortcut()
|
this.canExcecuteCommitShortcut()
|
||||||
) {
|
) {
|
||||||
this.createCommit()
|
this.createCommit()
|
||||||
|
|
|
@ -3,6 +3,7 @@ import * as React from 'react'
|
||||||
import { Dispatcher } from '../dispatcher'
|
import { Dispatcher } from '../dispatcher'
|
||||||
import { getDefaultDir, setDefaultDir } from '../lib/default-dir'
|
import { getDefaultDir, setDefaultDir } from '../lib/default-dir'
|
||||||
import { Account } from '../../models/account'
|
import { Account } from '../../models/account'
|
||||||
|
import { FoldoutType } from '../../lib/app-state'
|
||||||
import {
|
import {
|
||||||
IRepositoryIdentifier,
|
IRepositoryIdentifier,
|
||||||
parseRepositoryIdentifier,
|
parseRepositoryIdentifier,
|
||||||
|
@ -728,6 +729,7 @@ export class CloneRepository extends React.Component<
|
||||||
|
|
||||||
const { url, defaultBranch } = cloneInfo
|
const { url, defaultBranch } = cloneInfo
|
||||||
|
|
||||||
|
this.props.dispatcher.closeFoldout(FoldoutType.Repository)
|
||||||
try {
|
try {
|
||||||
this.cloneImpl(url.trim(), path, defaultBranch)
|
this.cloneImpl(url.trim(), path, defaultBranch)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
@ -4,14 +4,13 @@ import { Octicon } from '../octicons'
|
||||||
import * as OcticonSymbol from '../octicons/octicons.generated'
|
import * as OcticonSymbol from '../octicons/octicons.generated'
|
||||||
import { RadioButton } from '../lib/radio-button'
|
import { RadioButton } from '../lib/radio-button'
|
||||||
import { Popover, PopoverCaretPosition } from '../lib/popover'
|
import { Popover, PopoverCaretPosition } from '../lib/popover'
|
||||||
import { RepositorySectionTab } from '../../lib/app-state'
|
|
||||||
|
|
||||||
interface IDiffOptionsProps {
|
interface IDiffOptionsProps {
|
||||||
readonly sourceTab: RepositorySectionTab
|
readonly isInteractiveDiff: boolean
|
||||||
readonly hideWhitespaceChanges: boolean
|
readonly hideWhitespaceChanges: boolean
|
||||||
readonly onHideWhitespaceChangesChanged: (
|
readonly onHideWhitespaceChangesChanged: (
|
||||||
hideWhitespaceChanges: boolean
|
hideWhitespaceChanges: boolean
|
||||||
) => Promise<void>
|
) => void
|
||||||
|
|
||||||
readonly showSideBySideDiff: boolean
|
readonly showSideBySideDiff: boolean
|
||||||
readonly onShowSideBySideDiffChanged: (showSideBySideDiff: boolean) => void
|
readonly onShowSideBySideDiffChanged: (showSideBySideDiff: boolean) => void
|
||||||
|
@ -144,7 +143,7 @@ export class DiffOptions extends React.Component<
|
||||||
__DARWIN__ ? 'Hide Whitespace Changes' : 'Hide whitespace changes'
|
__DARWIN__ ? 'Hide Whitespace Changes' : 'Hide whitespace changes'
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
{this.props.sourceTab === RepositorySectionTab.Changes && (
|
{this.props.isInteractiveDiff && (
|
||||||
<p className="secondary-text">
|
<p className="secondary-text">
|
||||||
Interacting with individual lines or hunks will be disabled while
|
Interacting with individual lines or hunks will be disabled while
|
||||||
hiding whitespace.
|
hiding whitespace.
|
||||||
|
|
|
@ -2122,6 +2122,19 @@ export class Dispatcher {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Change the hide whitespace in pull request diff setting */
|
||||||
|
public onHideWhitespaceInPullRequestDiffChanged(
|
||||||
|
hideWhitespaceInDiff: boolean,
|
||||||
|
repository: Repository,
|
||||||
|
file: CommittedFileChange | null = null
|
||||||
|
) {
|
||||||
|
this.appStore._setHideWhitespaceInPullRequestDiff(
|
||||||
|
hideWhitespaceInDiff,
|
||||||
|
repository,
|
||||||
|
file
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/** Change the side by side diff setting */
|
/** Change the side by side diff setting */
|
||||||
public onShowSideBySideDiffChanged(showSideBySideDiff: boolean) {
|
public onShowSideBySideDiffChanged(showSideBySideDiff: boolean) {
|
||||||
return this.appStore._setShowSideBySideDiff(showSideBySideDiff)
|
return this.appStore._setShowSideBySideDiff(showSideBySideDiff)
|
||||||
|
@ -2338,6 +2351,10 @@ export class Dispatcher {
|
||||||
await this.appStore._loadStatus(repository)
|
await this.appStore._loadStatus(repository)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public setConfirmDiscardStashSetting(value: boolean) {
|
||||||
|
return this.appStore._setConfirmDiscardStashSetting(value)
|
||||||
|
}
|
||||||
|
|
||||||
public setConfirmForcePushSetting(value: boolean) {
|
public setConfirmForcePushSetting(value: boolean) {
|
||||||
return this.appStore._setConfirmForcePushSetting(value)
|
return this.appStore._setConfirmForcePushSetting(value)
|
||||||
}
|
}
|
||||||
|
@ -3963,9 +3980,33 @@ export class Dispatcher {
|
||||||
|
|
||||||
public startPullRequest(repository: Repository) {
|
public startPullRequest(repository: Repository) {
|
||||||
this.appStore._startPullRequest(repository)
|
this.appStore._startPullRequest(repository)
|
||||||
|
}
|
||||||
|
|
||||||
this.showPopup({
|
/**
|
||||||
type: PopupType.StartPullRequest,
|
* Change the selected changed file of the current pull request state.
|
||||||
})
|
*/
|
||||||
|
public changePullRequestFileSelection(
|
||||||
|
repository: Repository,
|
||||||
|
file: CommittedFileChange
|
||||||
|
): Promise<void> {
|
||||||
|
return this.appStore._changePullRequestFileSelection(repository, file)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the width of the file list column in the pull request files changed
|
||||||
|
*/
|
||||||
|
public setPullRequestFileListWidth(width: number): Promise<void> {
|
||||||
|
return this.appStore._setPullRequestFileListWidth(width)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset the width of the file list column in the pull request files changed
|
||||||
|
*/
|
||||||
|
public resetPullRequestFileListWidth(): Promise<void> {
|
||||||
|
return this.appStore._resetPullRequestFileListWidth()
|
||||||
|
}
|
||||||
|
|
||||||
|
public updatePullRequestBaseBranch(repository: Repository, branch: Branch) {
|
||||||
|
this.appStore._updatePullRequestBaseBranch(repository, branch)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,6 @@ import { CommitAttribution } from '../lib/commit-attribution'
|
||||||
import { Tokenizer, TokenResult } from '../../lib/text-token-parser'
|
import { Tokenizer, TokenResult } from '../../lib/text-token-parser'
|
||||||
import { wrapRichTextCommitMessage } from '../../lib/wrap-rich-text-commit-message'
|
import { wrapRichTextCommitMessage } from '../../lib/wrap-rich-text-commit-message'
|
||||||
import { DiffOptions } from '../diff/diff-options'
|
import { DiffOptions } from '../diff/diff-options'
|
||||||
import { RepositorySectionTab } from '../../lib/app-state'
|
|
||||||
import { IChangesetData } from '../../lib/git'
|
import { IChangesetData } from '../../lib/git'
|
||||||
import { TooltippedContent } from '../lib/tooltipped-content'
|
import { TooltippedContent } from '../lib/tooltipped-content'
|
||||||
import { AppFileStatusKind } from '../../models/status'
|
import { AppFileStatusKind } from '../../models/status'
|
||||||
|
@ -505,7 +504,7 @@ export class CommitSummary extends React.Component<
|
||||||
title="Diff Options"
|
title="Diff Options"
|
||||||
>
|
>
|
||||||
<DiffOptions
|
<DiffOptions
|
||||||
sourceTab={RepositorySectionTab.History}
|
isInteractiveDiff={false}
|
||||||
hideWhitespaceChanges={this.props.hideWhitespaceInDiff}
|
hideWhitespaceChanges={this.props.hideWhitespaceInDiff}
|
||||||
onHideWhitespaceChangesChanged={
|
onHideWhitespaceChangesChanged={
|
||||||
this.props.onHideWhitespaceInDiffChanged
|
this.props.onHideWhitespaceInDiffChanged
|
||||||
|
|
133
app/src/ui/lib/popover-dropdown.tsx
Normal file
133
app/src/ui/lib/popover-dropdown.tsx
Normal file
|
@ -0,0 +1,133 @@
|
||||||
|
import * as React from 'react'
|
||||||
|
import { Button } from './button'
|
||||||
|
import { Popover, PopoverCaretPosition } from './popover'
|
||||||
|
import { Octicon } from '../octicons'
|
||||||
|
import * as OcticonSymbol from '../octicons/octicons.generated'
|
||||||
|
import classNames from 'classnames'
|
||||||
|
|
||||||
|
const defaultPopoverContentHeight = 300
|
||||||
|
const maxPopoverContentHeight = 500
|
||||||
|
|
||||||
|
interface IPopoverDropdownProps {
|
||||||
|
readonly className?: string
|
||||||
|
readonly contentTitle: string
|
||||||
|
readonly buttonContent: JSX.Element | string
|
||||||
|
readonly label: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IPopoverDropdownState {
|
||||||
|
readonly showPopover: boolean
|
||||||
|
readonly popoverContentHeight: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A dropdown component for displaying a dropdown button that opens
|
||||||
|
* a popover to display contents relative to the button content.
|
||||||
|
*/
|
||||||
|
export class PopoverDropdown extends React.Component<
|
||||||
|
IPopoverDropdownProps,
|
||||||
|
IPopoverDropdownState
|
||||||
|
> {
|
||||||
|
private invokeButtonRef: HTMLButtonElement | null = null
|
||||||
|
|
||||||
|
public constructor(props: IPopoverDropdownProps) {
|
||||||
|
super(props)
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
showPopover: false,
|
||||||
|
popoverContentHeight: defaultPopoverContentHeight,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentDidMount() {
|
||||||
|
this.calculateDropdownListHeight()
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentDidUpdate() {
|
||||||
|
this.calculateDropdownListHeight()
|
||||||
|
}
|
||||||
|
|
||||||
|
private calculateDropdownListHeight = () => {
|
||||||
|
if (this.invokeButtonRef === null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const windowHeight = window.innerHeight
|
||||||
|
const bottomOfButton = this.invokeButtonRef.getBoundingClientRect().bottom
|
||||||
|
const listHeaderHeight = 75
|
||||||
|
const calcMaxHeight = Math.round(
|
||||||
|
windowHeight - bottomOfButton - listHeaderHeight
|
||||||
|
)
|
||||||
|
|
||||||
|
const popoverContentHeight =
|
||||||
|
calcMaxHeight > maxPopoverContentHeight
|
||||||
|
? maxPopoverContentHeight
|
||||||
|
: calcMaxHeight
|
||||||
|
if (popoverContentHeight !== this.state.popoverContentHeight) {
|
||||||
|
this.setState({ popoverContentHeight })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private onInvokeButtonRef = (buttonRef: HTMLButtonElement | null) => {
|
||||||
|
this.invokeButtonRef = buttonRef
|
||||||
|
}
|
||||||
|
|
||||||
|
private togglePopover = () => {
|
||||||
|
this.setState({ showPopover: !this.state.showPopover })
|
||||||
|
}
|
||||||
|
|
||||||
|
public closePopover = () => {
|
||||||
|
this.setState({ showPopover: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderPopover() {
|
||||||
|
if (!this.state.showPopover) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const { contentTitle } = this.props
|
||||||
|
const { popoverContentHeight } = this.state
|
||||||
|
const contentStyle = { height: `${popoverContentHeight}px` }
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover
|
||||||
|
className="popover-dropdown-popover"
|
||||||
|
caretPosition={PopoverCaretPosition.TopLeft}
|
||||||
|
onClickOutside={this.closePopover}
|
||||||
|
>
|
||||||
|
<div className="popover-dropdown-header">
|
||||||
|
{contentTitle}
|
||||||
|
<button
|
||||||
|
className="close"
|
||||||
|
onClick={this.closePopover}
|
||||||
|
aria-label="close"
|
||||||
|
>
|
||||||
|
<Octicon symbol={OcticonSymbol.x} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="popover-dropdown-content" style={contentStyle}>
|
||||||
|
{this.props.children}
|
||||||
|
</div>
|
||||||
|
</Popover>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
const { className, buttonContent, label } = this.props
|
||||||
|
const cn = classNames('popover-dropdown-component', className)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn}>
|
||||||
|
<Button
|
||||||
|
onClick={this.togglePopover}
|
||||||
|
onButtonRef={this.onInvokeButtonRef}
|
||||||
|
>
|
||||||
|
<span className="popover-dropdown-button-label">{label}</span>
|
||||||
|
<span className="button-content">{buttonContent}</span>
|
||||||
|
<Octicon symbol={OcticonSymbol.triangleDown} />
|
||||||
|
</Button>
|
||||||
|
{this.renderPopover()}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -299,7 +299,13 @@ export class Tooltip<T extends TooltipTarget> extends React.Component<
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private updateMouseRect = (event: MouseEvent) => {
|
||||||
|
this.mouseRect = new DOMRect(event.clientX - 10, event.clientY - 10, 20, 20)
|
||||||
|
}
|
||||||
|
|
||||||
private onTargetMouseEnter = (event: MouseEvent) => {
|
private onTargetMouseEnter = (event: MouseEvent) => {
|
||||||
|
this.updateMouseRect(event)
|
||||||
|
|
||||||
this.mouseOverTarget = true
|
this.mouseOverTarget = true
|
||||||
this.cancelHideTooltip()
|
this.cancelHideTooltip()
|
||||||
if (!this.state.show) {
|
if (!this.state.show) {
|
||||||
|
@ -308,7 +314,7 @@ export class Tooltip<T extends TooltipTarget> extends React.Component<
|
||||||
}
|
}
|
||||||
|
|
||||||
private onTargetMouseMove = (event: MouseEvent) => {
|
private onTargetMouseMove = (event: MouseEvent) => {
|
||||||
this.mouseRect = new DOMRect(event.clientX - 10, event.clientY - 10, 20, 20)
|
this.updateMouseRect(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
private onTargetMouseDown = (event: MouseEvent) => {
|
private onTargetMouseDown = (event: MouseEvent) => {
|
||||||
|
|
|
@ -93,6 +93,8 @@ export abstract class BaseMultiCommitOperation extends React.Component<IMultiCom
|
||||||
{targetBranch !== null ? <strong>{targetBranch.name}</strong> : null}
|
{targetBranch !== null ? <strong>{targetBranch.name}</strong> : null}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
this.props.dispatcher.closePopup(PopupType.MultiCommitOperation)
|
||||||
return dispatcher.onConflictsFoundBanner(
|
return dispatcher.onConflictsFoundBanner(
|
||||||
repository,
|
repository,
|
||||||
operationDescription,
|
operationDescription,
|
||||||
|
|
|
@ -1,10 +1,18 @@
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { IPullRequestState } from '../../lib/app-state'
|
import { IConstrainedValue, IPullRequestState } from '../../lib/app-state'
|
||||||
|
import { getDotComAPIEndpoint } from '../../lib/api'
|
||||||
import { Branch } from '../../models/branch'
|
import { Branch } from '../../models/branch'
|
||||||
|
import { ImageDiffType } from '../../models/diff'
|
||||||
import { Repository } from '../../models/repository'
|
import { Repository } from '../../models/repository'
|
||||||
import { DialogFooter, OkCancelButtonGroup, Dialog } from '../dialog'
|
import { DialogFooter, OkCancelButtonGroup, Dialog } from '../dialog'
|
||||||
import { Dispatcher } from '../dispatcher'
|
import { Dispatcher } from '../dispatcher'
|
||||||
|
import { Ref } from '../lib/ref'
|
||||||
|
import { Octicon } from '../octicons'
|
||||||
|
import * as OcticonSymbol from '../octicons/octicons.generated'
|
||||||
import { OpenPullRequestDialogHeader } from './open-pull-request-header'
|
import { OpenPullRequestDialogHeader } from './open-pull-request-header'
|
||||||
|
import { PullRequestFilesChanged } from './pull-request-files-changed'
|
||||||
|
import { PullRequestMergeStatus } from './pull-request-merge-status'
|
||||||
|
import { ComputedAction } from '../../models/computed-action'
|
||||||
|
|
||||||
interface IOpenPullRequestDialogProps {
|
interface IOpenPullRequestDialogProps {
|
||||||
readonly repository: Repository
|
readonly repository: Repository
|
||||||
|
@ -35,6 +43,25 @@ interface IOpenPullRequestDialogProps {
|
||||||
*/
|
*/
|
||||||
readonly recentBranches: ReadonlyArray<Branch>
|
readonly recentBranches: ReadonlyArray<Branch>
|
||||||
|
|
||||||
|
/** Whether we should display side by side diffs. */
|
||||||
|
readonly showSideBySideDiff: boolean
|
||||||
|
|
||||||
|
/** Whether we should hide whitespace in diff. */
|
||||||
|
readonly hideWhitespaceInDiff: boolean
|
||||||
|
|
||||||
|
/** The type of image diff to display. */
|
||||||
|
readonly imageDiffType: ImageDiffType
|
||||||
|
|
||||||
|
/** Label for selected external editor */
|
||||||
|
readonly externalEditorLabel?: string
|
||||||
|
|
||||||
|
/** Width to use for the files list pane in the files changed view */
|
||||||
|
readonly fileListWidth: IConstrainedValue
|
||||||
|
|
||||||
|
/** If the latest commit of the pull request is not local, this will contain
|
||||||
|
* it's SHA */
|
||||||
|
readonly nonLocalCommitSHA: string | null
|
||||||
|
|
||||||
/** Called to dismiss the dialog */
|
/** Called to dismiss the dialog */
|
||||||
readonly onDismissed: () => void
|
readonly onDismissed: () => void
|
||||||
}
|
}
|
||||||
|
@ -47,6 +74,11 @@ export class OpenPullRequestDialog extends React.Component<IOpenPullRequestDialo
|
||||||
this.props.dispatcher.recordCreatePullRequest()
|
this.props.dispatcher.recordCreatePullRequest()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private onBranchChange = (branch: Branch) => {
|
||||||
|
const { repository } = this.props
|
||||||
|
this.props.dispatcher.updatePullRequestBaseBranch(repository, branch)
|
||||||
|
}
|
||||||
|
|
||||||
private renderHeader() {
|
private renderHeader() {
|
||||||
const {
|
const {
|
||||||
currentBranch,
|
currentBranch,
|
||||||
|
@ -64,22 +96,107 @@ export class OpenPullRequestDialog extends React.Component<IOpenPullRequestDialo
|
||||||
allBranches={allBranches}
|
allBranches={allBranches}
|
||||||
recentBranches={recentBranches}
|
recentBranches={recentBranches}
|
||||||
commitCount={commitSHAs?.length ?? 0}
|
commitCount={commitSHAs?.length ?? 0}
|
||||||
|
onBranchChange={this.onBranchChange}
|
||||||
onDismissed={this.props.onDismissed}
|
onDismissed={this.props.onDismissed}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderContent() {
|
private renderContent() {
|
||||||
return <div>Content</div>
|
return (
|
||||||
|
<div className="open-pull-request-content">
|
||||||
|
{this.renderNoChanges()}
|
||||||
|
{this.renderFilesChanged()}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderFilesChanged() {
|
||||||
|
const {
|
||||||
|
dispatcher,
|
||||||
|
externalEditorLabel,
|
||||||
|
hideWhitespaceInDiff,
|
||||||
|
imageDiffType,
|
||||||
|
pullRequestState,
|
||||||
|
repository,
|
||||||
|
fileListWidth,
|
||||||
|
nonLocalCommitSHA,
|
||||||
|
} = this.props
|
||||||
|
const { commitSelection } = pullRequestState
|
||||||
|
const { diff, file, changesetData, shas } = commitSelection
|
||||||
|
const { files } = changesetData
|
||||||
|
|
||||||
|
if (shas.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PullRequestFilesChanged
|
||||||
|
diff={diff}
|
||||||
|
dispatcher={dispatcher}
|
||||||
|
externalEditorLabel={externalEditorLabel}
|
||||||
|
fileListWidth={fileListWidth}
|
||||||
|
files={files}
|
||||||
|
hideWhitespaceInDiff={hideWhitespaceInDiff}
|
||||||
|
imageDiffType={imageDiffType}
|
||||||
|
nonLocalCommitSHA={nonLocalCommitSHA}
|
||||||
|
selectedFile={file}
|
||||||
|
showSideBySideDiff={this.props.showSideBySideDiff}
|
||||||
|
repository={repository}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderNoChanges() {
|
||||||
|
const { pullRequestState, currentBranch } = this.props
|
||||||
|
const { commitSelection, baseBranch, mergeStatus } = pullRequestState
|
||||||
|
const { shas } = commitSelection
|
||||||
|
|
||||||
|
if (shas.length !== 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const hasMergeBase = mergeStatus?.kind !== ComputedAction.Invalid
|
||||||
|
const message = hasMergeBase ? (
|
||||||
|
<>
|
||||||
|
<Ref>{baseBranch.name}</Ref> is up to date with all commits from{' '}
|
||||||
|
<Ref>{currentBranch.name}</Ref>.
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Ref>{baseBranch.name}</Ref> and <Ref>{currentBranch.name}</Ref> are
|
||||||
|
entirely different commit histories.
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
<div className="open-pull-request-no-changes">
|
||||||
|
<div>
|
||||||
|
<Octicon symbol={OcticonSymbol.gitPullRequest} />
|
||||||
|
<h3>There are no changes.</h3>
|
||||||
|
{message}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderFooter() {
|
private renderFooter() {
|
||||||
|
const { mergeStatus, commitSHAs } = this.props.pullRequestState
|
||||||
|
const gitHubRepository = this.props.repository.gitHubRepository
|
||||||
|
const isEnterprise =
|
||||||
|
gitHubRepository && gitHubRepository.endpoint !== getDotComAPIEndpoint()
|
||||||
|
const buttonTitle = `Create pull request on GitHub${
|
||||||
|
isEnterprise ? ' Enterprise' : ''
|
||||||
|
}.`
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
|
<PullRequestMergeStatus mergeStatus={mergeStatus} />
|
||||||
<OkCancelButtonGroup
|
<OkCancelButtonGroup
|
||||||
okButtonText="Create Pull Request"
|
okButtonText={
|
||||||
okButtonTitle="Create pull request on GitHub."
|
__DARWIN__ ? 'Create Pull Request' : 'Create pull request'
|
||||||
|
}
|
||||||
|
okButtonTitle={buttonTitle}
|
||||||
cancelButtonText="Cancel"
|
cancelButtonText="Cancel"
|
||||||
|
okButtonDisabled={commitSHAs === null || commitSHAs.length === 0}
|
||||||
/>
|
/>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
)
|
)
|
||||||
|
@ -93,8 +210,7 @@ export class OpenPullRequestDialog extends React.Component<IOpenPullRequestDialo
|
||||||
onDismissed={this.props.onDismissed}
|
onDismissed={this.props.onDismissed}
|
||||||
>
|
>
|
||||||
{this.renderHeader()}
|
{this.renderHeader()}
|
||||||
<div className="content">{this.renderContent()}</div>
|
{this.renderContent()}
|
||||||
|
|
||||||
{this.renderFooter()}
|
{this.renderFooter()}
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
|
|
|
@ -30,6 +30,9 @@ interface IOpenPullRequestDialogHeaderProps {
|
||||||
/** The count of commits of the pull request */
|
/** The count of commits of the pull request */
|
||||||
readonly commitCount: number
|
readonly commitCount: number
|
||||||
|
|
||||||
|
/** When the branch selection changes */
|
||||||
|
readonly onBranchChange: (branch: Branch) => void
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Event triggered when the dialog is dismissed by the user in the
|
* Event triggered when the dialog is dismissed by the user in the
|
||||||
* ways described in the dismissable prop.
|
* ways described in the dismissable prop.
|
||||||
|
@ -54,6 +57,7 @@ export class OpenPullRequestDialogHeader extends React.Component<
|
||||||
allBranches,
|
allBranches,
|
||||||
recentBranches,
|
recentBranches,
|
||||||
commitCount,
|
commitCount,
|
||||||
|
onBranchChange,
|
||||||
onDismissed,
|
onDismissed,
|
||||||
} = this.props
|
} = this.props
|
||||||
const commits = `${commitCount} commit${commitCount > 1 ? 's' : ''}`
|
const commits = `${commitCount} commit${commitCount > 1 ? 's' : ''}`
|
||||||
|
@ -74,6 +78,7 @@ export class OpenPullRequestDialogHeader extends React.Component<
|
||||||
currentBranch={currentBranch}
|
currentBranch={currentBranch}
|
||||||
allBranches={allBranches}
|
allBranches={allBranches}
|
||||||
recentBranches={recentBranches}
|
recentBranches={recentBranches}
|
||||||
|
onChange={onBranchChange}
|
||||||
/>{' '}
|
/>{' '}
|
||||||
from <Ref>{currentBranch.name}</Ref>.
|
from <Ref>{currentBranch.name}</Ref>.
|
||||||
</div>
|
</div>
|
||||||
|
|
307
app/src/ui/open-pull-request/pull-request-files-changed.tsx
Normal file
307
app/src/ui/open-pull-request/pull-request-files-changed.tsx
Normal file
|
@ -0,0 +1,307 @@
|
||||||
|
import * as React from 'react'
|
||||||
|
import * as Path from 'path'
|
||||||
|
import { IDiff, ImageDiffType } from '../../models/diff'
|
||||||
|
import { Repository } from '../../models/repository'
|
||||||
|
import { CommittedFileChange } from '../../models/status'
|
||||||
|
import { SeamlessDiffSwitcher } from '../diff/seamless-diff-switcher'
|
||||||
|
import { Dispatcher } from '../dispatcher'
|
||||||
|
import { openFile } from '../lib/open-file'
|
||||||
|
import { Resizable } from '../resizable'
|
||||||
|
import { FileList } from '../history/file-list'
|
||||||
|
import { IMenuItem, showContextualMenu } from '../../lib/menu-item'
|
||||||
|
import { pathExists } from '../lib/path-exists'
|
||||||
|
import {
|
||||||
|
CopyFilePathLabel,
|
||||||
|
CopyRelativeFilePathLabel,
|
||||||
|
DefaultEditorLabel,
|
||||||
|
isSafeFileExtension,
|
||||||
|
OpenWithDefaultProgramLabel,
|
||||||
|
RevealInFileManagerLabel,
|
||||||
|
} from '../lib/context-menu'
|
||||||
|
import { revealInFileManager } from '../../lib/app-shell'
|
||||||
|
import { clipboard } from 'electron'
|
||||||
|
import { IConstrainedValue } from '../../lib/app-state'
|
||||||
|
import { clamp } from '../../lib/clamp'
|
||||||
|
import { getDotComAPIEndpoint } from '../../lib/api'
|
||||||
|
import { createCommitURL } from '../../lib/commit-url'
|
||||||
|
import { DiffOptions } from '../diff/diff-options'
|
||||||
|
|
||||||
|
interface IPullRequestFilesChangedProps {
|
||||||
|
readonly repository: Repository
|
||||||
|
readonly dispatcher: Dispatcher
|
||||||
|
|
||||||
|
/** The file whose diff should be displayed. */
|
||||||
|
readonly selectedFile: CommittedFileChange | null
|
||||||
|
|
||||||
|
/** The files changed in the pull request. */
|
||||||
|
readonly files: ReadonlyArray<CommittedFileChange>
|
||||||
|
|
||||||
|
/** The diff that should be rendered */
|
||||||
|
readonly diff: IDiff | null
|
||||||
|
|
||||||
|
/** The type of image diff to display. */
|
||||||
|
readonly imageDiffType: ImageDiffType
|
||||||
|
|
||||||
|
/** Whether we should display side by side diffs. */
|
||||||
|
readonly showSideBySideDiff: boolean
|
||||||
|
|
||||||
|
/** Whether we should hide whitespace in diff. */
|
||||||
|
readonly hideWhitespaceInDiff: boolean
|
||||||
|
|
||||||
|
/** Label for selected external editor */
|
||||||
|
readonly externalEditorLabel?: string
|
||||||
|
|
||||||
|
/** Width to use for the files list pane */
|
||||||
|
readonly fileListWidth: IConstrainedValue
|
||||||
|
|
||||||
|
/** If the latest commit of the pull request is not local, this will contain
|
||||||
|
* it's SHA */
|
||||||
|
readonly nonLocalCommitSHA: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IPullRequestFilesChangedState {
|
||||||
|
readonly showSideBySideDiff: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A component for viewing the file changes for a pull request.
|
||||||
|
*/
|
||||||
|
export class PullRequestFilesChanged extends React.Component<
|
||||||
|
IPullRequestFilesChangedProps,
|
||||||
|
IPullRequestFilesChangedState
|
||||||
|
> {
|
||||||
|
public constructor(props: IPullRequestFilesChangedProps) {
|
||||||
|
super(props)
|
||||||
|
|
||||||
|
this.state = { showSideBySideDiff: props.showSideBySideDiff }
|
||||||
|
}
|
||||||
|
|
||||||
|
private onOpenFile = (path: string) => {
|
||||||
|
const fullPath = Path.join(this.props.repository.path, path)
|
||||||
|
this.onOpenBinaryFile(fullPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens a binary file in an the system-assigned application for
|
||||||
|
* said file type.
|
||||||
|
*/
|
||||||
|
private onOpenBinaryFile = (fullPath: string) => {
|
||||||
|
openFile(fullPath, this.props.dispatcher)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Called when the user changes the hide whitespace in diffs setting. */
|
||||||
|
private onHideWhitespaceInDiffChanged = (hideWhitespaceInDiff: boolean) => {
|
||||||
|
const { selectedFile } = this.props
|
||||||
|
return this.props.dispatcher.onHideWhitespaceInPullRequestDiffChanged(
|
||||||
|
hideWhitespaceInDiff,
|
||||||
|
this.props.repository,
|
||||||
|
selectedFile
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private onShowSideBySideDiffChanged = (showSideBySideDiff: boolean) => {
|
||||||
|
this.setState({ showSideBySideDiff })
|
||||||
|
}
|
||||||
|
|
||||||
|
private onDiffOptionsOpened = () => {
|
||||||
|
this.props.dispatcher.recordDiffOptionsViewed()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the user is viewing an image diff and requests
|
||||||
|
* to change the diff presentation mode.
|
||||||
|
*/
|
||||||
|
private onChangeImageDiffType = (imageDiffType: ImageDiffType) => {
|
||||||
|
this.props.dispatcher.changeImageDiffType(imageDiffType)
|
||||||
|
}
|
||||||
|
|
||||||
|
private onFileListResize = (width: number) => {
|
||||||
|
this.props.dispatcher.setPullRequestFileListWidth(width)
|
||||||
|
}
|
||||||
|
|
||||||
|
private onFileListSizeReset = () => {
|
||||||
|
this.props.dispatcher.resetPullRequestFileListWidth()
|
||||||
|
}
|
||||||
|
|
||||||
|
private onViewOnGitHub = (file: CommittedFileChange) => {
|
||||||
|
const { nonLocalCommitSHA, repository, dispatcher } = this.props
|
||||||
|
const { gitHubRepository } = repository
|
||||||
|
|
||||||
|
if (gitHubRepository === null || nonLocalCommitSHA === null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const commitURL = createCommitURL(
|
||||||
|
gitHubRepository,
|
||||||
|
nonLocalCommitSHA,
|
||||||
|
file.path
|
||||||
|
)
|
||||||
|
|
||||||
|
if (commitURL === null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatcher.openInBrowser(commitURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
private onFileContextMenu = async (
|
||||||
|
file: CommittedFileChange,
|
||||||
|
event: React.MouseEvent<HTMLDivElement>
|
||||||
|
) => {
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
const { repository } = this.props
|
||||||
|
|
||||||
|
const fullPath = Path.join(repository.path, file.path)
|
||||||
|
const fileExistsOnDisk = await pathExists(fullPath)
|
||||||
|
if (!fileExistsOnDisk) {
|
||||||
|
showContextualMenu([
|
||||||
|
{
|
||||||
|
label: __DARWIN__
|
||||||
|
? 'File Does Not Exist on Disk'
|
||||||
|
: 'File does not exist on disk',
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const { externalEditorLabel, dispatcher } = this.props
|
||||||
|
|
||||||
|
const extension = Path.extname(file.path)
|
||||||
|
const isSafeExtension = isSafeFileExtension(extension)
|
||||||
|
const openInExternalEditor =
|
||||||
|
externalEditorLabel !== undefined
|
||||||
|
? `Open in ${externalEditorLabel}`
|
||||||
|
: DefaultEditorLabel
|
||||||
|
|
||||||
|
const items: IMenuItem[] = [
|
||||||
|
{
|
||||||
|
label: RevealInFileManagerLabel,
|
||||||
|
action: () => revealInFileManager(repository, file.path),
|
||||||
|
enabled: fileExistsOnDisk,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: openInExternalEditor,
|
||||||
|
action: () => dispatcher.openInExternalEditor(fullPath),
|
||||||
|
enabled: fileExistsOnDisk,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: OpenWithDefaultProgramLabel,
|
||||||
|
action: () => this.onOpenFile(file.path),
|
||||||
|
enabled: isSafeExtension && fileExistsOnDisk,
|
||||||
|
},
|
||||||
|
{ type: 'separator' },
|
||||||
|
{
|
||||||
|
label: CopyFilePathLabel,
|
||||||
|
action: () => clipboard.writeText(fullPath),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: CopyRelativeFilePathLabel,
|
||||||
|
action: () => clipboard.writeText(Path.normalize(file.path)),
|
||||||
|
},
|
||||||
|
{ type: 'separator' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const { nonLocalCommitSHA } = this.props
|
||||||
|
const { gitHubRepository } = repository
|
||||||
|
const isEnterprise =
|
||||||
|
gitHubRepository && gitHubRepository.endpoint !== getDotComAPIEndpoint()
|
||||||
|
|
||||||
|
items.push({
|
||||||
|
label: `View on GitHub${isEnterprise ? ' Enterprise' : ''}`,
|
||||||
|
action: () => this.onViewOnGitHub(file),
|
||||||
|
enabled: nonLocalCommitSHA !== null && gitHubRepository !== null,
|
||||||
|
})
|
||||||
|
|
||||||
|
showContextualMenu(items)
|
||||||
|
}
|
||||||
|
|
||||||
|
private onFileSelected = (file: CommittedFileChange) => {
|
||||||
|
this.props.dispatcher.changePullRequestFileSelection(
|
||||||
|
this.props.repository,
|
||||||
|
file
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderHeader() {
|
||||||
|
const { hideWhitespaceInDiff } = this.props
|
||||||
|
const { showSideBySideDiff } = this.state
|
||||||
|
return (
|
||||||
|
<div className="files-changed-header">
|
||||||
|
<div className="commits-displayed">
|
||||||
|
Showing changes from all commits
|
||||||
|
</div>
|
||||||
|
<DiffOptions
|
||||||
|
isInteractiveDiff={false}
|
||||||
|
hideWhitespaceChanges={hideWhitespaceInDiff}
|
||||||
|
onHideWhitespaceChangesChanged={this.onHideWhitespaceInDiffChanged}
|
||||||
|
showSideBySideDiff={showSideBySideDiff}
|
||||||
|
onShowSideBySideDiffChanged={this.onShowSideBySideDiffChanged}
|
||||||
|
onDiffOptionsOpened={this.onDiffOptionsOpened}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderFileList() {
|
||||||
|
const { files, selectedFile, fileListWidth } = this.props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Resizable
|
||||||
|
width={fileListWidth.value}
|
||||||
|
minimumWidth={fileListWidth.min}
|
||||||
|
maximumWidth={fileListWidth.max}
|
||||||
|
onResize={this.onFileListResize}
|
||||||
|
onReset={this.onFileListSizeReset}
|
||||||
|
>
|
||||||
|
<FileList
|
||||||
|
files={files}
|
||||||
|
onSelectedFileChanged={this.onFileSelected}
|
||||||
|
selectedFile={selectedFile}
|
||||||
|
availableWidth={clamp(fileListWidth)}
|
||||||
|
onContextMenu={this.onFileContextMenu}
|
||||||
|
/>
|
||||||
|
</Resizable>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderDiff() {
|
||||||
|
const { selectedFile } = this.props
|
||||||
|
|
||||||
|
if (selectedFile === null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const { diff, repository, imageDiffType, hideWhitespaceInDiff } = this.props
|
||||||
|
|
||||||
|
const { showSideBySideDiff } = this.state
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SeamlessDiffSwitcher
|
||||||
|
repository={repository}
|
||||||
|
imageDiffType={imageDiffType}
|
||||||
|
file={selectedFile}
|
||||||
|
diff={diff}
|
||||||
|
readOnly={true}
|
||||||
|
hideWhitespaceInDiff={hideWhitespaceInDiff}
|
||||||
|
showSideBySideDiff={showSideBySideDiff}
|
||||||
|
onOpenBinaryFile={this.onOpenBinaryFile}
|
||||||
|
onChangeImageDiffType={this.onChangeImageDiffType}
|
||||||
|
onHideWhitespaceInDiffChanged={this.onHideWhitespaceInDiffChanged}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
return (
|
||||||
|
<div className="pull-request-files-changed">
|
||||||
|
{this.renderHeader()}
|
||||||
|
<div className="files-diff-viewer">
|
||||||
|
{this.renderFileList()}
|
||||||
|
{this.renderDiff()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
68
app/src/ui/open-pull-request/pull-request-merge-status.tsx
Normal file
68
app/src/ui/open-pull-request/pull-request-merge-status.tsx
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
import * as React from 'react'
|
||||||
|
import { assertNever } from '../../lib/fatal-error'
|
||||||
|
import { ComputedAction } from '../../models/computed-action'
|
||||||
|
import { MergeTreeResult } from '../../models/merge'
|
||||||
|
import { Octicon } from '../octicons'
|
||||||
|
import * as OcticonSymbol from '../octicons/octicons.generated'
|
||||||
|
|
||||||
|
interface IPullRequestMergeStatusProps {
|
||||||
|
/** The result of merging the pull request branch into the base branch */
|
||||||
|
readonly mergeStatus: MergeTreeResult | null
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The component to display message about the result of merging the pull
|
||||||
|
* request. */
|
||||||
|
export class PullRequestMergeStatus extends React.Component<IPullRequestMergeStatusProps> {
|
||||||
|
private getMergeStatusDescription = () => {
|
||||||
|
const { mergeStatus } = this.props
|
||||||
|
if (mergeStatus === null) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const { kind } = mergeStatus
|
||||||
|
switch (kind) {
|
||||||
|
case ComputedAction.Loading:
|
||||||
|
return (
|
||||||
|
<span className="pr-merge-status-loading">
|
||||||
|
<strong>Checking mergeability…</strong> Don’t worry, you can
|
||||||
|
still create the pull request.
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
case ComputedAction.Invalid:
|
||||||
|
return (
|
||||||
|
<span className="pr-merge-status-invalid">
|
||||||
|
<strong>Error checking merge status.</strong> Unable to merge
|
||||||
|
unrelated histories in this repository
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
case ComputedAction.Clean:
|
||||||
|
return (
|
||||||
|
<span className="pr-merge-status-clean">
|
||||||
|
<strong>
|
||||||
|
<Octicon symbol={OcticonSymbol.check} /> Able to merge.
|
||||||
|
</strong>{' '}
|
||||||
|
These branches can be automatically merged.
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
case ComputedAction.Conflicts:
|
||||||
|
return (
|
||||||
|
<span className="pr-merge-status-conflicts">
|
||||||
|
<strong>
|
||||||
|
<Octicon symbol={OcticonSymbol.x} /> Can't automatically merge.
|
||||||
|
</strong>{' '}
|
||||||
|
Don’t worry, you can still create the pull request.
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
default:
|
||||||
|
return assertNever(kind, `Unknown merge status kind of ${kind}.`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
return (
|
||||||
|
<div className="pull-request-merge-status">
|
||||||
|
{this.getMergeStatusDescription()}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -54,6 +54,7 @@ interface IPreferencesProps {
|
||||||
readonly confirmRepositoryRemoval: boolean
|
readonly confirmRepositoryRemoval: boolean
|
||||||
readonly confirmDiscardChanges: boolean
|
readonly confirmDiscardChanges: boolean
|
||||||
readonly confirmDiscardChangesPermanently: boolean
|
readonly confirmDiscardChangesPermanently: boolean
|
||||||
|
readonly confirmDiscardStash: boolean
|
||||||
readonly confirmForcePush: boolean
|
readonly confirmForcePush: boolean
|
||||||
readonly confirmUndoCommit: boolean
|
readonly confirmUndoCommit: boolean
|
||||||
readonly uncommittedChangesStrategy: UncommittedChangesStrategy
|
readonly uncommittedChangesStrategy: UncommittedChangesStrategy
|
||||||
|
@ -79,6 +80,7 @@ interface IPreferencesState {
|
||||||
readonly confirmRepositoryRemoval: boolean
|
readonly confirmRepositoryRemoval: boolean
|
||||||
readonly confirmDiscardChanges: boolean
|
readonly confirmDiscardChanges: boolean
|
||||||
readonly confirmDiscardChangesPermanently: boolean
|
readonly confirmDiscardChangesPermanently: boolean
|
||||||
|
readonly confirmDiscardStash: boolean
|
||||||
readonly confirmForcePush: boolean
|
readonly confirmForcePush: boolean
|
||||||
readonly confirmUndoCommit: boolean
|
readonly confirmUndoCommit: boolean
|
||||||
readonly uncommittedChangesStrategy: UncommittedChangesStrategy
|
readonly uncommittedChangesStrategy: UncommittedChangesStrategy
|
||||||
|
@ -121,6 +123,7 @@ export class Preferences extends React.Component<
|
||||||
confirmRepositoryRemoval: false,
|
confirmRepositoryRemoval: false,
|
||||||
confirmDiscardChanges: false,
|
confirmDiscardChanges: false,
|
||||||
confirmDiscardChangesPermanently: false,
|
confirmDiscardChangesPermanently: false,
|
||||||
|
confirmDiscardStash: false,
|
||||||
confirmForcePush: false,
|
confirmForcePush: false,
|
||||||
confirmUndoCommit: false,
|
confirmUndoCommit: false,
|
||||||
uncommittedChangesStrategy: defaultUncommittedChangesStrategy,
|
uncommittedChangesStrategy: defaultUncommittedChangesStrategy,
|
||||||
|
@ -178,6 +181,7 @@ export class Preferences extends React.Component<
|
||||||
confirmDiscardChanges: this.props.confirmDiscardChanges,
|
confirmDiscardChanges: this.props.confirmDiscardChanges,
|
||||||
confirmDiscardChangesPermanently:
|
confirmDiscardChangesPermanently:
|
||||||
this.props.confirmDiscardChangesPermanently,
|
this.props.confirmDiscardChangesPermanently,
|
||||||
|
confirmDiscardStash: this.props.confirmDiscardStash,
|
||||||
confirmForcePush: this.props.confirmForcePush,
|
confirmForcePush: this.props.confirmForcePush,
|
||||||
confirmUndoCommit: this.props.confirmUndoCommit,
|
confirmUndoCommit: this.props.confirmUndoCommit,
|
||||||
uncommittedChangesStrategy: this.props.uncommittedChangesStrategy,
|
uncommittedChangesStrategy: this.props.uncommittedChangesStrategy,
|
||||||
|
@ -333,12 +337,14 @@ export class Preferences extends React.Component<
|
||||||
confirmDiscardChangesPermanently={
|
confirmDiscardChangesPermanently={
|
||||||
this.state.confirmDiscardChangesPermanently
|
this.state.confirmDiscardChangesPermanently
|
||||||
}
|
}
|
||||||
|
confirmDiscardStash={this.state.confirmDiscardStash}
|
||||||
confirmForcePush={this.state.confirmForcePush}
|
confirmForcePush={this.state.confirmForcePush}
|
||||||
confirmUndoCommit={this.state.confirmUndoCommit}
|
confirmUndoCommit={this.state.confirmUndoCommit}
|
||||||
onConfirmRepositoryRemovalChanged={
|
onConfirmRepositoryRemovalChanged={
|
||||||
this.onConfirmRepositoryRemovalChanged
|
this.onConfirmRepositoryRemovalChanged
|
||||||
}
|
}
|
||||||
onConfirmDiscardChangesChanged={this.onConfirmDiscardChangesChanged}
|
onConfirmDiscardChangesChanged={this.onConfirmDiscardChangesChanged}
|
||||||
|
onConfirmDiscardStashChanged={this.onConfirmDiscardStashChanged}
|
||||||
onConfirmForcePushChanged={this.onConfirmForcePushChanged}
|
onConfirmForcePushChanged={this.onConfirmForcePushChanged}
|
||||||
onConfirmDiscardChangesPermanentlyChanged={
|
onConfirmDiscardChangesPermanentlyChanged={
|
||||||
this.onConfirmDiscardChangesPermanentlyChanged
|
this.onConfirmDiscardChangesPermanentlyChanged
|
||||||
|
@ -410,6 +416,10 @@ export class Preferences extends React.Component<
|
||||||
this.setState({ confirmDiscardChanges: value })
|
this.setState({ confirmDiscardChanges: value })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private onConfirmDiscardStashChanged = (value: boolean) => {
|
||||||
|
this.setState({ confirmDiscardStash: value })
|
||||||
|
}
|
||||||
|
|
||||||
private onConfirmDiscardChangesPermanentlyChanged = (value: boolean) => {
|
private onConfirmDiscardChangesPermanentlyChanged = (value: boolean) => {
|
||||||
this.setState({ confirmDiscardChangesPermanently: value })
|
this.setState({ confirmDiscardChangesPermanently: value })
|
||||||
}
|
}
|
||||||
|
@ -562,6 +572,10 @@ export class Preferences extends React.Component<
|
||||||
this.state.confirmForcePush
|
this.state.confirmForcePush
|
||||||
)
|
)
|
||||||
|
|
||||||
|
await this.props.dispatcher.setConfirmDiscardStashSetting(
|
||||||
|
this.state.confirmDiscardStash
|
||||||
|
)
|
||||||
|
|
||||||
await this.props.dispatcher.setConfirmUndoCommitSetting(
|
await this.props.dispatcher.setConfirmUndoCommitSetting(
|
||||||
this.state.confirmUndoCommit
|
this.state.confirmUndoCommit
|
||||||
)
|
)
|
||||||
|
|
|
@ -6,10 +6,12 @@ interface IPromptsPreferencesProps {
|
||||||
readonly confirmRepositoryRemoval: boolean
|
readonly confirmRepositoryRemoval: boolean
|
||||||
readonly confirmDiscardChanges: boolean
|
readonly confirmDiscardChanges: boolean
|
||||||
readonly confirmDiscardChangesPermanently: boolean
|
readonly confirmDiscardChangesPermanently: boolean
|
||||||
|
readonly confirmDiscardStash: boolean
|
||||||
readonly confirmForcePush: boolean
|
readonly confirmForcePush: boolean
|
||||||
readonly confirmUndoCommit: boolean
|
readonly confirmUndoCommit: boolean
|
||||||
readonly onConfirmDiscardChangesChanged: (checked: boolean) => void
|
readonly onConfirmDiscardChangesChanged: (checked: boolean) => void
|
||||||
readonly onConfirmDiscardChangesPermanentlyChanged: (checked: boolean) => void
|
readonly onConfirmDiscardChangesPermanentlyChanged: (checked: boolean) => void
|
||||||
|
readonly onConfirmDiscardStashChanged: (checked: boolean) => void
|
||||||
readonly onConfirmRepositoryRemovalChanged: (checked: boolean) => void
|
readonly onConfirmRepositoryRemovalChanged: (checked: boolean) => void
|
||||||
readonly onConfirmForcePushChanged: (checked: boolean) => void
|
readonly onConfirmForcePushChanged: (checked: boolean) => void
|
||||||
readonly onConfirmUndoCommitChanged: (checked: boolean) => void
|
readonly onConfirmUndoCommitChanged: (checked: boolean) => void
|
||||||
|
@ -19,6 +21,7 @@ interface IPromptsPreferencesState {
|
||||||
readonly confirmRepositoryRemoval: boolean
|
readonly confirmRepositoryRemoval: boolean
|
||||||
readonly confirmDiscardChanges: boolean
|
readonly confirmDiscardChanges: boolean
|
||||||
readonly confirmDiscardChangesPermanently: boolean
|
readonly confirmDiscardChangesPermanently: boolean
|
||||||
|
readonly confirmDiscardStash: boolean
|
||||||
readonly confirmForcePush: boolean
|
readonly confirmForcePush: boolean
|
||||||
readonly confirmUndoCommit: boolean
|
readonly confirmUndoCommit: boolean
|
||||||
}
|
}
|
||||||
|
@ -35,6 +38,7 @@ export class Prompts extends React.Component<
|
||||||
confirmDiscardChanges: this.props.confirmDiscardChanges,
|
confirmDiscardChanges: this.props.confirmDiscardChanges,
|
||||||
confirmDiscardChangesPermanently:
|
confirmDiscardChangesPermanently:
|
||||||
this.props.confirmDiscardChangesPermanently,
|
this.props.confirmDiscardChangesPermanently,
|
||||||
|
confirmDiscardStash: this.props.confirmDiscardStash,
|
||||||
confirmForcePush: this.props.confirmForcePush,
|
confirmForcePush: this.props.confirmForcePush,
|
||||||
confirmUndoCommit: this.props.confirmUndoCommit,
|
confirmUndoCommit: this.props.confirmUndoCommit,
|
||||||
}
|
}
|
||||||
|
@ -58,6 +62,15 @@ export class Prompts extends React.Component<
|
||||||
this.props.onConfirmDiscardChangesPermanentlyChanged(value)
|
this.props.onConfirmDiscardChangesPermanentlyChanged(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private onConfirmDiscardStashChanged = (
|
||||||
|
event: React.FormEvent<HTMLInputElement>
|
||||||
|
) => {
|
||||||
|
const value = event.currentTarget.checked
|
||||||
|
|
||||||
|
this.setState({ confirmDiscardStash: value })
|
||||||
|
this.props.onConfirmDiscardStashChanged(value)
|
||||||
|
}
|
||||||
|
|
||||||
private onConfirmForcePushChanged = (
|
private onConfirmForcePushChanged = (
|
||||||
event: React.FormEvent<HTMLInputElement>
|
event: React.FormEvent<HTMLInputElement>
|
||||||
) => {
|
) => {
|
||||||
|
@ -116,6 +129,15 @@ export class Prompts extends React.Component<
|
||||||
}
|
}
|
||||||
onChange={this.onConfirmDiscardChangesPermanentlyChanged}
|
onChange={this.onConfirmDiscardChangesPermanentlyChanged}
|
||||||
/>
|
/>
|
||||||
|
<Checkbox
|
||||||
|
label="Discarding stash"
|
||||||
|
value={
|
||||||
|
this.state.confirmDiscardStash
|
||||||
|
? CheckboxValue.On
|
||||||
|
: CheckboxValue.Off
|
||||||
|
}
|
||||||
|
onChange={this.onConfirmDiscardStashChanged}
|
||||||
|
/>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
label="Force pushing"
|
label="Force pushing"
|
||||||
value={
|
value={
|
||||||
|
|
|
@ -50,6 +50,7 @@ interface IRepositoryViewProps {
|
||||||
readonly hideWhitespaceInHistoryDiff: boolean
|
readonly hideWhitespaceInHistoryDiff: boolean
|
||||||
readonly showSideBySideDiff: boolean
|
readonly showSideBySideDiff: boolean
|
||||||
readonly askForConfirmationOnDiscardChanges: boolean
|
readonly askForConfirmationOnDiscardChanges: boolean
|
||||||
|
readonly askForConfirmationOnDiscardStash: boolean
|
||||||
readonly focusCommitMessage: boolean
|
readonly focusCommitMessage: boolean
|
||||||
readonly commitSpellcheckEnabled: boolean
|
readonly commitSpellcheckEnabled: boolean
|
||||||
readonly accounts: ReadonlyArray<Account>
|
readonly accounts: ReadonlyArray<Account>
|
||||||
|
@ -355,6 +356,9 @@ export class RepositoryView extends React.Component<
|
||||||
fileListWidth={this.props.stashedFilesWidth}
|
fileListWidth={this.props.stashedFilesWidth}
|
||||||
repository={this.props.repository}
|
repository={this.props.repository}
|
||||||
dispatcher={this.props.dispatcher}
|
dispatcher={this.props.dispatcher}
|
||||||
|
askForConfirmationOnDiscardStash={
|
||||||
|
this.props.askForConfirmationOnDiscardStash
|
||||||
|
}
|
||||||
isWorkingTreeClean={isWorkingTreeClean}
|
isWorkingTreeClean={isWorkingTreeClean}
|
||||||
showSideBySideDiff={this.props.showSideBySideDiff}
|
showSideBySideDiff={this.props.showSideBySideDiff}
|
||||||
onOpenBinaryFile={this.onOpenBinaryFile}
|
onOpenBinaryFile={this.onOpenBinaryFile}
|
||||||
|
|
|
@ -5,16 +5,19 @@ import { Dispatcher } from '../dispatcher'
|
||||||
import { Row } from '../lib/row'
|
import { Row } from '../lib/row'
|
||||||
import { IStashEntry } from '../../models/stash-entry'
|
import { IStashEntry } from '../../models/stash-entry'
|
||||||
import { OkCancelButtonGroup } from '../dialog/ok-cancel-button-group'
|
import { OkCancelButtonGroup } from '../dialog/ok-cancel-button-group'
|
||||||
|
import { Checkbox, CheckboxValue } from '../lib/checkbox'
|
||||||
|
|
||||||
interface IConfirmDiscardStashProps {
|
interface IConfirmDiscardStashProps {
|
||||||
readonly dispatcher: Dispatcher
|
readonly dispatcher: Dispatcher
|
||||||
readonly repository: Repository
|
readonly repository: Repository
|
||||||
readonly stash: IStashEntry
|
readonly stash: IStashEntry
|
||||||
|
readonly askForConfirmationOnDiscardStash: boolean
|
||||||
readonly onDismissed: () => void
|
readonly onDismissed: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IConfirmDiscardStashState {
|
interface IConfirmDiscardStashState {
|
||||||
readonly isDiscarding: boolean
|
readonly isDiscarding: boolean
|
||||||
|
readonly confirmDiscardStash: boolean
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Dialog to confirm dropping a stash
|
* Dialog to confirm dropping a stash
|
||||||
|
@ -28,6 +31,7 @@ export class ConfirmDiscardStashDialog extends React.Component<
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
isDiscarding: false,
|
isDiscarding: false,
|
||||||
|
confirmDiscardStash: props.askForConfirmationOnDiscardStash,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,6 +50,17 @@ export class ConfirmDiscardStashDialog extends React.Component<
|
||||||
>
|
>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<Row>Are you sure you want to discard these stashed changes?</Row>
|
<Row>Are you sure you want to discard these stashed changes?</Row>
|
||||||
|
<Row>
|
||||||
|
<Checkbox
|
||||||
|
label="Do not show this message again"
|
||||||
|
value={
|
||||||
|
this.state.confirmDiscardStash
|
||||||
|
? CheckboxValue.Off
|
||||||
|
: CheckboxValue.On
|
||||||
|
}
|
||||||
|
onChange={this.onAskForConfirmationOnDiscardStashChanged}
|
||||||
|
/>
|
||||||
|
</Row>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<OkCancelButtonGroup destructive={true} okButtonText="Discard" />
|
<OkCancelButtonGroup destructive={true} okButtonText="Discard" />
|
||||||
|
@ -54,6 +69,14 @@ export class ConfirmDiscardStashDialog extends React.Component<
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private onAskForConfirmationOnDiscardStashChanged = (
|
||||||
|
event: React.FormEvent<HTMLInputElement>
|
||||||
|
) => {
|
||||||
|
const value = !event.currentTarget.checked
|
||||||
|
|
||||||
|
this.setState({ confirmDiscardStash: value })
|
||||||
|
}
|
||||||
|
|
||||||
private onSubmit = async () => {
|
private onSubmit = async () => {
|
||||||
const { dispatcher, repository, stash, onDismissed } = this.props
|
const { dispatcher, repository, stash, onDismissed } = this.props
|
||||||
|
|
||||||
|
@ -62,6 +85,7 @@ export class ConfirmDiscardStashDialog extends React.Component<
|
||||||
})
|
})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
dispatcher.setConfirmDiscardStashSetting(this.state.confirmDiscardStash)
|
||||||
await dispatcher.dropStash(repository, stash)
|
await dispatcher.dropStash(repository, stash)
|
||||||
} finally {
|
} finally {
|
||||||
this.setState({
|
this.setState({
|
||||||
|
|
|
@ -11,11 +11,13 @@ interface IStashDiffHeaderProps {
|
||||||
readonly stashEntry: IStashEntry
|
readonly stashEntry: IStashEntry
|
||||||
readonly repository: Repository
|
readonly repository: Repository
|
||||||
readonly dispatcher: Dispatcher
|
readonly dispatcher: Dispatcher
|
||||||
|
readonly askForConfirmationOnDiscardStash: boolean
|
||||||
readonly isWorkingTreeClean: boolean
|
readonly isWorkingTreeClean: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IStashDiffHeaderState {
|
interface IStashDiffHeaderState {
|
||||||
readonly isRestoring: boolean
|
readonly isRestoring: boolean
|
||||||
|
readonly isDiscarding: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -31,12 +33,13 @@ export class StashDiffHeader extends React.Component<
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
isRestoring: false,
|
isRestoring: false,
|
||||||
|
isDiscarding: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const { isWorkingTreeClean } = this.props
|
const { isWorkingTreeClean } = this.props
|
||||||
const { isRestoring } = this.state
|
const { isRestoring, isDiscarding } = this.state
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="header">
|
<div className="header">
|
||||||
|
@ -44,10 +47,12 @@ export class StashDiffHeader extends React.Component<
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<OkCancelButtonGroup
|
<OkCancelButtonGroup
|
||||||
okButtonText="Restore"
|
okButtonText="Restore"
|
||||||
okButtonDisabled={isRestoring || !isWorkingTreeClean}
|
okButtonDisabled={
|
||||||
|
isRestoring || !isWorkingTreeClean || isDiscarding
|
||||||
|
}
|
||||||
onOkButtonClick={this.onRestoreClick}
|
onOkButtonClick={this.onRestoreClick}
|
||||||
cancelButtonText="Discard"
|
cancelButtonText="Discard"
|
||||||
cancelButtonDisabled={isRestoring}
|
cancelButtonDisabled={isRestoring || isDiscarding}
|
||||||
onCancelButtonClick={this.onDiscardClick}
|
onCancelButtonClick={this.onDiscardClick}
|
||||||
/>
|
/>
|
||||||
{this.renderExplanatoryText()}
|
{this.renderExplanatoryText()}
|
||||||
|
@ -80,13 +85,33 @@ export class StashDiffHeader extends React.Component<
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private onDiscardClick = () => {
|
private onDiscardClick = async () => {
|
||||||
const { dispatcher, repository, stashEntry } = this.props
|
const {
|
||||||
dispatcher.showPopup({
|
dispatcher,
|
||||||
type: PopupType.ConfirmDiscardStash,
|
|
||||||
stash: stashEntry,
|
|
||||||
repository,
|
repository,
|
||||||
})
|
stashEntry,
|
||||||
|
askForConfirmationOnDiscardStash,
|
||||||
|
} = this.props
|
||||||
|
|
||||||
|
if (!askForConfirmationOnDiscardStash) {
|
||||||
|
this.setState({
|
||||||
|
isDiscarding: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
await dispatcher.dropStash(repository, stashEntry)
|
||||||
|
} finally {
|
||||||
|
this.setState({
|
||||||
|
isDiscarding: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dispatcher.showPopup({
|
||||||
|
type: PopupType.ConfirmDiscardStash,
|
||||||
|
stash: stashEntry,
|
||||||
|
repository,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private onRestoreClick = async () => {
|
private onRestoreClick = async () => {
|
||||||
|
|
|
@ -27,6 +27,9 @@ interface IStashDiffViewerProps {
|
||||||
readonly repository: Repository
|
readonly repository: Repository
|
||||||
readonly dispatcher: Dispatcher
|
readonly dispatcher: Dispatcher
|
||||||
|
|
||||||
|
/** Should the app propt the user to confirm a discard stash */
|
||||||
|
readonly askForConfirmationOnDiscardStash: boolean
|
||||||
|
|
||||||
/** Whether we should display side by side diffs. */
|
/** Whether we should display side by side diffs. */
|
||||||
readonly showSideBySideDiff: boolean
|
readonly showSideBySideDiff: boolean
|
||||||
|
|
||||||
|
@ -113,6 +116,9 @@ export class StashDiffViewer extends React.PureComponent<IStashDiffViewerProps>
|
||||||
repository={repository}
|
repository={repository}
|
||||||
dispatcher={dispatcher}
|
dispatcher={dispatcher}
|
||||||
isWorkingTreeClean={isWorkingTreeClean}
|
isWorkingTreeClean={isWorkingTreeClean}
|
||||||
|
askForConfirmationOnDiscardStash={
|
||||||
|
this.props.askForConfirmationOnDiscardStash
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<div className="commit-details">
|
<div className="commit-details">
|
||||||
<Resizable
|
<Resizable
|
||||||
|
|
|
@ -99,4 +99,7 @@
|
||||||
@import 'ui/_pull-request-quick-view';
|
@import 'ui/_pull-request-quick-view';
|
||||||
@import 'ui/discard-changes-retry';
|
@import 'ui/discard-changes-retry';
|
||||||
@import 'ui/_git-email-not-found-warning';
|
@import 'ui/_git-email-not-found-warning';
|
||||||
@import 'ui/_branch-select.scss';
|
@import 'ui/_branch-select';
|
||||||
|
@import 'ui/_popover-dropdown';
|
||||||
|
@import 'ui/_pull-request-files-changed';
|
||||||
|
@import 'ui/_pull-request-merge-status';
|
||||||
|
|
|
@ -1,66 +0,0 @@
|
||||||
.branch-select-component {
|
|
||||||
display: inline-flex;
|
|
||||||
|
|
||||||
.base-label {
|
|
||||||
font-weight: var(--font-weight-semibold);
|
|
||||||
color: var(--text-secondary-color);
|
|
||||||
margin: 0 var(--spacing-half);
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
border: none;
|
|
||||||
background-color: inherit;
|
|
||||||
border: none;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
font-style: normal;
|
|
||||||
font-family: var(--font-family-monospace);
|
|
||||||
|
|
||||||
.ref-component {
|
|
||||||
padding: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.button-component {
|
|
||||||
overflow: visible;
|
|
||||||
|
|
||||||
&:hover,
|
|
||||||
&:focus {
|
|
||||||
border: none;
|
|
||||||
box-shadow: none;
|
|
||||||
|
|
||||||
.ref-component {
|
|
||||||
border-color: var(--path-segment-background-focus);
|
|
||||||
box-shadow: 0 0 0 1px var(--path-segment-background-focus);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.ref-component {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.branch-select-dropdown {
|
|
||||||
position: absolute;
|
|
||||||
min-height: 200px;
|
|
||||||
width: 365px;
|
|
||||||
padding: 0;
|
|
||||||
margin-top: 25px;
|
|
||||||
|
|
||||||
.branch-select-dropdown-header {
|
|
||||||
padding: var(--spacing);
|
|
||||||
font-weight: var(--font-weight-semibold);
|
|
||||||
display: flex;
|
|
||||||
border-bottom: var(--base-border);
|
|
||||||
|
|
||||||
.close {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.branch-select-dropdown-list {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
36
app/styles/ui/_popover-dropdown.scss
Normal file
36
app/styles/ui/_popover-dropdown.scss
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
.popover-dropdown-component {
|
||||||
|
display: inline-flex;
|
||||||
|
|
||||||
|
.button-content,
|
||||||
|
.popover-dropdown-button-label {
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.popover-dropdown-button-label {
|
||||||
|
color: var(--text-secondary-color);
|
||||||
|
margin: 0 var(--spacing-half);
|
||||||
|
}
|
||||||
|
|
||||||
|
.popover-dropdown-popover {
|
||||||
|
position: absolute;
|
||||||
|
min-height: 200px;
|
||||||
|
width: 365px;
|
||||||
|
padding: 0;
|
||||||
|
margin-top: 25px;
|
||||||
|
|
||||||
|
.popover-dropdown-header {
|
||||||
|
padding: var(--spacing);
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
display: flex;
|
||||||
|
border-bottom: var(--base-border);
|
||||||
|
|
||||||
|
.close {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.popover-dropdown-content {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
23
app/styles/ui/_pull-request-files-changed.scss
Normal file
23
app/styles/ui/_pull-request-files-changed.scss
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
.pull-request-files-changed {
|
||||||
|
border: var(--base-border);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
|
||||||
|
.files-changed-header {
|
||||||
|
padding: var(--spacing);
|
||||||
|
border-bottom: var(--base-border);
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.commits-displayed {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.files-diff-viewer {
|
||||||
|
height: 500px;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-list {
|
||||||
|
border-right: var(--base-border);
|
||||||
|
}
|
||||||
|
}
|
31
app/styles/ui/_pull-request-merge-status.scss
Normal file
31
app/styles/ui/_pull-request-merge-status.scss
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
.pull-request-merge-status {
|
||||||
|
flex-grow: 1;
|
||||||
|
color: var(--text-secondary-color);
|
||||||
|
|
||||||
|
.octicon {
|
||||||
|
vertical-align: text-bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
strong {
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pr-merge-status-loading {
|
||||||
|
strong {
|
||||||
|
color: var(--file-warning-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pr-merge-status-invalid,
|
||||||
|
.pr-merge-status-conflicts {
|
||||||
|
strong {
|
||||||
|
color: var(--status-error-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pr-merge-status-clean {
|
||||||
|
strong {
|
||||||
|
color: var(--status-success-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,7 @@
|
||||||
.open-pull-request {
|
.open-pull-request {
|
||||||
|
width: 850px;
|
||||||
|
max-width: none;
|
||||||
|
|
||||||
header.dialog-header {
|
header.dialog-header {
|
||||||
padding-bottom: var(--spacing);
|
padding-bottom: var(--spacing);
|
||||||
|
|
||||||
|
@ -15,4 +18,25 @@
|
||||||
padding: var(--spacing-half);
|
padding: var(--spacing-half);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.open-pull-request-content {
|
||||||
|
padding: var(--spacing);
|
||||||
|
}
|
||||||
|
|
||||||
|
.open-pull-request-no-changes {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: var(--spacing-double);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-footer {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pull-request-merge-status {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ body > .tooltip,
|
||||||
max-width: 300px;
|
max-width: 300px;
|
||||||
|
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
|
word-break: break-word;
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
|
|
||||||
background-color: var(--tooltip-background-color);
|
background-color: var(--tooltip-background-color);
|
||||||
|
|
|
@ -575,9 +575,38 @@ describe('git/diff', () => {
|
||||||
'feature-branch',
|
'feature-branch',
|
||||||
'irrelevantToTest'
|
'irrelevantToTest'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
expect(changesetData).not.toBeNull()
|
||||||
|
if (changesetData === null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
expect(changesetData.files).toHaveLength(1)
|
expect(changesetData.files).toHaveLength(1)
|
||||||
expect(changesetData.files[0].path).toBe('feature.md')
|
expect(changesetData.files[0].path).toBe('feature.md')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('returns null for unrelated histories', async () => {
|
||||||
|
// create a second branch that's orphaned from our current branch
|
||||||
|
await GitProcess.exec(
|
||||||
|
['checkout', '--orphan', 'orphaned-branch'],
|
||||||
|
repository.path
|
||||||
|
)
|
||||||
|
|
||||||
|
// add a commit to this new branch
|
||||||
|
await GitProcess.exec(
|
||||||
|
['commit', '--allow-empty', '-m', `first commit on gh-pages`],
|
||||||
|
repository.path
|
||||||
|
)
|
||||||
|
|
||||||
|
const changesetData = await getBranchMergeBaseChangedFiles(
|
||||||
|
repository,
|
||||||
|
'master',
|
||||||
|
'feature-branch',
|
||||||
|
'irrelevantToTest'
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(changesetData).toBeNull()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('getBranchMergeBaseDiff', () => {
|
describe('getBranchMergeBaseDiff', () => {
|
||||||
|
|
|
@ -1,6 +1,12 @@
|
||||||
{
|
{
|
||||||
"releases": {
|
"releases": {
|
||||||
"3.1.2": ["[Improved] Upgrade embedded Git to 2.35.5"],
|
"3.1.2": ["[Improved] Upgrade embedded Git to 2.35.5"],
|
||||||
|
"3.1.2-beta1": [
|
||||||
|
"[Added] You can preview the changes a pull request from your current branch would make - #11517",
|
||||||
|
"[Fixed] App correctly remembers undo commit prompt setting - #15408",
|
||||||
|
"[Improved] Add support for zooming out at the 67%, 75%, 80% and 90% zoom levels - #15401. Thanks @sathvikrijo!",
|
||||||
|
"[Improved] Add option to disable discard stash confirmation - #15379. Thanks @tsvetilian-ty!"
|
||||||
|
],
|
||||||
"3.1.1": [
|
"3.1.1": [
|
||||||
"[Fixed] App correctly remembers undo commit prompt setting - #15408"
|
"[Fixed] App correctly remembers undo commit prompt setting - #15408"
|
||||||
],
|
],
|
||||||
|
|
|
@ -19,13 +19,13 @@ versions look similar to the below output:
|
||||||
|
|
||||||
```shellsession
|
```shellsession
|
||||||
$ node -v
|
$ node -v
|
||||||
v10.15.4
|
v16.13.0
|
||||||
|
|
||||||
$ yarn -v
|
$ yarn -v
|
||||||
1.15.2
|
1.21.1
|
||||||
|
|
||||||
$ python --version
|
$ python --version
|
||||||
Python 2.7.15
|
Python 3.9.x
|
||||||
```
|
```
|
||||||
|
|
||||||
There are also [additional resources](tooling.md) to configure your favorite
|
There are also [additional resources](tooling.md) to configure your favorite
|
||||||
|
|
|
@ -102,7 +102,11 @@ function packageWindows() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldMakeDelta()) {
|
if (shouldMakeDelta()) {
|
||||||
options.remoteReleases = getUpdatesURL()
|
const url = new URL(getUpdatesURL())
|
||||||
|
// Make sure Squirrel.Windows isn't affected by partially or completely
|
||||||
|
// disabled releases.
|
||||||
|
url.searchParams.set('bypassStaggeredRelease', '1')
|
||||||
|
options.remoteReleases = url.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isAppveyor() || isGitHubActions()) {
|
if (isAppveyor() || isGitHubActions()) {
|
||||||
|
|
Loading…
Reference in a new issue