Merge branch 'development' into releases/3.1.2

This commit is contained in:
Sergio Padrino 2022-10-20 10:39:33 +02:00 committed by GitHub
commit 9a89aa1eed
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 1350 additions and 283 deletions

View file

@ -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:

View file

@ -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

View file

@ -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
View 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}`
}

View file

@ -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> {

View file

@ -110,5 +110,5 @@ export function enableSubmoduleDiff(): boolean {
/** Should we enable starting pull requests? */
export function enableStartingPullRequests(): boolean {
return enableDevelopmentFeatures()
return enableBetaFeatures()
}

View file

@ -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
)
}

View file

@ -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 = () => {
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
.finally(() => {
this.currentMergeTreePromise = null
})
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()
}
)
}
}
/**

View file

@ -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()
/**

View file

@ -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
}

View file

@ -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
const commitURL = createCommitURL(
repository.gitHubRepository,
SHA,
filePath
)
let fileSuffix = ''
if (filePath != null) {
const fileHash = crypto
.createHash('sha256')
.update(filePath)
.digest('hex')
fileSuffix = '#diff-' + fileHash
if (commitURL === null) {
return
}
if (baseURL) {
this.props.dispatcher.openInBrowser(
`${baseURL}/commit/${SHA}${fileSuffix}`
)
}
this.props.dispatcher.openInBrowser(commitURL)
}
private onBranchDeleted = (repository: Repository) => {

View file

@ -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,67 +73,32 @@ 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}
<PopoverDropdown
contentTitle="Choose a base branch"
buttonContent={selectedBranch?.name ?? ''}
label="base:"
ref={this.popoverRef}
>
<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` }}
>
<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>
<BranchList
allBranches={allBranches}
currentBranch={currentBranch}
defaultBranch={defaultBranch}
recentBranches={recentBranches}
filterText={filterText}
onFilterTextChanged={this.onFilterTextChanged}
selectedBranch={selectedBranch}
canCreateNewBranch={false}
renderBranch={this.renderBranch}
onItemClick={this.onItemClick}
/>
</PopoverDropdown>
)
}
}

View file

@ -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
}

View file

@ -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()

View file

@ -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) {

View file

@ -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.

View file

@ -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)
}
}

View file

@ -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

View 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>
)
}
}

View file

@ -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) => {

View file

@ -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,

View file

@ -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>
)

View file

@ -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>

View 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>
)
}
}

View 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&hellip;</strong> Dont 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>{' '}
Dont 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>
)
}
}

View file

@ -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
)

View file

@ -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={

View file

@ -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}

View file

@ -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({

View file

@ -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,13 +85,33 @@ export class StashDiffHeader extends React.Component<
)
}
private onDiscardClick = () => {
const { dispatcher, repository, stashEntry } = this.props
dispatcher.showPopup({
type: PopupType.ConfirmDiscardStash,
stash: stashEntry,
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 () => {

View file

@ -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

View file

@ -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';

View file

@ -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;
}
}
}

View 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;
}
}
}

View 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);
}
}

View 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);
}
}
}

View file

@ -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;
}
}

View file

@ -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);

View file

@ -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', () => {

View file

@ -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"
],

View file

@ -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

View file

@ -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()) {