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 }}
|
||||
|
||||
- name: Create Release Pull Request
|
||||
uses: peter-evans/create-pull-request@v4.1.1
|
||||
uses: peter-evans/create-pull-request@v4.1.4
|
||||
if: |
|
||||
startsWith(github.ref, 'refs/heads/releases/') && !contains(github.ref, 'test')
|
||||
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
|
||||
install GitHub Desktop:
|
||||
- Windows users can install using [Chocolatey](https://chocolatey.org/) package manager:
|
||||
`c:\> choco install github-desktop`
|
||||
- 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`
|
||||
- macOS users can install using [Homebrew](https://brew.sh/) package manager:
|
||||
`$ 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.
|
||||
|
||||
## Building Desktop
|
||||
|
||||
To get your development environment set up for building Desktop, see [setup.md](./docs/contributing/setup.md).
|
||||
|
||||
## More Resources
|
||||
|
||||
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 */
|
||||
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
|
||||
* Alt key is pressed. Only applicable on non-macOS platforms.
|
||||
|
@ -194,6 +197,9 @@ export interface IAppState {
|
|||
/** Whether we should show a confirmation dialog */
|
||||
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? */
|
||||
readonly askForConfirmationOnForcePush: boolean
|
||||
|
||||
|
@ -230,6 +236,9 @@ export interface IAppState {
|
|||
/** Whether we should hide white space changes in history diff */
|
||||
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 */
|
||||
readonly showSideBySideDiff: boolean
|
||||
|
||||
|
@ -965,4 +974,7 @@ export interface IPullRequestState {
|
|||
* diff between the latest commit and the earliest commits parent.
|
||||
*/
|
||||
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',
|
||||
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> {
|
||||
|
|
|
@ -110,5 +110,5 @@ export function enableSubmoduleDiff(): boolean {
|
|||
|
||||
/** Should we enable starting pull requests? */
|
||||
export function enableStartingPullRequests(): boolean {
|
||||
return enableDevelopmentFeatures()
|
||||
return enableBetaFeatures()
|
||||
}
|
||||
|
|
|
@ -254,7 +254,7 @@ export async function getBranchMergeBaseChangedFiles(
|
|||
baseBranchName: string,
|
||||
comparisonBranchName: string,
|
||||
latestComparisonBranchCommitRef: string
|
||||
): Promise<IChangesetData> {
|
||||
): Promise<IChangesetData | null> {
|
||||
const baseArgs = [
|
||||
'diff',
|
||||
'--merge-base',
|
||||
|
@ -268,22 +268,26 @@ export async function getBranchMergeBaseChangedFiles(
|
|||
'--',
|
||||
]
|
||||
|
||||
const result = await git(
|
||||
baseArgs,
|
||||
repository.path,
|
||||
'getBranchMergeBaseChangedFiles'
|
||||
)
|
||||
|
||||
const mergeBaseCommit = await getMergeBase(
|
||||
repository,
|
||||
baseBranchName,
|
||||
comparisonBranchName
|
||||
)
|
||||
|
||||
if (mergeBaseCommit === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
const result = await git(
|
||||
baseArgs,
|
||||
repository.path,
|
||||
'getBranchMergeBaseChangedFiles'
|
||||
)
|
||||
|
||||
return parseRawLogWithNumstat(
|
||||
result.combinedOutput,
|
||||
`${latestComparisonBranchCommitRef}`,
|
||||
mergeBaseCommit ?? NullTreeSHA
|
||||
mergeBaseCommit
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -323,15 +323,20 @@ const commitSummaryWidthConfigKey: string = 'commit-summary-width'
|
|||
const defaultStashedFilesWidth: number = 250
|
||||
const stashedFilesWidthConfigKey: string = 'stashed-files-width'
|
||||
|
||||
const defaultPullRequestFileListWidth: number = 250
|
||||
const pullRequestFileListConfigKey: string = 'pull-request-files-width'
|
||||
|
||||
const askToMoveToApplicationsFolderDefault: boolean = true
|
||||
const confirmRepoRemovalDefault: boolean = true
|
||||
const confirmDiscardChangesDefault: boolean = true
|
||||
const confirmDiscardChangesPermanentlyDefault: boolean = true
|
||||
const confirmDiscardStashDefault: boolean = true
|
||||
const askForConfirmationOnForcePushDefault = true
|
||||
const confirmUndoCommitDefault: boolean = true
|
||||
const askToMoveToApplicationsFolderKey: string = 'askToMoveToApplicationsFolder'
|
||||
const confirmRepoRemovalKey: string = 'confirmRepoRemoval'
|
||||
const confirmDiscardChangesKey: string = 'confirmDiscardChanges'
|
||||
const confirmDiscardStashKey: string = 'confirmDiscardStash'
|
||||
const confirmDiscardChangesPermanentlyKey: string =
|
||||
'confirmDiscardChangesPermanentlyKey'
|
||||
const confirmForcePushKey: string = 'confirmForcePush'
|
||||
|
@ -348,6 +353,9 @@ const hideWhitespaceInChangesDiffDefault = false
|
|||
const hideWhitespaceInChangesDiffKey = 'hide-whitespace-in-changes-diff'
|
||||
const hideWhitespaceInHistoryDiffDefault = false
|
||||
const hideWhitespaceInHistoryDiffKey = 'hide-whitespace-in-diff'
|
||||
const hideWhitespaceInPullRequestDiffDefault = false
|
||||
const hideWhitespaceInPullRequestDiffKey =
|
||||
'hide-whitespace-in-pull-request-diff'
|
||||
|
||||
const commitSpellcheckEnabledDefault = true
|
||||
const commitSpellcheckEnabledKey = 'commit-spellcheck-enabled'
|
||||
|
@ -424,6 +432,7 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
private sidebarWidth = constrain(defaultSidebarWidth)
|
||||
private commitSummaryWidth = constrain(defaultCommitSummaryWidth)
|
||||
private stashedFilesWidth = constrain(defaultStashedFilesWidth)
|
||||
private pullRequestFileListWidth = constrain(defaultPullRequestFileListWidth)
|
||||
|
||||
private windowState: WindowState | null = null
|
||||
private windowZoomFactor: number = 1
|
||||
|
@ -437,6 +446,7 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
private confirmDiscardChanges: boolean = confirmDiscardChangesDefault
|
||||
private confirmDiscardChangesPermanently: boolean =
|
||||
confirmDiscardChangesPermanentlyDefault
|
||||
private confirmDiscardStash: boolean = confirmDiscardStashDefault
|
||||
private askForConfirmationOnForcePush = askForConfirmationOnForcePushDefault
|
||||
private confirmUndoCommit: boolean = confirmUndoCommitDefault
|
||||
private imageDiffType: ImageDiffType = imageDiffTypeDefault
|
||||
|
@ -444,6 +454,8 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
hideWhitespaceInChangesDiffDefault
|
||||
private hideWhitespaceInHistoryDiff: boolean =
|
||||
hideWhitespaceInHistoryDiffDefault
|
||||
private hideWhitespaceInPullRequestDiff: boolean =
|
||||
hideWhitespaceInPullRequestDiffDefault
|
||||
/** Whether or not the spellchecker is enabled for commit summary and description */
|
||||
private commitSpellcheckEnabled: boolean = commitSpellcheckEnabledDefault
|
||||
private showSideBySideDiff: boolean = ShowSideBySideDiffDefault
|
||||
|
@ -901,6 +913,7 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
sidebarWidth: this.sidebarWidth,
|
||||
commitSummaryWidth: this.commitSummaryWidth,
|
||||
stashedFilesWidth: this.stashedFilesWidth,
|
||||
pullRequestFilesListWidth: this.pullRequestFileListWidth,
|
||||
appMenuState: this.appMenu ? this.appMenu.openMenus : [],
|
||||
highlightAccessKeys: this.highlightAccessKeys,
|
||||
isUpdateAvailableBannerVisible: this.isUpdateAvailableBannerVisible,
|
||||
|
@ -913,6 +926,7 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
askForConfirmationOnDiscardChanges: this.confirmDiscardChanges,
|
||||
askForConfirmationOnDiscardChangesPermanently:
|
||||
this.confirmDiscardChangesPermanently,
|
||||
askForConfirmationOnDiscardStash: this.confirmDiscardStash,
|
||||
askForConfirmationOnForcePush: this.askForConfirmationOnForcePush,
|
||||
askForConfirmationOnUndoCommit: this.confirmUndoCommit,
|
||||
uncommittedChangesStrategy: this.uncommittedChangesStrategy,
|
||||
|
@ -920,6 +934,7 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
imageDiffType: this.imageDiffType,
|
||||
hideWhitespaceInChangesDiff: this.hideWhitespaceInChangesDiff,
|
||||
hideWhitespaceInHistoryDiff: this.hideWhitespaceInHistoryDiff,
|
||||
hideWhitespaceInPullRequestDiff: this.hideWhitespaceInPullRequestDiff,
|
||||
showSideBySideDiff: this.showSideBySideDiff,
|
||||
selectedShell: this.selectedShell,
|
||||
repositoryFilterText: this.repositoryFilterText,
|
||||
|
@ -1426,17 +1441,11 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
}
|
||||
|
||||
if (tip.kind === TipState.Valid && aheadBehind.behind > 0) {
|
||||
const mergeTreePromise = promiseWithMinimumTimeout(
|
||||
() => determineMergeability(repository, tip.branch, action.branch),
|
||||
500
|
||||
this.currentMergeTreePromise = this.setupMergabilityPromise(
|
||||
repository,
|
||||
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 => {
|
||||
this.repositoryStateCache.updateCompareState(repository, () => ({
|
||||
mergeStatus,
|
||||
|
@ -1444,16 +1453,9 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
|
||||
this.emitUpdate()
|
||||
})
|
||||
|
||||
const cleanup = () => {
|
||||
.finally(() => {
|
||||
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
|
||||
} 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`. */
|
||||
public _updateCompareForm<K extends keyof ICompareFormUpdate>(
|
||||
repository: Repository,
|
||||
|
@ -1951,8 +1970,13 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
this.stashedFilesWidth = constrain(
|
||||
getNumber(stashedFilesWidthConfigKey, defaultStashedFilesWidth)
|
||||
)
|
||||
this.pullRequestFileListWidth = constrain(
|
||||
getNumber(pullRequestFileListConfigKey, defaultPullRequestFileListWidth)
|
||||
)
|
||||
|
||||
this.updateResizableConstraints()
|
||||
// TODO: Initiliaze here for now... maybe move to dialog mounting
|
||||
this.updatePullRequestResizableConstraints()
|
||||
|
||||
this.askToMoveToApplicationsFolderSetting = getBoolean(
|
||||
askToMoveToApplicationsFolderKey,
|
||||
|
@ -1974,6 +1998,11 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
confirmDiscardChangesPermanentlyDefault
|
||||
)
|
||||
|
||||
this.confirmDiscardStash = getBoolean(
|
||||
confirmDiscardStashKey,
|
||||
confirmDiscardStashDefault
|
||||
)
|
||||
|
||||
this.askForConfirmationOnForcePush = getBoolean(
|
||||
confirmForcePushKey,
|
||||
askForConfirmationOnForcePushDefault
|
||||
|
@ -2011,6 +2040,10 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
hideWhitespaceInHistoryDiffKey,
|
||||
false
|
||||
)
|
||||
this.hideWhitespaceInPullRequestDiff = getBoolean(
|
||||
hideWhitespaceInPullRequestDiffKey,
|
||||
false
|
||||
)
|
||||
this.commitSpellcheckEnabled = getBoolean(
|
||||
commitSpellcheckEnabledKey,
|
||||
commitSpellcheckEnabledDefault
|
||||
|
@ -2077,6 +2110,41 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
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(
|
||||
selectedEditor: string | null
|
||||
): Promise<void> {
|
||||
|
@ -5193,6 +5261,15 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
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> {
|
||||
this.askForConfirmationOnForcePush = 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) {
|
||||
if (showSideBySideDiff !== this.showSideBySideDiff) {
|
||||
setShowSideBySideDiff(showSideBySideDiff)
|
||||
|
@ -7146,27 +7236,36 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
if (defaultBranch === null || tip.kind !== TipState.Valid) {
|
||||
return
|
||||
}
|
||||
|
||||
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 pullRequestCommits = await gitStore.getCommitsBetweenBranches(
|
||||
defaultBranch,
|
||||
baseBranch,
|
||||
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.
|
||||
const emptyChangeSet = { files: [], linesAdded: 0, linesDeleted: 0 }
|
||||
const changesetData =
|
||||
commitSHAs.length > 0
|
||||
commitsBetweenBranches.length > 0
|
||||
? await gitStore.performFailableOperation(() =>
|
||||
getBranchMergeBaseChangedFiles(
|
||||
repository,
|
||||
defaultBranch.name,
|
||||
baseBranch.name,
|
||||
currentBranch.name,
|
||||
commitSHAs[0]
|
||||
commitsBetweenBranches[0]
|
||||
)
|
||||
)
|
||||
: emptyChangeSet
|
||||
|
@ -7175,25 +7274,64 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
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, {
|
||||
baseBranch: defaultBranch,
|
||||
baseBranch,
|
||||
commitSHAs,
|
||||
commitSelection: {
|
||||
shas: commitSHAs,
|
||||
shasInDiff: commitSHAs,
|
||||
isContiguous: true,
|
||||
changesetData,
|
||||
changesetData: changesetData ?? emptyChangeSet,
|
||||
file: 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(
|
||||
repository,
|
||||
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(
|
||||
|
@ -7223,6 +7361,7 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
diff: null,
|
||||
})
|
||||
)
|
||||
|
||||
this.emitUpdate()
|
||||
|
||||
if (commitSHAs.length === 0) {
|
||||
|
@ -7240,7 +7379,7 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
file,
|
||||
baseBranch.name,
|
||||
currentBranch.name,
|
||||
this.hideWhitespaceInHistoryDiff,
|
||||
this.hideWhitespaceInPullRequestDiff,
|
||||
commitSHAs[0]
|
||||
)
|
||||
)) ?? null
|
||||
|
@ -7263,6 +7402,66 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
|
||||
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 */
|
||||
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()
|
||||
|
||||
/**
|
||||
|
|
|
@ -14,7 +14,7 @@ import { Commit, CommitOneLine, ICommitContext } from './commit'
|
|||
import { IStashEntry } from './stash-entry'
|
||||
import { Account } from '../models/account'
|
||||
import { Progress } from './progress'
|
||||
import { ITextDiff, DiffSelection } from './diff'
|
||||
import { ITextDiff, DiffSelection, ImageDiffType } from './diff'
|
||||
import { RepositorySettingsTab } from '../ui/repository-settings/repository-settings'
|
||||
import { ICommitMessage } from './commit-message'
|
||||
import { IAuthor } from './author'
|
||||
|
@ -362,4 +362,13 @@ export type Popup =
|
|||
}
|
||||
| {
|
||||
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 crypto from 'crypto'
|
||||
import { TransitionGroup, CSSTransition } from 'react-transition-group'
|
||||
import {
|
||||
IAppState,
|
||||
|
@ -158,6 +157,8 @@ import { SSHUserPassword } from './ssh/ssh-user-password'
|
|||
import { showContextualMenu } from '../lib/menu-item'
|
||||
import { UnreachableCommitsDialog } from './history/unreachable-commits-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 HourInMilliseconds = MinuteInMilliseconds * 60
|
||||
|
@ -1481,6 +1482,7 @@ export class App extends React.Component<IAppProps, IAppState> {
|
|||
confirmDiscardChangesPermanently={
|
||||
this.state.askForConfirmationOnDiscardChangesPermanently
|
||||
}
|
||||
confirmDiscardStash={this.state.askForConfirmationOnDiscardStash}
|
||||
confirmForcePush={this.state.askForConfirmationOnForcePush}
|
||||
confirmUndoCommit={this.state.askForConfirmationOnUndoCommit}
|
||||
uncommittedChangesStrategy={this.state.uncommittedChangesStrategy}
|
||||
|
@ -1849,6 +1851,9 @@ export class App extends React.Component<IAppProps, IAppState> {
|
|||
<ConfirmDiscardStashDialog
|
||||
key="confirm-discard-stash-dialog"
|
||||
dispatcher={this.props.dispatcher}
|
||||
askForConfirmationOnDiscardStash={
|
||||
this.state.askForConfirmationOnDiscardStash
|
||||
}
|
||||
repository={repository}
|
||||
stash={stash}
|
||||
onDismissed={onPopupDismissedFn}
|
||||
|
@ -2243,25 +2248,35 @@ export class App extends React.Component<IAppProps, IAppState> {
|
|||
)
|
||||
}
|
||||
case PopupType.StartPullRequest: {
|
||||
const { selectedState } = this.state
|
||||
if (
|
||||
selectedState == null ||
|
||||
selectedState.type !== SelectionType.Repository
|
||||
) {
|
||||
// Intentionally chose to get the current pull request state on
|
||||
// rerender because state variables such as file selection change
|
||||
// via the dispatcher.
|
||||
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
|
||||
}
|
||||
|
||||
const { state: repoState, repository } = selectedState
|
||||
const { pullRequestState, branchesState } = repoState
|
||||
if (
|
||||
pullRequestState === null ||
|
||||
branchesState.tip.kind !== TipState.Valid
|
||||
) {
|
||||
return null
|
||||
}
|
||||
const { allBranches, recentBranches, defaultBranch, tip } =
|
||||
branchesState
|
||||
const currentBranch = tip.branch
|
||||
const { pullRequestFilesListWidth, hideWhitespaceInPullRequestDiff } =
|
||||
this.state
|
||||
|
||||
const {
|
||||
allBranches,
|
||||
currentBranch,
|
||||
defaultBranch,
|
||||
imageDiffType,
|
||||
externalEditorLabel,
|
||||
nonLocalCommitSHA,
|
||||
recentBranches,
|
||||
repository,
|
||||
showSideBySideDiff,
|
||||
} = popup
|
||||
|
||||
return (
|
||||
<OpenPullRequestDialog
|
||||
|
@ -2270,9 +2285,15 @@ export class App extends React.Component<IAppProps, IAppState> {
|
|||
currentBranch={currentBranch}
|
||||
defaultBranch={defaultBranch}
|
||||
dispatcher={this.props.dispatcher}
|
||||
fileListWidth={pullRequestFilesListWidth}
|
||||
hideWhitespaceInDiff={hideWhitespaceInPullRequestDiff}
|
||||
imageDiffType={imageDiffType}
|
||||
nonLocalCommitSHA={nonLocalCommitSHA}
|
||||
pullRequestState={pullRequestState}
|
||||
recentBranches={recentBranches}
|
||||
repository={repository}
|
||||
externalEditorLabel={externalEditorLabel}
|
||||
showSideBySideDiff={showSideBySideDiff}
|
||||
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(
|
||||
onBegin: () => void,
|
||||
onPopupDismissedFn: () => void
|
||||
|
@ -2955,6 +2988,9 @@ export class App extends React.Component<IAppProps, IAppState> {
|
|||
askForConfirmationOnDiscardChanges={
|
||||
state.askForConfirmationOnDiscardChanges
|
||||
}
|
||||
askForConfirmationOnDiscardStash={
|
||||
state.askForConfirmationOnDiscardStash
|
||||
}
|
||||
accounts={state.accounts}
|
||||
externalEditorLabel={externalEditorLabel}
|
||||
resolvedExternalEditor={state.resolvedExternalEditor}
|
||||
|
@ -3049,22 +3085,17 @@ export class App extends React.Component<IAppProps, IAppState> {
|
|||
return
|
||||
}
|
||||
|
||||
const baseURL = repository.gitHubRepository.htmlURL
|
||||
|
||||
let fileSuffix = ''
|
||||
if (filePath != null) {
|
||||
const fileHash = crypto
|
||||
.createHash('sha256')
|
||||
.update(filePath)
|
||||
.digest('hex')
|
||||
fileSuffix = '#diff-' + fileHash
|
||||
}
|
||||
|
||||
if (baseURL) {
|
||||
this.props.dispatcher.openInBrowser(
|
||||
`${baseURL}/commit/${SHA}${fileSuffix}`
|
||||
const commitURL = createCommitURL(
|
||||
repository.gitHubRepository,
|
||||
SHA,
|
||||
filePath
|
||||
)
|
||||
|
||||
if (commitURL === null) {
|
||||
return
|
||||
}
|
||||
|
||||
this.props.dispatcher.openInBrowser(commitURL)
|
||||
}
|
||||
|
||||
private onBranchDeleted = (repository: Repository) => {
|
||||
|
|
|
@ -1,19 +1,12 @@
|
|||
import * as React from 'react'
|
||||
import { IMatches } from '../../lib/fuzzy-find'
|
||||
import { Branch } from '../../models/branch'
|
||||
import { Button } from '../lib/button'
|
||||
import { ClickSource } from '../lib/list'
|
||||
import { Popover } from '../lib/popover'
|
||||
import { Ref } from '../lib/ref'
|
||||
import { Octicon } from '../octicons'
|
||||
import * as OcticonSymbol from '../octicons/octicons.generated'
|
||||
import { PopoverDropdown } from '../lib/popover-dropdown'
|
||||
import { BranchList } from './branch-list'
|
||||
import { renderDefaultBranch } from './branch-renderer'
|
||||
import { IBranchListItem } from './group-branches'
|
||||
|
||||
const defaultDropdownListHeight = 300
|
||||
const maxDropdownListHeight = 500
|
||||
|
||||
interface IBranchSelectProps {
|
||||
/** The initially selected branch. */
|
||||
readonly branch: Branch
|
||||
|
@ -43,10 +36,8 @@ interface IBranchSelectProps {
|
|||
}
|
||||
|
||||
interface IBranchSelectState {
|
||||
readonly showBranchDropdown: boolean
|
||||
readonly selectedBranch: Branch | null
|
||||
readonly filterText: string
|
||||
readonly dropdownListHeight: number
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -56,67 +47,25 @@ export class BranchSelect extends React.Component<
|
|||
IBranchSelectProps,
|
||||
IBranchSelectState
|
||||
> {
|
||||
private invokeButtonRef: HTMLButtonElement | null = null
|
||||
private popoverRef = React.createRef<PopoverDropdown>()
|
||||
|
||||
public constructor(props: IBranchSelectProps) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
showBranchDropdown: false,
|
||||
selectedBranch: props.branch,
|
||||
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) => {
|
||||
return renderDefaultBranch(item, matches, this.props.currentBranch)
|
||||
}
|
||||
|
||||
private onItemClick = (branch: Branch, source: ClickSource) => {
|
||||
source.event.preventDefault()
|
||||
this.setState({ showBranchDropdown: false, selectedBranch: branch })
|
||||
this.popoverRef.current?.closePopover()
|
||||
this.setState({ selectedBranch: branch })
|
||||
this.props.onChange?.(branch)
|
||||
}
|
||||
|
||||
|
@ -124,34 +73,18 @@ export class BranchSelect extends React.Component<
|
|||
this.setState({ filterText })
|
||||
}
|
||||
|
||||
public renderBranchDropdown() {
|
||||
if (!this.state.showBranchDropdown) {
|
||||
return
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { currentBranch, defaultBranch, recentBranches, allBranches } =
|
||||
this.props
|
||||
|
||||
const { filterText, selectedBranch, dropdownListHeight } = this.state
|
||||
const { filterText, selectedBranch } = this.state
|
||||
|
||||
return (
|
||||
<Popover
|
||||
className="branch-select-dropdown"
|
||||
onClickOutside={this.closeBranchDropdown}
|
||||
>
|
||||
<div className="branch-select-dropdown-header">
|
||||
Choose a base branch
|
||||
<button
|
||||
className="close"
|
||||
onClick={this.closeBranchDropdown}
|
||||
aria-label="close"
|
||||
>
|
||||
<Octicon symbol={OcticonSymbol.x} />
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
className="branch-select-dropdown-list"
|
||||
style={{ height: `${dropdownListHeight}px` }}
|
||||
<PopoverDropdown
|
||||
contentTitle="Choose a base branch"
|
||||
buttonContent={selectedBranch?.name ?? ''}
|
||||
label="base:"
|
||||
ref={this.popoverRef}
|
||||
>
|
||||
<BranchList
|
||||
allBranches={allBranches}
|
||||
|
@ -165,26 +98,7 @@ export class BranchSelect extends React.Component<
|
|||
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>
|
||||
</PopoverDropdown>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,6 @@ import { Octicon, iconForStatus } from '../octicons'
|
|||
import * as OcticonSymbol from '../octicons/octicons.generated'
|
||||
import { mapStatus } from '../../lib/status'
|
||||
import { DiffOptions } from '../diff/diff-options'
|
||||
import { RepositorySectionTab } from '../../lib/app-state'
|
||||
|
||||
interface IChangedFileDetailsProps {
|
||||
readonly path: string
|
||||
|
@ -61,7 +60,7 @@ export class ChangedFileDetails extends React.Component<
|
|||
|
||||
return (
|
||||
<DiffOptions
|
||||
sourceTab={RepositorySectionTab.Changes}
|
||||
isInteractiveDiff={true}
|
||||
onHideWhitespaceChangesChanged={
|
||||
this.props.onHideWhitespaceInDiffChanged
|
||||
}
|
||||
|
|
|
@ -351,7 +351,7 @@ export class CommitMessage extends React.Component<
|
|||
if (
|
||||
isShortcutKey &&
|
||||
event.key === 'Enter' &&
|
||||
this.canCommit() &&
|
||||
(this.canCommit() || this.canAmend()) &&
|
||||
this.canExcecuteCommitShortcut()
|
||||
) {
|
||||
this.createCommit()
|
||||
|
|
|
@ -3,6 +3,7 @@ import * as React from 'react'
|
|||
import { Dispatcher } from '../dispatcher'
|
||||
import { getDefaultDir, setDefaultDir } from '../lib/default-dir'
|
||||
import { Account } from '../../models/account'
|
||||
import { FoldoutType } from '../../lib/app-state'
|
||||
import {
|
||||
IRepositoryIdentifier,
|
||||
parseRepositoryIdentifier,
|
||||
|
@ -728,6 +729,7 @@ export class CloneRepository extends React.Component<
|
|||
|
||||
const { url, defaultBranch } = cloneInfo
|
||||
|
||||
this.props.dispatcher.closeFoldout(FoldoutType.Repository)
|
||||
try {
|
||||
this.cloneImpl(url.trim(), path, defaultBranch)
|
||||
} catch (e) {
|
||||
|
|
|
@ -4,14 +4,13 @@ import { Octicon } from '../octicons'
|
|||
import * as OcticonSymbol from '../octicons/octicons.generated'
|
||||
import { RadioButton } from '../lib/radio-button'
|
||||
import { Popover, PopoverCaretPosition } from '../lib/popover'
|
||||
import { RepositorySectionTab } from '../../lib/app-state'
|
||||
|
||||
interface IDiffOptionsProps {
|
||||
readonly sourceTab: RepositorySectionTab
|
||||
readonly isInteractiveDiff: boolean
|
||||
readonly hideWhitespaceChanges: boolean
|
||||
readonly onHideWhitespaceChangesChanged: (
|
||||
hideWhitespaceChanges: boolean
|
||||
) => Promise<void>
|
||||
) => void
|
||||
|
||||
readonly showSideBySideDiff: boolean
|
||||
readonly onShowSideBySideDiffChanged: (showSideBySideDiff: boolean) => void
|
||||
|
@ -144,7 +143,7 @@ export class DiffOptions extends React.Component<
|
|||
__DARWIN__ ? 'Hide Whitespace Changes' : 'Hide whitespace changes'
|
||||
}
|
||||
/>
|
||||
{this.props.sourceTab === RepositorySectionTab.Changes && (
|
||||
{this.props.isInteractiveDiff && (
|
||||
<p className="secondary-text">
|
||||
Interacting with individual lines or hunks will be disabled while
|
||||
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 */
|
||||
public onShowSideBySideDiffChanged(showSideBySideDiff: boolean) {
|
||||
return this.appStore._setShowSideBySideDiff(showSideBySideDiff)
|
||||
|
@ -2338,6 +2351,10 @@ export class Dispatcher {
|
|||
await this.appStore._loadStatus(repository)
|
||||
}
|
||||
|
||||
public setConfirmDiscardStashSetting(value: boolean) {
|
||||
return this.appStore._setConfirmDiscardStashSetting(value)
|
||||
}
|
||||
|
||||
public setConfirmForcePushSetting(value: boolean) {
|
||||
return this.appStore._setConfirmForcePushSetting(value)
|
||||
}
|
||||
|
@ -3963,9 +3980,33 @@ export class Dispatcher {
|
|||
|
||||
public startPullRequest(repository: 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 { wrapRichTextCommitMessage } from '../../lib/wrap-rich-text-commit-message'
|
||||
import { DiffOptions } from '../diff/diff-options'
|
||||
import { RepositorySectionTab } from '../../lib/app-state'
|
||||
import { IChangesetData } from '../../lib/git'
|
||||
import { TooltippedContent } from '../lib/tooltipped-content'
|
||||
import { AppFileStatusKind } from '../../models/status'
|
||||
|
@ -505,7 +504,7 @@ export class CommitSummary extends React.Component<
|
|||
title="Diff Options"
|
||||
>
|
||||
<DiffOptions
|
||||
sourceTab={RepositorySectionTab.History}
|
||||
isInteractiveDiff={false}
|
||||
hideWhitespaceChanges={this.props.hideWhitespaceInDiff}
|
||||
onHideWhitespaceChangesChanged={
|
||||
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) => {
|
||||
this.updateMouseRect(event)
|
||||
|
||||
this.mouseOverTarget = true
|
||||
this.cancelHideTooltip()
|
||||
if (!this.state.show) {
|
||||
|
@ -308,7 +314,7 @@ export class Tooltip<T extends TooltipTarget> extends React.Component<
|
|||
}
|
||||
|
||||
private onTargetMouseMove = (event: MouseEvent) => {
|
||||
this.mouseRect = new DOMRect(event.clientX - 10, event.clientY - 10, 20, 20)
|
||||
this.updateMouseRect(event)
|
||||
}
|
||||
|
||||
private onTargetMouseDown = (event: MouseEvent) => {
|
||||
|
|
|
@ -93,6 +93,8 @@ export abstract class BaseMultiCommitOperation extends React.Component<IMultiCom
|
|||
{targetBranch !== null ? <strong>{targetBranch.name}</strong> : null}
|
||||
</>
|
||||
)
|
||||
|
||||
this.props.dispatcher.closePopup(PopupType.MultiCommitOperation)
|
||||
return dispatcher.onConflictsFoundBanner(
|
||||
repository,
|
||||
operationDescription,
|
||||
|
|
|
@ -1,10 +1,18 @@
|
|||
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 { ImageDiffType } from '../../models/diff'
|
||||
import { Repository } from '../../models/repository'
|
||||
import { DialogFooter, OkCancelButtonGroup, Dialog } from '../dialog'
|
||||
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 { PullRequestFilesChanged } from './pull-request-files-changed'
|
||||
import { PullRequestMergeStatus } from './pull-request-merge-status'
|
||||
import { ComputedAction } from '../../models/computed-action'
|
||||
|
||||
interface IOpenPullRequestDialogProps {
|
||||
readonly repository: Repository
|
||||
|
@ -35,6 +43,25 @@ interface IOpenPullRequestDialogProps {
|
|||
*/
|
||||
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 */
|
||||
readonly onDismissed: () => void
|
||||
}
|
||||
|
@ -47,6 +74,11 @@ export class OpenPullRequestDialog extends React.Component<IOpenPullRequestDialo
|
|||
this.props.dispatcher.recordCreatePullRequest()
|
||||
}
|
||||
|
||||
private onBranchChange = (branch: Branch) => {
|
||||
const { repository } = this.props
|
||||
this.props.dispatcher.updatePullRequestBaseBranch(repository, branch)
|
||||
}
|
||||
|
||||
private renderHeader() {
|
||||
const {
|
||||
currentBranch,
|
||||
|
@ -64,22 +96,107 @@ export class OpenPullRequestDialog extends React.Component<IOpenPullRequestDialo
|
|||
allBranches={allBranches}
|
||||
recentBranches={recentBranches}
|
||||
commitCount={commitSHAs?.length ?? 0}
|
||||
onBranchChange={this.onBranchChange}
|
||||
onDismissed={this.props.onDismissed}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
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() {
|
||||
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 (
|
||||
<DialogFooter>
|
||||
<PullRequestMergeStatus mergeStatus={mergeStatus} />
|
||||
<OkCancelButtonGroup
|
||||
okButtonText="Create Pull Request"
|
||||
okButtonTitle="Create pull request on GitHub."
|
||||
okButtonText={
|
||||
__DARWIN__ ? 'Create Pull Request' : 'Create pull request'
|
||||
}
|
||||
okButtonTitle={buttonTitle}
|
||||
cancelButtonText="Cancel"
|
||||
okButtonDisabled={commitSHAs === null || commitSHAs.length === 0}
|
||||
/>
|
||||
</DialogFooter>
|
||||
)
|
||||
|
@ -93,8 +210,7 @@ export class OpenPullRequestDialog extends React.Component<IOpenPullRequestDialo
|
|||
onDismissed={this.props.onDismissed}
|
||||
>
|
||||
{this.renderHeader()}
|
||||
<div className="content">{this.renderContent()}</div>
|
||||
|
||||
{this.renderContent()}
|
||||
{this.renderFooter()}
|
||||
</Dialog>
|
||||
)
|
||||
|
|
|
@ -30,6 +30,9 @@ interface IOpenPullRequestDialogHeaderProps {
|
|||
/** The count of commits of the pull request */
|
||||
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
|
||||
* ways described in the dismissable prop.
|
||||
|
@ -54,6 +57,7 @@ export class OpenPullRequestDialogHeader extends React.Component<
|
|||
allBranches,
|
||||
recentBranches,
|
||||
commitCount,
|
||||
onBranchChange,
|
||||
onDismissed,
|
||||
} = this.props
|
||||
const commits = `${commitCount} commit${commitCount > 1 ? 's' : ''}`
|
||||
|
@ -74,6 +78,7 @@ export class OpenPullRequestDialogHeader extends React.Component<
|
|||
currentBranch={currentBranch}
|
||||
allBranches={allBranches}
|
||||
recentBranches={recentBranches}
|
||||
onChange={onBranchChange}
|
||||
/>{' '}
|
||||
from <Ref>{currentBranch.name}</Ref>.
|
||||
</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 confirmDiscardChanges: boolean
|
||||
readonly confirmDiscardChangesPermanently: boolean
|
||||
readonly confirmDiscardStash: boolean
|
||||
readonly confirmForcePush: boolean
|
||||
readonly confirmUndoCommit: boolean
|
||||
readonly uncommittedChangesStrategy: UncommittedChangesStrategy
|
||||
|
@ -79,6 +80,7 @@ interface IPreferencesState {
|
|||
readonly confirmRepositoryRemoval: boolean
|
||||
readonly confirmDiscardChanges: boolean
|
||||
readonly confirmDiscardChangesPermanently: boolean
|
||||
readonly confirmDiscardStash: boolean
|
||||
readonly confirmForcePush: boolean
|
||||
readonly confirmUndoCommit: boolean
|
||||
readonly uncommittedChangesStrategy: UncommittedChangesStrategy
|
||||
|
@ -121,6 +123,7 @@ export class Preferences extends React.Component<
|
|||
confirmRepositoryRemoval: false,
|
||||
confirmDiscardChanges: false,
|
||||
confirmDiscardChangesPermanently: false,
|
||||
confirmDiscardStash: false,
|
||||
confirmForcePush: false,
|
||||
confirmUndoCommit: false,
|
||||
uncommittedChangesStrategy: defaultUncommittedChangesStrategy,
|
||||
|
@ -178,6 +181,7 @@ export class Preferences extends React.Component<
|
|||
confirmDiscardChanges: this.props.confirmDiscardChanges,
|
||||
confirmDiscardChangesPermanently:
|
||||
this.props.confirmDiscardChangesPermanently,
|
||||
confirmDiscardStash: this.props.confirmDiscardStash,
|
||||
confirmForcePush: this.props.confirmForcePush,
|
||||
confirmUndoCommit: this.props.confirmUndoCommit,
|
||||
uncommittedChangesStrategy: this.props.uncommittedChangesStrategy,
|
||||
|
@ -333,12 +337,14 @@ export class Preferences extends React.Component<
|
|||
confirmDiscardChangesPermanently={
|
||||
this.state.confirmDiscardChangesPermanently
|
||||
}
|
||||
confirmDiscardStash={this.state.confirmDiscardStash}
|
||||
confirmForcePush={this.state.confirmForcePush}
|
||||
confirmUndoCommit={this.state.confirmUndoCommit}
|
||||
onConfirmRepositoryRemovalChanged={
|
||||
this.onConfirmRepositoryRemovalChanged
|
||||
}
|
||||
onConfirmDiscardChangesChanged={this.onConfirmDiscardChangesChanged}
|
||||
onConfirmDiscardStashChanged={this.onConfirmDiscardStashChanged}
|
||||
onConfirmForcePushChanged={this.onConfirmForcePushChanged}
|
||||
onConfirmDiscardChangesPermanentlyChanged={
|
||||
this.onConfirmDiscardChangesPermanentlyChanged
|
||||
|
@ -410,6 +416,10 @@ export class Preferences extends React.Component<
|
|||
this.setState({ confirmDiscardChanges: value })
|
||||
}
|
||||
|
||||
private onConfirmDiscardStashChanged = (value: boolean) => {
|
||||
this.setState({ confirmDiscardStash: value })
|
||||
}
|
||||
|
||||
private onConfirmDiscardChangesPermanentlyChanged = (value: boolean) => {
|
||||
this.setState({ confirmDiscardChangesPermanently: value })
|
||||
}
|
||||
|
@ -562,6 +572,10 @@ export class Preferences extends React.Component<
|
|||
this.state.confirmForcePush
|
||||
)
|
||||
|
||||
await this.props.dispatcher.setConfirmDiscardStashSetting(
|
||||
this.state.confirmDiscardStash
|
||||
)
|
||||
|
||||
await this.props.dispatcher.setConfirmUndoCommitSetting(
|
||||
this.state.confirmUndoCommit
|
||||
)
|
||||
|
|
|
@ -6,10 +6,12 @@ interface IPromptsPreferencesProps {
|
|||
readonly confirmRepositoryRemoval: boolean
|
||||
readonly confirmDiscardChanges: boolean
|
||||
readonly confirmDiscardChangesPermanently: boolean
|
||||
readonly confirmDiscardStash: boolean
|
||||
readonly confirmForcePush: boolean
|
||||
readonly confirmUndoCommit: boolean
|
||||
readonly onConfirmDiscardChangesChanged: (checked: boolean) => void
|
||||
readonly onConfirmDiscardChangesPermanentlyChanged: (checked: boolean) => void
|
||||
readonly onConfirmDiscardStashChanged: (checked: boolean) => void
|
||||
readonly onConfirmRepositoryRemovalChanged: (checked: boolean) => void
|
||||
readonly onConfirmForcePushChanged: (checked: boolean) => void
|
||||
readonly onConfirmUndoCommitChanged: (checked: boolean) => void
|
||||
|
@ -19,6 +21,7 @@ interface IPromptsPreferencesState {
|
|||
readonly confirmRepositoryRemoval: boolean
|
||||
readonly confirmDiscardChanges: boolean
|
||||
readonly confirmDiscardChangesPermanently: boolean
|
||||
readonly confirmDiscardStash: boolean
|
||||
readonly confirmForcePush: boolean
|
||||
readonly confirmUndoCommit: boolean
|
||||
}
|
||||
|
@ -35,6 +38,7 @@ export class Prompts extends React.Component<
|
|||
confirmDiscardChanges: this.props.confirmDiscardChanges,
|
||||
confirmDiscardChangesPermanently:
|
||||
this.props.confirmDiscardChangesPermanently,
|
||||
confirmDiscardStash: this.props.confirmDiscardStash,
|
||||
confirmForcePush: this.props.confirmForcePush,
|
||||
confirmUndoCommit: this.props.confirmUndoCommit,
|
||||
}
|
||||
|
@ -58,6 +62,15 @@ export class Prompts extends React.Component<
|
|||
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 = (
|
||||
event: React.FormEvent<HTMLInputElement>
|
||||
) => {
|
||||
|
@ -116,6 +129,15 @@ export class Prompts extends React.Component<
|
|||
}
|
||||
onChange={this.onConfirmDiscardChangesPermanentlyChanged}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Discarding stash"
|
||||
value={
|
||||
this.state.confirmDiscardStash
|
||||
? CheckboxValue.On
|
||||
: CheckboxValue.Off
|
||||
}
|
||||
onChange={this.onConfirmDiscardStashChanged}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Force pushing"
|
||||
value={
|
||||
|
|
|
@ -50,6 +50,7 @@ interface IRepositoryViewProps {
|
|||
readonly hideWhitespaceInHistoryDiff: boolean
|
||||
readonly showSideBySideDiff: boolean
|
||||
readonly askForConfirmationOnDiscardChanges: boolean
|
||||
readonly askForConfirmationOnDiscardStash: boolean
|
||||
readonly focusCommitMessage: boolean
|
||||
readonly commitSpellcheckEnabled: boolean
|
||||
readonly accounts: ReadonlyArray<Account>
|
||||
|
@ -355,6 +356,9 @@ export class RepositoryView extends React.Component<
|
|||
fileListWidth={this.props.stashedFilesWidth}
|
||||
repository={this.props.repository}
|
||||
dispatcher={this.props.dispatcher}
|
||||
askForConfirmationOnDiscardStash={
|
||||
this.props.askForConfirmationOnDiscardStash
|
||||
}
|
||||
isWorkingTreeClean={isWorkingTreeClean}
|
||||
showSideBySideDiff={this.props.showSideBySideDiff}
|
||||
onOpenBinaryFile={this.onOpenBinaryFile}
|
||||
|
|
|
@ -5,16 +5,19 @@ import { Dispatcher } from '../dispatcher'
|
|||
import { Row } from '../lib/row'
|
||||
import { IStashEntry } from '../../models/stash-entry'
|
||||
import { OkCancelButtonGroup } from '../dialog/ok-cancel-button-group'
|
||||
import { Checkbox, CheckboxValue } from '../lib/checkbox'
|
||||
|
||||
interface IConfirmDiscardStashProps {
|
||||
readonly dispatcher: Dispatcher
|
||||
readonly repository: Repository
|
||||
readonly stash: IStashEntry
|
||||
readonly askForConfirmationOnDiscardStash: boolean
|
||||
readonly onDismissed: () => void
|
||||
}
|
||||
|
||||
interface IConfirmDiscardStashState {
|
||||
readonly isDiscarding: boolean
|
||||
readonly confirmDiscardStash: boolean
|
||||
}
|
||||
/**
|
||||
* Dialog to confirm dropping a stash
|
||||
|
@ -28,6 +31,7 @@ export class ConfirmDiscardStashDialog extends React.Component<
|
|||
|
||||
this.state = {
|
||||
isDiscarding: false,
|
||||
confirmDiscardStash: props.askForConfirmationOnDiscardStash,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -46,6 +50,17 @@ export class ConfirmDiscardStashDialog extends React.Component<
|
|||
>
|
||||
<DialogContent>
|
||||
<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>
|
||||
<DialogFooter>
|
||||
<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 () => {
|
||||
const { dispatcher, repository, stash, onDismissed } = this.props
|
||||
|
||||
|
@ -62,6 +85,7 @@ export class ConfirmDiscardStashDialog extends React.Component<
|
|||
})
|
||||
|
||||
try {
|
||||
dispatcher.setConfirmDiscardStashSetting(this.state.confirmDiscardStash)
|
||||
await dispatcher.dropStash(repository, stash)
|
||||
} finally {
|
||||
this.setState({
|
||||
|
|
|
@ -11,11 +11,13 @@ interface IStashDiffHeaderProps {
|
|||
readonly stashEntry: IStashEntry
|
||||
readonly repository: Repository
|
||||
readonly dispatcher: Dispatcher
|
||||
readonly askForConfirmationOnDiscardStash: boolean
|
||||
readonly isWorkingTreeClean: boolean
|
||||
}
|
||||
|
||||
interface IStashDiffHeaderState {
|
||||
readonly isRestoring: boolean
|
||||
readonly isDiscarding: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -31,12 +33,13 @@ export class StashDiffHeader extends React.Component<
|
|||
|
||||
this.state = {
|
||||
isRestoring: false,
|
||||
isDiscarding: false,
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { isWorkingTreeClean } = this.props
|
||||
const { isRestoring } = this.state
|
||||
const { isRestoring, isDiscarding } = this.state
|
||||
|
||||
return (
|
||||
<div className="header">
|
||||
|
@ -44,10 +47,12 @@ export class StashDiffHeader extends React.Component<
|
|||
<div className="row">
|
||||
<OkCancelButtonGroup
|
||||
okButtonText="Restore"
|
||||
okButtonDisabled={isRestoring || !isWorkingTreeClean}
|
||||
okButtonDisabled={
|
||||
isRestoring || !isWorkingTreeClean || isDiscarding
|
||||
}
|
||||
onOkButtonClick={this.onRestoreClick}
|
||||
cancelButtonText="Discard"
|
||||
cancelButtonDisabled={isRestoring}
|
||||
cancelButtonDisabled={isRestoring || isDiscarding}
|
||||
onCancelButtonClick={this.onDiscardClick}
|
||||
/>
|
||||
{this.renderExplanatoryText()}
|
||||
|
@ -80,14 +85,34 @@ export class StashDiffHeader extends React.Component<
|
|||
)
|
||||
}
|
||||
|
||||
private onDiscardClick = () => {
|
||||
const { dispatcher, repository, stashEntry } = this.props
|
||||
private onDiscardClick = async () => {
|
||||
const {
|
||||
dispatcher,
|
||||
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 () => {
|
||||
const { dispatcher, repository, stashEntry } = this.props
|
||||
|
|
|
@ -27,6 +27,9 @@ interface IStashDiffViewerProps {
|
|||
readonly repository: Repository
|
||||
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. */
|
||||
readonly showSideBySideDiff: boolean
|
||||
|
||||
|
@ -113,6 +116,9 @@ export class StashDiffViewer extends React.PureComponent<IStashDiffViewerProps>
|
|||
repository={repository}
|
||||
dispatcher={dispatcher}
|
||||
isWorkingTreeClean={isWorkingTreeClean}
|
||||
askForConfirmationOnDiscardStash={
|
||||
this.props.askForConfirmationOnDiscardStash
|
||||
}
|
||||
/>
|
||||
<div className="commit-details">
|
||||
<Resizable
|
||||
|
|
|
@ -99,4 +99,7 @@
|
|||
@import 'ui/_pull-request-quick-view';
|
||||
@import 'ui/discard-changes-retry';
|
||||
@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 {
|
||||
width: 850px;
|
||||
max-width: none;
|
||||
|
||||
header.dialog-header {
|
||||
padding-bottom: var(--spacing);
|
||||
|
||||
|
@ -15,4 +18,25 @@
|
|||
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;
|
||||
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
overflow-wrap: break-word;
|
||||
|
||||
background-color: var(--tooltip-background-color);
|
||||
|
|
|
@ -575,9 +575,38 @@ describe('git/diff', () => {
|
|||
'feature-branch',
|
||||
'irrelevantToTest'
|
||||
)
|
||||
|
||||
expect(changesetData).not.toBeNull()
|
||||
if (changesetData === null) {
|
||||
return
|
||||
}
|
||||
|
||||
expect(changesetData.files).toHaveLength(1)
|
||||
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', () => {
|
||||
|
|
|
@ -1,6 +1,12 @@
|
|||
{
|
||||
"releases": {
|
||||
"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": [
|
||||
"[Fixed] App correctly remembers undo commit prompt setting - #15408"
|
||||
],
|
||||
|
|
|
@ -19,13 +19,13 @@ versions look similar to the below output:
|
|||
|
||||
```shellsession
|
||||
$ node -v
|
||||
v10.15.4
|
||||
v16.13.0
|
||||
|
||||
$ yarn -v
|
||||
1.15.2
|
||||
1.21.1
|
||||
|
||||
$ python --version
|
||||
Python 2.7.15
|
||||
Python 3.9.x
|
||||
```
|
||||
|
||||
There are also [additional resources](tooling.md) to configure your favorite
|
||||
|
|
|
@ -102,7 +102,11 @@ function packageWindows() {
|
|||
}
|
||||
|
||||
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()) {
|
||||
|
|
Loading…
Reference in a new issue