From c1da7f1db7ee12802ac524cfc2602e2c978be878 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 5 Jun 2018 14:27:35 -0400 Subject: [PATCH 0001/4252] :art: spelling --- lib/models/repository-states/present.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js index bd8c2fb7c9..44ec3099e9 100644 --- a/lib/models/repository-states/present.js +++ b/lib/models/repository-states/present.js @@ -787,7 +787,7 @@ function buildHunksFromDiff(diff) { } else if (status === 'nonewline') { hunkLine = new HunkLine(text.substr(1), status, -1, -1, diffLineNumber++); } else { - throw new Error(`unknow status type: ${status}`); + throw new Error(`unknown status type: ${status}`); } return hunkLine; }); From d83ebfed9a85b9724ed58990a63003368b098d8e Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 6 Jun 2018 08:36:42 -0400 Subject: [PATCH 0002/4252] Trigger refModel after the has been initialized --- lib/atom/atom-text-editor.js | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/lib/atom/atom-text-editor.js b/lib/atom/atom-text-editor.js index 73139403e3..ab7eedf561 100644 --- a/lib/atom/atom-text-editor.js +++ b/lib/atom/atom-text-editor.js @@ -61,10 +61,6 @@ export default class AtomTextEditor extends React.PureComponent { this.refElement = new RefHolder(); this.refModel = new RefHolder(); - - this.subs.add( - this.refElement.observe(element => this.refModel.setter(element.getModel())), - ); } render() { @@ -81,11 +77,19 @@ export default class AtomTextEditor extends React.PureComponent { componentDidMount() { this.setAttributesOnElement(this.props); - const editor = this.getModel(); - this.subs.add( - editor.onDidChange(this.didChange), - editor.onDidChangeCursorPosition(this.didChangeCursorPosition), - ); + this.refElement.map(element => { + const editor = element.getModel(); + + this.refModel.setter(editor); + + this.subs.add( + editor.onDidChange(this.didChange), + editor.onDidChangeCursorPosition(this.didChangeCursorPosition), + ); + + // shhh, eslint. shhhh + return null; + }); } componentDidUpdate(prevProps) { From f04bb7d18b5c18f64719a7f22f7e4a5c06627166 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 6 Jun 2018 09:14:30 -0400 Subject: [PATCH 0003/4252] It begins --- lib/containers/file-patch-container.js | 53 ++ lib/controllers/file-patch-controller.js | 543 +------------- lib/controllers/file-patch-controller.old.js | 550 ++++++++++++++ lib/controllers/root-controller.js | 24 +- lib/items/file-patch-item.js | 91 +++ lib/views/file-patch-view.js | 717 ++----------------- lib/views/file-patch-view.old.js | 716 ++++++++++++++++++ 7 files changed, 1462 insertions(+), 1232 deletions(-) create mode 100644 lib/containers/file-patch-container.js create mode 100644 lib/controllers/file-patch-controller.old.js create mode 100644 lib/items/file-patch-item.js create mode 100644 lib/views/file-patch-view.old.js diff --git a/lib/containers/file-patch-container.js b/lib/containers/file-patch-container.js new file mode 100644 index 0000000000..9e5d5a4eee --- /dev/null +++ b/lib/containers/file-patch-container.js @@ -0,0 +1,53 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import yubikiri from 'yubikiri'; + +import {autobind} from '../helpers'; +import ObserveModel from '../views/observe-model'; +import FilePatchController from '../controllers/file-patch-controller'; + +export default class FilePatchContainer extends React.Component { + static propTypes = { + repository: PropTypes.object.isRequired, + stagingStatus: PropTypes.oneOf(['staged', 'unstaged']), + relPath: PropTypes.string.isRequired, + + tooltips: PropTypes.object.isRequired, + } + + constructor(props) { + super(props); + + autobind(this, 'fetchData', 'renderWithData'); + } + + fetchData(repository) { + return yubikiri({ + filePatch: repository.getFilePatchForPath(this.props.relPath, {staged: this.props.stagingStatus === 'staged'}), + isPartiallyStaged: repository.isPartiallyStaged(this.props.relPath), + }); + } + + render() { + return ( + + {this.renderWithData} + + ); + } + + renderWithData(data) { + if (data === null) { + return null; + } + + return ( + + ); + } +} diff --git a/lib/controllers/file-patch-controller.js b/lib/controllers/file-patch-controller.js index 90d6a67df8..3df858ab0a 100644 --- a/lib/controllers/file-patch-controller.js +++ b/lib/controllers/file-patch-controller.js @@ -1,550 +1,9 @@ -import path from 'path'; import React from 'react'; -import PropTypes from 'prop-types'; -import {Point} from 'atom'; -import {Emitter, CompositeDisposable} from 'event-kit'; -import Switchboard from '../switchboard'; import FilePatchView from '../views/file-patch-view'; -import ModelObserver from '../models/model-observer'; -import {autobind} from '../helpers'; export default class FilePatchController extends React.Component { - static propTypes = { - largeDiffByteThreshold: PropTypes.number, - getRepositoryForWorkdir: PropTypes.func.isRequired, - workingDirectoryPath: PropTypes.string.isRequired, - commandRegistry: PropTypes.object.isRequired, - deserializers: PropTypes.object.isRequired, - tooltips: PropTypes.object.isRequired, - filePath: PropTypes.string.isRequired, - lineNumber: PropTypes.number, - initialStagingStatus: PropTypes.oneOf(['unstaged', 'staged']).isRequired, - discardLines: PropTypes.func.isRequired, - didSurfaceFile: PropTypes.func.isRequired, - quietlySelectItem: PropTypes.func.isRequired, - undoLastDiscard: PropTypes.func.isRequired, - openFiles: PropTypes.func.isRequired, - switchboard: PropTypes.instanceOf(Switchboard), - } - - static defaultProps = { - largeDiffByteThreshold: 32768, - switchboard: new Switchboard(), - } - - static uriPattern = 'atom-github://file-patch/{relpath...}?workdir={workdir}&stagingStatus={stagingStatus}' - - static buildURI(relPath, workdir, stagingStatus) { - return 'atom-github://file-patch/' + - relPath + - `?workdir=${encodeURIComponent(workdir)}` + - `&stagingStatus=${encodeURIComponent(stagingStatus)}`; - } - - static confirmedLargeFilePatches = new Set() - - static resetConfirmedLargeFilePatches() { - this.confirmedLargeFilePatches = new Set(); - } - - constructor(props, context) { - super(props, context); - autobind( - this, - 'onRepoRefresh', 'handleShowDiffClick', 'attemptHunkStageOperation', 'attemptFileStageOperation', - 'attemptModeStageOperation', 'attemptSymlinkStageOperation', 'attemptLineStageOperation', 'didSurfaceFile', - 'diveIntoCorrespondingFilePatch', 'focus', 'openCurrentFile', 'discardLines', 'undoLastDiscard', 'hasUndoHistory', - ); - - this.stagingOperationInProgress = false; - this.emitter = new Emitter(); - - this.state = { - filePatch: null, - stagingStatus: props.initialStagingStatus, - isPartiallyStaged: false, - }; - - this.repositoryObserver = new ModelObserver({ - didUpdate: repo => this.onRepoRefresh(repo), - }); - this.repositoryObserver.setActiveModel(props.getRepositoryForWorkdir(this.props.workingDirectoryPath)); - - this.filePatchLoadedPromise = new Promise(res => { - this.resolveFilePatchLoadedPromise = res; - }); - - this.subscriptions = new CompositeDisposable(); - this.subscriptions.add( - this.props.switchboard.onDidFinishActiveContextUpdate(() => { - this.repositoryObserver.setActiveModel(this.props.getRepositoryForWorkdir(this.props.workingDirectoryPath)); - }), - ); - } - - getFilePatchLoadedPromise() { - return this.filePatchLoadedPromise; - } - - getStagingStatus() { - return this.state.stagingStatus; - } - - getFilePath() { - return this.props.filePath; - } - - getWorkingDirectory() { - return this.props.workingDirectoryPath; - } - - getTitle() { - let title = this.isStaged() ? 'Staged' : 'Unstaged'; - title += ' Changes: '; - title += this.props.filePath; - return title; - } - - serialize() { - return { - deserializer: 'FilePatchControllerStub', - uri: this.getURI(), - }; - } - - onDidDestroy(callback) { - return this.emitter.on('did-destroy', callback); - } - - terminatePendingState() { - if (!this.hasTerminatedPendingState) { - this.emitter.emit('did-terminate-pending-state'); - this.hasTerminatedPendingState = true; - } - } - - onDidTerminatePendingState(callback) { - return this.emitter.on('did-terminate-pending-state', callback); - } - - async onRepoRefresh(repository) { - const staged = this.isStaged(); - let filePatch = await this.getFilePatchForPath(this.props.filePath, staged); - const isPartiallyStaged = await repository.isPartiallyStaged(this.props.filePath); - - const onFinish = () => { - this.props.switchboard.didFinishRepositoryRefresh(); - }; - - if (filePatch) { - this.resolveFilePatchLoadedPromise(); - if (!this.destroyed) { - this.setState({filePatch, isPartiallyStaged}, onFinish); - } else { - onFinish(); - } - } else { - const oldFilePatch = this.state.filePatch; - if (oldFilePatch) { - filePatch = oldFilePatch.clone({ - oldFile: oldFilePatch.oldFile.clone({mode: null, symlink: null}), - newFile: oldFilePatch.newFile.clone({mode: null, symlink: null}), - patch: oldFilePatch.getPatch().clone({hunks: []}), - }); - if (!this.destroyed) { - this.setState({filePatch, isPartiallyStaged}, onFinish); - } else { - onFinish(); - } - } - } - } - - getFilePatchForPath(filePath, staged) { - const repository = this.repositoryObserver.getActiveModel(); - return repository.getFilePatchForPath(filePath, {staged}); - } - - componentDidUpdate(_prevProps, prevState) { - if (prevState.stagingStatus !== this.state.stagingStatus) { - this.emitter.emit('did-change-title'); - } - } - - goToDiffLine(lineNumber) { - this.filePatchView.goToDiffLine(lineNumber); - } - - componentWillUnmount() { - this.destroy(); - } - render() { - const fp = this.state.filePatch; - const hunks = fp ? fp.getHunks() : []; - const executableModeChange = fp && fp.didChangeExecutableMode() ? - {oldMode: fp.getOldMode(), newMode: fp.getNewMode()} : - null; - const symlinkChange = fp && fp.hasSymlink() ? - { - oldSymlink: fp.getOldSymlink(), - newSymlink: fp.getNewSymlink(), - typechange: fp.hasTypechange(), - filePatchStatus: fp.getStatus(), - } : null; - const repository = this.repositoryObserver.getActiveModel(); - if (repository.isUndetermined() || repository.isLoading()) { - return ( -
- -
- ); - } else if (repository.isAbsent()) { - return ( -
- - The repository for {this.props.workingDirectoryPath} is not open in Atom. - -
- ); - } else { - const hasUndoHistory = repository ? this.hasUndoHistory() : false; - return ( -
- { this.filePatchView = c; }} - commandRegistry={this.props.commandRegistry} - tooltips={this.props.tooltips} - displayLargeDiffMessage={!this.shouldDisplayLargeDiff(this.state.filePatch)} - byteCount={this.byteCount} - handleShowDiffClick={this.handleShowDiffClick} - hunks={hunks} - executableModeChange={executableModeChange} - symlinkChange={symlinkChange} - filePath={this.props.filePath} - workingDirectoryPath={this.getWorkingDirectory()} - stagingStatus={this.state.stagingStatus} - isPartiallyStaged={this.state.isPartiallyStaged} - attemptLineStageOperation={this.attemptLineStageOperation} - attemptHunkStageOperation={this.attemptHunkStageOperation} - attemptFileStageOperation={this.attemptFileStageOperation} - attemptModeStageOperation={this.attemptModeStageOperation} - attemptSymlinkStageOperation={this.attemptSymlinkStageOperation} - didSurfaceFile={this.didSurfaceFile} - didDiveIntoCorrespondingFilePatch={this.diveIntoCorrespondingFilePatch} - switchboard={this.props.switchboard} - openCurrentFile={this.openCurrentFile} - discardLines={this.discardLines} - undoLastDiscard={this.undoLastDiscard} - hasUndoHistory={hasUndoHistory} - lineNumber={this.props.lineNumber} - /> -
- ); - } - } - - shouldDisplayLargeDiff(filePatch) { - if (!filePatch) { return true; } - - const fullPath = path.join(this.getWorkingDirectory(), this.props.filePath); - if (FilePatchController.confirmedLargeFilePatches.has(fullPath)) { - return true; - } - - this.byteCount = filePatch.getByteSize(); - return this.byteCount < this.props.largeDiffByteThreshold; - } - - onDidChangeTitle(callback) { - return this.emitter.on('did-change-title', callback); - } - - handleShowDiffClick() { - if (this.repositoryObserver.getActiveModel()) { - const fullPath = path.join(this.getWorkingDirectory(), this.props.filePath); - FilePatchController.confirmedLargeFilePatches.add(fullPath); - this.forceUpdate(); - } - } - - async stageHunk(hunk) { - this.props.switchboard.didBeginStageOperation({stage: true, hunk: true}); - - await this.repositoryObserver.getActiveModel().applyPatchToIndex( - this.state.filePatch.getStagePatchForHunk(hunk), - ); - - this.props.switchboard.didFinishStageOperation({stage: true, hunk: true}); - } - - async unstageHunk(hunk) { - this.props.switchboard.didBeginStageOperation({unstage: true, hunk: true}); - - await this.repositoryObserver.getActiveModel().applyPatchToIndex( - this.state.filePatch.getUnstagePatchForHunk(hunk), - ); - - this.props.switchboard.didFinishStageOperation({unstage: true, hunk: true}); - } - - stageOrUnstageHunk(hunk) { - const stagingStatus = this.state.stagingStatus; - if (stagingStatus === 'unstaged') { - return this.stageHunk(hunk); - } else if (stagingStatus === 'staged') { - return this.unstageHunk(hunk); - } else { - throw new Error(`Unknown stagingStatus: ${stagingStatus}`); - } - } - - async stageFile() { - this.props.switchboard.didBeginStageOperation({stage: true, file: true}); - - await this.repositoryObserver.getActiveModel().stageFiles([this.props.filePath]); - this.props.switchboard.didFinishStageOperation({stage: true, file: true}); - } - - async unstageFile() { - this.props.switchboard.didBeginStageOperation({unstage: true, file: true}); - - await this.repositoryObserver.getActiveModel().unstageFiles([this.props.filePath]); - - this.props.switchboard.didFinishStageOperation({unstage: true, file: true}); - } - - stageOrUnstageFile() { - const stagingStatus = this.state.stagingStatus; - if (stagingStatus === 'unstaged') { - return this.stageFile(); - } else if (stagingStatus === 'staged') { - return this.unstageFile(); - } else { - throw new Error(`Unknown stagingStatus: ${stagingStatus}`); - } - } - - async stageModeChange(mode) { - this.props.switchboard.didBeginStageOperation({stage: true, mode: true}); - - await this.repositoryObserver.getActiveModel().stageFileModeChange( - this.props.filePath, mode, - ); - this.props.switchboard.didFinishStageOperation({stage: true, mode: true}); - } - - async unstageModeChange(mode) { - this.props.switchboard.didBeginStageOperation({unstage: true, mode: true}); - - await this.repositoryObserver.getActiveModel().stageFileModeChange( - this.props.filePath, mode, - ); - this.props.switchboard.didFinishStageOperation({unstage: true, mode: true}); - } - - stageOrUnstageModeChange() { - const stagingStatus = this.state.stagingStatus; - const oldMode = this.state.filePatch.getOldMode(); - const newMode = this.state.filePatch.getNewMode(); - if (stagingStatus === 'unstaged') { - return this.stageModeChange(newMode); - } else if (stagingStatus === 'staged') { - return this.unstageModeChange(oldMode); - } else { - throw new Error(`Unknown stagingStatus: ${stagingStatus}`); - } - } - - async stageSymlinkChange() { - this.props.switchboard.didBeginStageOperation({stage: true, symlink: true}); - - const filePatch = this.state.filePatch; - if (filePatch.hasTypechange() && filePatch.getStatus() === 'added') { - await this.repositoryObserver.getActiveModel().stageFileSymlinkChange(this.props.filePath); - } else { - await this.repositoryObserver.getActiveModel().stageFiles([this.props.filePath]); - } - this.props.switchboard.didFinishStageOperation({stage: true, symlink: true}); - } - - async unstageSymlinkChange() { - this.props.switchboard.didBeginStageOperation({unstage: true, symlink: true}); - - const filePatch = this.state.filePatch; - if (filePatch.hasTypechange() && filePatch.getStatus() === 'deleted') { - await this.repositoryObserver.getActiveModel().stageFileSymlinkChange(this.props.filePath); - } else { - await this.repositoryObserver.getActiveModel().unstageFiles([this.props.filePath]); - } - this.props.switchboard.didFinishStageOperation({unstage: true, symlink: true}); - } - - stageOrUnstageSymlinkChange() { - const stagingStatus = this.state.stagingStatus; - if (stagingStatus === 'unstaged') { - return this.stageSymlinkChange(); - } else if (stagingStatus === 'staged') { - return this.unstageSymlinkChange(); - } else { - throw new Error(`Unknown stagingStatus: ${stagingStatus}`); - } - } - - attemptHunkStageOperation(hunk) { - if (this.stagingOperationInProgress) { - return; - } - - this.stagingOperationInProgress = true; - this.props.switchboard.getChangePatchPromise().then(() => { - this.stagingOperationInProgress = false; - }); - - this.stageOrUnstageHunk(hunk); - } - - attemptFileStageOperation() { - if (this.stagingOperationInProgress) { - return; - } - - this.stagingOperationInProgress = true; - this.props.switchboard.getChangePatchPromise().then(() => { - this.stagingOperationInProgress = false; - }); - - this.stageOrUnstageFile(); - } - - attemptModeStageOperation() { - if (this.stagingOperationInProgress) { - return; - } - - this.stagingOperationInProgress = true; - this.props.switchboard.getChangePatchPromise().then(() => { - this.stagingOperationInProgress = false; - }); - - this.stageOrUnstageModeChange(); - } - - attemptSymlinkStageOperation() { - if (this.stagingOperationInProgress) { - return; - } - - this.stagingOperationInProgress = true; - this.props.switchboard.getChangePatchPromise().then(() => { - this.stagingOperationInProgress = false; - }); - - this.stageOrUnstageSymlinkChange(); - } - - async stageLines(lines) { - this.props.switchboard.didBeginStageOperation({stage: true, line: true}); - - await this.repositoryObserver.getActiveModel().applyPatchToIndex( - this.state.filePatch.getStagePatchForLines(lines), - ); - - this.props.switchboard.didFinishStageOperation({stage: true, line: true}); - } - - async unstageLines(lines) { - this.props.switchboard.didBeginStageOperation({unstage: true, line: true}); - - await this.repositoryObserver.getActiveModel().applyPatchToIndex( - this.state.filePatch.getUnstagePatchForLines(lines), - ); - - this.props.switchboard.didFinishStageOperation({unstage: true, line: true}); - } - - stageOrUnstageLines(lines) { - const stagingStatus = this.state.stagingStatus; - if (stagingStatus === 'unstaged') { - return this.stageLines(lines); - } else if (stagingStatus === 'staged') { - return this.unstageLines(lines); - } else { - throw new Error(`Unknown stagingStatus: ${stagingStatus}`); - } - } - - attemptLineStageOperation(lines) { - if (this.stagingOperationInProgress) { - return; - } - - this.stagingOperationInProgress = true; - this.props.switchboard.getChangePatchPromise().then(() => { - this.stagingOperationInProgress = false; - }); - - this.stageOrUnstageLines(lines); - } - - didSurfaceFile() { - if (this.props.didSurfaceFile) { - this.props.didSurfaceFile(this.props.filePath, this.state.stagingStatus); - } - } - - async diveIntoCorrespondingFilePatch() { - const stagingStatus = this.isStaged() ? 'unstaged' : 'staged'; - const filePatch = await this.getFilePatchForPath(this.props.filePath, stagingStatus === 'staged'); - this.props.quietlySelectItem(this.props.filePath, stagingStatus); - this.setState({filePatch, stagingStatus}); - } - - isStaged() { - return this.state.stagingStatus === 'staged'; - } - - isEmpty() { - return !this.state.filePatch || this.state.filePatch.getHunks().length === 0; - } - - focus() { - if (this.filePatchView) { - this.filePatchView.focus(); - } - } - - wasActivated(isStillActive) { - process.nextTick(() => { - isStillActive() && this.focus(); - }); - } - - async openCurrentFile({lineNumber} = {}) { - const [textEditor] = await this.props.openFiles([this.props.filePath]); - const position = new Point(lineNumber ? lineNumber - 1 : 0, 0); - textEditor.scrollToBufferPosition(position, {center: true}); - textEditor.setCursorBufferPosition(position); - return textEditor; - } - - discardLines(lines) { - return this.props.discardLines(this.state.filePatch, lines, this.repositoryObserver.getActiveModel()); - } - - undoLastDiscard() { - return this.props.undoLastDiscard(this.props.filePath, this.repositoryObserver.getActiveModel()); - } - - hasUndoHistory() { - return this.repositoryObserver.getActiveModel().hasDiscardHistory(this.props.filePath); - } - - destroy() { - this.destroyed = true; - this.subscriptions.dispose(); - this.repositoryObserver.destroy(); - this.emitter.emit('did-destroy'); + return ; } } diff --git a/lib/controllers/file-patch-controller.old.js b/lib/controllers/file-patch-controller.old.js new file mode 100644 index 0000000000..90d6a67df8 --- /dev/null +++ b/lib/controllers/file-patch-controller.old.js @@ -0,0 +1,550 @@ +import path from 'path'; +import React from 'react'; +import PropTypes from 'prop-types'; +import {Point} from 'atom'; +import {Emitter, CompositeDisposable} from 'event-kit'; + +import Switchboard from '../switchboard'; +import FilePatchView from '../views/file-patch-view'; +import ModelObserver from '../models/model-observer'; +import {autobind} from '../helpers'; + +export default class FilePatchController extends React.Component { + static propTypes = { + largeDiffByteThreshold: PropTypes.number, + getRepositoryForWorkdir: PropTypes.func.isRequired, + workingDirectoryPath: PropTypes.string.isRequired, + commandRegistry: PropTypes.object.isRequired, + deserializers: PropTypes.object.isRequired, + tooltips: PropTypes.object.isRequired, + filePath: PropTypes.string.isRequired, + lineNumber: PropTypes.number, + initialStagingStatus: PropTypes.oneOf(['unstaged', 'staged']).isRequired, + discardLines: PropTypes.func.isRequired, + didSurfaceFile: PropTypes.func.isRequired, + quietlySelectItem: PropTypes.func.isRequired, + undoLastDiscard: PropTypes.func.isRequired, + openFiles: PropTypes.func.isRequired, + switchboard: PropTypes.instanceOf(Switchboard), + } + + static defaultProps = { + largeDiffByteThreshold: 32768, + switchboard: new Switchboard(), + } + + static uriPattern = 'atom-github://file-patch/{relpath...}?workdir={workdir}&stagingStatus={stagingStatus}' + + static buildURI(relPath, workdir, stagingStatus) { + return 'atom-github://file-patch/' + + relPath + + `?workdir=${encodeURIComponent(workdir)}` + + `&stagingStatus=${encodeURIComponent(stagingStatus)}`; + } + + static confirmedLargeFilePatches = new Set() + + static resetConfirmedLargeFilePatches() { + this.confirmedLargeFilePatches = new Set(); + } + + constructor(props, context) { + super(props, context); + autobind( + this, + 'onRepoRefresh', 'handleShowDiffClick', 'attemptHunkStageOperation', 'attemptFileStageOperation', + 'attemptModeStageOperation', 'attemptSymlinkStageOperation', 'attemptLineStageOperation', 'didSurfaceFile', + 'diveIntoCorrespondingFilePatch', 'focus', 'openCurrentFile', 'discardLines', 'undoLastDiscard', 'hasUndoHistory', + ); + + this.stagingOperationInProgress = false; + this.emitter = new Emitter(); + + this.state = { + filePatch: null, + stagingStatus: props.initialStagingStatus, + isPartiallyStaged: false, + }; + + this.repositoryObserver = new ModelObserver({ + didUpdate: repo => this.onRepoRefresh(repo), + }); + this.repositoryObserver.setActiveModel(props.getRepositoryForWorkdir(this.props.workingDirectoryPath)); + + this.filePatchLoadedPromise = new Promise(res => { + this.resolveFilePatchLoadedPromise = res; + }); + + this.subscriptions = new CompositeDisposable(); + this.subscriptions.add( + this.props.switchboard.onDidFinishActiveContextUpdate(() => { + this.repositoryObserver.setActiveModel(this.props.getRepositoryForWorkdir(this.props.workingDirectoryPath)); + }), + ); + } + + getFilePatchLoadedPromise() { + return this.filePatchLoadedPromise; + } + + getStagingStatus() { + return this.state.stagingStatus; + } + + getFilePath() { + return this.props.filePath; + } + + getWorkingDirectory() { + return this.props.workingDirectoryPath; + } + + getTitle() { + let title = this.isStaged() ? 'Staged' : 'Unstaged'; + title += ' Changes: '; + title += this.props.filePath; + return title; + } + + serialize() { + return { + deserializer: 'FilePatchControllerStub', + uri: this.getURI(), + }; + } + + onDidDestroy(callback) { + return this.emitter.on('did-destroy', callback); + } + + terminatePendingState() { + if (!this.hasTerminatedPendingState) { + this.emitter.emit('did-terminate-pending-state'); + this.hasTerminatedPendingState = true; + } + } + + onDidTerminatePendingState(callback) { + return this.emitter.on('did-terminate-pending-state', callback); + } + + async onRepoRefresh(repository) { + const staged = this.isStaged(); + let filePatch = await this.getFilePatchForPath(this.props.filePath, staged); + const isPartiallyStaged = await repository.isPartiallyStaged(this.props.filePath); + + const onFinish = () => { + this.props.switchboard.didFinishRepositoryRefresh(); + }; + + if (filePatch) { + this.resolveFilePatchLoadedPromise(); + if (!this.destroyed) { + this.setState({filePatch, isPartiallyStaged}, onFinish); + } else { + onFinish(); + } + } else { + const oldFilePatch = this.state.filePatch; + if (oldFilePatch) { + filePatch = oldFilePatch.clone({ + oldFile: oldFilePatch.oldFile.clone({mode: null, symlink: null}), + newFile: oldFilePatch.newFile.clone({mode: null, symlink: null}), + patch: oldFilePatch.getPatch().clone({hunks: []}), + }); + if (!this.destroyed) { + this.setState({filePatch, isPartiallyStaged}, onFinish); + } else { + onFinish(); + } + } + } + } + + getFilePatchForPath(filePath, staged) { + const repository = this.repositoryObserver.getActiveModel(); + return repository.getFilePatchForPath(filePath, {staged}); + } + + componentDidUpdate(_prevProps, prevState) { + if (prevState.stagingStatus !== this.state.stagingStatus) { + this.emitter.emit('did-change-title'); + } + } + + goToDiffLine(lineNumber) { + this.filePatchView.goToDiffLine(lineNumber); + } + + componentWillUnmount() { + this.destroy(); + } + + render() { + const fp = this.state.filePatch; + const hunks = fp ? fp.getHunks() : []; + const executableModeChange = fp && fp.didChangeExecutableMode() ? + {oldMode: fp.getOldMode(), newMode: fp.getNewMode()} : + null; + const symlinkChange = fp && fp.hasSymlink() ? + { + oldSymlink: fp.getOldSymlink(), + newSymlink: fp.getNewSymlink(), + typechange: fp.hasTypechange(), + filePatchStatus: fp.getStatus(), + } : null; + const repository = this.repositoryObserver.getActiveModel(); + if (repository.isUndetermined() || repository.isLoading()) { + return ( +
+ +
+ ); + } else if (repository.isAbsent()) { + return ( +
+ + The repository for {this.props.workingDirectoryPath} is not open in Atom. + +
+ ); + } else { + const hasUndoHistory = repository ? this.hasUndoHistory() : false; + return ( +
+ { this.filePatchView = c; }} + commandRegistry={this.props.commandRegistry} + tooltips={this.props.tooltips} + displayLargeDiffMessage={!this.shouldDisplayLargeDiff(this.state.filePatch)} + byteCount={this.byteCount} + handleShowDiffClick={this.handleShowDiffClick} + hunks={hunks} + executableModeChange={executableModeChange} + symlinkChange={symlinkChange} + filePath={this.props.filePath} + workingDirectoryPath={this.getWorkingDirectory()} + stagingStatus={this.state.stagingStatus} + isPartiallyStaged={this.state.isPartiallyStaged} + attemptLineStageOperation={this.attemptLineStageOperation} + attemptHunkStageOperation={this.attemptHunkStageOperation} + attemptFileStageOperation={this.attemptFileStageOperation} + attemptModeStageOperation={this.attemptModeStageOperation} + attemptSymlinkStageOperation={this.attemptSymlinkStageOperation} + didSurfaceFile={this.didSurfaceFile} + didDiveIntoCorrespondingFilePatch={this.diveIntoCorrespondingFilePatch} + switchboard={this.props.switchboard} + openCurrentFile={this.openCurrentFile} + discardLines={this.discardLines} + undoLastDiscard={this.undoLastDiscard} + hasUndoHistory={hasUndoHistory} + lineNumber={this.props.lineNumber} + /> +
+ ); + } + } + + shouldDisplayLargeDiff(filePatch) { + if (!filePatch) { return true; } + + const fullPath = path.join(this.getWorkingDirectory(), this.props.filePath); + if (FilePatchController.confirmedLargeFilePatches.has(fullPath)) { + return true; + } + + this.byteCount = filePatch.getByteSize(); + return this.byteCount < this.props.largeDiffByteThreshold; + } + + onDidChangeTitle(callback) { + return this.emitter.on('did-change-title', callback); + } + + handleShowDiffClick() { + if (this.repositoryObserver.getActiveModel()) { + const fullPath = path.join(this.getWorkingDirectory(), this.props.filePath); + FilePatchController.confirmedLargeFilePatches.add(fullPath); + this.forceUpdate(); + } + } + + async stageHunk(hunk) { + this.props.switchboard.didBeginStageOperation({stage: true, hunk: true}); + + await this.repositoryObserver.getActiveModel().applyPatchToIndex( + this.state.filePatch.getStagePatchForHunk(hunk), + ); + + this.props.switchboard.didFinishStageOperation({stage: true, hunk: true}); + } + + async unstageHunk(hunk) { + this.props.switchboard.didBeginStageOperation({unstage: true, hunk: true}); + + await this.repositoryObserver.getActiveModel().applyPatchToIndex( + this.state.filePatch.getUnstagePatchForHunk(hunk), + ); + + this.props.switchboard.didFinishStageOperation({unstage: true, hunk: true}); + } + + stageOrUnstageHunk(hunk) { + const stagingStatus = this.state.stagingStatus; + if (stagingStatus === 'unstaged') { + return this.stageHunk(hunk); + } else if (stagingStatus === 'staged') { + return this.unstageHunk(hunk); + } else { + throw new Error(`Unknown stagingStatus: ${stagingStatus}`); + } + } + + async stageFile() { + this.props.switchboard.didBeginStageOperation({stage: true, file: true}); + + await this.repositoryObserver.getActiveModel().stageFiles([this.props.filePath]); + this.props.switchboard.didFinishStageOperation({stage: true, file: true}); + } + + async unstageFile() { + this.props.switchboard.didBeginStageOperation({unstage: true, file: true}); + + await this.repositoryObserver.getActiveModel().unstageFiles([this.props.filePath]); + + this.props.switchboard.didFinishStageOperation({unstage: true, file: true}); + } + + stageOrUnstageFile() { + const stagingStatus = this.state.stagingStatus; + if (stagingStatus === 'unstaged') { + return this.stageFile(); + } else if (stagingStatus === 'staged') { + return this.unstageFile(); + } else { + throw new Error(`Unknown stagingStatus: ${stagingStatus}`); + } + } + + async stageModeChange(mode) { + this.props.switchboard.didBeginStageOperation({stage: true, mode: true}); + + await this.repositoryObserver.getActiveModel().stageFileModeChange( + this.props.filePath, mode, + ); + this.props.switchboard.didFinishStageOperation({stage: true, mode: true}); + } + + async unstageModeChange(mode) { + this.props.switchboard.didBeginStageOperation({unstage: true, mode: true}); + + await this.repositoryObserver.getActiveModel().stageFileModeChange( + this.props.filePath, mode, + ); + this.props.switchboard.didFinishStageOperation({unstage: true, mode: true}); + } + + stageOrUnstageModeChange() { + const stagingStatus = this.state.stagingStatus; + const oldMode = this.state.filePatch.getOldMode(); + const newMode = this.state.filePatch.getNewMode(); + if (stagingStatus === 'unstaged') { + return this.stageModeChange(newMode); + } else if (stagingStatus === 'staged') { + return this.unstageModeChange(oldMode); + } else { + throw new Error(`Unknown stagingStatus: ${stagingStatus}`); + } + } + + async stageSymlinkChange() { + this.props.switchboard.didBeginStageOperation({stage: true, symlink: true}); + + const filePatch = this.state.filePatch; + if (filePatch.hasTypechange() && filePatch.getStatus() === 'added') { + await this.repositoryObserver.getActiveModel().stageFileSymlinkChange(this.props.filePath); + } else { + await this.repositoryObserver.getActiveModel().stageFiles([this.props.filePath]); + } + this.props.switchboard.didFinishStageOperation({stage: true, symlink: true}); + } + + async unstageSymlinkChange() { + this.props.switchboard.didBeginStageOperation({unstage: true, symlink: true}); + + const filePatch = this.state.filePatch; + if (filePatch.hasTypechange() && filePatch.getStatus() === 'deleted') { + await this.repositoryObserver.getActiveModel().stageFileSymlinkChange(this.props.filePath); + } else { + await this.repositoryObserver.getActiveModel().unstageFiles([this.props.filePath]); + } + this.props.switchboard.didFinishStageOperation({unstage: true, symlink: true}); + } + + stageOrUnstageSymlinkChange() { + const stagingStatus = this.state.stagingStatus; + if (stagingStatus === 'unstaged') { + return this.stageSymlinkChange(); + } else if (stagingStatus === 'staged') { + return this.unstageSymlinkChange(); + } else { + throw new Error(`Unknown stagingStatus: ${stagingStatus}`); + } + } + + attemptHunkStageOperation(hunk) { + if (this.stagingOperationInProgress) { + return; + } + + this.stagingOperationInProgress = true; + this.props.switchboard.getChangePatchPromise().then(() => { + this.stagingOperationInProgress = false; + }); + + this.stageOrUnstageHunk(hunk); + } + + attemptFileStageOperation() { + if (this.stagingOperationInProgress) { + return; + } + + this.stagingOperationInProgress = true; + this.props.switchboard.getChangePatchPromise().then(() => { + this.stagingOperationInProgress = false; + }); + + this.stageOrUnstageFile(); + } + + attemptModeStageOperation() { + if (this.stagingOperationInProgress) { + return; + } + + this.stagingOperationInProgress = true; + this.props.switchboard.getChangePatchPromise().then(() => { + this.stagingOperationInProgress = false; + }); + + this.stageOrUnstageModeChange(); + } + + attemptSymlinkStageOperation() { + if (this.stagingOperationInProgress) { + return; + } + + this.stagingOperationInProgress = true; + this.props.switchboard.getChangePatchPromise().then(() => { + this.stagingOperationInProgress = false; + }); + + this.stageOrUnstageSymlinkChange(); + } + + async stageLines(lines) { + this.props.switchboard.didBeginStageOperation({stage: true, line: true}); + + await this.repositoryObserver.getActiveModel().applyPatchToIndex( + this.state.filePatch.getStagePatchForLines(lines), + ); + + this.props.switchboard.didFinishStageOperation({stage: true, line: true}); + } + + async unstageLines(lines) { + this.props.switchboard.didBeginStageOperation({unstage: true, line: true}); + + await this.repositoryObserver.getActiveModel().applyPatchToIndex( + this.state.filePatch.getUnstagePatchForLines(lines), + ); + + this.props.switchboard.didFinishStageOperation({unstage: true, line: true}); + } + + stageOrUnstageLines(lines) { + const stagingStatus = this.state.stagingStatus; + if (stagingStatus === 'unstaged') { + return this.stageLines(lines); + } else if (stagingStatus === 'staged') { + return this.unstageLines(lines); + } else { + throw new Error(`Unknown stagingStatus: ${stagingStatus}`); + } + } + + attemptLineStageOperation(lines) { + if (this.stagingOperationInProgress) { + return; + } + + this.stagingOperationInProgress = true; + this.props.switchboard.getChangePatchPromise().then(() => { + this.stagingOperationInProgress = false; + }); + + this.stageOrUnstageLines(lines); + } + + didSurfaceFile() { + if (this.props.didSurfaceFile) { + this.props.didSurfaceFile(this.props.filePath, this.state.stagingStatus); + } + } + + async diveIntoCorrespondingFilePatch() { + const stagingStatus = this.isStaged() ? 'unstaged' : 'staged'; + const filePatch = await this.getFilePatchForPath(this.props.filePath, stagingStatus === 'staged'); + this.props.quietlySelectItem(this.props.filePath, stagingStatus); + this.setState({filePatch, stagingStatus}); + } + + isStaged() { + return this.state.stagingStatus === 'staged'; + } + + isEmpty() { + return !this.state.filePatch || this.state.filePatch.getHunks().length === 0; + } + + focus() { + if (this.filePatchView) { + this.filePatchView.focus(); + } + } + + wasActivated(isStillActive) { + process.nextTick(() => { + isStillActive() && this.focus(); + }); + } + + async openCurrentFile({lineNumber} = {}) { + const [textEditor] = await this.props.openFiles([this.props.filePath]); + const position = new Point(lineNumber ? lineNumber - 1 : 0, 0); + textEditor.scrollToBufferPosition(position, {center: true}); + textEditor.setCursorBufferPosition(position); + return textEditor; + } + + discardLines(lines) { + return this.props.discardLines(this.state.filePatch, lines, this.repositoryObserver.getActiveModel()); + } + + undoLastDiscard() { + return this.props.undoLastDiscard(this.props.filePath, this.repositoryObserver.getActiveModel()); + } + + hasUndoHistory() { + return this.repositoryObserver.getActiveModel().hasDiscardHistory(this.props.filePath); + } + + destroy() { + this.destroyed = true; + this.subscriptions.dispose(); + this.repositoryObserver.destroy(); + this.emitter.emit('did-destroy'); + } +} diff --git a/lib/controllers/root-controller.js b/lib/controllers/root-controller.js index a10a28980f..6dbedb6469 100644 --- a/lib/controllers/root-controller.js +++ b/lib/controllers/root-controller.js @@ -16,7 +16,7 @@ import CredentialDialog from '../views/credential-dialog'; import Commands, {Command} from '../atom/commands'; import GitTimingsView from '../views/git-timings-view'; import GithubTabController from './github-tab-controller'; -import FilePatchController from './file-patch-controller'; +import FilePatchItem from '../items/file-patch-item'; import IssueishPaneItem from '../items/issueish-pane-item'; import GitTabItem from '../items/git-tab-item'; import StatusBarTileController from './status-bar-tile-controller'; @@ -318,24 +318,14 @@ export default class RootController extends React.Component { + uriPattern={FilePatchItem.uriPattern}> {({itemHolder, params}) => ( - )} @@ -527,7 +517,7 @@ export default class RootController extends React.Component { } const lineNum = editor.getCursorBufferPosition().row + 1; const filePatchItem = await this.props.workspace.open( - FilePatchController.buildURI(filePath, repoPath, stagingStatus), + FilePatchItem.buildURI(filePath, repoPath, stagingStatus), {pending: true, activatePane: true, activateItem: true}, ); await filePatchItem.getRealItemPromise(); diff --git a/lib/items/file-patch-item.js b/lib/items/file-patch-item.js new file mode 100644 index 0000000000..1c02262dac --- /dev/null +++ b/lib/items/file-patch-item.js @@ -0,0 +1,91 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import {Emitter} from 'event-kit'; + +import FilePatchContainer from '../containers/file-patch-container'; + +export default class FilePatchItem extends React.Component { + static propTypes = { + repository: PropTypes.object.isRequired, + stagingStatus: PropTypes.oneOf(['staged', 'unstaged']), + relPath: PropTypes.string.isRequired, + + tooltips: PropTypes.object.isRequired, + } + + static uriPattern = 'atom-github://file-patch/{relPath...}?workdir={workdir}&stagingStatus={stagingStatus}' + + static buildURI(relPath, workdir, stagingStatus) { + return 'atom-github://file-patch/' + + relPath + + `?workdir=${encodeURIComponent(workdir)}` + + `&stagingStatus=${encodeURIComponent(stagingStatus)}`; + } + + constructor(props) { + super(props); + + this.emitter = new Emitter(); + this.isDestroyed = false; + this.hasTerminatedPendingState = false; + } + + getTitle() { + let title = this.props.stagingStatus === 'staged' ? 'Staged' : 'Unstaged'; + title += ' Changes: '; + title += this.props.relPath; + return title; + } + + terminatePendingState() { + if (!this.hasTerminatedPendingState) { + this.emitter.emit('did-terminate-pending-state'); + this.hasTerminatedPendingState = true; + } + } + + onDidTerminatePendingState(callback) { + return this.emitter.on('did-terminate-pending-state', callback); + } + + destroy() { + if (!this.isDestroyed) { + this.emitter.emit('did-destroy'); + this.isDestroyed = true; + } + } + + onDidDestroy(callback) { + return this.emitter.on('did-destroy', callback); + } + + render() { + return ( + + ); + } + + serialize() { + return { + deserializer: 'FilePatchControllerStub', + uri: this.getURI(), + }; + } + + getStagingStatus() { + return this.props.stagingStatus; + } + + getFilePath() { + return this.props.relPath; + } + + getWorkingDirectory() { + return this.props.repository.getWorkingDirectoryPath(); + } +} diff --git a/lib/views/file-patch-view.js b/lib/views/file-patch-view.js index e8067025dc..f774c89437 100644 --- a/lib/views/file-patch-view.js +++ b/lib/views/file-patch-view.js @@ -1,716 +1,87 @@ import React from 'react'; import PropTypes from 'prop-types'; -import {CompositeDisposable, Disposable} from 'event-kit'; import cx from 'classnames'; -import bytes from 'bytes'; -import HunkView from './hunk-view'; -import SimpleTooltip from '../atom/simple-tooltip'; -import Commands, {Command} from '../atom/commands'; import FilePatchSelection from '../models/file-patch-selection'; -import Switchboard from '../switchboard'; -import RefHolder from '../models/ref-holder'; -import {autobind} from '../helpers'; - -const executableText = { - 100644: 'non executable 100644', - 100755: 'executable 100755', -}; +import AtomTextEditor from '../atom/atom-text-editor'; +import Marker from '../atom/marker'; +import Decoration from '../atom/decoration'; export default class FilePatchView extends React.Component { static propTypes = { - commandRegistry: PropTypes.object.isRequired, - tooltips: PropTypes.object.isRequired, - filePath: PropTypes.string.isRequired, - hunks: PropTypes.arrayOf(PropTypes.object).isRequired, - executableModeChange: PropTypes.shape({ - oldMode: PropTypes.string.isRequired, - newMode: PropTypes.string.isRequired, - }), - symlinkChange: PropTypes.shape({ - oldSymlink: PropTypes.string, - newSymlink: PropTypes.string, - typechange: PropTypes.bool, - filePatchStatus: PropTypes.string, - }), - stagingStatus: PropTypes.oneOf(['unstaged', 'staged']).isRequired, + stagingStatus: PropTypes.oneOf(['staged', 'unstaged']).isRequired, isPartiallyStaged: PropTypes.bool.isRequired, - hasUndoHistory: PropTypes.bool.isRequired, - attemptLineStageOperation: PropTypes.func.isRequired, - attemptHunkStageOperation: PropTypes.func.isRequired, - attemptFileStageOperation: PropTypes.func.isRequired, - attemptModeStageOperation: PropTypes.func.isRequired, - attemptSymlinkStageOperation: PropTypes.func.isRequired, - discardLines: PropTypes.func.isRequired, - undoLastDiscard: PropTypes.func.isRequired, - openCurrentFile: PropTypes.func.isRequired, - didSurfaceFile: PropTypes.func.isRequired, - didDiveIntoCorrespondingFilePatch: PropTypes.func.isRequired, - switchboard: PropTypes.instanceOf(Switchboard), - displayLargeDiffMessage: PropTypes.bool, - byteCount: PropTypes.number, - handleShowDiffClick: PropTypes.func.isRequired, - } + filePatch: PropTypes.object.isRequired, - static defaultProps = { - switchboard: new Switchboard(), + tooltips: PropTypes.object.isRequired, } - constructor(props, context) { - super(props, context); - autobind( - this, - 'registerCommands', 'renderButtonGroup', 'renderExecutableModeChange', 'renderSymlinkChange', 'contextMenuOnItem', - 'mousedownOnLine', 'mousemoveOnLine', 'mouseup', 'togglePatchSelectionMode', 'selectNext', 'selectNextElement', - 'selectToNext', 'selectPrevious', 'selectPreviousElement', 'selectToPrevious', 'selectFirst', 'selectToFirst', - 'selectLast', 'selectToLast', 'selectAll', 'didConfirm', 'didMoveRight', 'focus', 'openFile', 'stageOrUnstageAll', - 'stageOrUnstageModeChange', 'stageOrUnstageSymlinkChange', 'discardSelection', - ); - - this.mouseSelectionInProgress = false; - this.disposables = new CompositeDisposable(); - - this.refElement = new RefHolder(); + constructor(props) { + super(props); this.state = { - selection: new FilePatchSelection(this.props.hunks), + selection: new FilePatchSelection(this.props.filePatch.getHunks()), }; } - componentDidMount() { - window.addEventListener('mouseup', this.mouseup); - this.disposables.add(new Disposable(() => window.removeEventListener('mouseup', this.mouseup))); - } - - // eslint-disable-next-line camelcase - UNSAFE_componentWillReceiveProps(nextProps) { - const hunksChanged = this.props.hunks.length !== nextProps.hunks.length || - this.props.hunks.some((hunk, index) => hunk !== nextProps.hunks[index]); - - if (hunksChanged) { - this.setState(prevState => { - return { - selection: prevState.selection.updateHunks(nextProps.hunks), - }; - }, () => { - nextProps.switchboard.didChangePatch(); - }); - } - } - - shouldComponentUpdate(nextProps, nextState) { - const deepProps = { - executableModeChange: ['oldMode', 'newMode'], - symlinkChange: ['oldSymlink', 'newSymlink', 'typechange', 'filePatchStatus'], - }; - - for (const propKey in this.constructor.propTypes) { - const subKeys = deepProps[propKey]; - const oldProp = this.props[propKey]; - const newProp = nextProps[propKey]; - - if (subKeys) { - const oldExists = (oldProp !== null && oldProp !== undefined); - const newExists = (newProp !== null && newProp !== undefined); - - if (oldExists !== newExists) { - return true; - } - - if (!oldExists && !newExists) { - continue; - } - - if (subKeys.some(subKey => this.props[propKey][subKey] !== nextProps[propKey][subKey])) { - return true; - } - } else { - if (oldProp !== newProp) { - return true; - } - } - } - - if (this.state.selection !== nextState.selection) { - return true; - } - - return false; - } - - renderEmptyDiffMessage() { - return ( -
- File has no contents -
- ); - } - - renderLargeDiffMessage() { - const human = bytes.format(this.props.byteCount); - - return ( -
-

- This is a large {human} diff. For performance reasons, it is not rendered by default. -

- -
- ); - } - - renderHunks() { - // Render hunks for symlink change only if 'typechange' (which indicates symlink change AND file content change) - const {symlinkChange} = this.props; - if (symlinkChange && !symlinkChange.typechange) { return null; } - - const selectedHunks = this.state.selection.getSelectedHunks(); - const selectedLines = this.state.selection.getSelectedLines(); - const headHunk = this.state.selection.getHeadHunk(); - const headLine = this.state.selection.getHeadLine(); - const hunkSelectionMode = this.state.selection.getMode() === 'hunk'; - - const unstaged = this.props.stagingStatus === 'unstaged'; - const stageButtonLabelPrefix = unstaged ? 'Stage' : 'Unstage'; - - if (this.props.hunks.length === 0) { - return this.renderEmptyDiffMessage(); - } - - return this.props.hunks.map(hunk => { - const isSelected = selectedHunks.has(hunk); - let stageButtonSuffix = (hunkSelectionMode || !isSelected) ? ' Hunk' : ' Selection'; - if (selectedHunks.size > 1 && selectedHunks.has(hunk)) { - stageButtonSuffix += 's'; - } - const stageButtonLabel = stageButtonLabelPrefix + stageButtonSuffix; - const discardButtonLabel = 'Discard' + stageButtonSuffix; - - return ( - this.mousedownOnHeader(e, hunk)} - mousedownOnLine={this.mousedownOnLine} - mousemoveOnLine={this.mousemoveOnLine} - contextMenuOnItem={this.contextMenuOnItem} - didClickStageButton={() => this.didClickStageButtonForHunk(hunk)} - didClickDiscardButton={() => this.didClickDiscardButtonForHunk(hunk)} - /> - ); - }); - - } - render() { - const unstaged = this.props.stagingStatus === 'unstaged'; + const text = this.props.filePatch.getHunks().map(h => h.toString()).join('\n'); + return (
- - {this.registerCommands()} + className={cx('github-FilePatchView', {'is-staged': !this.isUnstaged(), 'is-unstaged': this.isUnstaged()})} + tabIndex="-1"> -
- - {unstaged ? 'Unstaged Changes for ' : 'Staged Changes for '} - {this.props.filePath} - - {this.renderButtonGroup()} -
+ + + + {this.renderFileHeader()} + + + -
- {this.props.executableModeChange && this.renderExecutableModeChange(unstaged)} - {this.props.symlinkChange && this.renderSymlinkChange(unstaged)} - {this.props.displayLargeDiffMessage ? this.renderLargeDiffMessage() : this.renderHunks()} -
); } - registerCommands() { + renderFileHeader() { return ( -
- - - - - - - - - - - - - - - - - this.props.isPartiallyStaged && this.props.didDiveIntoCorrespondingFilePatch()} - /> - - this.props.hasUndoHistory && this.props.undoLastDiscard()} - /> - {this.props.executableModeChange && - } - {this.props.symlinkChange && - } - - - - this.props.hasUndoHistory && this.props.undoLastDiscard()} - /> - - -
+
+ + {this.isUnstaged() ? 'Unstaged Changes for ' : 'Staged Changes for '} + {this.props.filePatch.getPath()} + + {this.renderButtonGroup()} +
); } renderButtonGroup() { - const unstaged = this.props.stagingStatus === 'unstaged'; + const hasHunks = this.props.filePatch.getHunks().length > 0; return ( - {this.props.hasUndoHistory && unstaged ? ( - - ) : null} - {this.props.isPartiallyStaged || !this.props.hunks.length ? ( - - ) : null } ); } - renderExecutableModeChange(unstaged) { - const {executableModeChange} = this.props; - return ( -
-
-
-

Mode change

-
- -
-
-
- File changed mode - - -
-
-
- ); - } - - renderSymlinkChange(unstaged) { - const {symlinkChange} = this.props; - const {oldSymlink, newSymlink} = symlinkChange; - - if (oldSymlink && !newSymlink) { - return ( -
-
-
-

Symlink deleted

-
- -
-
-
- Symlink - - to {oldSymlink} - - deleted. -
-
-
- ); - } else if (!oldSymlink && newSymlink) { - return ( -
-
-
-

Symlink added

-
- -
-
-
- Symlink - - to {newSymlink} - - created. -
-
-
- ); - } else if (oldSymlink && newSymlink) { - return ( -
-
-
-

Symlink changed

-
- -
-
-
- - from {oldSymlink} - - - to {newSymlink} - -
-
-
- ); - } else { - return new Error('Symlink change detected, but missing symlink paths'); - } - } - - componentWillUnmount() { - this.disposables.dispose(); - } - - contextMenuOnItem(event, hunk, line) { - const resend = () => { - const newEvent = new MouseEvent(event.type, event); - setImmediate(() => event.target.parentNode.dispatchEvent(newEvent)); - }; - - const mode = this.state.selection.getMode(); - if (mode === 'hunk' && !this.state.selection.getSelectedHunks().has(hunk)) { - event.stopPropagation(); - - this.setState(prevState => { - return {selection: prevState.selection.selectHunk(hunk, event.shiftKey)}; - }, resend); - } else if (mode === 'line' && !this.state.selection.getSelectedLines().has(line)) { - event.stopPropagation(); - - this.setState(prevState => { - return {selection: prevState.selection.selectLine(line, event.shiftKey)}; - }, resend); - } - } - - mousedownOnHeader(event, hunk) { - if (event.button !== 0) { return; } - const windows = process.platform === 'win32'; - if (event.ctrlKey && !windows) { return; } // simply open context menu - - this.mouseSelectionInProgress = true; - event.persist && event.persist(); - - this.setState(prevState => { - let selection = prevState.selection; - if (event.metaKey || (event.ctrlKey && windows)) { - if (selection.getMode() === 'hunk') { - selection = selection.addOrSubtractHunkSelection(hunk); - } else { - // TODO: optimize - selection = hunk.getLines().reduce( - (current, line) => current.addOrSubtractLineSelection(line).coalesce(), - selection, - ); - } - } else if (event.shiftKey) { - if (selection.getMode() === 'hunk') { - selection = selection.selectHunk(hunk, true); - } else { - const hunkLines = hunk.getLines(); - const tailIndex = selection.getLineSelectionTailIndex(); - const selectedHunkAfterTail = tailIndex < hunkLines[0].diffLineNumber; - if (selectedHunkAfterTail) { - selection = selection.selectLine(hunkLines[hunkLines.length - 1], true); - } else { - selection = selection.selectLine(hunkLines[0], true); - } - } - } else { - selection = selection.selectHunk(hunk, false); - } - - return {selection}; - }); - } - - mousedownOnLine(event, hunk, line) { - if (event.button !== 0) { return; } - const windows = process.platform === 'win32'; - if (event.ctrlKey && !windows) { return; } // simply open context menu - - this.mouseSelectionInProgress = true; - event.persist && event.persist(); - - this.setState(prevState => { - let selection = prevState.selection; - - if (event.metaKey || (event.ctrlKey && windows)) { - if (selection.getMode() === 'hunk') { - selection = selection.addOrSubtractHunkSelection(hunk); - } else { - selection = selection.addOrSubtractLineSelection(line); - } - } else if (event.shiftKey) { - if (selection.getMode() === 'hunk') { - selection = selection.selectHunk(hunk, true); - } else { - selection = selection.selectLine(line, true); - } - } else if (event.detail === 1) { - selection = selection.selectLine(line, false); - } else if (event.detail === 2) { - selection = selection.selectHunk(hunk, false); - } - - return {selection}; - }); - } - - mousemoveOnLine(event, hunk, line) { - if (!this.mouseSelectionInProgress) { return; } - - this.setState(prevState => { - let selection = null; - if (prevState.selection.getMode() === 'hunk') { - selection = prevState.selection.selectHunk(hunk, true); - } else { - selection = prevState.selection.selectLine(line, true); - } - return {selection}; - }); - } - - mouseup() { - this.mouseSelectionInProgress = false; - this.setState(prevState => { - return {selection: prevState.selection.coalesce()}; - }); - } - - togglePatchSelectionMode() { - this.setState(prevState => ({selection: prevState.selection.toggleMode()})); - } - - getPatchSelectionMode() { - return this.state.selection.getMode(); - } - - getSelectedHunks() { - return this.state.selection.getSelectedHunks(); - } - - getSelectedLines() { - return this.state.selection.getSelectedLines(); - } - - selectNext() { - this.setState(prevState => ({selection: prevState.selection.selectNext()})); - } - - selectNextElement() { - if (this.state.selection.isEmpty() && this.props.didSurfaceFile) { - this.props.didSurfaceFile(); - } else { - this.setState(prevState => ({selection: prevState.selection.jumpToNextHunk()})); - } - } - - selectToNext() { - this.setState(prevState => { - return {selection: prevState.selection.selectNext(true).coalesce()}; - }); - } - - selectPrevious() { - this.setState(prevState => ({selection: prevState.selection.selectPrevious()})); - } - - selectPreviousElement() { - if (this.state.selection.isEmpty() && this.props.didSurfaceFile) { - this.props.didSurfaceFile(); - } else { - this.setState(prevState => ({selection: prevState.selection.jumpToPreviousHunk()})); - } - } - - selectToPrevious() { - this.setState(prevState => { - return {selection: prevState.selection.selectPrevious(true).coalesce()}; - }); - } - - selectFirst() { - this.setState(prevState => ({selection: prevState.selection.selectFirst()})); - } - - selectToFirst() { - this.setState(prevState => ({selection: prevState.selection.selectFirst(true)})); - } - - selectLast() { - this.setState(prevState => ({selection: prevState.selection.selectLast()})); - } - - selectToLast() { - this.setState(prevState => ({selection: prevState.selection.selectLast(true)})); - } - - selectAll() { - return new Promise(resolve => { - this.setState(prevState => ({selection: prevState.selection.selectAll()}), resolve); - }); - } - - getNextHunkUpdatePromise() { - return this.state.selection.getNextUpdatePromise(); - } - - didClickStageButtonForHunk(hunk) { - if (this.state.selection.getSelectedHunks().has(hunk)) { - this.props.attemptLineStageOperation(this.state.selection.getSelectedLines()); - } else { - this.setState(prevState => ({selection: prevState.selection.selectHunk(hunk)}), () => { - this.props.attemptHunkStageOperation(hunk); - }); - } - } - - didClickDiscardButtonForHunk(hunk) { - if (this.state.selection.getSelectedHunks().has(hunk)) { - this.discardSelection(); - } else { - this.setState(prevState => ({selection: prevState.selection.selectHunk(hunk)}), () => { - this.discardSelection(); - }); - } - } - - didConfirm() { - return this.didClickStageButtonForHunk([...this.state.selection.getSelectedHunks()][0]); - } - - didMoveRight() { - if (this.props.didSurfaceFile) { - this.props.didSurfaceFile(); - } - } - - focus() { - this.refElement.get().focus(); - } - - openFile() { - let lineNumber = 0; - const firstSelectedLine = Array.from(this.state.selection.getSelectedLines())[0]; - if (firstSelectedLine && firstSelectedLine.newLineNumber > -1) { - lineNumber = firstSelectedLine.newLineNumber; - } else { - const firstSelectedHunk = Array.from(this.state.selection.getSelectedHunks())[0]; - lineNumber = firstSelectedHunk ? firstSelectedHunk.getNewStartRow() : 0; - } - return this.props.openCurrentFile({lineNumber}); - } - - stageOrUnstageAll() { - this.props.attemptFileStageOperation(); - } - - stageOrUnstageModeChange() { - this.props.attemptModeStageOperation(); - } - - stageOrUnstageSymlinkChange() { - this.props.attemptSymlinkStageOperation(); - } - - discardSelection() { - const selectedLines = this.state.selection.getSelectedLines(); - return selectedLines.size ? this.props.discardLines(selectedLines) : null; - } - - goToDiffLine(lineNumber) { - this.setState(prevState => ({selection: prevState.selection.goToDiffLine(lineNumber)})); + isUnstaged() { + return this.props.stagingStatus === 'unstaged'; } } diff --git a/lib/views/file-patch-view.old.js b/lib/views/file-patch-view.old.js new file mode 100644 index 0000000000..e8067025dc --- /dev/null +++ b/lib/views/file-patch-view.old.js @@ -0,0 +1,716 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import {CompositeDisposable, Disposable} from 'event-kit'; +import cx from 'classnames'; +import bytes from 'bytes'; + +import HunkView from './hunk-view'; +import SimpleTooltip from '../atom/simple-tooltip'; +import Commands, {Command} from '../atom/commands'; +import FilePatchSelection from '../models/file-patch-selection'; +import Switchboard from '../switchboard'; +import RefHolder from '../models/ref-holder'; +import {autobind} from '../helpers'; + +const executableText = { + 100644: 'non executable 100644', + 100755: 'executable 100755', +}; + +export default class FilePatchView extends React.Component { + static propTypes = { + commandRegistry: PropTypes.object.isRequired, + tooltips: PropTypes.object.isRequired, + filePath: PropTypes.string.isRequired, + hunks: PropTypes.arrayOf(PropTypes.object).isRequired, + executableModeChange: PropTypes.shape({ + oldMode: PropTypes.string.isRequired, + newMode: PropTypes.string.isRequired, + }), + symlinkChange: PropTypes.shape({ + oldSymlink: PropTypes.string, + newSymlink: PropTypes.string, + typechange: PropTypes.bool, + filePatchStatus: PropTypes.string, + }), + stagingStatus: PropTypes.oneOf(['unstaged', 'staged']).isRequired, + isPartiallyStaged: PropTypes.bool.isRequired, + hasUndoHistory: PropTypes.bool.isRequired, + attemptLineStageOperation: PropTypes.func.isRequired, + attemptHunkStageOperation: PropTypes.func.isRequired, + attemptFileStageOperation: PropTypes.func.isRequired, + attemptModeStageOperation: PropTypes.func.isRequired, + attemptSymlinkStageOperation: PropTypes.func.isRequired, + discardLines: PropTypes.func.isRequired, + undoLastDiscard: PropTypes.func.isRequired, + openCurrentFile: PropTypes.func.isRequired, + didSurfaceFile: PropTypes.func.isRequired, + didDiveIntoCorrespondingFilePatch: PropTypes.func.isRequired, + switchboard: PropTypes.instanceOf(Switchboard), + displayLargeDiffMessage: PropTypes.bool, + byteCount: PropTypes.number, + handleShowDiffClick: PropTypes.func.isRequired, + } + + static defaultProps = { + switchboard: new Switchboard(), + } + + constructor(props, context) { + super(props, context); + autobind( + this, + 'registerCommands', 'renderButtonGroup', 'renderExecutableModeChange', 'renderSymlinkChange', 'contextMenuOnItem', + 'mousedownOnLine', 'mousemoveOnLine', 'mouseup', 'togglePatchSelectionMode', 'selectNext', 'selectNextElement', + 'selectToNext', 'selectPrevious', 'selectPreviousElement', 'selectToPrevious', 'selectFirst', 'selectToFirst', + 'selectLast', 'selectToLast', 'selectAll', 'didConfirm', 'didMoveRight', 'focus', 'openFile', 'stageOrUnstageAll', + 'stageOrUnstageModeChange', 'stageOrUnstageSymlinkChange', 'discardSelection', + ); + + this.mouseSelectionInProgress = false; + this.disposables = new CompositeDisposable(); + + this.refElement = new RefHolder(); + + this.state = { + selection: new FilePatchSelection(this.props.hunks), + }; + } + + componentDidMount() { + window.addEventListener('mouseup', this.mouseup); + this.disposables.add(new Disposable(() => window.removeEventListener('mouseup', this.mouseup))); + } + + // eslint-disable-next-line camelcase + UNSAFE_componentWillReceiveProps(nextProps) { + const hunksChanged = this.props.hunks.length !== nextProps.hunks.length || + this.props.hunks.some((hunk, index) => hunk !== nextProps.hunks[index]); + + if (hunksChanged) { + this.setState(prevState => { + return { + selection: prevState.selection.updateHunks(nextProps.hunks), + }; + }, () => { + nextProps.switchboard.didChangePatch(); + }); + } + } + + shouldComponentUpdate(nextProps, nextState) { + const deepProps = { + executableModeChange: ['oldMode', 'newMode'], + symlinkChange: ['oldSymlink', 'newSymlink', 'typechange', 'filePatchStatus'], + }; + + for (const propKey in this.constructor.propTypes) { + const subKeys = deepProps[propKey]; + const oldProp = this.props[propKey]; + const newProp = nextProps[propKey]; + + if (subKeys) { + const oldExists = (oldProp !== null && oldProp !== undefined); + const newExists = (newProp !== null && newProp !== undefined); + + if (oldExists !== newExists) { + return true; + } + + if (!oldExists && !newExists) { + continue; + } + + if (subKeys.some(subKey => this.props[propKey][subKey] !== nextProps[propKey][subKey])) { + return true; + } + } else { + if (oldProp !== newProp) { + return true; + } + } + } + + if (this.state.selection !== nextState.selection) { + return true; + } + + return false; + } + + renderEmptyDiffMessage() { + return ( +
+ File has no contents +
+ ); + } + + renderLargeDiffMessage() { + const human = bytes.format(this.props.byteCount); + + return ( +
+

+ This is a large {human} diff. For performance reasons, it is not rendered by default. +

+ +
+ ); + } + + renderHunks() { + // Render hunks for symlink change only if 'typechange' (which indicates symlink change AND file content change) + const {symlinkChange} = this.props; + if (symlinkChange && !symlinkChange.typechange) { return null; } + + const selectedHunks = this.state.selection.getSelectedHunks(); + const selectedLines = this.state.selection.getSelectedLines(); + const headHunk = this.state.selection.getHeadHunk(); + const headLine = this.state.selection.getHeadLine(); + const hunkSelectionMode = this.state.selection.getMode() === 'hunk'; + + const unstaged = this.props.stagingStatus === 'unstaged'; + const stageButtonLabelPrefix = unstaged ? 'Stage' : 'Unstage'; + + if (this.props.hunks.length === 0) { + return this.renderEmptyDiffMessage(); + } + + return this.props.hunks.map(hunk => { + const isSelected = selectedHunks.has(hunk); + let stageButtonSuffix = (hunkSelectionMode || !isSelected) ? ' Hunk' : ' Selection'; + if (selectedHunks.size > 1 && selectedHunks.has(hunk)) { + stageButtonSuffix += 's'; + } + const stageButtonLabel = stageButtonLabelPrefix + stageButtonSuffix; + const discardButtonLabel = 'Discard' + stageButtonSuffix; + + return ( + this.mousedownOnHeader(e, hunk)} + mousedownOnLine={this.mousedownOnLine} + mousemoveOnLine={this.mousemoveOnLine} + contextMenuOnItem={this.contextMenuOnItem} + didClickStageButton={() => this.didClickStageButtonForHunk(hunk)} + didClickDiscardButton={() => this.didClickDiscardButtonForHunk(hunk)} + /> + ); + }); + + } + + render() { + const unstaged = this.props.stagingStatus === 'unstaged'; + return ( +
+ + {this.registerCommands()} + +
+ + {unstaged ? 'Unstaged Changes for ' : 'Staged Changes for '} + {this.props.filePath} + + {this.renderButtonGroup()} +
+ +
+ {this.props.executableModeChange && this.renderExecutableModeChange(unstaged)} + {this.props.symlinkChange && this.renderSymlinkChange(unstaged)} + {this.props.displayLargeDiffMessage ? this.renderLargeDiffMessage() : this.renderHunks()} +
+
+ ); + } + + registerCommands() { + return ( +
+ + + + + + + + + + + + + + + + + this.props.isPartiallyStaged && this.props.didDiveIntoCorrespondingFilePatch()} + /> + + this.props.hasUndoHistory && this.props.undoLastDiscard()} + /> + {this.props.executableModeChange && + } + {this.props.symlinkChange && + } + + + + this.props.hasUndoHistory && this.props.undoLastDiscard()} + /> + + +
+ ); + } + + renderButtonGroup() { + const unstaged = this.props.stagingStatus === 'unstaged'; + + return ( + + {this.props.hasUndoHistory && unstaged ? ( + + ) : null} + {this.props.isPartiallyStaged || !this.props.hunks.length ? ( + + + ) : null } + + ); + } + + renderExecutableModeChange(unstaged) { + const {executableModeChange} = this.props; + return ( +
+
+
+

Mode change

+
+ +
+
+
+ File changed mode + + +
+
+
+ ); + } + + renderSymlinkChange(unstaged) { + const {symlinkChange} = this.props; + const {oldSymlink, newSymlink} = symlinkChange; + + if (oldSymlink && !newSymlink) { + return ( +
+
+
+

Symlink deleted

+
+ +
+
+
+ Symlink + + to {oldSymlink} + + deleted. +
+
+
+ ); + } else if (!oldSymlink && newSymlink) { + return ( +
+
+
+

Symlink added

+
+ +
+
+
+ Symlink + + to {newSymlink} + + created. +
+
+
+ ); + } else if (oldSymlink && newSymlink) { + return ( +
+
+
+

Symlink changed

+
+ +
+
+
+ + from {oldSymlink} + + + to {newSymlink} + +
+
+
+ ); + } else { + return new Error('Symlink change detected, but missing symlink paths'); + } + } + + componentWillUnmount() { + this.disposables.dispose(); + } + + contextMenuOnItem(event, hunk, line) { + const resend = () => { + const newEvent = new MouseEvent(event.type, event); + setImmediate(() => event.target.parentNode.dispatchEvent(newEvent)); + }; + + const mode = this.state.selection.getMode(); + if (mode === 'hunk' && !this.state.selection.getSelectedHunks().has(hunk)) { + event.stopPropagation(); + + this.setState(prevState => { + return {selection: prevState.selection.selectHunk(hunk, event.shiftKey)}; + }, resend); + } else if (mode === 'line' && !this.state.selection.getSelectedLines().has(line)) { + event.stopPropagation(); + + this.setState(prevState => { + return {selection: prevState.selection.selectLine(line, event.shiftKey)}; + }, resend); + } + } + + mousedownOnHeader(event, hunk) { + if (event.button !== 0) { return; } + const windows = process.platform === 'win32'; + if (event.ctrlKey && !windows) { return; } // simply open context menu + + this.mouseSelectionInProgress = true; + event.persist && event.persist(); + + this.setState(prevState => { + let selection = prevState.selection; + if (event.metaKey || (event.ctrlKey && windows)) { + if (selection.getMode() === 'hunk') { + selection = selection.addOrSubtractHunkSelection(hunk); + } else { + // TODO: optimize + selection = hunk.getLines().reduce( + (current, line) => current.addOrSubtractLineSelection(line).coalesce(), + selection, + ); + } + } else if (event.shiftKey) { + if (selection.getMode() === 'hunk') { + selection = selection.selectHunk(hunk, true); + } else { + const hunkLines = hunk.getLines(); + const tailIndex = selection.getLineSelectionTailIndex(); + const selectedHunkAfterTail = tailIndex < hunkLines[0].diffLineNumber; + if (selectedHunkAfterTail) { + selection = selection.selectLine(hunkLines[hunkLines.length - 1], true); + } else { + selection = selection.selectLine(hunkLines[0], true); + } + } + } else { + selection = selection.selectHunk(hunk, false); + } + + return {selection}; + }); + } + + mousedownOnLine(event, hunk, line) { + if (event.button !== 0) { return; } + const windows = process.platform === 'win32'; + if (event.ctrlKey && !windows) { return; } // simply open context menu + + this.mouseSelectionInProgress = true; + event.persist && event.persist(); + + this.setState(prevState => { + let selection = prevState.selection; + + if (event.metaKey || (event.ctrlKey && windows)) { + if (selection.getMode() === 'hunk') { + selection = selection.addOrSubtractHunkSelection(hunk); + } else { + selection = selection.addOrSubtractLineSelection(line); + } + } else if (event.shiftKey) { + if (selection.getMode() === 'hunk') { + selection = selection.selectHunk(hunk, true); + } else { + selection = selection.selectLine(line, true); + } + } else if (event.detail === 1) { + selection = selection.selectLine(line, false); + } else if (event.detail === 2) { + selection = selection.selectHunk(hunk, false); + } + + return {selection}; + }); + } + + mousemoveOnLine(event, hunk, line) { + if (!this.mouseSelectionInProgress) { return; } + + this.setState(prevState => { + let selection = null; + if (prevState.selection.getMode() === 'hunk') { + selection = prevState.selection.selectHunk(hunk, true); + } else { + selection = prevState.selection.selectLine(line, true); + } + return {selection}; + }); + } + + mouseup() { + this.mouseSelectionInProgress = false; + this.setState(prevState => { + return {selection: prevState.selection.coalesce()}; + }); + } + + togglePatchSelectionMode() { + this.setState(prevState => ({selection: prevState.selection.toggleMode()})); + } + + getPatchSelectionMode() { + return this.state.selection.getMode(); + } + + getSelectedHunks() { + return this.state.selection.getSelectedHunks(); + } + + getSelectedLines() { + return this.state.selection.getSelectedLines(); + } + + selectNext() { + this.setState(prevState => ({selection: prevState.selection.selectNext()})); + } + + selectNextElement() { + if (this.state.selection.isEmpty() && this.props.didSurfaceFile) { + this.props.didSurfaceFile(); + } else { + this.setState(prevState => ({selection: prevState.selection.jumpToNextHunk()})); + } + } + + selectToNext() { + this.setState(prevState => { + return {selection: prevState.selection.selectNext(true).coalesce()}; + }); + } + + selectPrevious() { + this.setState(prevState => ({selection: prevState.selection.selectPrevious()})); + } + + selectPreviousElement() { + if (this.state.selection.isEmpty() && this.props.didSurfaceFile) { + this.props.didSurfaceFile(); + } else { + this.setState(prevState => ({selection: prevState.selection.jumpToPreviousHunk()})); + } + } + + selectToPrevious() { + this.setState(prevState => { + return {selection: prevState.selection.selectPrevious(true).coalesce()}; + }); + } + + selectFirst() { + this.setState(prevState => ({selection: prevState.selection.selectFirst()})); + } + + selectToFirst() { + this.setState(prevState => ({selection: prevState.selection.selectFirst(true)})); + } + + selectLast() { + this.setState(prevState => ({selection: prevState.selection.selectLast()})); + } + + selectToLast() { + this.setState(prevState => ({selection: prevState.selection.selectLast(true)})); + } + + selectAll() { + return new Promise(resolve => { + this.setState(prevState => ({selection: prevState.selection.selectAll()}), resolve); + }); + } + + getNextHunkUpdatePromise() { + return this.state.selection.getNextUpdatePromise(); + } + + didClickStageButtonForHunk(hunk) { + if (this.state.selection.getSelectedHunks().has(hunk)) { + this.props.attemptLineStageOperation(this.state.selection.getSelectedLines()); + } else { + this.setState(prevState => ({selection: prevState.selection.selectHunk(hunk)}), () => { + this.props.attemptHunkStageOperation(hunk); + }); + } + } + + didClickDiscardButtonForHunk(hunk) { + if (this.state.selection.getSelectedHunks().has(hunk)) { + this.discardSelection(); + } else { + this.setState(prevState => ({selection: prevState.selection.selectHunk(hunk)}), () => { + this.discardSelection(); + }); + } + } + + didConfirm() { + return this.didClickStageButtonForHunk([...this.state.selection.getSelectedHunks()][0]); + } + + didMoveRight() { + if (this.props.didSurfaceFile) { + this.props.didSurfaceFile(); + } + } + + focus() { + this.refElement.get().focus(); + } + + openFile() { + let lineNumber = 0; + const firstSelectedLine = Array.from(this.state.selection.getSelectedLines())[0]; + if (firstSelectedLine && firstSelectedLine.newLineNumber > -1) { + lineNumber = firstSelectedLine.newLineNumber; + } else { + const firstSelectedHunk = Array.from(this.state.selection.getSelectedHunks())[0]; + lineNumber = firstSelectedHunk ? firstSelectedHunk.getNewStartRow() : 0; + } + return this.props.openCurrentFile({lineNumber}); + } + + stageOrUnstageAll() { + this.props.attemptFileStageOperation(); + } + + stageOrUnstageModeChange() { + this.props.attemptModeStageOperation(); + } + + stageOrUnstageSymlinkChange() { + this.props.attemptSymlinkStageOperation(); + } + + discardSelection() { + const selectedLines = this.state.selection.getSelectedLines(); + return selectedLines.size ? this.props.discardLines(selectedLines) : null; + } + + goToDiffLine(lineNumber) { + this.setState(prevState => ({selection: prevState.selection.goToDiffLine(lineNumber)})); + } +} From c8d5640c217a4ec71435cb6f6ef212d4b32ee8e8 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 6 Jun 2018 13:17:23 -0400 Subject: [PATCH 0004/4252] Convert a FilePatch to a PresentedFilePatch for rendering within a TextEditor --- lib/models/file-patch.js | 5 ++ lib/models/presented-file-patch.js | 59 +++++++++++++++++++++ test/models/file-patch.test.js | 83 ++++++++++++++++++++++++++++++ 3 files changed, 147 insertions(+) create mode 100644 lib/models/presented-file-patch.js diff --git a/lib/models/file-patch.js b/lib/models/file-patch.js index 0e0a91cf0f..41a6976bf8 100644 --- a/lib/models/file-patch.js +++ b/lib/models/file-patch.js @@ -1,5 +1,6 @@ import Hunk from './hunk'; import {toGitPathSep} from '../helpers'; +import PresentedFilePatch from './presented-file-patch'; class File { static empty() { @@ -322,6 +323,10 @@ export default class FilePatch { return hunks; } + present() { + return new PresentedFilePatch(this); + } + toString() { if (this.hasTypechange()) { const left = this.clone({ diff --git a/lib/models/presented-file-patch.js b/lib/models/presented-file-patch.js new file mode 100644 index 0000000000..aef7b69b34 --- /dev/null +++ b/lib/models/presented-file-patch.js @@ -0,0 +1,59 @@ +import {Point} from 'atom'; + +export default class PresentedFilePatch { + constructor(filePatch) { + this.filePatch = filePatch; + + this.hunkStartPositions = []; + this.bufferPositions = { + unchanged: [], + added: [], + deleted: [], + nonewline: [], + }; + + let bufferLine = 0; + this.text = filePatch.getHunks().reduce((str, hunk) => { + this.hunkStartPositions.push(new Point(bufferLine, 0)); + + return hunk.getLines().reduce((hunkStr, line) => { + hunkStr += line.getText() + '\n'; + + this.bufferPositions[line.getStatus()].push( + new Point(bufferLine, 0), + ); + + bufferLine++; + return hunkStr; + }, str); + }, ''); + } + + getFilePatch() { + return this.filePatch; + } + + getText() { + return this.text; + } + + getHunkStartPositions() { + return this.hunkStartPositions; + } + + getUnchangedBufferPositions() { + return this.bufferPositions.unchanged; + } + + getAddedBufferPositions() { + return this.bufferPositions.added; + } + + getDeletedBufferPositions() { + return this.bufferPositions.deleted; + } + + getNoNewlineBufferPositions() { + return this.bufferPositions.nonewline; + } +} diff --git a/test/models/file-patch.test.js b/test/models/file-patch.test.js index e0e7aab8c2..e78e625a73 100644 --- a/test/models/file-patch.test.js +++ b/test/models/file-patch.test.js @@ -387,4 +387,87 @@ describe('FilePatch', function() { assert.strictEqual(filePatch.getByteSize(), 36); }); + + describe('present()', function() { + let presented; + + beforeEach(function() { + const patch = createFilePatch('a.txt', 'a.txt', 'modified', [ + new Hunk(1, 1, 1, 3, '@@ -1,1 +2,2', [ + new HunkLine('line-1', 'added', -1, 1), + new HunkLine('line-2', 'added', -1, 2), + new HunkLine('line-3', 'unchanged', 1, 3), + ]), + new Hunk(5, 7, 5, 4, '@@ -3,3 +4,4', [ + new HunkLine('line-4', 'unchanged', 5, 7), + new HunkLine('line-5', 'deleted', 6, -1), + new HunkLine('line-6', 'deleted', 7, -1), + new HunkLine('line-7', 'added', -1, 8), + new HunkLine('line-8', 'added', -1, 9), + new HunkLine('line-9', 'added', -1, 10), + new HunkLine('line-10', 'deleted', 8, -1), + new HunkLine('line-11', 'deleted', 9, -1), + new HunkLine('line-12', 'unchanged', 5, 7), + ]), + new Hunk(20, 19, 2, 2, '@@ -5,5 +6,6', [ + new HunkLine('line-13', 'deleted', 20, -1), + new HunkLine('line-14', 'added', -1, 19), + new HunkLine('line-15', 'unchanged', 21, 20), + new HunkLine('No newline at end of file', 'nonewline', -1, -1), + ]), + ]); + + presented = patch.present(); + }); + + function assertPositions(actualPositions, expectedPositions) { + assert.lengthOf(actualPositions, expectedPositions.length); + for (let i = 0; i < expectedPositions.length; i++) { + const actual = actualPositions[i]; + const expected = expectedPositions[i]; + + assert.isTrue(actual.isEqual(expected), + `range ${i}: ${actual.toString()} does not equal [${expected.map(e => e.toString()).join(', ')}]`); + } + } + + it('unifies hunks into a continuous, unadorned string of text', function() { + const actualText = presented.getText(); + const expectedText = + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15].map(num => `line-${num}\n`).join('') + + 'No newline at end of file\n'; + + assert.strictEqual(actualText, expectedText); + }); + + it("returns the buffer positions corresponding to each hunk's beginning", function() { + assertPositions(presented.getHunkStartPositions(), [ + [0, 0], [3, 0], [12, 0], + ]); + }); + + it('returns the buffer positions corresponding to unchanged lines', function() { + assertPositions(presented.getUnchangedBufferPositions(), [ + [2, 0], [3, 0], [11, 0], [14, 0], + ]); + }); + + it('returns the buffer positions corresponding to added lines', function() { + assertPositions(presented.getAddedBufferPositions(), [ + [0, 0], [1, 0], [6, 0], [7, 0], [8, 0], [13, 0], + ]); + }); + + it('returns the buffer positions corresponding to deleted lines', function() { + assertPositions(presented.getDeletedBufferPositions(), [ + [4, 0], [5, 0], [9, 0], [10, 0], [12, 0], + ]); + }); + + it('returns the buffer position of a "no newline" trailer', function() { + assertPositions(presented.getNoNewlineBufferPositions(), [ + [15, 0], + ]); + }); + }); }); From 462f266fac3736bc23e0b3727e1d4134bf970795 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 6 Jun 2018 13:18:05 -0400 Subject: [PATCH 0005/4252] Change StagingView tests to check for FilePatchItems --- lib/views/staging-view.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/views/staging-view.js b/lib/views/staging-view.js index 0409f73dad..1b563d77d6 100644 --- a/lib/views/staging-view.js +++ b/lib/views/staging-view.js @@ -12,7 +12,7 @@ import ObserveModel from './observe-model'; import MergeConflictListItemView from './merge-conflict-list-item-view'; import CompositeListSelection from '../models/composite-list-selection'; import ResolutionProgress from '../models/conflicts/resolution-progress'; -import FilePatchController from '../controllers/file-patch-controller'; +import FilePatchItem from '../items/file-patch-item'; import Commands, {Command} from '../atom/commands'; import {autobind} from '../helpers'; @@ -541,7 +541,7 @@ export default class StagingView extends React.Component { return; } - const isFilePatchController = realItem instanceof FilePatchController; + const isFilePatchController = realItem instanceof FilePatchItem; const isMatch = realItem.getWorkingDirectory && item.getWorkingDirectory() === this.props.workingDirectoryPath; if (isFilePatchController && isMatch) { @@ -657,7 +657,7 @@ export default class StagingView extends React.Component { const activePane = this.props.workspace.getCenter().getActivePane(); const activePendingItem = activePane.getPendingItem(); const activePaneHasPendingFilePatchItem = activePendingItem && activePendingItem.getRealItem && - activePendingItem.getRealItem() instanceof FilePatchController; + activePendingItem.getRealItem() instanceof FilePatchItem; if (activePaneHasPendingFilePatchItem) { await this.showFilePatchItem(selectedItem.filePath, this.state.selection.getActiveListKey(), { activate: false, @@ -676,7 +676,7 @@ export default class StagingView extends React.Component { const pendingItem = pane.getPendingItem(); if (!pendingItem || !pendingItem.getRealItem) { return false; } const realItem = pendingItem.getRealItem(); - const isDiffViewItem = realItem instanceof FilePatchController; + const isDiffViewItem = realItem instanceof FilePatchItem; // We only want to update pending diff views for currently active repo const isInActiveRepo = realItem.getWorkingDirectory() === this.props.workingDirectoryPath; const isStale = !this.changedFileExists(realItem.getFilePath(), realItem.getStagingStatus()); @@ -691,7 +691,7 @@ export default class StagingView extends React.Component { } async showFilePatchItem(filePath, stagingStatus, {activate, pane} = {activate: false}) { - const uri = FilePatchController.buildURI(filePath, this.props.workingDirectoryPath, stagingStatus); + const uri = FilePatchItem.buildURI(filePath, this.props.workingDirectoryPath, stagingStatus); const filePatchItem = await this.props.workspace.open( uri, {pending: true, activatePane: activate, activateItem: activate, pane}, ); From 0e3e7d39b5768d23a774cfe7193804c08bda3896 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 6 Jun 2018 13:18:24 -0400 Subject: [PATCH 0006/4252] Use a PresentedFilePatch to render --- lib/views/file-patch-view.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/lib/views/file-patch-view.js b/lib/views/file-patch-view.js index f774c89437..f6244e056a 100644 --- a/lib/views/file-patch-view.js +++ b/lib/views/file-patch-view.js @@ -21,18 +21,27 @@ export default class FilePatchView extends React.Component { this.state = { selection: new FilePatchSelection(this.props.filePatch.getHunks()), + presentedFilePatch: this.props.filePatch.present(), }; } - render() { - const text = this.props.filePatch.getHunks().map(h => h.toString()).join('\n'); + static getDerivedStateFromProps(props, state) { + if (props.filePatch !== state.presentedFilePatch.getFilePatch()) { + return { + presentedFilePatch: props.filePatch.present(), + }; + } + + return null; + } + render() { return (
- + {this.renderFileHeader()} From d6189565cd6a618d65a9fb51c2608efa508f146d Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 6 Jun 2018 14:53:55 -0400 Subject: [PATCH 0007/4252] FilePatchItem tests --- lib/items/file-patch-item.js | 25 +++---- lib/prop-types.js | 4 ++ test/items/file-patch-item.test.js | 102 +++++++++++++++++++++++++++++ 3 files changed, 119 insertions(+), 12 deletions(-) create mode 100644 test/items/file-patch-item.test.js diff --git a/lib/items/file-patch-item.js b/lib/items/file-patch-item.js index 1c02262dac..3ab27842fc 100644 --- a/lib/items/file-patch-item.js +++ b/lib/items/file-patch-item.js @@ -2,23 +2,24 @@ import React from 'react'; import PropTypes from 'prop-types'; import {Emitter} from 'event-kit'; +import {WorkdirContextPoolPropType} from '../prop-types'; import FilePatchContainer from '../containers/file-patch-container'; export default class FilePatchItem extends React.Component { static propTypes = { - repository: PropTypes.object.isRequired, - stagingStatus: PropTypes.oneOf(['staged', 'unstaged']), - relPath: PropTypes.string.isRequired, + workdirContextPool: WorkdirContextPoolPropType.isRequired, - tooltips: PropTypes.object.isRequired, + relPath: PropTypes.string.isRequired, + workingDirectory: PropTypes.string.isRequired, + stagingStatus: PropTypes.oneOf(['staged', 'unstaged']), } - static uriPattern = 'atom-github://file-patch/{relPath...}?workdir={workdir}&stagingStatus={stagingStatus}' + static uriPattern = 'atom-github://file-patch/{relPath...}?workdir={workingDirectory}&stagingStatus={stagingStatus}' - static buildURI(relPath, workdir, stagingStatus) { + static buildURI(relPath, workingDirectory, stagingStatus) { return 'atom-github://file-patch/' + relPath + - `?workdir=${encodeURIComponent(workdir)}` + + `?workdir=${encodeURIComponent(workingDirectory)}` + `&stagingStatus=${encodeURIComponent(stagingStatus)}`; } @@ -60,12 +61,12 @@ export default class FilePatchItem extends React.Component { } render() { + const repository = this.props.workdirContextPool.getContext(this.props.workingDirectory).getRepository(); + return ( ); } @@ -86,6 +87,6 @@ export default class FilePatchItem extends React.Component { } getWorkingDirectory() { - return this.props.repository.getWorkingDirectoryPath(); + return this.props.workingDirectory; } } diff --git a/lib/prop-types.js b/lib/prop-types.js index 2d17a1994a..31425b7db5 100644 --- a/lib/prop-types.js +++ b/lib/prop-types.js @@ -10,6 +10,10 @@ export const DOMNodePropType = (props, propName, componentName) => { } }; +export const WorkdirContextPoolPropType = PropTypes.shape({ + getContext: PropTypes.func.isRequired, +}); + export const RemotePropType = PropTypes.shape({ getName: PropTypes.func.isRequired, getUrl: PropTypes.func.isRequired, diff --git a/test/items/file-patch-item.test.js b/test/items/file-patch-item.test.js new file mode 100644 index 0000000000..90a0124db5 --- /dev/null +++ b/test/items/file-patch-item.test.js @@ -0,0 +1,102 @@ +import path from 'path'; +import React from 'react'; +import {mount} from 'enzyme'; + +import PaneItem from '../../lib/atom/pane-item'; +import FilePatchItem from '../../lib/items/file-patch-item'; +import WorkdirContextPool from '../../lib/models/workdir-context-pool'; +import {cloneRepository, buildRepository} from '../helpers'; + +describe('FilePatchItem', function() { + let atomEnv, repository, pool; + + beforeEach(async function() { + atomEnv = global.buildAtomEnvironment(); + + const workdirPath = await cloneRepository(); + repository = await buildRepository(workdirPath); + + pool = new WorkdirContextPool({ + workspace: atomEnv.workspace, + }); + repository = pool.add(workdirPath).getRepository(); + }); + + afterEach(function() { + atomEnv.destroy(); + pool.clear(); + }); + + function buildPaneApp(overrideProps = {}) { + const props = { + workdirContextPool: pool, + tooltips: atomEnv.tooltips, + ...overrideProps, + }; + + return ( + + {({itemHolder, params}) => { + return ( + + ); + }} + + ); + } + + function open(wrapper, options = {}) { + const opts = { + relPath: 'a.txt', + workingDirectory: repository.getWorkingDirectoryPath(), + stagingStatus: 'unstaged', + ...options, + }; + const uri = FilePatchItem.buildURI(opts.relPath, opts.workingDirectory, opts.stagingStatus); + return atomEnv.workspace.open(uri); + } + + it('locates the repository from the context pool', async function() { + const wrapper = mount(buildPaneApp()); + await open(wrapper); + + assert.strictEqual(wrapper.update().find('FilePatchContainer').prop('repository'), repository); + }); + + it('passes an absent repository if the working directory is unrecognized', async function() { + const wrapper = mount(buildPaneApp()); + await open(wrapper, {workingDirectory: '/nope'}); + + assert.isTrue(wrapper.update().find('FilePatchContainer').prop('repository').isAbsent()); + }); + + it('passes other props to the container', async function() { + const other = Symbol('other'); + const wrapper = mount(buildPaneApp({other})); + await open(wrapper); + + assert.strictEqual(wrapper.update().find('FilePatchContainer').prop('other'), other); + }); + + describe('getTitle()', function() { + it('renders an unstaged title', async function() { + const wrapper = mount(buildPaneApp()); + const item = await open(wrapper, {stagingStatus: 'unstaged'}); + + assert.strictEqual(item.getTitle(), 'Unstaged Changes: a.txt'); + }); + + it('renders a staged title', async function() { + const wrapper = mount(buildPaneApp()); + const item = await open(wrapper, {stagingStatus: 'staged'}); + + assert.strictEqual(item.getTitle(), 'Staged Changes: a.txt'); + }); + }); +}); From 46a67c57d2d7e5cdb08ecc0b6d997208dbb96b4d Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 6 Jun 2018 14:54:28 -0400 Subject: [PATCH 0008/4252] Return null for an empty patch for now --- lib/containers/file-patch-container.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/containers/file-patch-container.js b/lib/containers/file-patch-container.js index 9e5d5a4eee..3d7b31d89d 100644 --- a/lib/containers/file-patch-container.js +++ b/lib/containers/file-patch-container.js @@ -41,6 +41,10 @@ export default class FilePatchContainer extends React.Component { return null; } + if (data.filePatch === null) { + return null; + } + return ( Date: Wed, 6 Jun 2018 14:56:42 -0400 Subject: [PATCH 0009/4252] Repository is created from the context pool --- test/items/file-patch-item.test.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/items/file-patch-item.test.js b/test/items/file-patch-item.test.js index 90a0124db5..d588233824 100644 --- a/test/items/file-patch-item.test.js +++ b/test/items/file-patch-item.test.js @@ -5,7 +5,7 @@ import {mount} from 'enzyme'; import PaneItem from '../../lib/atom/pane-item'; import FilePatchItem from '../../lib/items/file-patch-item'; import WorkdirContextPool from '../../lib/models/workdir-context-pool'; -import {cloneRepository, buildRepository} from '../helpers'; +import {cloneRepository} from '../helpers'; describe('FilePatchItem', function() { let atomEnv, repository, pool; @@ -14,7 +14,6 @@ describe('FilePatchItem', function() { atomEnv = global.buildAtomEnvironment(); const workdirPath = await cloneRepository(); - repository = await buildRepository(workdirPath); pool = new WorkdirContextPool({ workspace: atomEnv.workspace, From b6b92f937424b0ae48771cb33ecd07d826677c60 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 6 Jun 2018 16:05:34 -0400 Subject: [PATCH 0010/4252] FilePatchContainer tests --- lib/containers/file-patch-container.js | 16 +++-- test/containers/file-patch-container.test.js | 65 ++++++++++++++++++++ 2 files changed, 77 insertions(+), 4 deletions(-) create mode 100644 test/containers/file-patch-container.test.js diff --git a/lib/containers/file-patch-container.js b/lib/containers/file-patch-container.js index 3d7b31d89d..5375d24f72 100644 --- a/lib/containers/file-patch-container.js +++ b/lib/containers/file-patch-container.js @@ -4,6 +4,7 @@ import yubikiri from 'yubikiri'; import {autobind} from '../helpers'; import ObserveModel from '../views/observe-model'; +import LoadingView from '../views/loading-view'; import FilePatchController from '../controllers/file-patch-controller'; export default class FilePatchContainer extends React.Component { @@ -38,20 +39,27 @@ export default class FilePatchContainer extends React.Component { renderWithData(data) { if (data === null) { - return null; + return ; } if (data.filePatch === null) { - return null; + return this.renderEmptyPatchMessage(); } return ( ); } + + renderEmptyPatchMessage() { + return ( +
+ No changes to display +
+ ); + } } diff --git a/test/containers/file-patch-container.test.js b/test/containers/file-patch-container.test.js new file mode 100644 index 0000000000..2f01eb3772 --- /dev/null +++ b/test/containers/file-patch-container.test.js @@ -0,0 +1,65 @@ +import path from 'path'; +import fs from 'fs-extra'; +import React from 'react'; +import {mount} from 'enzyme'; + +import FilePatchContainer from '../../lib/containers/file-patch-container'; +import {cloneRepository, buildRepository} from '../helpers'; + +describe('FilePatchContainer', function() { + let atomEnv, repository; + + beforeEach(async function() { + atomEnv = global.buildAtomEnvironment(); + + const workdirPath = await cloneRepository(); + repository = await buildRepository(workdirPath); + sinon.spy(repository, 'getFilePatchForPath'); + sinon.spy(repository, 'isPartiallyStaged'); + + // a.txt: unstaged changes + await fs.writeFile(path.join(workdirPath, 'a.txt'), 'changed\n'); + + // b.txt: staged changes + await fs.writeFile(path.join(workdirPath, 'b.txt'), 'changed\n'); + await repository.stageFiles(['b.txt']); + + // c.txt: untouched + }); + + afterEach(function() { + atomEnv.destroy(); + }); + + function buildApp(overrideProps = {}) { + const props = { + repository, + stagingStatus: 'unstaged', + relPath: 'a.txt', + ...overrideProps, + }; + + return ; + } + + it('renders a loading spinner before file patch data arrives', function() { + const wrapper = mount(buildApp()); + assert.isTrue(wrapper.find('LoadingView').exists()); + }); + + it('renders a message for an empty patch', async function() { + const wrapper = mount(buildApp({relPath: 'c.txt'})); + await assert.async.isTrue(wrapper.update().find('span.icon-info').exists()); + }); + + it('renders a FilePatchView', async function() { + const wrapper = mount(buildApp({relPath: 'a.txt', stagingStatus: 'unstaged'})); + await assert.async.isTrue(wrapper.update().find('FilePatchView').exists()); + }); + + it('passes unrecognized props to the FilePatchView', async function() { + const extra = Symbol('extra'); + const wrapper = mount(buildApp({relPath: 'a.txt', stagingStatus: 'unstaged', extra})); + await assert.async.strictEqual(wrapper.update().find('FilePatchView').prop('extra'), extra); + }); +}); From 3dd99de35a7d7aaf9d480d4b5d0649b6bf644acf Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 6 Jun 2018 16:16:40 -0400 Subject: [PATCH 0011/4252] Stub test for the stub FilePatchController --- .../controllers/file-patch-controller.test.js | 820 +----------------- .../file-patch-controller.test.old.js | 811 +++++++++++++++++ 2 files changed, 838 insertions(+), 793 deletions(-) create mode 100644 test/controllers/file-patch-controller.test.old.js diff --git a/test/controllers/file-patch-controller.test.js b/test/controllers/file-patch-controller.test.js index a7b075e8ec..ad18b3cb4d 100644 --- a/test/controllers/file-patch-controller.test.js +++ b/test/controllers/file-patch-controller.test.js @@ -1,811 +1,45 @@ -import React from 'react'; -import {shallow, mount} from 'enzyme'; -import until from 'test-until'; - -import fs from 'fs'; import path from 'path'; +import fs from 'fs'; +import React from 'react'; +import {mount} from 'enzyme'; -import {cloneRepository, buildRepository} from '../helpers'; -import FilePatch from '../../lib/models/file-patch'; import FilePatchController from '../../lib/controllers/file-patch-controller'; -import Hunk from '../../lib/models/hunk'; -import HunkLine from '../../lib/models/hunk-line'; -import ResolutionProgress from '../../lib/models/conflicts/resolution-progress'; -import Switchboard from '../../lib/switchboard'; - -function createFilePatch(oldFilePath, newFilePath, status, hunks) { - const oldFile = new FilePatch.File({path: oldFilePath}); - const newFile = new FilePatch.File({path: newFilePath}); - const patch = new FilePatch.Patch({status, hunks}); - - return new FilePatch(oldFile, newFile, patch); -} - -let atomEnv, commandRegistry, tooltips, deserializers; -let switchboard, getFilePatchForPath; -let discardLines, didSurfaceFile, didDiveIntoFilePath, quietlySelectItem, undoLastDiscard, openFiles, getRepositoryForWorkdir; -let getSelectedStagingViewItems, resolutionProgress; - -function createComponent(repository, filePath) { - atomEnv = global.buildAtomEnvironment(); - commandRegistry = atomEnv.commands; - deserializers = atomEnv.deserializers; - tooltips = atomEnv.tooltips; - - switchboard = new Switchboard(); - - discardLines = sinon.spy(); - didSurfaceFile = sinon.spy(); - didDiveIntoFilePath = sinon.spy(); - quietlySelectItem = sinon.spy(); - undoLastDiscard = sinon.spy(); - openFiles = sinon.spy(); - getSelectedStagingViewItems = sinon.spy(); +import {cloneRepository, buildRepository} from '../helpers'; - getRepositoryForWorkdir = () => repository; - resolutionProgress = new ResolutionProgress(); +describe('FilePatchController', function() { + let atomEnv, repository, filePatch; - FilePatchController.resetConfirmedLargeFilePatches(); + beforeEach(async function() { + atomEnv = global.buildAtomEnvironment(); - return ( - - ); -} + const workdirPath = await cloneRepository(); + repository = await buildRepository(workdirPath); -async function refreshRepository(wrapper) { - const workDir = wrapper.prop('workingDirectoryPath'); - const repository = wrapper.prop('getRepositoryForWorkdir')(workDir); + // a.txt: unstaged changes + await fs.writeFile(path.join(workdirPath, 'a.txt'), 'changed\n'); - const promise = wrapper.prop('switchboard').getFinishRepositoryRefreshPromise(); - repository.refresh(); - await promise; - wrapper.update(); -} + filePatch = await repository.getFilePatchForPath('a.txt', {staged: false}); + }); -describe('FilePatchController', function() { afterEach(function() { atomEnv.destroy(); }); - describe('unit tests', function() { - let workdirPath, repository, filePath, component; - beforeEach(async function() { - workdirPath = await cloneRepository('multi-line-file'); - repository = await buildRepository(workdirPath); - filePath = 'sample.js'; - component = createComponent(repository, filePath); - - getFilePatchForPath = sinon.stub(repository, 'getFilePatchForPath'); - }); - - describe('when the FilePatch is too large', function() { - it('renders a confirmation widget', async function() { - const hunk1 = new Hunk(0, 0, 1, 1, '', [ - new HunkLine('line-1', 'added', 1, 1), - new HunkLine('line-2', 'added', 2, 2), - new HunkLine('line-3', 'added', 3, 3), - new HunkLine('line-4', 'added', 4, 4), - new HunkLine('line-5', 'added', 5, 5), - new HunkLine('line-6', 'added', 6, 6), - ]); - const filePatch = createFilePatch(filePath, filePath, 'modified', [hunk1]); - - getFilePatchForPath.returns(filePatch); - - const wrapper = mount(React.cloneElement(component, {largeDiffByteThreshold: 5})); - - await assert.async.match(wrapper.text(), /large .+ diff/); - }); - - it('renders the full diff when the confirmation is clicked', async function() { - const hunk = new Hunk(0, 0, 1, 1, '', [ - new HunkLine('line-1', 'added', 1, 1), - new HunkLine('line-2', 'added', 2, 2), - new HunkLine('line-3', 'added', 3, 3), - new HunkLine('line-4', 'added', 4, 4), - new HunkLine('line-5', 'added', 5, 5), - new HunkLine('line-6', 'added', 6, 6), - ]); - const filePatch = createFilePatch(filePath, filePath, 'modified', [hunk]); - getFilePatchForPath.returns(filePatch); - - const wrapper = mount(React.cloneElement(component, {largeDiffByteThreshold: 5})); - - await assert.async.isTrue(wrapper.update().find('.large-file-patch').exists()); - wrapper.find('.large-file-patch').find('button').simulate('click'); - - assert.isTrue(wrapper.find('HunkView').exists()); - }); - - it('renders the full diff if the file has been confirmed before', async function() { - const hunk = new Hunk(0, 0, 1, 1, '', [ - new HunkLine('line-1', 'added', 1, 1), - new HunkLine('line-2', 'added', 2, 2), - new HunkLine('line-3', 'added', 3, 3), - new HunkLine('line-4', 'added', 4, 4), - new HunkLine('line-5', 'added', 5, 5), - new HunkLine('line-6', 'added', 6, 6), - ]); - const filePatch1 = createFilePatch(filePath, filePath, 'modified', [hunk]); - const filePatch2 = createFilePatch('b.txt', 'b.txt', 'modified', [hunk]); - - getFilePatchForPath.returns(filePatch1); - - const wrapper = mount(React.cloneElement(component, { - filePath: filePatch1.getPath(), largeDiffByteThreshold: 5, - })); - - await assert.async.isTrue(wrapper.update().find('.large-file-patch').exists()); - wrapper.find('.large-file-patch').find('button').simulate('click'); - assert.isTrue(wrapper.find('HunkView').exists()); - - getFilePatchForPath.returns(filePatch2); - wrapper.setProps({filePath: filePatch2.getPath()}); - await assert.async.isTrue(wrapper.update().find('.large-file-patch').exists()); - - getFilePatchForPath.returns(filePatch1); - wrapper.setProps({filePath: filePatch1.getPath()}); - assert.isTrue(wrapper.update().find('HunkView').exists()); - }); - }); - - describe('onRepoRefresh', function() { - it('sets the correct FilePatch as state', async function() { - repository.getFilePatchForPath.restore(); - fs.writeFileSync(path.join(workdirPath, filePath), 'change', 'utf8'); - - const wrapper = mount(component); - - await assert.async.isNotNull(wrapper.state('filePatch')); - - const originalFilePatch = wrapper.state('filePatch'); - assert.equal(wrapper.state('stagingStatus'), 'unstaged'); - - fs.writeFileSync(path.join(workdirPath, 'file.txt'), 'change\nand again!', 'utf8'); - await refreshRepository(wrapper); - - assert.notEqual(originalFilePatch, wrapper.state('filePatch')); - assert.equal(wrapper.state('stagingStatus'), 'unstaged'); - }); - }); - - it('renders FilePatchView only if FilePatch has hunks', async function() { - const emptyFilePatch = createFilePatch(filePath, filePath, 'modified', []); - getFilePatchForPath.returns(emptyFilePatch); - - const wrapper = mount(component); - - assert.isTrue(wrapper.find('FilePatchView').exists()); - assert.isTrue(wrapper.find('FilePatchView').text().includes('File has no contents')); - - const hunk1 = new Hunk(0, 0, 1, 1, '', [new HunkLine('line-1', 'added', 1, 1)]); - const filePatch = createFilePatch(filePath, filePath, 'modified', [hunk1]); - getFilePatchForPath.returns(filePatch); - - wrapper.instance().onRepoRefresh(repository); - - await assert.async.isTrue(wrapper.update().find('HunkView').exists()); - assert.isTrue(wrapper.find('HunkView').text().includes('@@ -0,1 +0,1 @@')); - }); - - it('updates the FilePatch after a repo update', async function() { - const hunk1 = new Hunk(5, 5, 2, 1, '', [new HunkLine('line-1', 'added', -1, 5)]); - const hunk2 = new Hunk(8, 8, 1, 1, '', [new HunkLine('line-5', 'deleted', 8, -1)]); - const filePatch0 = createFilePatch(filePath, filePath, 'modified', [hunk1, hunk2]); - getFilePatchForPath.returns(filePatch0); - - const wrapper = shallow(component); - - let view0; - await until(() => { - view0 = wrapper.update().find('FilePatchView').shallow(); - return view0.find({hunk: hunk1}).exists(); - }); - assert.isTrue(view0.find({hunk: hunk2}).exists()); - - const hunk3 = new Hunk(8, 8, 1, 1, '', [new HunkLine('line-10', 'modified', 10, 10)]); - const filePatch1 = createFilePatch(filePath, filePath, 'modified', [hunk1, hunk3]); - getFilePatchForPath.returns(filePatch1); - - wrapper.instance().onRepoRefresh(repository); - let view1; - await until(() => { - view1 = wrapper.update().find('FilePatchView').shallow(); - return view1.find({hunk: hunk3}).exists(); - }); - assert.isTrue(view1.find({hunk: hunk1}).exists()); - assert.isFalse(view1.find({hunk: hunk2}).exists()); - }); - - it('invokes a didSurfaceFile callback with the current file path', async function() { - const filePatch = createFilePatch(filePath, filePath, 'modified', [new Hunk(1, 1, 1, 3, '', [])]); - getFilePatchForPath.returns(filePatch); - - const wrapper = mount(component); - - await assert.async.isTrue(wrapper.update().find('Commands').exists()); - commandRegistry.dispatch(wrapper.find('FilePatchView').getDOMNode(), 'core:move-right'); - assert.isTrue(didSurfaceFile.calledWith(filePath, 'unstaged')); - }); - - describe('openCurrentFile({lineNumber})', () => { - it('sets the cursor on the correct line of the opened text editor', async function() { - const editorSpy = { - relativePath: null, - scrollToBufferPosition: sinon.spy(), - setCursorBufferPosition: sinon.spy(), - }; - - const openFilesStub = relativePaths => { - assert.lengthOf(relativePaths, 1); - editorSpy.relativePath = relativePaths[0]; - return Promise.resolve([editorSpy]); - }; - - const hunk = new Hunk(5, 5, 2, 1, '', [new HunkLine('line-1', 'added', -1, 5)]); - const filePatch = createFilePatch(filePath, filePath, 'modified', [hunk]); - getFilePatchForPath.returns(filePatch); - - const wrapper = mount(React.cloneElement(component, {openFiles: openFilesStub})); - - await assert.async.isTrue(wrapper.update().find('HunkView').exists()); - - wrapper.find('LineView').simulate('mousedown', {button: 0, detail: 1}); - window.dispatchEvent(new MouseEvent('mouseup')); - commandRegistry.dispatch(wrapper.find('FilePatchView').getDOMNode(), 'github:open-file'); - wrapper.update(); - - await assert.async.isTrue(editorSpy.setCursorBufferPosition.called); - - assert.isTrue(editorSpy.relativePath === filePath); - - const scrollCall = editorSpy.scrollToBufferPosition.firstCall; - assert.isTrue(scrollCall.args[0].isEqual([4, 0])); - assert.deepEqual(scrollCall.args[1], {center: true}); - - const cursorCall = editorSpy.setCursorBufferPosition.firstCall; - assert.isTrue(cursorCall.args[0].isEqual([4, 0])); - }); - }); - }); - - describe('integration tests', function() { - describe('handling symlink files', function() { - async function indexModeAndOid(repository, filename) { - const output = await repository.git.exec(['ls-files', '-s', '--', filename]); - if (output) { - const parts = output.split(' '); - return {mode: parts[0], oid: parts[1]}; - } else { - return null; - } - } - - it('unstages added lines that don\'t require symlink change', async function() { - const workingDirPath = await cloneRepository('symlinks'); - const repository = await buildRepository(workingDirPath); - - // correctly handle symlinks on Windows - await repository.git.exec(['config', 'core.symlinks', 'true']); - - const deletedSymlinkAddedFilePath = 'symlink.txt'; - fs.unlinkSync(path.join(workingDirPath, deletedSymlinkAddedFilePath)); - fs.writeFileSync(path.join(workingDirPath, deletedSymlinkAddedFilePath), 'qux\nfoo\nbar\nbaz\nzoo\n', 'utf8'); - - // Stage whole file - await repository.stageFiles([deletedSymlinkAddedFilePath]); - - const component = createComponent(repository, deletedSymlinkAddedFilePath); - const wrapper = mount(React.cloneElement(component, {filePath: deletedSymlinkAddedFilePath, initialStagingStatus: 'staged'})); - - // index shows symlink deltion and added lines - assert.autocrlfEqual(await repository.readFileFromIndex(deletedSymlinkAddedFilePath), 'qux\nfoo\nbar\nbaz\nzoo\n'); - assert.equal((await indexModeAndOid(repository, deletedSymlinkAddedFilePath)).mode, '100644'); - - // Unstage a couple added lines, but not all - await assert.async.isTrue(wrapper.update().find('HunkView').exists()); - const opPromise0 = switchboard.getFinishStageOperationPromise(); - const hunkView0 = wrapper.find('HunkView').at(0); - hunkView0.find('LineView').at(1).find('.github-HunkView-line').simulate('mousedown', {button: 0, detail: 1}); - hunkView0.find('LineView').at(2).find('.github-HunkView-line').simulate('mousemove', {}); - window.dispatchEvent(new MouseEvent('mouseup')); - hunkView0.find('button.github-HunkView-stageButton').simulate('click'); - await opPromise0; - - await refreshRepository(wrapper); - - // index shows symlink deletions still staged, only a couple of lines have been unstaged - assert.equal((await indexModeAndOid(repository, deletedSymlinkAddedFilePath)).mode, '100644'); - assert.autocrlfEqual(await repository.readFileFromIndex(deletedSymlinkAddedFilePath), 'qux\nbaz\nzoo\n'); - }); - - it('stages deleted lines that don\'t require symlink change', async function() { - const workingDirPath = await cloneRepository('symlinks'); - const repository = await buildRepository(workingDirPath); - - const deletedFileAddedSymlinkPath = 'a.txt'; - fs.unlinkSync(path.join(workingDirPath, deletedFileAddedSymlinkPath)); - fs.symlinkSync(path.join(workingDirPath, 'regular-file.txt'), path.join(workingDirPath, deletedFileAddedSymlinkPath)); - - const component = createComponent(repository, deletedFileAddedSymlinkPath); - const wrapper = mount(React.cloneElement(component, {filePath: deletedFileAddedSymlinkPath, initialStagingStatus: 'unstaged'})); - - // index shows file is not a symlink, no deleted lines - assert.equal((await indexModeAndOid(repository, deletedFileAddedSymlinkPath)).mode, '100644'); - assert.autocrlfEqual(await repository.readFileFromIndex(deletedFileAddedSymlinkPath), 'foo\nbar\nbaz\n\n'); - - // stage a couple of lines, but not all - await assert.async.isTrue(wrapper.update().find('HunkView').exists()); - const opPromise0 = switchboard.getFinishStageOperationPromise(); - const hunkView0 = wrapper.find('HunkView').at(0); - hunkView0.find('LineView').at(1).find('.github-HunkView-line').simulate('mousedown', {button: 0, detail: 1}); - hunkView0.find('LineView').at(2).find('.github-HunkView-line').simulate('mousemove', {}); - window.dispatchEvent(new MouseEvent('mouseup')); - hunkView0.find('button.github-HunkView-stageButton').simulate('click'); - await opPromise0; - - await refreshRepository(wrapper); - - // index shows symlink change has not been staged, a couple of lines have been deleted - assert.equal((await indexModeAndOid(repository, deletedFileAddedSymlinkPath)).mode, '100644'); - assert.autocrlfEqual(await repository.readFileFromIndex(deletedFileAddedSymlinkPath), 'foo\n\n'); - }); - - it('stages symlink change when staging added lines that depend on change', async function() { - const workingDirPath = await cloneRepository('symlinks'); - const repository = await buildRepository(workingDirPath); - - // correctly handle symlinks on Windows - await repository.git.exec(['config', 'core.symlinks', 'true']); - - const deletedSymlinkAddedFilePath = 'symlink.txt'; - fs.unlinkSync(path.join(workingDirPath, deletedSymlinkAddedFilePath)); - fs.writeFileSync(path.join(workingDirPath, deletedSymlinkAddedFilePath), 'qux\nfoo\nbar\nbaz\nzoo\n', 'utf8'); - - const component = createComponent(repository, deletedSymlinkAddedFilePath); - const wrapper = mount(React.cloneElement(component, {filePath: deletedSymlinkAddedFilePath})); - - // index shows file is symlink - assert.equal((await indexModeAndOid(repository, deletedSymlinkAddedFilePath)).mode, '120000'); - - // Stage a couple added lines, but not all - await assert.async.isTrue(wrapper.update().find('HunkView').exists()); - const opPromise0 = switchboard.getFinishStageOperationPromise(); - const hunkView0 = wrapper.find('HunkView').at(0); - hunkView0.find('LineView').at(1).find('.github-HunkView-line').simulate('mousedown', {button: 0, detail: 1}); - hunkView0.find('LineView').at(2).find('.github-HunkView-line').simulate('mousemove', {}); - window.dispatchEvent(new MouseEvent('mouseup')); - hunkView0.find('button.github-HunkView-stageButton').simulate('click'); - await opPromise0; - - await refreshRepository(wrapper); - - // index no longer shows file is symlink (symlink has been deleted), now a regular file with contents - assert.equal((await indexModeAndOid(repository, deletedSymlinkAddedFilePath)).mode, '100644'); - assert.autocrlfEqual(await repository.readFileFromIndex(deletedSymlinkAddedFilePath), 'foo\nbar\n'); - }); - - it('unstages symlink change when unstaging deleted lines that depend on change', async function() { - const workingDirPath = await cloneRepository('symlinks'); - const repository = await buildRepository(workingDirPath); - - const deletedFileAddedSymlinkPath = 'a.txt'; - fs.unlinkSync(path.join(workingDirPath, deletedFileAddedSymlinkPath)); - fs.symlinkSync(path.join(workingDirPath, 'regular-file.txt'), path.join(workingDirPath, deletedFileAddedSymlinkPath)); - await repository.stageFiles([deletedFileAddedSymlinkPath]); - - const component = createComponent(repository, deletedFileAddedSymlinkPath); - const wrapper = mount(React.cloneElement(component, {filePath: deletedFileAddedSymlinkPath, initialStagingStatus: 'staged'})); - - // index shows file is symlink - assert.equal((await indexModeAndOid(repository, deletedFileAddedSymlinkPath)).mode, '120000'); - - // unstage a couple of lines, but not all - await assert.async.isTrue(wrapper.update().find('HunkView').exists()); - const opPromise0 = switchboard.getFinishStageOperationPromise(); - const hunkView0 = wrapper.find('HunkView').at(0); - hunkView0.find('LineView').at(1).find('.github-HunkView-line').simulate('mousedown', {button: 0, detail: 1}); - hunkView0.find('LineView').at(2).find('.github-HunkView-line').simulate('mousemove', {}); - window.dispatchEvent(new MouseEvent('mouseup')); - hunkView0.find('button.github-HunkView-stageButton').simulate('click'); - await opPromise0; - - await refreshRepository(wrapper); - - // index no longer shows file is symlink (symlink creation has been unstaged), shows contents of file that existed prior to symlink - assert.equal((await indexModeAndOid(repository, deletedFileAddedSymlinkPath)).mode, '100644'); - assert.autocrlfEqual(await repository.readFileFromIndex(deletedFileAddedSymlinkPath), 'bar\nbaz\n'); - }); - - it('stages file deletion when all deleted lines are staged', async function() { - const workingDirPath = await cloneRepository('symlinks'); - const repository = await buildRepository(workingDirPath); - await repository.getLoadPromise(); - - const deletedFileAddedSymlinkPath = 'a.txt'; - fs.unlinkSync(path.join(workingDirPath, deletedFileAddedSymlinkPath)); - fs.symlinkSync(path.join(workingDirPath, 'regular-file.txt'), path.join(workingDirPath, deletedFileAddedSymlinkPath)); - - const component = createComponent(repository, deletedFileAddedSymlinkPath); - const wrapper = mount(React.cloneElement(component, {filePath: deletedFileAddedSymlinkPath})); - - assert.equal((await indexModeAndOid(repository, deletedFileAddedSymlinkPath)).mode, '100644'); - - // stage all deleted lines - await assert.async.isTrue(wrapper.update().find('HunkView').exists()); - const opPromise0 = switchboard.getFinishStageOperationPromise(); - const hunkView0 = wrapper.find('HunkView').at(0); - hunkView0.find('.github-HunkView-title').simulate('click'); - hunkView0.find('button.github-HunkView-stageButton').simulate('click'); - await opPromise0; - - await refreshRepository(wrapper); - - // File is not on index, file deletion has been staged - assert.isNull(await indexModeAndOid(repository, deletedFileAddedSymlinkPath)); - const {stagedFiles, unstagedFiles} = await repository.getStatusesForChangedFiles(); - assert.equal(unstagedFiles[deletedFileAddedSymlinkPath], 'added'); - assert.equal(stagedFiles[deletedFileAddedSymlinkPath], 'deleted'); - }); - - it('unstages file creation when all added lines are unstaged', async function() { - const workingDirPath = await cloneRepository('symlinks'); - const repository = await buildRepository(workingDirPath); - - await repository.git.exec(['config', 'core.symlinks', 'true']); - - const deletedSymlinkAddedFilePath = 'symlink.txt'; - fs.unlinkSync(path.join(workingDirPath, deletedSymlinkAddedFilePath)); - fs.writeFileSync(path.join(workingDirPath, deletedSymlinkAddedFilePath), 'qux\nfoo\nbar\nbaz\nzoo\n', 'utf8'); - await repository.stageFiles([deletedSymlinkAddedFilePath]); - - const component = createComponent(repository, deletedSymlinkAddedFilePath); - const wrapper = mount(React.cloneElement(component, {filePath: deletedSymlinkAddedFilePath, initialStagingStatus: 'staged'})); - - assert.equal((await indexModeAndOid(repository, deletedSymlinkAddedFilePath)).mode, '100644'); - - // unstage all added lines - await assert.async.isTrue(wrapper.update().find('HunkView').exists()); - const opPromise0 = switchboard.getFinishStageOperationPromise(); - const hunkView0 = wrapper.find('HunkView').at(0); - hunkView0.find('.github-HunkView-title').simulate('click'); - hunkView0.find('button.github-HunkView-stageButton').simulate('click'); - await opPromise0; - - await refreshRepository(wrapper); - - // File is not on index, file creation has been unstaged - assert.isNull(await indexModeAndOid(repository, deletedSymlinkAddedFilePath)); - const {stagedFiles, unstagedFiles} = await repository.getStatusesForChangedFiles(); - assert.equal(unstagedFiles[deletedSymlinkAddedFilePath], 'added'); - assert.equal(stagedFiles[deletedSymlinkAddedFilePath], 'deleted'); - }); - }); - - describe('handling non-symlink changes', function() { - let workdirPath, repository, filePath, component; - beforeEach(async function() { - workdirPath = await cloneRepository('multi-line-file'); - repository = await buildRepository(workdirPath); - filePath = 'sample.js'; - component = createComponent(repository, filePath); - }); - - it('stages and unstages hunks when the stage button is clicked on hunk views with no individual lines selected', async function() { - const absFilePath = path.join(workdirPath, filePath); - const originalLines = fs.readFileSync(absFilePath, 'utf8').split('\n'); - const unstagedLines = originalLines.slice(); - unstagedLines.splice(1, 1, - 'this is a modified line', - 'this is a new line', - 'this is another new line', - ); - unstagedLines.splice(11, 2, 'this is a modified line'); - fs.writeFileSync(absFilePath, unstagedLines.join('\n')); - - const wrapper = mount(component); - - await assert.async.isTrue(wrapper.update().find('HunkView').exists()); - commandRegistry.dispatch(wrapper.find('FilePatchView').getDOMNode(), 'core:move-down'); - - await assert.async.isTrue(wrapper.update().find('HunkView').exists()); - const hunkView0 = wrapper.find('HunkView').at(0); - assert.isFalse(hunkView0.prop('isSelected')); - const opPromise0 = switchboard.getFinishStageOperationPromise(); - hunkView0.find('button.github-HunkView-stageButton').simulate('click'); - await opPromise0; - - const expectedStagedLines = originalLines.slice(); - expectedStagedLines.splice(1, 1, - 'this is a modified line', - 'this is a new line', - 'this is another new line', - ); - assert.autocrlfEqual(await repository.readFileFromIndex('sample.js'), expectedStagedLines.join('\n')); - const updatePromise0 = switchboard.getChangePatchPromise(); - const stagedFilePatch = await repository.getFilePatchForPath('sample.js', {staged: true}); - wrapper.setState({ - stagingStatus: 'staged', - filePatch: stagedFilePatch, - }); - await updatePromise0; - const hunkView1 = wrapper.find('HunkView').at(0); - const opPromise1 = switchboard.getFinishStageOperationPromise(); - hunkView1.find('button.github-HunkView-stageButton').simulate('click'); - await opPromise1; - assert.autocrlfEqual(await repository.readFileFromIndex('sample.js'), originalLines.join('\n')); - }); - - it('stages and unstages individual lines when the stage button is clicked on a hunk with selected lines', async function() { - const absFilePath = path.join(workdirPath, filePath); - const originalLines = fs.readFileSync(absFilePath, 'utf8').split('\n'); - - // write some unstaged changes - const unstagedLines = originalLines.slice(); - unstagedLines.splice(1, 1, - 'this is a modified line', - 'this is a new line', - 'this is another new line', - ); - unstagedLines.splice(11, 2, 'this is a modified line'); - fs.writeFileSync(absFilePath, unstagedLines.join('\n')); - - // stage a subset of lines from first hunk - const wrapper = mount(component); - - await assert.async.isTrue(wrapper.update().find('HunkView').exists()); - const opPromise0 = switchboard.getFinishStageOperationPromise(); - const hunkView0 = wrapper.find('HunkView').at(0); - hunkView0.find('LineView').at(1).find('.github-HunkView-line').simulate('mousedown', {button: 0, detail: 1}); - hunkView0.find('LineView').at(3).find('.github-HunkView-line').simulate('mousemove', {}); - window.dispatchEvent(new MouseEvent('mouseup')); - hunkView0.find('button.github-HunkView-stageButton').simulate('click'); - await opPromise0; - - await refreshRepository(wrapper); - - const expectedLines0 = originalLines.slice(); - expectedLines0.splice(1, 1, - 'this is a modified line', - 'this is a new line', - ); - assert.autocrlfEqual(await repository.readFileFromIndex('sample.js'), expectedLines0.join('\n')); - - // stage remaining lines in hunk - const opPromise1 = switchboard.getFinishStageOperationPromise(); - wrapper.find('HunkView').at(0).find('button.github-HunkView-stageButton').simulate('click'); - await opPromise1; - - await refreshRepository(wrapper); - - const expectedLines1 = originalLines.slice(); - expectedLines1.splice(1, 1, - 'this is a modified line', - 'this is a new line', - 'this is another new line', - ); - assert.autocrlfEqual(await repository.readFileFromIndex('sample.js'), expectedLines1.join('\n')); - - // unstage a subset of lines from the first hunk - wrapper.setState({stagingStatus: 'staged'}); - await refreshRepository(wrapper); - - const hunkView2 = wrapper.find('HunkView').at(0); - hunkView2.find('LineView').at(1).find('.github-HunkView-line') - .simulate('mousedown', {button: 0, detail: 1}); - window.dispatchEvent(new MouseEvent('mouseup')); - hunkView2.find('LineView').at(2).find('.github-HunkView-line') - .simulate('mousedown', {button: 0, detail: 1, metaKey: true}); - window.dispatchEvent(new MouseEvent('mouseup')); - - const opPromise2 = switchboard.getFinishStageOperationPromise(); - hunkView2.find('button.github-HunkView-stageButton').simulate('click'); - await opPromise2; - - await refreshRepository(wrapper); - - const expectedLines2 = originalLines.slice(); - expectedLines2.splice(2, 0, - 'this is a new line', - 'this is another new line', - ); - assert.autocrlfEqual(await repository.readFileFromIndex('sample.js'), expectedLines2.join('\n')); - - // unstage the rest of the hunk - commandRegistry.dispatch(wrapper.find('FilePatchView').getDOMNode(), 'github:toggle-patch-selection-mode'); - - const opPromise3 = switchboard.getFinishStageOperationPromise(); - wrapper.find('HunkView').at(0).find('button.github-HunkView-stageButton').simulate('click'); - await opPromise3; - - assert.autocrlfEqual(await repository.readFileFromIndex('sample.js'), originalLines.join('\n')); - }); - - // https://github.com/atom/github/issues/417 - describe('when unstaging the last lines/hunks from a file', function() { - it('removes added files from index when last hunk is unstaged', async function() { - const absFilePath = path.join(workdirPath, 'new-file.txt'); - - fs.writeFileSync(absFilePath, 'foo\n'); - await repository.stageFiles(['new-file.txt']); - - const wrapper = mount(React.cloneElement(component, { - filePath: 'new-file.txt', - initialStagingStatus: 'staged', - })); - - await assert.async.isTrue(wrapper.update().find('HunkView').exists()); - - const opPromise = switchboard.getFinishStageOperationPromise(); - wrapper.find('HunkView').at(0).find('button.github-HunkView-stageButton').simulate('click'); - await opPromise; - - const stagedChanges = await repository.getStagedChanges(); - assert.equal(stagedChanges.length, 0); - }); - - it('removes added files from index when last lines are unstaged', async function() { - const absFilePath = path.join(workdirPath, 'new-file.txt'); - - fs.writeFileSync(absFilePath, 'foo\n'); - await repository.stageFiles(['new-file.txt']); - - const wrapper = mount(React.cloneElement(component, { - filePath: 'new-file.txt', - initialStagingStatus: 'staged', - })); - - await assert.async.isTrue(wrapper.update().find('HunkView').exists()); - - const viewNode = wrapper.find('FilePatchView').getDOMNode(); - commandRegistry.dispatch(viewNode, 'github:toggle-patch-selection-mode'); - commandRegistry.dispatch(viewNode, 'core:select-all'); - - const opPromise = switchboard.getFinishStageOperationPromise(); - wrapper.find('HunkView').at(0).find('button.github-HunkView-stageButton').simulate('click'); - await opPromise; - - const stagedChanges = await repository.getStagedChanges(); - assert.lengthOf(stagedChanges, 0); - }); - }); - - // https://github.com/atom/github/issues/341 - describe('when duplicate staging occurs', function() { - it('avoids patch conflicts with pending line staging operations', async function() { - const absFilePath = path.join(workdirPath, filePath); - const originalLines = fs.readFileSync(absFilePath, 'utf8').split('\n'); - - // write some unstaged changes - const unstagedLines = originalLines.slice(); - unstagedLines.splice(1, 0, - 'this is a modified line', - 'this is a new line', - 'this is another new line', - ); - unstagedLines.splice(11, 2, 'this is a modified line'); - fs.writeFileSync(absFilePath, unstagedLines.join('\n')); - - const wrapper = mount(component); - - await assert.async.isTrue(wrapper.update().find('HunkView').exists()); - const hunkView0 = wrapper.find('HunkView').at(0); - hunkView0.find('LineView').at(1).find('.github-HunkView-line') - .simulate('mousedown', {button: 0, detail: 1}); - window.dispatchEvent(new MouseEvent('mouseup')); - - // stage lines in rapid succession - // second stage action is a no-op since the first staging operation is in flight - const line1StagingPromise = switchboard.getFinishStageOperationPromise(); - hunkView0.find('.github-HunkView-stageButton').simulate('click'); - hunkView0.find('.github-HunkView-stageButton').simulate('click'); - await line1StagingPromise; - - const changePatchPromise = switchboard.getChangePatchPromise(); - - // assert that only line 1 has been staged - await refreshRepository(wrapper); // clear the cached file patches - let expectedLines = originalLines.slice(); - expectedLines.splice(1, 0, - 'this is a modified line', - ); - let actualLines = await repository.readFileFromIndex(filePath); - assert.autocrlfEqual(actualLines, expectedLines.join('\n')); - await changePatchPromise; - wrapper.update(); - - const hunkView1 = wrapper.find('HunkView').at(0); - hunkView1.find('LineView').at(2).find('.github-HunkView-line') - .simulate('mousedown', {button: 0, detail: 1}); - window.dispatchEvent(new MouseEvent('mouseup')); - const line2StagingPromise = switchboard.getFinishStageOperationPromise(); - hunkView1.find('.github-HunkView-stageButton').simulate('click'); - await line2StagingPromise; - - // assert that line 2 has now been staged - expectedLines = originalLines.slice(); - expectedLines.splice(1, 0, - 'this is a modified line', - 'this is a new line', - ); - actualLines = await repository.readFileFromIndex(filePath); - assert.autocrlfEqual(actualLines, expectedLines.join('\n')); - }); - - it('avoids patch conflicts with pending hunk staging operations', async function() { - const absFilePath = path.join(workdirPath, filePath); - const originalLines = fs.readFileSync(absFilePath, 'utf8').split('\n'); - - // write some unstaged changes - const unstagedLines = originalLines.slice(); - unstagedLines.splice(1, 0, - 'this is a modified line', - 'this is a new line', - 'this is another new line', - ); - unstagedLines.splice(11, 2, 'this is a modified line'); - fs.writeFileSync(absFilePath, unstagedLines.join('\n')); - - const wrapper = mount(component); - - await assert.async.isTrue(wrapper.update().find('HunkView').exists()); - - // ensure staging the same hunk twice does not cause issues - // second stage action is a no-op since the first staging operation is in flight - const hunk1StagingPromise = switchboard.getFinishStageOperationPromise(); - wrapper.find('HunkView').at(0).find('.github-HunkView-stageButton').simulate('click'); - wrapper.find('HunkView').at(0).find('.github-HunkView-stageButton').simulate('click'); - await hunk1StagingPromise; - - const patchPromise0 = switchboard.getChangePatchPromise(); - await refreshRepository(wrapper); // clear the cached file patches - const modifiedFilePatch = await repository.getFilePatchForPath(filePath); - wrapper.setState({filePatch: modifiedFilePatch}); - await patchPromise0; + function buildApp(overrideProps = {}) { + const props = { + stagingStatus: 'unstaged', + isPartiallyStaged: false, + filePatch, + ...overrideProps, + }; - let expectedLines = originalLines.slice(); - expectedLines.splice(1, 0, - 'this is a modified line', - 'this is a new line', - 'this is another new line', - ); - let actualLines = await repository.readFileFromIndex(filePath); - assert.autocrlfEqual(actualLines, expectedLines.join('\n')); + return ; + } - const hunk2StagingPromise = switchboard.getFinishStageOperationPromise(); - wrapper.find('HunkView').at(0).find('.github-HunkView-stageButton').simulate('click'); - await hunk2StagingPromise; + it('passes extra props to the FilePatchView', function() { + const extra = Symbol('extra'); + const wrapper = mount(buildApp({extra})); - expectedLines = originalLines.slice(); - expectedLines.splice(1, 0, - 'this is a modified line', - 'this is a new line', - 'this is another new line', - ); - expectedLines.splice(11, 2, 'this is a modified line'); - actualLines = await repository.readFileFromIndex(filePath); - assert.autocrlfEqual(actualLines, expectedLines.join('\n')); - }); - }); - }); + assert.strictEqual(wrapper.find('FilePatchView').prop('extra'), extra); }); }); diff --git a/test/controllers/file-patch-controller.test.old.js b/test/controllers/file-patch-controller.test.old.js new file mode 100644 index 0000000000..a7b075e8ec --- /dev/null +++ b/test/controllers/file-patch-controller.test.old.js @@ -0,0 +1,811 @@ +import React from 'react'; +import {shallow, mount} from 'enzyme'; +import until from 'test-until'; + +import fs from 'fs'; +import path from 'path'; + +import {cloneRepository, buildRepository} from '../helpers'; +import FilePatch from '../../lib/models/file-patch'; +import FilePatchController from '../../lib/controllers/file-patch-controller'; +import Hunk from '../../lib/models/hunk'; +import HunkLine from '../../lib/models/hunk-line'; +import ResolutionProgress from '../../lib/models/conflicts/resolution-progress'; +import Switchboard from '../../lib/switchboard'; + +function createFilePatch(oldFilePath, newFilePath, status, hunks) { + const oldFile = new FilePatch.File({path: oldFilePath}); + const newFile = new FilePatch.File({path: newFilePath}); + const patch = new FilePatch.Patch({status, hunks}); + + return new FilePatch(oldFile, newFile, patch); +} + +let atomEnv, commandRegistry, tooltips, deserializers; +let switchboard, getFilePatchForPath; +let discardLines, didSurfaceFile, didDiveIntoFilePath, quietlySelectItem, undoLastDiscard, openFiles, getRepositoryForWorkdir; +let getSelectedStagingViewItems, resolutionProgress; + +function createComponent(repository, filePath) { + atomEnv = global.buildAtomEnvironment(); + commandRegistry = atomEnv.commands; + deserializers = atomEnv.deserializers; + tooltips = atomEnv.tooltips; + + switchboard = new Switchboard(); + + discardLines = sinon.spy(); + didSurfaceFile = sinon.spy(); + didDiveIntoFilePath = sinon.spy(); + quietlySelectItem = sinon.spy(); + undoLastDiscard = sinon.spy(); + openFiles = sinon.spy(); + getSelectedStagingViewItems = sinon.spy(); + + getRepositoryForWorkdir = () => repository; + resolutionProgress = new ResolutionProgress(); + + FilePatchController.resetConfirmedLargeFilePatches(); + + return ( + + ); +} + +async function refreshRepository(wrapper) { + const workDir = wrapper.prop('workingDirectoryPath'); + const repository = wrapper.prop('getRepositoryForWorkdir')(workDir); + + const promise = wrapper.prop('switchboard').getFinishRepositoryRefreshPromise(); + repository.refresh(); + await promise; + wrapper.update(); +} + +describe('FilePatchController', function() { + afterEach(function() { + atomEnv.destroy(); + }); + + describe('unit tests', function() { + let workdirPath, repository, filePath, component; + beforeEach(async function() { + workdirPath = await cloneRepository('multi-line-file'); + repository = await buildRepository(workdirPath); + filePath = 'sample.js'; + component = createComponent(repository, filePath); + + getFilePatchForPath = sinon.stub(repository, 'getFilePatchForPath'); + }); + + describe('when the FilePatch is too large', function() { + it('renders a confirmation widget', async function() { + const hunk1 = new Hunk(0, 0, 1, 1, '', [ + new HunkLine('line-1', 'added', 1, 1), + new HunkLine('line-2', 'added', 2, 2), + new HunkLine('line-3', 'added', 3, 3), + new HunkLine('line-4', 'added', 4, 4), + new HunkLine('line-5', 'added', 5, 5), + new HunkLine('line-6', 'added', 6, 6), + ]); + const filePatch = createFilePatch(filePath, filePath, 'modified', [hunk1]); + + getFilePatchForPath.returns(filePatch); + + const wrapper = mount(React.cloneElement(component, {largeDiffByteThreshold: 5})); + + await assert.async.match(wrapper.text(), /large .+ diff/); + }); + + it('renders the full diff when the confirmation is clicked', async function() { + const hunk = new Hunk(0, 0, 1, 1, '', [ + new HunkLine('line-1', 'added', 1, 1), + new HunkLine('line-2', 'added', 2, 2), + new HunkLine('line-3', 'added', 3, 3), + new HunkLine('line-4', 'added', 4, 4), + new HunkLine('line-5', 'added', 5, 5), + new HunkLine('line-6', 'added', 6, 6), + ]); + const filePatch = createFilePatch(filePath, filePath, 'modified', [hunk]); + getFilePatchForPath.returns(filePatch); + + const wrapper = mount(React.cloneElement(component, {largeDiffByteThreshold: 5})); + + await assert.async.isTrue(wrapper.update().find('.large-file-patch').exists()); + wrapper.find('.large-file-patch').find('button').simulate('click'); + + assert.isTrue(wrapper.find('HunkView').exists()); + }); + + it('renders the full diff if the file has been confirmed before', async function() { + const hunk = new Hunk(0, 0, 1, 1, '', [ + new HunkLine('line-1', 'added', 1, 1), + new HunkLine('line-2', 'added', 2, 2), + new HunkLine('line-3', 'added', 3, 3), + new HunkLine('line-4', 'added', 4, 4), + new HunkLine('line-5', 'added', 5, 5), + new HunkLine('line-6', 'added', 6, 6), + ]); + const filePatch1 = createFilePatch(filePath, filePath, 'modified', [hunk]); + const filePatch2 = createFilePatch('b.txt', 'b.txt', 'modified', [hunk]); + + getFilePatchForPath.returns(filePatch1); + + const wrapper = mount(React.cloneElement(component, { + filePath: filePatch1.getPath(), largeDiffByteThreshold: 5, + })); + + await assert.async.isTrue(wrapper.update().find('.large-file-patch').exists()); + wrapper.find('.large-file-patch').find('button').simulate('click'); + assert.isTrue(wrapper.find('HunkView').exists()); + + getFilePatchForPath.returns(filePatch2); + wrapper.setProps({filePath: filePatch2.getPath()}); + await assert.async.isTrue(wrapper.update().find('.large-file-patch').exists()); + + getFilePatchForPath.returns(filePatch1); + wrapper.setProps({filePath: filePatch1.getPath()}); + assert.isTrue(wrapper.update().find('HunkView').exists()); + }); + }); + + describe('onRepoRefresh', function() { + it('sets the correct FilePatch as state', async function() { + repository.getFilePatchForPath.restore(); + fs.writeFileSync(path.join(workdirPath, filePath), 'change', 'utf8'); + + const wrapper = mount(component); + + await assert.async.isNotNull(wrapper.state('filePatch')); + + const originalFilePatch = wrapper.state('filePatch'); + assert.equal(wrapper.state('stagingStatus'), 'unstaged'); + + fs.writeFileSync(path.join(workdirPath, 'file.txt'), 'change\nand again!', 'utf8'); + await refreshRepository(wrapper); + + assert.notEqual(originalFilePatch, wrapper.state('filePatch')); + assert.equal(wrapper.state('stagingStatus'), 'unstaged'); + }); + }); + + it('renders FilePatchView only if FilePatch has hunks', async function() { + const emptyFilePatch = createFilePatch(filePath, filePath, 'modified', []); + getFilePatchForPath.returns(emptyFilePatch); + + const wrapper = mount(component); + + assert.isTrue(wrapper.find('FilePatchView').exists()); + assert.isTrue(wrapper.find('FilePatchView').text().includes('File has no contents')); + + const hunk1 = new Hunk(0, 0, 1, 1, '', [new HunkLine('line-1', 'added', 1, 1)]); + const filePatch = createFilePatch(filePath, filePath, 'modified', [hunk1]); + getFilePatchForPath.returns(filePatch); + + wrapper.instance().onRepoRefresh(repository); + + await assert.async.isTrue(wrapper.update().find('HunkView').exists()); + assert.isTrue(wrapper.find('HunkView').text().includes('@@ -0,1 +0,1 @@')); + }); + + it('updates the FilePatch after a repo update', async function() { + const hunk1 = new Hunk(5, 5, 2, 1, '', [new HunkLine('line-1', 'added', -1, 5)]); + const hunk2 = new Hunk(8, 8, 1, 1, '', [new HunkLine('line-5', 'deleted', 8, -1)]); + const filePatch0 = createFilePatch(filePath, filePath, 'modified', [hunk1, hunk2]); + getFilePatchForPath.returns(filePatch0); + + const wrapper = shallow(component); + + let view0; + await until(() => { + view0 = wrapper.update().find('FilePatchView').shallow(); + return view0.find({hunk: hunk1}).exists(); + }); + assert.isTrue(view0.find({hunk: hunk2}).exists()); + + const hunk3 = new Hunk(8, 8, 1, 1, '', [new HunkLine('line-10', 'modified', 10, 10)]); + const filePatch1 = createFilePatch(filePath, filePath, 'modified', [hunk1, hunk3]); + getFilePatchForPath.returns(filePatch1); + + wrapper.instance().onRepoRefresh(repository); + let view1; + await until(() => { + view1 = wrapper.update().find('FilePatchView').shallow(); + return view1.find({hunk: hunk3}).exists(); + }); + assert.isTrue(view1.find({hunk: hunk1}).exists()); + assert.isFalse(view1.find({hunk: hunk2}).exists()); + }); + + it('invokes a didSurfaceFile callback with the current file path', async function() { + const filePatch = createFilePatch(filePath, filePath, 'modified', [new Hunk(1, 1, 1, 3, '', [])]); + getFilePatchForPath.returns(filePatch); + + const wrapper = mount(component); + + await assert.async.isTrue(wrapper.update().find('Commands').exists()); + commandRegistry.dispatch(wrapper.find('FilePatchView').getDOMNode(), 'core:move-right'); + assert.isTrue(didSurfaceFile.calledWith(filePath, 'unstaged')); + }); + + describe('openCurrentFile({lineNumber})', () => { + it('sets the cursor on the correct line of the opened text editor', async function() { + const editorSpy = { + relativePath: null, + scrollToBufferPosition: sinon.spy(), + setCursorBufferPosition: sinon.spy(), + }; + + const openFilesStub = relativePaths => { + assert.lengthOf(relativePaths, 1); + editorSpy.relativePath = relativePaths[0]; + return Promise.resolve([editorSpy]); + }; + + const hunk = new Hunk(5, 5, 2, 1, '', [new HunkLine('line-1', 'added', -1, 5)]); + const filePatch = createFilePatch(filePath, filePath, 'modified', [hunk]); + getFilePatchForPath.returns(filePatch); + + const wrapper = mount(React.cloneElement(component, {openFiles: openFilesStub})); + + await assert.async.isTrue(wrapper.update().find('HunkView').exists()); + + wrapper.find('LineView').simulate('mousedown', {button: 0, detail: 1}); + window.dispatchEvent(new MouseEvent('mouseup')); + commandRegistry.dispatch(wrapper.find('FilePatchView').getDOMNode(), 'github:open-file'); + wrapper.update(); + + await assert.async.isTrue(editorSpy.setCursorBufferPosition.called); + + assert.isTrue(editorSpy.relativePath === filePath); + + const scrollCall = editorSpy.scrollToBufferPosition.firstCall; + assert.isTrue(scrollCall.args[0].isEqual([4, 0])); + assert.deepEqual(scrollCall.args[1], {center: true}); + + const cursorCall = editorSpy.setCursorBufferPosition.firstCall; + assert.isTrue(cursorCall.args[0].isEqual([4, 0])); + }); + }); + }); + + describe('integration tests', function() { + describe('handling symlink files', function() { + async function indexModeAndOid(repository, filename) { + const output = await repository.git.exec(['ls-files', '-s', '--', filename]); + if (output) { + const parts = output.split(' '); + return {mode: parts[0], oid: parts[1]}; + } else { + return null; + } + } + + it('unstages added lines that don\'t require symlink change', async function() { + const workingDirPath = await cloneRepository('symlinks'); + const repository = await buildRepository(workingDirPath); + + // correctly handle symlinks on Windows + await repository.git.exec(['config', 'core.symlinks', 'true']); + + const deletedSymlinkAddedFilePath = 'symlink.txt'; + fs.unlinkSync(path.join(workingDirPath, deletedSymlinkAddedFilePath)); + fs.writeFileSync(path.join(workingDirPath, deletedSymlinkAddedFilePath), 'qux\nfoo\nbar\nbaz\nzoo\n', 'utf8'); + + // Stage whole file + await repository.stageFiles([deletedSymlinkAddedFilePath]); + + const component = createComponent(repository, deletedSymlinkAddedFilePath); + const wrapper = mount(React.cloneElement(component, {filePath: deletedSymlinkAddedFilePath, initialStagingStatus: 'staged'})); + + // index shows symlink deltion and added lines + assert.autocrlfEqual(await repository.readFileFromIndex(deletedSymlinkAddedFilePath), 'qux\nfoo\nbar\nbaz\nzoo\n'); + assert.equal((await indexModeAndOid(repository, deletedSymlinkAddedFilePath)).mode, '100644'); + + // Unstage a couple added lines, but not all + await assert.async.isTrue(wrapper.update().find('HunkView').exists()); + const opPromise0 = switchboard.getFinishStageOperationPromise(); + const hunkView0 = wrapper.find('HunkView').at(0); + hunkView0.find('LineView').at(1).find('.github-HunkView-line').simulate('mousedown', {button: 0, detail: 1}); + hunkView0.find('LineView').at(2).find('.github-HunkView-line').simulate('mousemove', {}); + window.dispatchEvent(new MouseEvent('mouseup')); + hunkView0.find('button.github-HunkView-stageButton').simulate('click'); + await opPromise0; + + await refreshRepository(wrapper); + + // index shows symlink deletions still staged, only a couple of lines have been unstaged + assert.equal((await indexModeAndOid(repository, deletedSymlinkAddedFilePath)).mode, '100644'); + assert.autocrlfEqual(await repository.readFileFromIndex(deletedSymlinkAddedFilePath), 'qux\nbaz\nzoo\n'); + }); + + it('stages deleted lines that don\'t require symlink change', async function() { + const workingDirPath = await cloneRepository('symlinks'); + const repository = await buildRepository(workingDirPath); + + const deletedFileAddedSymlinkPath = 'a.txt'; + fs.unlinkSync(path.join(workingDirPath, deletedFileAddedSymlinkPath)); + fs.symlinkSync(path.join(workingDirPath, 'regular-file.txt'), path.join(workingDirPath, deletedFileAddedSymlinkPath)); + + const component = createComponent(repository, deletedFileAddedSymlinkPath); + const wrapper = mount(React.cloneElement(component, {filePath: deletedFileAddedSymlinkPath, initialStagingStatus: 'unstaged'})); + + // index shows file is not a symlink, no deleted lines + assert.equal((await indexModeAndOid(repository, deletedFileAddedSymlinkPath)).mode, '100644'); + assert.autocrlfEqual(await repository.readFileFromIndex(deletedFileAddedSymlinkPath), 'foo\nbar\nbaz\n\n'); + + // stage a couple of lines, but not all + await assert.async.isTrue(wrapper.update().find('HunkView').exists()); + const opPromise0 = switchboard.getFinishStageOperationPromise(); + const hunkView0 = wrapper.find('HunkView').at(0); + hunkView0.find('LineView').at(1).find('.github-HunkView-line').simulate('mousedown', {button: 0, detail: 1}); + hunkView0.find('LineView').at(2).find('.github-HunkView-line').simulate('mousemove', {}); + window.dispatchEvent(new MouseEvent('mouseup')); + hunkView0.find('button.github-HunkView-stageButton').simulate('click'); + await opPromise0; + + await refreshRepository(wrapper); + + // index shows symlink change has not been staged, a couple of lines have been deleted + assert.equal((await indexModeAndOid(repository, deletedFileAddedSymlinkPath)).mode, '100644'); + assert.autocrlfEqual(await repository.readFileFromIndex(deletedFileAddedSymlinkPath), 'foo\n\n'); + }); + + it('stages symlink change when staging added lines that depend on change', async function() { + const workingDirPath = await cloneRepository('symlinks'); + const repository = await buildRepository(workingDirPath); + + // correctly handle symlinks on Windows + await repository.git.exec(['config', 'core.symlinks', 'true']); + + const deletedSymlinkAddedFilePath = 'symlink.txt'; + fs.unlinkSync(path.join(workingDirPath, deletedSymlinkAddedFilePath)); + fs.writeFileSync(path.join(workingDirPath, deletedSymlinkAddedFilePath), 'qux\nfoo\nbar\nbaz\nzoo\n', 'utf8'); + + const component = createComponent(repository, deletedSymlinkAddedFilePath); + const wrapper = mount(React.cloneElement(component, {filePath: deletedSymlinkAddedFilePath})); + + // index shows file is symlink + assert.equal((await indexModeAndOid(repository, deletedSymlinkAddedFilePath)).mode, '120000'); + + // Stage a couple added lines, but not all + await assert.async.isTrue(wrapper.update().find('HunkView').exists()); + const opPromise0 = switchboard.getFinishStageOperationPromise(); + const hunkView0 = wrapper.find('HunkView').at(0); + hunkView0.find('LineView').at(1).find('.github-HunkView-line').simulate('mousedown', {button: 0, detail: 1}); + hunkView0.find('LineView').at(2).find('.github-HunkView-line').simulate('mousemove', {}); + window.dispatchEvent(new MouseEvent('mouseup')); + hunkView0.find('button.github-HunkView-stageButton').simulate('click'); + await opPromise0; + + await refreshRepository(wrapper); + + // index no longer shows file is symlink (symlink has been deleted), now a regular file with contents + assert.equal((await indexModeAndOid(repository, deletedSymlinkAddedFilePath)).mode, '100644'); + assert.autocrlfEqual(await repository.readFileFromIndex(deletedSymlinkAddedFilePath), 'foo\nbar\n'); + }); + + it('unstages symlink change when unstaging deleted lines that depend on change', async function() { + const workingDirPath = await cloneRepository('symlinks'); + const repository = await buildRepository(workingDirPath); + + const deletedFileAddedSymlinkPath = 'a.txt'; + fs.unlinkSync(path.join(workingDirPath, deletedFileAddedSymlinkPath)); + fs.symlinkSync(path.join(workingDirPath, 'regular-file.txt'), path.join(workingDirPath, deletedFileAddedSymlinkPath)); + await repository.stageFiles([deletedFileAddedSymlinkPath]); + + const component = createComponent(repository, deletedFileAddedSymlinkPath); + const wrapper = mount(React.cloneElement(component, {filePath: deletedFileAddedSymlinkPath, initialStagingStatus: 'staged'})); + + // index shows file is symlink + assert.equal((await indexModeAndOid(repository, deletedFileAddedSymlinkPath)).mode, '120000'); + + // unstage a couple of lines, but not all + await assert.async.isTrue(wrapper.update().find('HunkView').exists()); + const opPromise0 = switchboard.getFinishStageOperationPromise(); + const hunkView0 = wrapper.find('HunkView').at(0); + hunkView0.find('LineView').at(1).find('.github-HunkView-line').simulate('mousedown', {button: 0, detail: 1}); + hunkView0.find('LineView').at(2).find('.github-HunkView-line').simulate('mousemove', {}); + window.dispatchEvent(new MouseEvent('mouseup')); + hunkView0.find('button.github-HunkView-stageButton').simulate('click'); + await opPromise0; + + await refreshRepository(wrapper); + + // index no longer shows file is symlink (symlink creation has been unstaged), shows contents of file that existed prior to symlink + assert.equal((await indexModeAndOid(repository, deletedFileAddedSymlinkPath)).mode, '100644'); + assert.autocrlfEqual(await repository.readFileFromIndex(deletedFileAddedSymlinkPath), 'bar\nbaz\n'); + }); + + it('stages file deletion when all deleted lines are staged', async function() { + const workingDirPath = await cloneRepository('symlinks'); + const repository = await buildRepository(workingDirPath); + await repository.getLoadPromise(); + + const deletedFileAddedSymlinkPath = 'a.txt'; + fs.unlinkSync(path.join(workingDirPath, deletedFileAddedSymlinkPath)); + fs.symlinkSync(path.join(workingDirPath, 'regular-file.txt'), path.join(workingDirPath, deletedFileAddedSymlinkPath)); + + const component = createComponent(repository, deletedFileAddedSymlinkPath); + const wrapper = mount(React.cloneElement(component, {filePath: deletedFileAddedSymlinkPath})); + + assert.equal((await indexModeAndOid(repository, deletedFileAddedSymlinkPath)).mode, '100644'); + + // stage all deleted lines + await assert.async.isTrue(wrapper.update().find('HunkView').exists()); + const opPromise0 = switchboard.getFinishStageOperationPromise(); + const hunkView0 = wrapper.find('HunkView').at(0); + hunkView0.find('.github-HunkView-title').simulate('click'); + hunkView0.find('button.github-HunkView-stageButton').simulate('click'); + await opPromise0; + + await refreshRepository(wrapper); + + // File is not on index, file deletion has been staged + assert.isNull(await indexModeAndOid(repository, deletedFileAddedSymlinkPath)); + const {stagedFiles, unstagedFiles} = await repository.getStatusesForChangedFiles(); + assert.equal(unstagedFiles[deletedFileAddedSymlinkPath], 'added'); + assert.equal(stagedFiles[deletedFileAddedSymlinkPath], 'deleted'); + }); + + it('unstages file creation when all added lines are unstaged', async function() { + const workingDirPath = await cloneRepository('symlinks'); + const repository = await buildRepository(workingDirPath); + + await repository.git.exec(['config', 'core.symlinks', 'true']); + + const deletedSymlinkAddedFilePath = 'symlink.txt'; + fs.unlinkSync(path.join(workingDirPath, deletedSymlinkAddedFilePath)); + fs.writeFileSync(path.join(workingDirPath, deletedSymlinkAddedFilePath), 'qux\nfoo\nbar\nbaz\nzoo\n', 'utf8'); + await repository.stageFiles([deletedSymlinkAddedFilePath]); + + const component = createComponent(repository, deletedSymlinkAddedFilePath); + const wrapper = mount(React.cloneElement(component, {filePath: deletedSymlinkAddedFilePath, initialStagingStatus: 'staged'})); + + assert.equal((await indexModeAndOid(repository, deletedSymlinkAddedFilePath)).mode, '100644'); + + // unstage all added lines + await assert.async.isTrue(wrapper.update().find('HunkView').exists()); + const opPromise0 = switchboard.getFinishStageOperationPromise(); + const hunkView0 = wrapper.find('HunkView').at(0); + hunkView0.find('.github-HunkView-title').simulate('click'); + hunkView0.find('button.github-HunkView-stageButton').simulate('click'); + await opPromise0; + + await refreshRepository(wrapper); + + // File is not on index, file creation has been unstaged + assert.isNull(await indexModeAndOid(repository, deletedSymlinkAddedFilePath)); + const {stagedFiles, unstagedFiles} = await repository.getStatusesForChangedFiles(); + assert.equal(unstagedFiles[deletedSymlinkAddedFilePath], 'added'); + assert.equal(stagedFiles[deletedSymlinkAddedFilePath], 'deleted'); + }); + }); + + describe('handling non-symlink changes', function() { + let workdirPath, repository, filePath, component; + beforeEach(async function() { + workdirPath = await cloneRepository('multi-line-file'); + repository = await buildRepository(workdirPath); + filePath = 'sample.js'; + component = createComponent(repository, filePath); + }); + + it('stages and unstages hunks when the stage button is clicked on hunk views with no individual lines selected', async function() { + const absFilePath = path.join(workdirPath, filePath); + const originalLines = fs.readFileSync(absFilePath, 'utf8').split('\n'); + const unstagedLines = originalLines.slice(); + unstagedLines.splice(1, 1, + 'this is a modified line', + 'this is a new line', + 'this is another new line', + ); + unstagedLines.splice(11, 2, 'this is a modified line'); + fs.writeFileSync(absFilePath, unstagedLines.join('\n')); + + const wrapper = mount(component); + + await assert.async.isTrue(wrapper.update().find('HunkView').exists()); + commandRegistry.dispatch(wrapper.find('FilePatchView').getDOMNode(), 'core:move-down'); + + await assert.async.isTrue(wrapper.update().find('HunkView').exists()); + const hunkView0 = wrapper.find('HunkView').at(0); + assert.isFalse(hunkView0.prop('isSelected')); + const opPromise0 = switchboard.getFinishStageOperationPromise(); + hunkView0.find('button.github-HunkView-stageButton').simulate('click'); + await opPromise0; + + const expectedStagedLines = originalLines.slice(); + expectedStagedLines.splice(1, 1, + 'this is a modified line', + 'this is a new line', + 'this is another new line', + ); + assert.autocrlfEqual(await repository.readFileFromIndex('sample.js'), expectedStagedLines.join('\n')); + const updatePromise0 = switchboard.getChangePatchPromise(); + const stagedFilePatch = await repository.getFilePatchForPath('sample.js', {staged: true}); + wrapper.setState({ + stagingStatus: 'staged', + filePatch: stagedFilePatch, + }); + await updatePromise0; + const hunkView1 = wrapper.find('HunkView').at(0); + const opPromise1 = switchboard.getFinishStageOperationPromise(); + hunkView1.find('button.github-HunkView-stageButton').simulate('click'); + await opPromise1; + assert.autocrlfEqual(await repository.readFileFromIndex('sample.js'), originalLines.join('\n')); + }); + + it('stages and unstages individual lines when the stage button is clicked on a hunk with selected lines', async function() { + const absFilePath = path.join(workdirPath, filePath); + const originalLines = fs.readFileSync(absFilePath, 'utf8').split('\n'); + + // write some unstaged changes + const unstagedLines = originalLines.slice(); + unstagedLines.splice(1, 1, + 'this is a modified line', + 'this is a new line', + 'this is another new line', + ); + unstagedLines.splice(11, 2, 'this is a modified line'); + fs.writeFileSync(absFilePath, unstagedLines.join('\n')); + + // stage a subset of lines from first hunk + const wrapper = mount(component); + + await assert.async.isTrue(wrapper.update().find('HunkView').exists()); + const opPromise0 = switchboard.getFinishStageOperationPromise(); + const hunkView0 = wrapper.find('HunkView').at(0); + hunkView0.find('LineView').at(1).find('.github-HunkView-line').simulate('mousedown', {button: 0, detail: 1}); + hunkView0.find('LineView').at(3).find('.github-HunkView-line').simulate('mousemove', {}); + window.dispatchEvent(new MouseEvent('mouseup')); + hunkView0.find('button.github-HunkView-stageButton').simulate('click'); + await opPromise0; + + await refreshRepository(wrapper); + + const expectedLines0 = originalLines.slice(); + expectedLines0.splice(1, 1, + 'this is a modified line', + 'this is a new line', + ); + assert.autocrlfEqual(await repository.readFileFromIndex('sample.js'), expectedLines0.join('\n')); + + // stage remaining lines in hunk + const opPromise1 = switchboard.getFinishStageOperationPromise(); + wrapper.find('HunkView').at(0).find('button.github-HunkView-stageButton').simulate('click'); + await opPromise1; + + await refreshRepository(wrapper); + + const expectedLines1 = originalLines.slice(); + expectedLines1.splice(1, 1, + 'this is a modified line', + 'this is a new line', + 'this is another new line', + ); + assert.autocrlfEqual(await repository.readFileFromIndex('sample.js'), expectedLines1.join('\n')); + + // unstage a subset of lines from the first hunk + wrapper.setState({stagingStatus: 'staged'}); + await refreshRepository(wrapper); + + const hunkView2 = wrapper.find('HunkView').at(0); + hunkView2.find('LineView').at(1).find('.github-HunkView-line') + .simulate('mousedown', {button: 0, detail: 1}); + window.dispatchEvent(new MouseEvent('mouseup')); + hunkView2.find('LineView').at(2).find('.github-HunkView-line') + .simulate('mousedown', {button: 0, detail: 1, metaKey: true}); + window.dispatchEvent(new MouseEvent('mouseup')); + + const opPromise2 = switchboard.getFinishStageOperationPromise(); + hunkView2.find('button.github-HunkView-stageButton').simulate('click'); + await opPromise2; + + await refreshRepository(wrapper); + + const expectedLines2 = originalLines.slice(); + expectedLines2.splice(2, 0, + 'this is a new line', + 'this is another new line', + ); + assert.autocrlfEqual(await repository.readFileFromIndex('sample.js'), expectedLines2.join('\n')); + + // unstage the rest of the hunk + commandRegistry.dispatch(wrapper.find('FilePatchView').getDOMNode(), 'github:toggle-patch-selection-mode'); + + const opPromise3 = switchboard.getFinishStageOperationPromise(); + wrapper.find('HunkView').at(0).find('button.github-HunkView-stageButton').simulate('click'); + await opPromise3; + + assert.autocrlfEqual(await repository.readFileFromIndex('sample.js'), originalLines.join('\n')); + }); + + // https://github.com/atom/github/issues/417 + describe('when unstaging the last lines/hunks from a file', function() { + it('removes added files from index when last hunk is unstaged', async function() { + const absFilePath = path.join(workdirPath, 'new-file.txt'); + + fs.writeFileSync(absFilePath, 'foo\n'); + await repository.stageFiles(['new-file.txt']); + + const wrapper = mount(React.cloneElement(component, { + filePath: 'new-file.txt', + initialStagingStatus: 'staged', + })); + + await assert.async.isTrue(wrapper.update().find('HunkView').exists()); + + const opPromise = switchboard.getFinishStageOperationPromise(); + wrapper.find('HunkView').at(0).find('button.github-HunkView-stageButton').simulate('click'); + await opPromise; + + const stagedChanges = await repository.getStagedChanges(); + assert.equal(stagedChanges.length, 0); + }); + + it('removes added files from index when last lines are unstaged', async function() { + const absFilePath = path.join(workdirPath, 'new-file.txt'); + + fs.writeFileSync(absFilePath, 'foo\n'); + await repository.stageFiles(['new-file.txt']); + + const wrapper = mount(React.cloneElement(component, { + filePath: 'new-file.txt', + initialStagingStatus: 'staged', + })); + + await assert.async.isTrue(wrapper.update().find('HunkView').exists()); + + const viewNode = wrapper.find('FilePatchView').getDOMNode(); + commandRegistry.dispatch(viewNode, 'github:toggle-patch-selection-mode'); + commandRegistry.dispatch(viewNode, 'core:select-all'); + + const opPromise = switchboard.getFinishStageOperationPromise(); + wrapper.find('HunkView').at(0).find('button.github-HunkView-stageButton').simulate('click'); + await opPromise; + + const stagedChanges = await repository.getStagedChanges(); + assert.lengthOf(stagedChanges, 0); + }); + }); + + // https://github.com/atom/github/issues/341 + describe('when duplicate staging occurs', function() { + it('avoids patch conflicts with pending line staging operations', async function() { + const absFilePath = path.join(workdirPath, filePath); + const originalLines = fs.readFileSync(absFilePath, 'utf8').split('\n'); + + // write some unstaged changes + const unstagedLines = originalLines.slice(); + unstagedLines.splice(1, 0, + 'this is a modified line', + 'this is a new line', + 'this is another new line', + ); + unstagedLines.splice(11, 2, 'this is a modified line'); + fs.writeFileSync(absFilePath, unstagedLines.join('\n')); + + const wrapper = mount(component); + + await assert.async.isTrue(wrapper.update().find('HunkView').exists()); + const hunkView0 = wrapper.find('HunkView').at(0); + hunkView0.find('LineView').at(1).find('.github-HunkView-line') + .simulate('mousedown', {button: 0, detail: 1}); + window.dispatchEvent(new MouseEvent('mouseup')); + + // stage lines in rapid succession + // second stage action is a no-op since the first staging operation is in flight + const line1StagingPromise = switchboard.getFinishStageOperationPromise(); + hunkView0.find('.github-HunkView-stageButton').simulate('click'); + hunkView0.find('.github-HunkView-stageButton').simulate('click'); + await line1StagingPromise; + + const changePatchPromise = switchboard.getChangePatchPromise(); + + // assert that only line 1 has been staged + await refreshRepository(wrapper); // clear the cached file patches + let expectedLines = originalLines.slice(); + expectedLines.splice(1, 0, + 'this is a modified line', + ); + let actualLines = await repository.readFileFromIndex(filePath); + assert.autocrlfEqual(actualLines, expectedLines.join('\n')); + await changePatchPromise; + wrapper.update(); + + const hunkView1 = wrapper.find('HunkView').at(0); + hunkView1.find('LineView').at(2).find('.github-HunkView-line') + .simulate('mousedown', {button: 0, detail: 1}); + window.dispatchEvent(new MouseEvent('mouseup')); + const line2StagingPromise = switchboard.getFinishStageOperationPromise(); + hunkView1.find('.github-HunkView-stageButton').simulate('click'); + await line2StagingPromise; + + // assert that line 2 has now been staged + expectedLines = originalLines.slice(); + expectedLines.splice(1, 0, + 'this is a modified line', + 'this is a new line', + ); + actualLines = await repository.readFileFromIndex(filePath); + assert.autocrlfEqual(actualLines, expectedLines.join('\n')); + }); + + it('avoids patch conflicts with pending hunk staging operations', async function() { + const absFilePath = path.join(workdirPath, filePath); + const originalLines = fs.readFileSync(absFilePath, 'utf8').split('\n'); + + // write some unstaged changes + const unstagedLines = originalLines.slice(); + unstagedLines.splice(1, 0, + 'this is a modified line', + 'this is a new line', + 'this is another new line', + ); + unstagedLines.splice(11, 2, 'this is a modified line'); + fs.writeFileSync(absFilePath, unstagedLines.join('\n')); + + const wrapper = mount(component); + + await assert.async.isTrue(wrapper.update().find('HunkView').exists()); + + // ensure staging the same hunk twice does not cause issues + // second stage action is a no-op since the first staging operation is in flight + const hunk1StagingPromise = switchboard.getFinishStageOperationPromise(); + wrapper.find('HunkView').at(0).find('.github-HunkView-stageButton').simulate('click'); + wrapper.find('HunkView').at(0).find('.github-HunkView-stageButton').simulate('click'); + await hunk1StagingPromise; + + const patchPromise0 = switchboard.getChangePatchPromise(); + await refreshRepository(wrapper); // clear the cached file patches + const modifiedFilePatch = await repository.getFilePatchForPath(filePath); + wrapper.setState({filePatch: modifiedFilePatch}); + await patchPromise0; + + let expectedLines = originalLines.slice(); + expectedLines.splice(1, 0, + 'this is a modified line', + 'this is a new line', + 'this is another new line', + ); + let actualLines = await repository.readFileFromIndex(filePath); + assert.autocrlfEqual(actualLines, expectedLines.join('\n')); + + const hunk2StagingPromise = switchboard.getFinishStageOperationPromise(); + wrapper.find('HunkView').at(0).find('.github-HunkView-stageButton').simulate('click'); + await hunk2StagingPromise; + + expectedLines = originalLines.slice(); + expectedLines.splice(1, 0, + 'this is a modified line', + 'this is a new line', + 'this is another new line', + ); + expectedLines.splice(11, 2, 'this is a modified line'); + actualLines = await repository.readFileFromIndex(filePath); + assert.autocrlfEqual(actualLines, expectedLines.join('\n')); + }); + }); + }); + }); +}); From 52e36e127a96ae556c91beec1513f56c96c09d3a Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 6 Jun 2018 16:16:50 -0400 Subject: [PATCH 0012/4252] :fire: unused spies --- test/containers/file-patch-container.test.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/containers/file-patch-container.test.js b/test/containers/file-patch-container.test.js index 2f01eb3772..02fdd93eed 100644 --- a/test/containers/file-patch-container.test.js +++ b/test/containers/file-patch-container.test.js @@ -14,8 +14,6 @@ describe('FilePatchContainer', function() { const workdirPath = await cloneRepository(); repository = await buildRepository(workdirPath); - sinon.spy(repository, 'getFilePatchForPath'); - sinon.spy(repository, 'isPartiallyStaged'); // a.txt: unstaged changes await fs.writeFile(path.join(workdirPath, 'a.txt'), 'changed\n'); From f872f576c9b7bde450110643ed78744e5b725ad4 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 6 Jun 2018 16:17:47 -0400 Subject: [PATCH 0013/4252] Out of the way, old FilePatchView tests --- .../{file-patch-view.test.js => file-patch-view.test.old.js} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/views/{file-patch-view.test.js => file-patch-view.test.old.js} (100%) diff --git a/test/views/file-patch-view.test.js b/test/views/file-patch-view.test.old.js similarity index 100% rename from test/views/file-patch-view.test.js rename to test/views/file-patch-view.test.old.js From d64d8b30c5931c0989d33b4c186a9ef7c7ba79fe Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 7 Jun 2018 09:28:33 -0400 Subject: [PATCH 0014/4252] (Failing) tests for FilePatchView --- test/views/file-patch-view.test.js | 60 ++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 test/views/file-patch-view.test.js diff --git a/test/views/file-patch-view.test.js b/test/views/file-patch-view.test.js new file mode 100644 index 0000000000..c5225a1564 --- /dev/null +++ b/test/views/file-patch-view.test.js @@ -0,0 +1,60 @@ +import path from 'path'; +import fs from 'fs-extra'; +import React from 'react'; +import {mount} from 'enzyme'; + +import {cloneRepository, buildRepository} from '../helpers'; +import FilePatchView from '../../lib/views/file-patch-view'; + +describe('FilePatchView', function() { + let atomEnv, filePatch; + + beforeEach(async function() { + atomEnv = global.buildAtomEnvironment(); + + const workdirPath = await cloneRepository(); + const repository = await buildRepository(workdirPath); + + // a.txt: unstaged changes + await fs.writeFile(path.join(workdirPath, 'a.txt'), 'changed\n'); + filePatch = await repository.getFilePatchForPath('a.txt', {staged: false}); + }); + + afterEach(function() { + atomEnv.destroy(); + }); + + function buildApp(overrideProps = {}) { + const props = { + stagingStatus: 'unstaged', + isPartiallyStaged: false, + filePatch, + tooltips: atomEnv.tooltips, + ...overrideProps, + }; + + return ; + } + + it('renders the file header', function() { + const wrapper = mount(buildApp()); + assert.isTrue(wrapper.find('FilePatchHeader').exists()); + }); + + it('renders the file patch within an editor', function() { + const wrapper = mount(buildApp()); + + const editor = wrapper.find('AtomTextEditor'); + assert.strictEqual(editor.instance().getModel().getText(), filePatch.present().getText()); + }); + + it('renders a header for each hunk'); + + describe('hunk lines', function() { + it('decorates added lines'); + + it('decorates deleted lines'); + + it('decorates the nonewlines line'); + }); +}); From b1e1fd8e97dcd1b573825ef72e1d45bfa054da77 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 7 Jun 2018 10:57:10 -0400 Subject: [PATCH 0015/4252] Write and test a FilePatchHeaderView component --- lib/views/file-patch-header-view.js | 135 +++++++++++++++++++ test/views/file-patch-header-view.test.js | 154 ++++++++++++++++++++++ 2 files changed, 289 insertions(+) create mode 100644 lib/views/file-patch-header-view.js create mode 100644 test/views/file-patch-header-view.test.js diff --git a/lib/views/file-patch-header-view.js b/lib/views/file-patch-header-view.js new file mode 100644 index 0000000000..3514460e6b --- /dev/null +++ b/lib/views/file-patch-header-view.js @@ -0,0 +1,135 @@ +import React, {Fragment} from 'react'; +import PropTypes from 'prop-types'; +import cx from 'classnames'; + +import RefHolder from '../models/ref-holder'; +import Tooltip from '../atom/tooltip'; + +export default class FilePatchHeaderView extends React.Component { + static propTypes = { + relPath: PropTypes.string.isRequired, + stagingStatus: PropTypes.oneOf(['staged', 'unstaged']).isRequired, + isPartiallyStaged: PropTypes.bool.isRequired, + hasHunks: PropTypes.bool.isRequired, + hasUndoHistory: PropTypes.bool.isRequired, + + tooltips: PropTypes.object.isRequired, + + undoLastDiscard: PropTypes.func.isRequired, + diveIntoMirrorPatch: PropTypes.func.isRequired, + openFile: PropTypes.func.isRequired, + toggleFile: PropTypes.func.isRequired, + }; + + constructor(props) { + super(props); + + this.refMirrorButton = new RefHolder(); + this.refOpenFileButton = new RefHolder(); + } + + render() { + return ( +
+ + {this.renderTitle()} + + {this.renderButtonGroup()} +
+ ); + } + + renderTitle() { + const status = this.props.stagingStatus; + return `${status[0].toUpperCase()}${status.slice(1)} Changes for ${this.props.relPath}`; + } + + renderButtonGroup() { + return ( + + {this.renderUndoDiscardButton()} + {this.renderMirrorPatchButton()} + {this.renderOpenFileButton()} + {this.renderToggleFileButton()} + + ); + } + + renderUndoDiscardButton() { + if (!this.props.hasUndoHistory || this.props.stagingStatus !== 'unstaged') { + return null; + } + + return ( + + ); + } + + renderMirrorPatchButton() { + if (!this.props.isPartiallyStaged && this.props.hasHunks) { + return null; + } + + const attrs = this.props.stagingStatus === 'unstaged' + ? { + iconClass: 'icon-tasklist', + tooltipText: 'View staged changes', + } + : { + iconClass: 'icon-list-unordered', + tooltipText: 'View unstaged changes', + }; + + return ( + + + ); + } +} diff --git a/test/views/file-patch-header-view.test.js b/test/views/file-patch-header-view.test.js new file mode 100644 index 0000000000..d56eb3605b --- /dev/null +++ b/test/views/file-patch-header-view.test.js @@ -0,0 +1,154 @@ +import React from 'react'; +import {shallow} from 'enzyme'; + +import FilePatchHeaderView from '../../lib/views/file-patch-header-view'; + +describe('FilePatchHeaderView', function() { + let atomEnv; + + beforeEach(function() { + atomEnv = global.buildAtomEnvironment(); + }); + + afterEach(function() { + atomEnv.destroy(); + }); + + function buildApp(overrideProps = {}) { + return ( + {}} + diveIntoMirrorPatch={() => {}} + openFile={() => {}} + toggleFile={() => {}} + + {...overrideProps} + /> + ); + } + + describe('the title', function() { + it('renders for an unstaged patch', function() { + const wrapper = shallow(buildApp({stagingStatus: 'unstaged'})); + assert.strictEqual(wrapper.find('.github-FilePatchView-title').text(), 'Unstaged Changes for dir/a.txt'); + }); + + it('renders for a staged patch', function() { + const wrapper = shallow(buildApp({stagingStatus: 'staged'})); + assert.strictEqual(wrapper.find('.github-FilePatchView-title').text(), 'Staged Changes for dir/a.txt'); + }); + }); + + describe('the button group', function() { + it('includes undo discard if undo history is available and the patch is unstaged', function() { + const undoLastDiscard = sinon.stub(); + const wrapper = shallow(buildApp({hasUndoHistory: true, stagingStatus: 'unstaged', undoLastDiscard})); + assert.isTrue(wrapper.find('button.icon-history').exists()); + + wrapper.find('button.icon-history').simulate('click'); + assert.isTrue(undoLastDiscard.called); + + wrapper.setProps({hasUndoHistory: false, stagingStatus: 'unstaged'}); + assert.isFalse(wrapper.find('button.icon-history').exists()); + + wrapper.setProps({hasUndoHistory: true, stagingStatus: 'staged'}); + assert.isFalse(wrapper.find('button.icon-history').exists()); + }); + + function createPatchToggleTest({overrideProps, stagingStatus, buttonClass, oppositeButtonClass, tooltip}) { + return function() { + const diveIntoMirrorPatch = sinon.stub(); + const wrapper = shallow(buildApp({stagingStatus, diveIntoMirrorPatch, ...overrideProps})); + + assert.isTrue(wrapper.find(`button.${buttonClass}`).exists(), + `${buttonClass} expected, but not found`); + assert.isFalse(wrapper.find(`button.${oppositeButtonClass}`).exists(), + `${oppositeButtonClass} not expected, but found`); + + wrapper.find(`button.${buttonClass}`).simulate('click'); + assert.isTrue(diveIntoMirrorPatch.called, `${buttonClass} click did nothing`); + + assert.isTrue(wrapper.find('Tooltip').someWhere(n => n.prop('title') === tooltip)); + }; + } + + function createUnstagedPatchToggleTest(overrideProps) { + return createPatchToggleTest({ + overrideProps, + stagingStatus: 'unstaged', + buttonClass: 'icon-tasklist', + oppositeButtonClass: 'icon-list-unordered', + tooltip: 'View staged changes', + }); + } + + function createStagedPatchToggleTest(overrideProps) { + return createPatchToggleTest({ + overrideProps, + stagingStatus: 'staged', + buttonClass: 'icon-list-unordered', + oppositeButtonClass: 'icon-tasklist', + tooltip: 'View unstaged changes', + }); + } + + describe('when the patch is partially staged', function() { + const props = {isPartiallyStaged: true}; + + it('includes a toggle to staged button when unstaged', createUnstagedPatchToggleTest(props)); + + it('includes a toggle to unstaged button when staged', createStagedPatchToggleTest(props)); + }); + + describe('when the patch contains no hunks', function() { + const props = {hasHunks: false}; + + it('includes a toggle to staged button when unstaged', createUnstagedPatchToggleTest(props)); + + it('includes a toggle to unstaged button when staged', createStagedPatchToggleTest(props)); + }); + + it('includes an open file button', function() { + const openFile = sinon.stub(); + const wrapper = shallow(buildApp({openFile})); + + wrapper.find('button.icon-code').simulate('click'); + assert.isTrue(openFile.called); + }); + + function createToggleFileTest({stagingStatus, buttonClass, oppositeButtonClass}) { + return function() { + const toggleFile = sinon.stub(); + const wrapper = shallow(buildApp({toggleFile, stagingStatus})); + + assert.isTrue(wrapper.find(`button.${buttonClass}`).exists(), + `${buttonClass} expected, but not found`); + assert.isFalse(wrapper.find(`button.${oppositeButtonClass}`).exists(), + `${oppositeButtonClass} not expected, but found`); + + wrapper.find(`button.${buttonClass}`).simulate('click'); + assert.isTrue(toggleFile.called, `${buttonClass} click did nothing`); + }; + } + + it('includes a stage file button when unstaged', createToggleFileTest({ + stagingStatus: 'unstaged', + buttonClass: 'icon-move-down', + oppositeButtonClass: 'icon-move-up', + })); + + it('includes an unstage file button when staged', createToggleFileTest({ + stagingStatus: 'staged', + buttonClass: 'icon-move-up', + oppositeButtonClass: 'icon-move-down', + })); + }); +}); From 7cdfaf223725eb862befea373b8e3f412a51aa3d Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 7 Jun 2018 11:11:04 -0400 Subject: [PATCH 0016/4252] Render the FilePatchHeaderView within the FilePatchView --- lib/views/file-patch-view.js | 77 ++++++++++++------------------ test/views/file-patch-view.test.js | 14 ++++-- 2 files changed, 41 insertions(+), 50 deletions(-) diff --git a/lib/views/file-patch-view.js b/lib/views/file-patch-view.js index f6244e056a..538ba31bf2 100644 --- a/lib/views/file-patch-view.js +++ b/lib/views/file-patch-view.js @@ -6,14 +6,22 @@ import FilePatchSelection from '../models/file-patch-selection'; import AtomTextEditor from '../atom/atom-text-editor'; import Marker from '../atom/marker'; import Decoration from '../atom/decoration'; +import FilePatchHeaderView from './file-patch-header-view'; export default class FilePatchView extends React.Component { static propTypes = { + relPath: PropTypes.string.isRequired, stagingStatus: PropTypes.oneOf(['staged', 'unstaged']).isRequired, isPartiallyStaged: PropTypes.bool.isRequired, filePatch: PropTypes.object.isRequired, + repository: PropTypes.object.isRequired, tooltips: PropTypes.object.isRequired, + + undoLastDiscard: PropTypes.func.isRequired, + diveIntoMirrorPatch: PropTypes.func.isRequired, + openFile: PropTypes.func.isRequired, + toggleFile: PropTypes.func.isRequired, } constructor(props) { @@ -38,59 +46,34 @@ export default class FilePatchView extends React.Component { render() { return (
- - - - {this.renderFileHeader()} - - - + 0} + hasUndoHistory={this.props.repository.hasDiscardHistory(this.props.relPath)} -
- ); - } + tooltips={this.props.tooltips} - renderFileHeader() { - return ( -
- - {this.isUnstaged() ? 'Unstaged Changes for ' : 'Staged Changes for '} - {this.props.filePatch.getPath()} - - {this.renderButtonGroup()} -
- ); - } + undoLastDiscard={this.props.undoLastDiscard} + diveIntoMirrorPatch={this.props.diveIntoMirrorPatch} + openFile={this.props.openFile} + toggleFile={this.props.toggleFile} + /> - renderButtonGroup() { - const hasHunks = this.props.filePatch.getHunks().length > 0; +
+ + + + + + +
- return ( - - {this.props.isPartiallyStaged || !hasHunks ? ( - - ) : null } - +
); } - - isUnstaged() { - return this.props.stagingStatus === 'unstaged'; - } } diff --git a/test/views/file-patch-view.test.js b/test/views/file-patch-view.test.js index c5225a1564..2d4b8095dc 100644 --- a/test/views/file-patch-view.test.js +++ b/test/views/file-patch-view.test.js @@ -7,13 +7,13 @@ import {cloneRepository, buildRepository} from '../helpers'; import FilePatchView from '../../lib/views/file-patch-view'; describe('FilePatchView', function() { - let atomEnv, filePatch; + let atomEnv, repository, filePatch; beforeEach(async function() { atomEnv = global.buildAtomEnvironment(); const workdirPath = await cloneRepository(); - const repository = await buildRepository(workdirPath); + repository = await buildRepository(workdirPath); // a.txt: unstaged changes await fs.writeFile(path.join(workdirPath, 'a.txt'), 'changed\n'); @@ -26,10 +26,18 @@ describe('FilePatchView', function() { function buildApp(overrideProps = {}) { const props = { + relPath: 'a.txt', stagingStatus: 'unstaged', isPartiallyStaged: false, filePatch, + repository, tooltips: atomEnv.tooltips, + + undoLastDiscard: () => {}, + diveIntoMirrorPatch: () => {}, + openFile: () => {}, + toggleFile: () => {}, + ...overrideProps, }; @@ -38,7 +46,7 @@ describe('FilePatchView', function() { it('renders the file header', function() { const wrapper = mount(buildApp()); - assert.isTrue(wrapper.find('FilePatchHeader').exists()); + assert.isTrue(wrapper.find('FilePatchHeaderView').exists()); }); it('renders the file patch within an editor', function() { From 36523deafec0ec228bda09be97c1af6a0cfa4e57 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 7 Jun 2018 14:15:29 -0400 Subject: [PATCH 0017/4252] Render FilePatchMetaViews for symlink and file mode changes --- lib/views/file-patch-meta-view.js | 37 +++++++ lib/views/file-patch-view.js | 130 +++++++++++++++++++++++- test/views/file-patch-meta-view.test.js | 53 ++++++++++ test/views/file-patch-view.test.js | 114 ++++++++++++++++++++- 4 files changed, 331 insertions(+), 3 deletions(-) create mode 100644 lib/views/file-patch-meta-view.js create mode 100644 test/views/file-patch-meta-view.test.js diff --git a/lib/views/file-patch-meta-view.js b/lib/views/file-patch-meta-view.js new file mode 100644 index 0000000000..4edbb69417 --- /dev/null +++ b/lib/views/file-patch-meta-view.js @@ -0,0 +1,37 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import cx from 'classnames'; + +export default class FilePatchMetaView extends React.Component { + static propTypes = { + title: PropTypes.string.isRequired, + actionIcon: PropTypes.string.isRequired, + actionText: PropTypes.string.isRequired, + + action: PropTypes.func.isRequired, + + children: PropTypes.element.isRequired, + }; + + render() { + return ( +
+
+
+

{this.props.title}

+
+ +
+
+
+ {this.props.children} +
+
+
+ ); + } +} diff --git a/lib/views/file-patch-view.js b/lib/views/file-patch-view.js index 538ba31bf2..056573a260 100644 --- a/lib/views/file-patch-view.js +++ b/lib/views/file-patch-view.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {Fragment} from 'react'; import PropTypes from 'prop-types'; import cx from 'classnames'; @@ -7,6 +7,12 @@ import AtomTextEditor from '../atom/atom-text-editor'; import Marker from '../atom/marker'; import Decoration from '../atom/decoration'; import FilePatchHeaderView from './file-patch-header-view'; +import FilePatchMetaView from './file-patch-meta-view'; + +const executableText = { + 100644: 'non executable', + 100755: 'executable', +}; export default class FilePatchView extends React.Component { static propTypes = { @@ -22,6 +28,8 @@ export default class FilePatchView extends React.Component { diveIntoMirrorPatch: PropTypes.func.isRequired, openFile: PropTypes.func.isRequired, toggleFile: PropTypes.func.isRequired, + toggleModeChange: PropTypes.func.isRequired, + toggleSymlinkChange: PropTypes.func.isRequired, } constructor(props) { @@ -68,6 +76,10 @@ export default class FilePatchView extends React.Component { + + {this.renderExecutableModeChangeMeta()} + {this.renderSymlinkChangeMeta()} + @@ -76,4 +88,120 @@ export default class FilePatchView extends React.Component { ); } + + renderExecutableModeChangeMeta() { + if (!this.props.filePatch.didChangeExecutableMode()) { + return null; + } + + const oldMode = this.props.filePatch.getOldMode(); + const newMode = this.props.filePatch.getNewMode(); + + const attrs = this.props.stagingStatus === 'unstaged' + ? { + actionIcon: 'icon-move-down', + actionText: 'Stage Mode Change', + } + : { + actionIcon: 'icon-move-up', + actionText: 'Unstage Mode Change', + }; + + return ( + + + File changed mode + + from {executableText[oldMode]} {oldMode} + + + to {executableText[newMode]} {newMode} + + + + ); + } + + renderSymlinkChangeMeta() { + if (!this.props.filePatch.hasSymlink()) { + return null; + } + + let detail =
; + let title = ''; + const oldSymlink = this.props.filePatch.getOldSymlink(); + const newSymlink = this.props.filePatch.getNewSymlink(); + if (oldSymlink && newSymlink) { + detail = ( + + Symlink changed + + from {oldSymlink} + + + to {newSymlink} + . + + ); + title = 'Symlink changed'; + } else if (oldSymlink && !newSymlink) { + detail = ( + + Symlink + + to {oldSymlink} + + deleted. + + ); + title = 'Symlink deleted'; + } else if (!oldSymlink && newSymlink) { + detail = ( + + Symlink + + to {newSymlink} + + created. + + ); + title = 'Symlink created'; + } else { + return null; + } + + const attrs = this.props.stagingStatus === 'unstaged' + ? { + actionIcon: 'icon-move-down', + actionText: 'Stage Symlink Change', + } + : { + actionIcon: 'icon-move-up', + actionText: 'Unstage Symlink Change', + }; + + return ( + + + {detail} + + + ); + } } diff --git a/test/views/file-patch-meta-view.test.js b/test/views/file-patch-meta-view.test.js new file mode 100644 index 0000000000..8602e9a9f5 --- /dev/null +++ b/test/views/file-patch-meta-view.test.js @@ -0,0 +1,53 @@ +import React from 'react'; +import {shallow} from 'enzyme'; + +import FilePatchMetaView from '../../lib/views/file-patch-meta-view'; + +describe('FilePatchMetaView', function() { + let atomEnv; + + beforeEach(function() { + atomEnv = global.buildAtomEnvironment(); + }); + + afterEach(function() { + atomEnv.destroy(); + }); + + function buildApp(overrideProps = {}, children =
) { + return ( + {}} + + {...overrideProps}> + {children} + + ); + } + + it('renders the title', function() { + const wrapper = shallow(buildApp({title: 'Yes'})); + assert.strictEqual(wrapper.find('.github-FilePatchView-metaTitle').text(), 'Yes'); + }); + + it('renders a control button with the correct text and callback', function() { + const action = sinon.stub(); + const wrapper = shallow(buildApp({action, actionText: 'do the thing', actionIcon: 'icon-move-down'})); + + const button = wrapper.find('button.icon-move-down'); + + assert.strictEqual(button.text(), 'do the thing'); + + button.simulate('click'); + assert.isTrue(action.called); + }); + + it('renders child elements as details', function() { + const wrapper = shallow(buildApp({},
)); + assert.isTrue(wrapper.find('.github-FilePatchView-metaDetails .child').exists()); + }); +}); diff --git a/test/views/file-patch-view.test.js b/test/views/file-patch-view.test.js index 2d4b8095dc..e5d477cc96 100644 --- a/test/views/file-patch-view.test.js +++ b/test/views/file-patch-view.test.js @@ -1,7 +1,7 @@ import path from 'path'; import fs from 'fs-extra'; import React from 'react'; -import {mount} from 'enzyme'; +import {shallow, mount} from 'enzyme'; import {cloneRepository, buildRepository} from '../helpers'; import FilePatchView from '../../lib/views/file-patch-view'; @@ -37,6 +37,8 @@ describe('FilePatchView', function() { diveIntoMirrorPatch: () => {}, openFile: () => {}, toggleFile: () => {}, + toggleModeChange: () => {}, + toggleSymlinkChange: () => {}, ...overrideProps, }; @@ -45,7 +47,7 @@ describe('FilePatchView', function() { } it('renders the file header', function() { - const wrapper = mount(buildApp()); + const wrapper = shallow(buildApp()); assert.isTrue(wrapper.find('FilePatchHeaderView').exists()); }); @@ -56,6 +58,114 @@ describe('FilePatchView', function() { assert.strictEqual(editor.instance().getModel().getText(), filePatch.present().getText()); }); + describe('executable mode changes', function() { + it('does not render if the mode has not changed', function() { + sinon.stub(filePatch, 'getOldMode').returns('100644'); + sinon.stub(filePatch, 'getNewMode').returns('100644'); + + const wrapper = shallow(buildApp()); + assert.isFalse(wrapper.find('FilePatchMetaView[title="Mode change"]').exists()); + }); + + it('renders change details within a meta container', function() { + sinon.stub(filePatch, 'getOldMode').returns('100644'); + sinon.stub(filePatch, 'getNewMode').returns('100755'); + + const wrapper = mount(buildApp({stagingStatus: 'unstaged'})); + + const meta = wrapper.find('FilePatchMetaView[title="Mode change"]'); + assert.isTrue(meta.exists()); + assert.strictEqual(meta.prop('actionIcon'), 'icon-move-down'); + assert.strictEqual(meta.prop('actionText'), 'Stage Mode Change'); + + const details = meta.find('.github-FilePatchView-metaDetails'); + assert.strictEqual(details.text(), 'File changed modefrom non executable 100644to executable 100755'); + }); + + it("stages or unstages the mode change when the meta container's action is triggered", function() { + sinon.stub(filePatch, 'getOldMode').returns('100644'); + sinon.stub(filePatch, 'getNewMode').returns('100755'); + + const toggleModeChange = sinon.stub(); + const wrapper = shallow(buildApp({stagingStatus: 'staged', toggleModeChange})); + + const meta = wrapper.find('FilePatchMetaView[title="Mode change"]'); + assert.isTrue(meta.exists()); + assert.strictEqual(meta.prop('actionIcon'), 'icon-move-up'); + assert.strictEqual(meta.prop('actionText'), 'Unstage Mode Change'); + + meta.prop('action')(); + assert.isTrue(toggleModeChange.called); + }); + }); + + describe('symlink changes', function() { + it('does not render if the symlink status is unchanged', function() { + const wrapper = mount(buildApp()); + assert.lengthOf(wrapper.find('FilePatchMetaView').filterWhere(v => v.prop('title').startsWith('Symlink')), 0); + }); + + it('renders symlink change information within a meta container', function() { + sinon.stub(filePatch, 'hasSymlink').returns(true); + sinon.stub(filePatch, 'getOldSymlink').returns('/old.txt'); + sinon.stub(filePatch, 'getNewSymlink').returns('/new.txt'); + + const wrapper = mount(buildApp({stagingStatus: 'unstaged'})); + const meta = wrapper.find('FilePatchMetaView[title="Symlink changed"]'); + assert.isTrue(meta.exists()); + assert.strictEqual(meta.prop('actionIcon'), 'icon-move-down'); + assert.strictEqual(meta.prop('actionText'), 'Stage Symlink Change'); + assert.strictEqual( + meta.find('.github-FilePatchView-metaDetails').text(), + 'Symlink changedfrom /old.txtto /new.txt.', + ); + }); + + it('stages or unstages the symlink change', function() { + const toggleSymlinkChange = sinon.stub(); + sinon.stub(filePatch, 'hasSymlink').returns(true); + sinon.stub(filePatch, 'getOldSymlink').returns('/old.txt'); + sinon.stub(filePatch, 'getNewSymlink').returns('/new.txt'); + + const wrapper = mount(buildApp({stagingStatus: 'staged', toggleSymlinkChange})); + const meta = wrapper.find('FilePatchMetaView[title="Symlink changed"]'); + assert.isTrue(meta.exists()); + assert.strictEqual(meta.prop('actionIcon'), 'icon-move-up'); + assert.strictEqual(meta.prop('actionText'), 'Unstage Symlink Change'); + + meta.find('button.icon-move-up').simulate('click'); + assert.isTrue(toggleSymlinkChange.called); + }); + + it('renders details for a symlink deletion', function() { + sinon.stub(filePatch, 'hasSymlink').returns(true); + sinon.stub(filePatch, 'getOldSymlink').returns('/old.txt'); + sinon.stub(filePatch, 'getNewSymlink').returns(null); + + const wrapper = mount(buildApp()); + const meta = wrapper.find('FilePatchMetaView[title="Symlink deleted"]'); + assert.isTrue(meta.exists()); + assert.strictEqual( + meta.find('.github-FilePatchView-metaDetails').text(), + 'Symlinkto /old.txtdeleted.', + ); + }); + + it('renders details for a symlink creation', function() { + sinon.stub(filePatch, 'hasSymlink').returns(true); + sinon.stub(filePatch, 'getOldSymlink').returns(null); + sinon.stub(filePatch, 'getNewSymlink').returns('/new.txt'); + + const wrapper = mount(buildApp()); + const meta = wrapper.find('FilePatchMetaView[title="Symlink created"]'); + assert.isTrue(meta.exists()); + assert.strictEqual( + meta.find('.github-FilePatchView-metaDetails').text(), + 'Symlinkto /new.txtcreated.', + ); + }); + }); + it('renders a header for each hunk'); describe('hunk lines', function() { From 33595a847fe7294d975ef2994288640e43bc51de Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Sat, 9 Jun 2018 20:38:52 -0400 Subject: [PATCH 0018/4252] HunkHeaderView --- lib/views/hunk-header-view.js | 60 ++++++++++++++++++++++ test/views/hunk-header-view.test.js | 78 +++++++++++++++++++++++++++++ 2 files changed, 138 insertions(+) create mode 100644 lib/views/hunk-header-view.js create mode 100644 test/views/hunk-header-view.test.js diff --git a/lib/views/hunk-header-view.js b/lib/views/hunk-header-view.js new file mode 100644 index 0000000000..e0ba9f9b46 --- /dev/null +++ b/lib/views/hunk-header-view.js @@ -0,0 +1,60 @@ +import React, {Fragment} from 'react'; +import PropTypes from 'prop-types'; +import cx from 'classnames'; + +import RefHolder from '../models/ref-holder'; +import Tooltip from '../atom/tooltip'; + +export default class HunkHeaderView extends React.Component { + static propTypes = { + hunk: PropTypes.object.isRequired, + isSelected: PropTypes.bool.isRequired, + stagingStatus: PropTypes.oneOf(['unstaged', 'staged']).isRequired, + selectionMode: PropTypes.oneOf(['hunk', 'line']).isRequired, + toggleSelectionLabel: PropTypes.string.isRequired, + discardSelectionLabel: PropTypes.string.isRequired, + + tooltips: PropTypes.object.isRequired, + + toggleSelection: PropTypes.func.isRequired, + discardSelection: PropTypes.func.isRequired, + }; + + constructor(props) { + super(props); + + this.refDiscardButton = new RefHolder(); + } + + render() { + const conditional = { + 'is-selected': this.props.isSelected, + 'is-hunkMode': this.props.selectionMode === 'hunk', + }; + + return ( +
+ + {this.props.hunk.getHeader().trim()} {this.props.hunk.getSectionHeading().trim()} + + + {this.props.stagingStatus === 'unstaged' && ( + +
+ ); + } +} diff --git a/test/views/hunk-header-view.test.js b/test/views/hunk-header-view.test.js new file mode 100644 index 0000000000..525aa9a10c --- /dev/null +++ b/test/views/hunk-header-view.test.js @@ -0,0 +1,78 @@ +import React from 'react'; +import {shallow} from 'enzyme'; + +import HunkHeaderView from '../../lib/views/hunk-header-view'; +import Hunk from '../../lib/models/hunk'; + +describe('HunkHeaderView', function() { + let atomEnv, hunk; + + beforeEach(function() { + atomEnv = global.buildAtomEnvironment(); + hunk = new Hunk(0, 1, 10, 11, 'section heading', []); + }); + + afterEach(function() { + atomEnv.destroy(); + }); + + function buildApp(overrideProps = {}) { + return ( + {}} + discardSelection={() => {}} + + {...overrideProps} + /> + ); + } + + it('applies a CSS class when selected', function() { + const wrapper = shallow(buildApp({isSelected: true})); + assert.isTrue(wrapper.find('.github-HunkHeaderView').hasClass('is-selected')); + + wrapper.setProps({isSelected: false}); + assert.isFalse(wrapper.find('.github-HunkHeaderView').hasClass('is-selected')); + }); + + it('applies a CSS class in hunk selection mode', function() { + const wrapper = shallow(buildApp({selectionMode: 'hunk'})); + assert.isTrue(wrapper.find('.github-HunkHeaderView').hasClass('is-hunkMode')); + + wrapper.setProps({selectionMode: 'line'}); + assert.isFalse(wrapper.find('.github-HunkHeaderView').hasClass('is-hunkMode')); + }); + + it('renders the hunk header title', function() { + const wrapper = shallow(buildApp()); + assert.strictEqual(wrapper.find('.github-HunkHeaderView-title').text(), '@@ -0,10 +1,11 @@ section heading'); + }); + + it('renders a button to toggle the selection', function() { + const toggleSelection = sinon.stub(); + const wrapper = shallow(buildApp({toggleSelectionLabel: 'Do the thing', toggleSelection})); + const button = wrapper.find('button.github-HunkHeaderView-stageButton'); + assert.strictEqual(button.text(), 'Do the thing'); + button.simulate('click'); + assert.isTrue(toggleSelection.called); + }); + + it('renders a button to discard an unstaged selection', function() { + const discardSelection = sinon.stub(); + const wrapper = shallow(buildApp({stagingStatus: 'unstaged', discardSelectionLabel: 'Nope', discardSelection})); + const button = wrapper.find('button.github-HunkHeaderView-discardButton'); + assert.isTrue(button.exists()); + assert.isTrue(wrapper.find('Tooltip[title="Nope"]').exists()); + button.simulate('click'); + assert.isTrue(discardSelection.called); + }); +}); From 381ff5b6a183a179942ac9938b3a6b7982bf1b6d Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 11 Jun 2018 11:15:49 -0400 Subject: [PATCH 0019/4252] Use PropTypes.node instead of PropTypes.element --- lib/atom/atom-text-editor.js | 2 +- lib/atom/decoration.js | 2 +- lib/atom/marker-layer.js | 2 +- lib/atom/marker.js | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/atom/atom-text-editor.js b/lib/atom/atom-text-editor.js index ab7eedf561..b2b77c276e 100644 --- a/lib/atom/atom-text-editor.js +++ b/lib/atom/atom-text-editor.js @@ -43,7 +43,7 @@ export default class AtomTextEditor extends React.PureComponent { text: PropTypes.string, didChange: PropTypes.func, didChangeCursorPosition: PropTypes.func, - children: PropTypes.element, + children: PropTypes.node, } static defaultProps = { diff --git a/lib/atom/decoration.js b/lib/atom/decoration.js index 121f9c46ed..1da7256eac 100644 --- a/lib/atom/decoration.js +++ b/lib/atom/decoration.js @@ -16,7 +16,7 @@ class WrappedDecoration extends React.Component { type: PropTypes.oneOf(['line', 'line-number', 'highlight', 'overlay', 'gutter', 'block']).isRequired, position: PropTypes.oneOf(['head', 'tail', 'before', 'after']), className: PropTypes.string, - children: PropTypes.element, + children: PropTypes.node, itemHolder: RefHolderPropType, options: PropTypes.object, } diff --git a/lib/atom/marker-layer.js b/lib/atom/marker-layer.js index 0ff781c1e2..5bf1e2f9cb 100644 --- a/lib/atom/marker-layer.js +++ b/lib/atom/marker-layer.js @@ -17,7 +17,7 @@ class WrappedMarkerLayer extends React.Component { static propTypes = { ...markerLayerProps, editor: PropTypes.object, - children: PropTypes.element, + children: PropTypes.node, handleID: PropTypes.func, }; diff --git a/lib/atom/marker.js b/lib/atom/marker.js index 34ed5db4f4..72db45c203 100644 --- a/lib/atom/marker.js +++ b/lib/atom/marker.js @@ -41,7 +41,7 @@ class WrappedMarker extends React.Component { screenRange: RangePropType, screenPosition: PointPropType, markableHolder: RefHolderPropType, - children: PropTypes.element, + children: PropTypes.node, handleID: PropTypes.func, } From d876ddc9db2956c1e9bece43542a2d5637f73122 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 11 Jun 2018 11:52:24 -0400 Subject: [PATCH 0020/4252] Decorate a parent Marker or MarkerLayer --- lib/atom/decoration.js | 19 +++++++++++++------ lib/atom/marker-layer.js | 10 +++++++++- lib/atom/marker.js | 11 ++++++++++- test/atom/decoration.test.js | 12 ++++++++++++ 4 files changed, 44 insertions(+), 8 deletions(-) diff --git a/lib/atom/decoration.js b/lib/atom/decoration.js index 1da7256eac..08ec2b901a 100644 --- a/lib/atom/decoration.js +++ b/lib/atom/decoration.js @@ -6,13 +6,14 @@ import {Disposable} from 'event-kit'; import {createItem, autobind} from '../helpers'; import {RefHolderPropType} from '../prop-types'; import {TextEditorContext} from './atom-text-editor'; -import {MarkerContext} from './marker'; +import {DecorableContext} from './marker'; import RefHolder from '../models/ref-holder'; class WrappedDecoration extends React.Component { static propTypes = { editorHolder: RefHolderPropType.isRequired, markerHolder: RefHolderPropType.isRequired, + decorateMethod: PropTypes.oneOf(['decorateMarker', 'decorateMarkerLayer']), type: PropTypes.oneOf(['line', 'line-number', 'highlight', 'overlay', 'gutter', 'block']).isRequired, position: PropTypes.oneOf(['head', 'tail', 'before', 'after']), className: PropTypes.string, @@ -22,6 +23,7 @@ class WrappedDecoration extends React.Component { } static defaultProps = { + decorateMethod: 'decorateMarker', options: {}, position: 'head', } @@ -102,7 +104,7 @@ class WrappedDecoration extends React.Component { const marker = this.props.markerHolder.get(); this.decorationHolder.setter( - editor.decorateMarker(marker, options), + editor[this.props.decorateMethod](marker, options), ); } @@ -160,11 +162,16 @@ export default class Decoration extends React.Component { return ( {editorHolder => ( - - {markerHolder => ( - + + {({holder, decorateMethod}) => ( + )} - + )} ); diff --git a/lib/atom/marker-layer.js b/lib/atom/marker-layer.js index 5bf1e2f9cb..8793c3ee50 100644 --- a/lib/atom/marker-layer.js +++ b/lib/atom/marker-layer.js @@ -5,6 +5,7 @@ import {Disposable} from 'event-kit'; import {autobind, extractProps} from '../helpers'; import RefHolder from '../models/ref-holder'; import {TextEditorContext} from './atom-text-editor'; +import {DecorableContext} from './marker'; const markerLayerProps = { maintainHistory: PropTypes.bool, @@ -35,6 +36,11 @@ class WrappedMarkerLayer extends React.Component { this.state = { editorHolder: RefHolder.on(this.props.editor), }; + + this.decorable = { + holder: this.layerHolder, + decorateMethod: 'decorateMarkerLayer', + }; } static getDerivedStateFromProps(props, state) { @@ -54,7 +60,9 @@ class WrappedMarkerLayer extends React.Component { render() { return ( - {this.props.children} + + {this.props.children} + ); } diff --git a/lib/atom/marker.js b/lib/atom/marker.js index 72db45c203..22e620f166 100644 --- a/lib/atom/marker.js +++ b/lib/atom/marker.js @@ -33,6 +33,8 @@ const markerProps = { export const MarkerContext = React.createContext(); +export const DecorableContext = React.createContext(); + class WrappedMarker extends React.Component { static propTypes = { ...markerProps, @@ -56,6 +58,11 @@ class WrappedMarker extends React.Component { this.sub = new Disposable(); this.markerHolder = new RefHolder(); + + this.decorable = { + holder: this.markerHolder, + decorateMethod: 'decorateMarker', + }; } componentDidMount() { @@ -65,7 +72,9 @@ class WrappedMarker extends React.Component { render() { return ( - {this.props.children} + + {this.props.children} + ); } diff --git a/test/atom/decoration.test.js b/test/atom/decoration.test.js index 45feeaee37..441c5da836 100644 --- a/test/atom/decoration.test.js +++ b/test/atom/decoration.test.js @@ -5,6 +5,7 @@ import {mount} from 'enzyme'; import Decoration from '../../lib/atom/decoration'; import AtomTextEditor from '../../lib/atom/atom-text-editor'; import Marker from '../../lib/atom/marker'; +import MarkerLayer from '../../lib/atom/marker-layer'; describe('Decoration', function() { let atomEnv, editor, marker; @@ -126,4 +127,15 @@ describe('Decoration', function() { assert.lengthOf(theEditor.getLineDecorations({position: 'head', class: 'whatever'}), 1); }); + + it('decorates a parent MarkerLayer', function() { + mount( + + + + + + , + ); + }); }); From d82f5649920fa88e0b72d7e57b5e73007f50cc68 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 11 Jun 2018 11:55:50 -0400 Subject: [PATCH 0021/4252] Render a HunkHeaderView for each hunk header --- lib/views/file-patch-view.js | 41 ++++++++++++++++++++++++++++++ test/views/file-patch-view.test.js | 12 ++++++++- 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/lib/views/file-patch-view.js b/lib/views/file-patch-view.js index 056573a260..429b468215 100644 --- a/lib/views/file-patch-view.js +++ b/lib/views/file-patch-view.js @@ -8,6 +8,7 @@ import Marker from '../atom/marker'; import Decoration from '../atom/decoration'; import FilePatchHeaderView from './file-patch-header-view'; import FilePatchMetaView from './file-patch-meta-view'; +import HunkHeaderView from './hunk-header-view'; const executableText = { 100644: 'non executable', @@ -74,6 +75,7 @@ export default class FilePatchView extends React.Component {
+ @@ -82,6 +84,8 @@ export default class FilePatchView extends React.Component { + + {this.renderHunkHeaders()}
@@ -204,4 +208,41 @@ export default class FilePatchView extends React.Component { ); } + + renderHunkHeaders() { + const selectedHunks = this.state.selection.getSelectedHunks(); + const isHunkSelectionMode = this.state.selection.getMode() === 'hunk'; + const toggleVerb = this.props.stagingStatus === 'unstaged' ? 'Stage' : 'Unstage'; + + return this.props.filePatch.getHunks().map((hunk, index) => { + const isSelected = selectedHunks.has(hunk); + let buttonSuffix = (isHunkSelectionMode || !isSelected) ? ' Hunk' : ' Selection'; + if (isSelected && selectedHunks.size > 1) { + buttonSuffix += 's'; + } + const toggleSelectionLabel = `${toggleVerb}${buttonSuffix}`; + const discardSelectionLabel = `Discard${buttonSuffix}`; + const bufferPosition = this.state.presentedFilePatch.getHunkStartPositions()[index]; + + return ( + + + {}} + discardSelection={() => {}} + /> + + + ); + }); + } } diff --git a/test/views/file-patch-view.test.js b/test/views/file-patch-view.test.js index e5d477cc96..244972580c 100644 --- a/test/views/file-patch-view.test.js +++ b/test/views/file-patch-view.test.js @@ -166,7 +166,17 @@ describe('FilePatchView', function() { }); }); - it('renders a header for each hunk'); + it('renders a header for each hunk', function() { + const hunks = [ + new Hunk(0, 0, 5, 5, 'hunk 0', []), + new Hunk(10, 10, 15, 15, 'hunk 1', []), + ]; + sinon.stub(filePatch, 'getHunks').returns(hunks); + + const wrapper = mount(buildApp()); + assert.isTrue(wrapper.find('HunkHeaderView').someWhere(h => h.prop('hunk') === hunks[0])); + assert.isTrue(wrapper.find('HunkHeaderView').someWhere(h => h.prop('hunk') === hunks[1])); + }); describe('hunk lines', function() { it('decorates added lines'); From 0850afe1f92485448cd1a4396e7cee6dcff3d6a3 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 11 Jun 2018 11:56:04 -0400 Subject: [PATCH 0022/4252] Decorate lines --- lib/views/file-patch-view.js | 27 ++++++++++++++ test/views/file-patch-view.test.js | 57 ++++++++++++++++++++++++++++-- 2 files changed, 81 insertions(+), 3 deletions(-) diff --git a/lib/views/file-patch-view.js b/lib/views/file-patch-view.js index 429b468215..b1ecb833e4 100644 --- a/lib/views/file-patch-view.js +++ b/lib/views/file-patch-view.js @@ -5,6 +5,7 @@ import cx from 'classnames'; import FilePatchSelection from '../models/file-patch-selection'; import AtomTextEditor from '../atom/atom-text-editor'; import Marker from '../atom/marker'; +import MarkerLayer from '../atom/marker-layer'; import Decoration from '../atom/decoration'; import FilePatchHeaderView from './file-patch-header-view'; import FilePatchMetaView from './file-patch-meta-view'; @@ -86,6 +87,20 @@ export default class FilePatchView extends React.Component { {this.renderHunkHeaders()} + + {this.renderLineDecorations( + this.state.presentedFilePatch.getAddedBufferPositions(), + 'github-FilePatchView-line--added', + )} + {this.renderLineDecorations( + this.state.presentedFilePatch.getDeletedBufferPositions(), + 'github-FilePatchView-line--deleted', + )} + {this.renderLineDecorations( + this.state.presentedFilePatch.getNoNewlineBufferPositions(), + 'github-FilePatchView-line--nonewline', + )} + @@ -245,4 +260,16 @@ export default class FilePatchView extends React.Component { ); }); } + + renderLineDecorations(positions, lineClass) { + return ( + + {positions.map((position, index) => { + return ; + })} + + + + ); + } } diff --git a/test/views/file-patch-view.test.js b/test/views/file-patch-view.test.js index 244972580c..2209313379 100644 --- a/test/views/file-patch-view.test.js +++ b/test/views/file-patch-view.test.js @@ -4,6 +4,8 @@ import React from 'react'; import {shallow, mount} from 'enzyme'; import {cloneRepository, buildRepository} from '../helpers'; +import Hunk from '../../lib/models/hunk'; +import HunkLine from '../../lib/models/hunk-line'; import FilePatchView from '../../lib/views/file-patch-view'; describe('FilePatchView', function() { @@ -179,10 +181,59 @@ describe('FilePatchView', function() { }); describe('hunk lines', function() { - it('decorates added lines'); + it('decorates added lines', function() { + const hunks = [ + new Hunk(0, 0, 1, 1, 'hunk 0', [ + new HunkLine('line 0', 'added', 0, 1, 0), + new HunkLine('line 1', 'deleted', 0, 1, 0), + ]), + ]; + sinon.stub(filePatch, 'getHunks').returns(hunks); - it('decorates deleted lines'); + const wrapper = mount(buildApp()); + assert.lengthOf( + wrapper.find('Decoration').filterWhere(h => { + return h.prop('type') === 'line' && h.prop('className') === 'github-FilePatchView-line--added'; + }), + 1, + ); + }); + + it('decorates deleted lines', function() { + const hunks = [ + new Hunk(0, 0, 1, 1, 'hunk 0', [ + new HunkLine('line 0', 'added', 0, 1, 0), + new HunkLine('line 1', 'deleted', 0, 1, 0), + ]), + ]; + sinon.stub(filePatch, 'getHunks').returns(hunks); + + const wrapper = mount(buildApp()); + assert.lengthOf( + wrapper.find('Decoration').filterWhere(h => { + return h.prop('type') === 'line' && h.prop('className') === 'github-FilePatchView-line--deleted'; + }), + 1, + ); + }); + + it('decorates the nonewline line', function() { + const hunks = [ + new Hunk(0, 0, 1, 1, 'hunk 0', [ + new HunkLine('line 0', 'added', 0, 1, 0), + new HunkLine('line 1', 'deleted', 0, 1, 0), + new HunkLine('no newline', 'nonewline', 0, 1, 0), + ]), + ]; + sinon.stub(filePatch, 'getHunks').returns(hunks); - it('decorates the nonewlines line'); + const wrapper = mount(buildApp()); + assert.lengthOf( + wrapper.find('Decoration').filterWhere(h => { + return h.prop('type') === 'line' && h.prop('className') === 'github-FilePatchView-line--nonewline'; + }), + 1, + ); + }); }); }); From 0b3faa3eef8eb27c368084f14035f982924b520c Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 11 Jun 2018 13:38:57 -0400 Subject: [PATCH 0023/4252] Pass the WorkdirContextPool into the React component tree --- lib/controllers/root-controller.js | 9 +++++++-- lib/github-package.js | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/controllers/root-controller.js b/lib/controllers/root-controller.js index 6dbedb6469..77ff3107bb 100644 --- a/lib/controllers/root-controller.js +++ b/lib/controllers/root-controller.js @@ -26,6 +26,7 @@ import GitCacheView from '../views/git-cache-view'; import Conflict from '../models/conflicts/conflict'; import RefHolder from '../models/ref-holder'; import Switchboard from '../switchboard'; +import {WorkdirContextPoolPropType} from '../prop-types'; import {destroyFilePatchPaneItems, destroyEmptyFilePatchPaneItems, autobind} from '../helpers'; import {GitError} from '../git-shell-out-strategy'; @@ -53,6 +54,7 @@ export default class RootController extends React.Component { destroyGitTabItem: PropTypes.func.isRequired, destroyGithubTabItem: PropTypes.func.isRequired, pipelineManager: PropTypes.object, + workdirContextPool: WorkdirContextPoolPropType.isRequired, } static defaultProps = { @@ -322,10 +324,13 @@ export default class RootController extends React.Component { {({itemHolder, params}) => ( )} diff --git a/lib/github-package.js b/lib/github-package.js index 1223b648ae..f5e6c4d9fc 100644 --- a/lib/github-package.js +++ b/lib/github-package.js @@ -288,7 +288,7 @@ export default class GithubPackage { destroyGitTabItem={this.destroyGitTabItem} destroyGithubTabItem={this.destroyGithubTabItem} removeFilePatchItem={this.removeFilePatchItem} - getRepositoryForWorkdir={this.getRepositoryForWorkdir} + workdirContextPool={this.contextPool} />, this.element, callback, ); } From 0017501d0c5cbc368562889a255e5ac8a31a6540 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 11 Jun 2018 13:39:07 -0400 Subject: [PATCH 0024/4252] Start shifting CSS around --- styles/file-patch-view.less | 20 +++ styles/hunk-header-view.less | 162 ++++++++++++++++++ styles/{hunk-view.less => hunk-view.old.less} | 0 3 files changed, 182 insertions(+) create mode 100644 styles/hunk-header-view.less rename styles/{hunk-view.less => hunk-view.old.less} (100%) diff --git a/styles/file-patch-view.less b/styles/file-patch-view.less index 224f1bb133..0c0dd50899 100644 --- a/styles/file-patch-view.less +++ b/styles/file-patch-view.less @@ -1,5 +1,8 @@ @import "variables"; +@hunk-fg-color: @text-color-subtle; +@hunk-bg-color: @pane-item-background-color; + .github-FilePatchView { display: flex; flex-direction: column; @@ -136,4 +139,21 @@ } } + // Line decorations + + &-line { + // mixin + .hunk-line-mixin(@fg; @bg) { + color: saturate( mix(@fg, @text-color-highlight, 20%), 20%); + background-color: saturate( mix(@bg, @hunk-bg-color, 15%), 20%); + } + + &--deleted { + .hunk-line-mixin(@text-color-error, @background-color-error); + } + + &--added { + .hunk-line-mixin(@text-color-success, @background-color-success); + } + } } diff --git a/styles/hunk-header-view.less b/styles/hunk-header-view.less new file mode 100644 index 0000000000..a1e1e19449 --- /dev/null +++ b/styles/hunk-header-view.less @@ -0,0 +1,162 @@ +@import "ui-variables"; + +@hunk-fg-color: @text-color-subtle; +@hunk-bg-color: @pane-item-background-color; + +.github-HunkHeaderView { + font-family: Menlo, Consolas, 'DejaVu Sans Mono', monospace; + + display: flex; + align-items: stretch; + font-size: .9em; + background-color: @panel-heading-background-color; + border-bottom: 1px solid @panel-heading-border-color; + + &-title { + flex: 1; + line-height: 2.4; + padding: 0 @component-padding; + color: @text-color-subtle; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + -webkit-font-smoothing: antialiased; + } + + &-stageButton, + &-discardButton { + line-height: 1; + padding-left: @component-padding; + padding-right: @component-padding; + font-family: @font-family; + border: none; + border-left: 1px solid @panel-heading-border-color; + background-color: transparent; + cursor: default; + &:hover { background-color: @button-background-color-hover; } + &:active { background-color: @panel-heading-border-color; } + } + + // pixel fit the icon + &-discardButton:before { + text-align: left; + width: auto; + } + + &-line { + display: table-row; + line-height: 1.5em; + color: @hunk-fg-color; + &.is-unchanged { + -webkit-font-smoothing: antialiased; + } + } + + &-lineNumber { + display: table-cell; + min-width: 3.5em; // min 4 chars + overflow: hidden; + padding: 0 .5em; + text-align: right; + border-right: 1px solid @base-border-color; + -webkit-font-smoothing: antialiased; + } + + &-plusMinus { + margin-right: 1ch; + color: fade(@text-color, 50%); + vertical-align: top; + } + + &-lineContent { + display: table-cell; + padding: 0 .5em 0 3ch; // indent 3 characters + text-indent: -2ch; // remove indentation for the +/- + white-space: pre-wrap; + word-break: break-word; + width: 100%; + vertical-align: top; + } + + &-lineText { + display: inline-block; + text-indent: 0; + } +} + + +// +// States +// ------------------------------- + +.github-HunkView.is-selected.is-hunkMode .github-HunkView-header { + background-color: @background-color-selected; + .github-HunkView-title { + color: @text-color; + } + .github-HunkView-stageButton, .github-HunkView-discardButton { + border-color: mix(@text-color, @background-color-selected, 25%); + } +} + +.github-HunkView-title:hover { + color: @text-color-highlight; +} + +.github-HunkView-line { + + // mixin + .hunk-line-mixin(@fg; @bg) { + &:hover { + background-color: @background-color-highlight; + } + &.is-selected { + color: @text-color; + background-color: @background-color-selected; + } + .github-HunkView-lineContent { + color: saturate( mix(@fg, @text-color-highlight, 20%), 20%); + background-color: saturate( mix(@bg, @hunk-bg-color, 15%), 20%); + } + // hightlight when focused + selected + .github-FilePatchView:focus &.is-selected .github-HunkView-lineContent { + color: saturate( mix(@fg, @text-color-highlight, 10%), 10%); + background-color: saturate( mix(@bg, @hunk-bg-color, 25%), 10%); + } + } + + &.is-deleted { + .hunk-line-mixin(@text-color-error, @background-color-error); + } + + &.is-added { + .hunk-line-mixin(@text-color-success, @background-color-success); + } + + // divider line between added and deleted lines + &.is-deleted + .is-added .github-HunkView-lineContent { + box-shadow: 0 -1px 0 hsla(0,0%,50%,.1); + } + +} + +// focus colors +.github-FilePatchView:focus { + .github-HunkView.is-selected.is-hunkMode .github-HunkView-title, + .github-HunkView.is-selected.is-hunkMode .github-HunkView-header, + .github-HunkView-line.is-selected .github-HunkView-lineNumber { + color: contrast(@button-background-color-selected); + background: @button-background-color-selected; + } + .github-HunkView-line.is-selected .github-HunkView-lineNumber { + border-color: mix(@button-border-color, @button-background-color-selected, 25%); + } + .github-HunkView.is-selected.is-hunkMode .github-HunkView { + &-stageButton, + &-discardButton { + border-color: mix(@hunk-bg-color, @button-background-color-selected, 30%); + &:hover { background-color: mix(@hunk-bg-color, @button-background-color-selected, 10%); } + &:active { background-color: @button-background-color-selected; } + } + } +} diff --git a/styles/hunk-view.less b/styles/hunk-view.old.less similarity index 100% rename from styles/hunk-view.less rename to styles/hunk-view.old.less From 02e9b05de6dcd4e6022a995571986374e0b9017e Mon Sep 17 00:00:00 2001 From: simurai Date: Fri, 20 Jul 2018 15:13:43 +0900 Subject: [PATCH 0025/4252] Remove (preview) from tab title --- lib/controllers/github-tab-controller.js | 2 +- lib/github-package.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/controllers/github-tab-controller.js b/lib/controllers/github-tab-controller.js index 0d6aeaad75..fa8c56e47c 100644 --- a/lib/controllers/github-tab-controller.js +++ b/lib/controllers/github-tab-controller.js @@ -136,7 +136,7 @@ export default class GithubTabController extends React.Component { } getTitle() { - return 'GitHub (preview)'; + return 'GitHub'; } getIconName() { diff --git a/lib/github-package.js b/lib/github-package.js index b881cd55f8..3d30f90121 100644 --- a/lib/github-package.js +++ b/lib/github-package.js @@ -363,7 +363,7 @@ export default class GithubPackage { createGithubTabControllerStub(uri) { return StubItem.create('github-tab-controller', { - title: 'GitHub (preview)', + title: 'GitHub', }, uri); } From e763bc5e681610a332d1ce937f573d9d5d745a7d Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 20 Jul 2018 17:59:17 -0400 Subject: [PATCH 0026/4252] package-lock updates --- package-lock.json | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7af141b276..210339e328 100644 --- a/package-lock.json +++ b/package-lock.json @@ -345,7 +345,7 @@ "dev": true, "requires": { "ast-types-flow": "0.0.7", - "commander": "2.16.0" + "commander": "^2.11.0" }, "dependencies": { "commander": { @@ -380,8 +380,8 @@ "integrity": "sha1-GEtI9i2S10UrsxsyMWXH+L0CJm0=", "dev": true, "requires": { - "define-properties": "1.1.2", - "es-abstract": "1.11.0" + "define-properties": "^1.1.2", + "es-abstract": "^1.7.0" } }, "array-union": { @@ -2782,14 +2782,14 @@ "integrity": "sha512-JsxNKqa3TwmPypeXNnI75FntkUktGzI1wSa1LgNZdSOMI+B4sxnr1lSF8m8lPiz4mKiC+14ysZQM4scewUrP7A==", "dev": true, "requires": { - "aria-query": "3.0.0", - "array-includes": "3.0.3", - "ast-types-flow": "0.0.7", - "axobject-query": "2.0.1", - "damerau-levenshtein": "1.0.4", - "emoji-regex": "6.5.1", - "has": "1.0.3", - "jsx-ast-utils": "2.0.1" + "aria-query": "^3.0.0", + "array-includes": "^3.0.3", + "ast-types-flow": "^0.0.7", + "axobject-query": "^2.0.1", + "damerau-levenshtein": "^1.0.4", + "emoji-regex": "^6.5.1", + "has": "^1.0.3", + "jsx-ast-utils": "^2.0.1" }, "dependencies": { "has": { @@ -2798,7 +2798,7 @@ "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", "dev": true, "requires": { - "function-bind": "1.1.1" + "function-bind": "^1.1.1" } }, "jsx-ast-utils": { @@ -2807,7 +2807,7 @@ "integrity": "sha1-6AGxs5mF4g//yHtA43SAgOLcrH8=", "dev": true, "requires": { - "array-includes": "3.0.3" + "array-includes": "^3.0.3" } } } From 8d598c20eb2df7af3e674ba335c78f56556bd5f6 Mon Sep 17 00:00:00 2001 From: simurai Date: Mon, 23 Jul 2018 12:05:03 +0900 Subject: [PATCH 0027/4252] Add PR meta info Commits, changed files and branch names --- .../issueishDetailContainerQuery.graphql.js | 13 +- .../issueishDetailViewRefetchQuery.graphql.js | 115 ++++++++++-------- .../issueishDetailView_issueish.graphql.js | 18 ++- lib/views/issueish-detail-view.js | 14 ++- styles/issueish-detail-view.less | 14 ++- 5 files changed, 116 insertions(+), 58 deletions(-) diff --git a/lib/containers/__generated__/issueishDetailContainerQuery.graphql.js b/lib/containers/__generated__/issueishDetailContainerQuery.graphql.js index 1af06e1212..ed618f16d9 100644 --- a/lib/containers/__generated__/issueishDetailContainerQuery.graphql.js +++ b/lib/containers/__generated__/issueishDetailContainerQuery.graphql.js @@ -1,6 +1,6 @@ /** * @flow - * @relayHash fd2fb50c17cd6fc965d556a7247314a9 + * @relayHash b7439356a73154992a0173f7672eb97d */ /* eslint-disable */ @@ -119,6 +119,8 @@ fragment issueishDetailView_issueish_3D8CP9 on IssueOrPullRequest { number title bodyHTML + baseRefName + headRefName author { __typename login @@ -920,7 +922,7 @@ return { "operationKind": "query", "name": "issueishDetailContainerQuery", "id": null, - "text": "query issueishDetailContainerQuery(\n $repoOwner: String!\n $repoName: String!\n $issueishNumber: Int!\n $timelineCount: Int!\n $timelineCursor: String\n) {\n repository(owner: $repoOwner, name: $repoName) {\n ...issueishDetailController_repository_n0A9R\n id\n }\n}\n\nfragment issueishDetailController_repository_n0A9R on Repository {\n ...issueishDetailView_repository\n name\n owner {\n __typename\n login\n id\n }\n issueish: issueOrPullRequest(number: $issueishNumber) {\n __typename\n ... on Issue {\n title\n number\n ...issueishDetailView_issueish_3D8CP9\n }\n ... on PullRequest {\n title\n number\n headRefName\n headRepository {\n name\n owner {\n __typename\n login\n id\n }\n url\n sshUrl\n id\n }\n ...issueishDetailView_issueish_3D8CP9\n }\n ... on Node {\n id\n }\n }\n}\n\nfragment issueishDetailView_repository on Repository {\n id\n name\n owner {\n __typename\n login\n id\n }\n}\n\nfragment issueishDetailView_issueish_3D8CP9 on IssueOrPullRequest {\n __typename\n ... on Node {\n id\n }\n ... on Issue {\n state\n number\n title\n bodyHTML\n author {\n __typename\n login\n avatarUrl\n ... on User {\n url\n }\n ... on Bot {\n url\n }\n ... on Node {\n id\n }\n }\n ...issueTimelineController_issue_3D8CP9\n }\n ... on PullRequest {\n ...prStatusesView_pullRequest\n state\n number\n title\n bodyHTML\n author {\n __typename\n login\n avatarUrl\n ... on User {\n url\n }\n ... on Bot {\n url\n }\n ... on Node {\n id\n }\n }\n ...prTimelineController_pullRequest_3D8CP9\n }\n ... on UniformResourceLocatable {\n url\n }\n ... on Reactable {\n reactionGroups {\n content\n users {\n totalCount\n }\n }\n }\n}\n\nfragment issueTimelineController_issue_3D8CP9 on Issue {\n url\n timeline(first: $timelineCount, after: $timelineCursor) {\n pageInfo {\n endCursor\n hasNextPage\n }\n edges {\n cursor\n node {\n __typename\n ...commitsView_nodes\n ...issueCommentView_item\n ...crossReferencedEventsView_nodes\n ... on Node {\n id\n }\n }\n }\n }\n}\n\nfragment prStatusesView_pullRequest on PullRequest {\n id\n commits(last: 1) {\n edges {\n node {\n commit {\n status {\n state\n contexts {\n id\n state\n ...prStatusContextView_context\n }\n id\n }\n id\n }\n id\n }\n }\n }\n}\n\nfragment prTimelineController_pullRequest_3D8CP9 on PullRequest {\n url\n ...headRefForcePushedEventView_issueish\n timeline(first: $timelineCount, after: $timelineCursor) {\n pageInfo {\n endCursor\n hasNextPage\n }\n edges {\n cursor\n node {\n __typename\n ...commitsView_nodes\n ...issueCommentView_item\n ...mergedEventView_item\n ...headRefForcePushedEventView_item\n ...commitCommentThreadView_item\n ...crossReferencedEventsView_nodes\n ... on Node {\n id\n }\n }\n }\n }\n}\n\nfragment headRefForcePushedEventView_issueish on PullRequest {\n headRefName\n headRepositoryOwner {\n __typename\n login\n id\n }\n repository {\n owner {\n __typename\n login\n id\n }\n id\n }\n}\n\nfragment commitsView_nodes on Commit {\n id\n author {\n name\n user {\n login\n id\n }\n }\n ...commitView_item\n}\n\nfragment issueCommentView_item on IssueComment {\n author {\n __typename\n avatarUrl\n login\n ... on Node {\n id\n }\n }\n bodyHTML\n createdAt\n url\n}\n\nfragment mergedEventView_item on MergedEvent {\n actor {\n __typename\n avatarUrl\n login\n ... on Node {\n id\n }\n }\n commit {\n oid\n id\n }\n mergeRefName\n createdAt\n}\n\nfragment headRefForcePushedEventView_item on HeadRefForcePushedEvent {\n actor {\n __typename\n avatarUrl\n login\n ... on Node {\n id\n }\n }\n beforeCommit {\n oid\n id\n }\n afterCommit {\n oid\n id\n }\n createdAt\n}\n\nfragment commitCommentThreadView_item on CommitCommentThread {\n commit {\n oid\n id\n }\n comments(first: 100) {\n edges {\n node {\n id\n ...commitCommentView_item\n }\n }\n }\n}\n\nfragment crossReferencedEventsView_nodes on CrossReferencedEvent {\n id\n referencedAt\n isCrossRepository\n actor {\n __typename\n login\n avatarUrl\n ... on Node {\n id\n }\n }\n source {\n __typename\n ... on RepositoryNode {\n repository {\n name\n owner {\n __typename\n login\n id\n }\n id\n }\n }\n ... on Node {\n id\n }\n }\n ...crossReferencedEventView_item\n}\n\nfragment crossReferencedEventView_item on CrossReferencedEvent {\n id\n isCrossRepository\n source {\n __typename\n ... on Issue {\n number\n title\n url\n issueState: state\n }\n ... on PullRequest {\n number\n title\n url\n prState: state\n }\n ... on RepositoryNode {\n repository {\n name\n isPrivate\n owner {\n __typename\n login\n id\n }\n id\n }\n }\n ... on Node {\n id\n }\n }\n}\n\nfragment commitCommentView_item on CommitComment {\n author {\n __typename\n login\n avatarUrl\n ... on Node {\n id\n }\n }\n commit {\n oid\n id\n }\n bodyHTML\n createdAt\n path\n position\n}\n\nfragment commitView_item on Commit {\n author {\n name\n avatarUrl\n user {\n login\n id\n }\n }\n committer {\n name\n avatarUrl\n user {\n login\n id\n }\n }\n authoredByCommitter\n oid\n message\n messageHeadlineHTML\n commitUrl\n}\n\nfragment prStatusContextView_context on StatusContext {\n context\n description\n state\n targetUrl\n}\n", + "text": "query issueishDetailContainerQuery(\n $repoOwner: String!\n $repoName: String!\n $issueishNumber: Int!\n $timelineCount: Int!\n $timelineCursor: String\n) {\n repository(owner: $repoOwner, name: $repoName) {\n ...issueishDetailController_repository_n0A9R\n id\n }\n}\n\nfragment issueishDetailController_repository_n0A9R on Repository {\n ...issueishDetailView_repository\n name\n owner {\n __typename\n login\n id\n }\n issueish: issueOrPullRequest(number: $issueishNumber) {\n __typename\n ... on Issue {\n title\n number\n ...issueishDetailView_issueish_3D8CP9\n }\n ... on PullRequest {\n title\n number\n headRefName\n headRepository {\n name\n owner {\n __typename\n login\n id\n }\n url\n sshUrl\n id\n }\n ...issueishDetailView_issueish_3D8CP9\n }\n ... on Node {\n id\n }\n }\n}\n\nfragment issueishDetailView_repository on Repository {\n id\n name\n owner {\n __typename\n login\n id\n }\n}\n\nfragment issueishDetailView_issueish_3D8CP9 on IssueOrPullRequest {\n __typename\n ... on Node {\n id\n }\n ... on Issue {\n state\n number\n title\n bodyHTML\n author {\n __typename\n login\n avatarUrl\n ... on User {\n url\n }\n ... on Bot {\n url\n }\n ... on Node {\n id\n }\n }\n ...issueTimelineController_issue_3D8CP9\n }\n ... on PullRequest {\n ...prStatusesView_pullRequest\n state\n number\n title\n bodyHTML\n baseRefName\n headRefName\n author {\n __typename\n login\n avatarUrl\n ... on User {\n url\n }\n ... on Bot {\n url\n }\n ... on Node {\n id\n }\n }\n ...prTimelineController_pullRequest_3D8CP9\n }\n ... on UniformResourceLocatable {\n url\n }\n ... on Reactable {\n reactionGroups {\n content\n users {\n totalCount\n }\n }\n }\n}\n\nfragment issueTimelineController_issue_3D8CP9 on Issue {\n url\n timeline(first: $timelineCount, after: $timelineCursor) {\n pageInfo {\n endCursor\n hasNextPage\n }\n edges {\n cursor\n node {\n __typename\n ...commitsView_nodes\n ...issueCommentView_item\n ...crossReferencedEventsView_nodes\n ... on Node {\n id\n }\n }\n }\n }\n}\n\nfragment prStatusesView_pullRequest on PullRequest {\n id\n commits(last: 1) {\n edges {\n node {\n commit {\n status {\n state\n contexts {\n id\n state\n ...prStatusContextView_context\n }\n id\n }\n id\n }\n id\n }\n }\n }\n}\n\nfragment prTimelineController_pullRequest_3D8CP9 on PullRequest {\n url\n ...headRefForcePushedEventView_issueish\n timeline(first: $timelineCount, after: $timelineCursor) {\n pageInfo {\n endCursor\n hasNextPage\n }\n edges {\n cursor\n node {\n __typename\n ...commitsView_nodes\n ...issueCommentView_item\n ...mergedEventView_item\n ...headRefForcePushedEventView_item\n ...commitCommentThreadView_item\n ...crossReferencedEventsView_nodes\n ... on Node {\n id\n }\n }\n }\n }\n}\n\nfragment headRefForcePushedEventView_issueish on PullRequest {\n headRefName\n headRepositoryOwner {\n __typename\n login\n id\n }\n repository {\n owner {\n __typename\n login\n id\n }\n id\n }\n}\n\nfragment commitsView_nodes on Commit {\n id\n author {\n name\n user {\n login\n id\n }\n }\n ...commitView_item\n}\n\nfragment issueCommentView_item on IssueComment {\n author {\n __typename\n avatarUrl\n login\n ... on Node {\n id\n }\n }\n bodyHTML\n createdAt\n url\n}\n\nfragment mergedEventView_item on MergedEvent {\n actor {\n __typename\n avatarUrl\n login\n ... on Node {\n id\n }\n }\n commit {\n oid\n id\n }\n mergeRefName\n createdAt\n}\n\nfragment headRefForcePushedEventView_item on HeadRefForcePushedEvent {\n actor {\n __typename\n avatarUrl\n login\n ... on Node {\n id\n }\n }\n beforeCommit {\n oid\n id\n }\n afterCommit {\n oid\n id\n }\n createdAt\n}\n\nfragment commitCommentThreadView_item on CommitCommentThread {\n commit {\n oid\n id\n }\n comments(first: 100) {\n edges {\n node {\n id\n ...commitCommentView_item\n }\n }\n }\n}\n\nfragment crossReferencedEventsView_nodes on CrossReferencedEvent {\n id\n referencedAt\n isCrossRepository\n actor {\n __typename\n login\n avatarUrl\n ... on Node {\n id\n }\n }\n source {\n __typename\n ... on RepositoryNode {\n repository {\n name\n owner {\n __typename\n login\n id\n }\n id\n }\n }\n ... on Node {\n id\n }\n }\n ...crossReferencedEventView_item\n}\n\nfragment crossReferencedEventView_item on CrossReferencedEvent {\n id\n isCrossRepository\n source {\n __typename\n ... on Issue {\n number\n title\n url\n issueState: state\n }\n ... on PullRequest {\n number\n title\n url\n prState: state\n }\n ... on RepositoryNode {\n repository {\n name\n isPrivate\n owner {\n __typename\n login\n id\n }\n id\n }\n }\n ... on Node {\n id\n }\n }\n}\n\nfragment commitCommentView_item on CommitComment {\n author {\n __typename\n login\n avatarUrl\n ... on Node {\n id\n }\n }\n commit {\n oid\n id\n }\n bodyHTML\n createdAt\n path\n position\n}\n\nfragment commitView_item on Commit {\n author {\n name\n avatarUrl\n user {\n login\n id\n }\n }\n committer {\n name\n avatarUrl\n user {\n login\n id\n }\n }\n authoredByCommitter\n oid\n message\n messageHeadlineHTML\n commitUrl\n}\n\nfragment prStatusContextView_context on StatusContext {\n context\n description\n state\n targetUrl\n}\n", "metadata": {}, "fragment": { "kind": "Fragment", @@ -1306,6 +1308,13 @@ return { }, v16, v21, + { + "kind": "ScalarField", + "alias": null, + "name": "baseRefName", + "args": null, + "storageKey": null + }, v29, v10, { diff --git a/lib/views/__generated__/issueishDetailViewRefetchQuery.graphql.js b/lib/views/__generated__/issueishDetailViewRefetchQuery.graphql.js index 212b2badeb..a623ce0f77 100644 --- a/lib/views/__generated__/issueishDetailViewRefetchQuery.graphql.js +++ b/lib/views/__generated__/issueishDetailViewRefetchQuery.graphql.js @@ -1,6 +1,6 @@ /** * @flow - * @relayHash e59fe378fd0cacef7e695703a6cb0022 + * @relayHash 6b0733755f9c894ca3a872a48956d78f */ /* eslint-disable */ @@ -89,6 +89,8 @@ fragment issueishDetailView_issueish_3D8CP9 on IssueOrPullRequest { number title bodyHTML + baseRefName + headRefName author { __typename login @@ -510,16 +512,44 @@ v10 = { "storageKey": null }, v11 = { + "kind": "ScalarField", + "alias": null, + "name": "state", + "args": null, + "storageKey": null +}, +v12 = { + "kind": "ScalarField", + "alias": null, + "name": "number", + "args": null, + "storageKey": null +}, +v13 = { + "kind": "ScalarField", + "alias": null, + "name": "title", + "args": null, + "storageKey": null +}, +v14 = { + "kind": "ScalarField", + "alias": null, + "name": "bodyHTML", + "args": null, + "storageKey": null +}, +v15 = { "kind": "ScalarField", "alias": null, "name": "avatarUrl", "args": null, "storageKey": null }, -v12 = [ +v16 = [ v10 ], -v13 = { +v17 = { "kind": "LinkedField", "alias": null, "name": "author", @@ -530,48 +560,20 @@ v13 = { "selections": [ v4, v7, - v11, + v15, v5, { "kind": "InlineFragment", "type": "Bot", - "selections": v12 + "selections": v16 }, { "kind": "InlineFragment", "type": "User", - "selections": v12 + "selections": v16 } ] }, -v14 = { - "kind": "ScalarField", - "alias": null, - "name": "state", - "args": null, - "storageKey": null -}, -v15 = { - "kind": "ScalarField", - "alias": null, - "name": "number", - "args": null, - "storageKey": null -}, -v16 = { - "kind": "ScalarField", - "alias": null, - "name": "title", - "args": null, - "storageKey": null -}, -v17 = { - "kind": "ScalarField", - "alias": null, - "name": "bodyHTML", - "args": null, - "storageKey": null -}, v18 = [ { "kind": "Variable", @@ -621,7 +623,7 @@ v20 = { v21 = [ v4, v7, - v11, + v15, v5 ], v22 = { @@ -688,8 +690,8 @@ v22 = { "kind": "InlineFragment", "type": "PullRequest", "selections": [ - v15, - v16, + v12, + v13, v10, { "kind": "ScalarField", @@ -704,8 +706,8 @@ v22 = { "kind": "InlineFragment", "type": "Issue", "selections": [ - v15, - v16, + v12, + v13, v10, { "kind": "ScalarField", @@ -750,7 +752,7 @@ v26 = { }, v27 = [ v4, - v11, + v15, v7, v5 ], @@ -778,7 +780,7 @@ v29 = { "plural": false, "selections": v27 }, - v17, + v14, v26, v10 ] @@ -811,7 +813,7 @@ v31 = { "selections": [ v6, v30, - v11 + v15 ] }, { @@ -824,7 +826,7 @@ v31 = { "plural": false, "selections": [ v6, - v11, + v15, v30 ] }, @@ -864,7 +866,7 @@ return { "operationKind": "query", "name": "issueishDetailViewRefetchQuery", "id": null, - "text": "query issueishDetailViewRefetchQuery(\n $repoId: ID!\n $issueishId: ID!\n $timelineCount: Int!\n $timelineCursor: String\n) {\n repository: node(id: $repoId) {\n __typename\n ...issueishDetailView_repository_3D8CP9\n id\n }\n issueish: node(id: $issueishId) {\n __typename\n ...issueishDetailView_issueish_3D8CP9\n id\n }\n}\n\nfragment issueishDetailView_repository_3D8CP9 on Repository {\n id\n name\n owner {\n __typename\n login\n id\n }\n}\n\nfragment issueishDetailView_issueish_3D8CP9 on IssueOrPullRequest {\n __typename\n ... on Node {\n id\n }\n ... on Issue {\n state\n number\n title\n bodyHTML\n author {\n __typename\n login\n avatarUrl\n ... on User {\n url\n }\n ... on Bot {\n url\n }\n ... on Node {\n id\n }\n }\n ...issueTimelineController_issue_3D8CP9\n }\n ... on PullRequest {\n ...prStatusesView_pullRequest\n state\n number\n title\n bodyHTML\n author {\n __typename\n login\n avatarUrl\n ... on User {\n url\n }\n ... on Bot {\n url\n }\n ... on Node {\n id\n }\n }\n ...prTimelineController_pullRequest_3D8CP9\n }\n ... on UniformResourceLocatable {\n url\n }\n ... on Reactable {\n reactionGroups {\n content\n users {\n totalCount\n }\n }\n }\n}\n\nfragment issueTimelineController_issue_3D8CP9 on Issue {\n url\n timeline(first: $timelineCount, after: $timelineCursor) {\n pageInfo {\n endCursor\n hasNextPage\n }\n edges {\n cursor\n node {\n __typename\n ...commitsView_nodes\n ...issueCommentView_item\n ...crossReferencedEventsView_nodes\n ... on Node {\n id\n }\n }\n }\n }\n}\n\nfragment prStatusesView_pullRequest on PullRequest {\n id\n commits(last: 1) {\n edges {\n node {\n commit {\n status {\n state\n contexts {\n id\n state\n ...prStatusContextView_context\n }\n id\n }\n id\n }\n id\n }\n }\n }\n}\n\nfragment prTimelineController_pullRequest_3D8CP9 on PullRequest {\n url\n ...headRefForcePushedEventView_issueish\n timeline(first: $timelineCount, after: $timelineCursor) {\n pageInfo {\n endCursor\n hasNextPage\n }\n edges {\n cursor\n node {\n __typename\n ...commitsView_nodes\n ...issueCommentView_item\n ...mergedEventView_item\n ...headRefForcePushedEventView_item\n ...commitCommentThreadView_item\n ...crossReferencedEventsView_nodes\n ... on Node {\n id\n }\n }\n }\n }\n}\n\nfragment headRefForcePushedEventView_issueish on PullRequest {\n headRefName\n headRepositoryOwner {\n __typename\n login\n id\n }\n repository {\n owner {\n __typename\n login\n id\n }\n id\n }\n}\n\nfragment commitsView_nodes on Commit {\n id\n author {\n name\n user {\n login\n id\n }\n }\n ...commitView_item\n}\n\nfragment issueCommentView_item on IssueComment {\n author {\n __typename\n avatarUrl\n login\n ... on Node {\n id\n }\n }\n bodyHTML\n createdAt\n url\n}\n\nfragment mergedEventView_item on MergedEvent {\n actor {\n __typename\n avatarUrl\n login\n ... on Node {\n id\n }\n }\n commit {\n oid\n id\n }\n mergeRefName\n createdAt\n}\n\nfragment headRefForcePushedEventView_item on HeadRefForcePushedEvent {\n actor {\n __typename\n avatarUrl\n login\n ... on Node {\n id\n }\n }\n beforeCommit {\n oid\n id\n }\n afterCommit {\n oid\n id\n }\n createdAt\n}\n\nfragment commitCommentThreadView_item on CommitCommentThread {\n commit {\n oid\n id\n }\n comments(first: 100) {\n edges {\n node {\n id\n ...commitCommentView_item\n }\n }\n }\n}\n\nfragment crossReferencedEventsView_nodes on CrossReferencedEvent {\n id\n referencedAt\n isCrossRepository\n actor {\n __typename\n login\n avatarUrl\n ... on Node {\n id\n }\n }\n source {\n __typename\n ... on RepositoryNode {\n repository {\n name\n owner {\n __typename\n login\n id\n }\n id\n }\n }\n ... on Node {\n id\n }\n }\n ...crossReferencedEventView_item\n}\n\nfragment crossReferencedEventView_item on CrossReferencedEvent {\n id\n isCrossRepository\n source {\n __typename\n ... on Issue {\n number\n title\n url\n issueState: state\n }\n ... on PullRequest {\n number\n title\n url\n prState: state\n }\n ... on RepositoryNode {\n repository {\n name\n isPrivate\n owner {\n __typename\n login\n id\n }\n id\n }\n }\n ... on Node {\n id\n }\n }\n}\n\nfragment commitCommentView_item on CommitComment {\n author {\n __typename\n login\n avatarUrl\n ... on Node {\n id\n }\n }\n commit {\n oid\n id\n }\n bodyHTML\n createdAt\n path\n position\n}\n\nfragment commitView_item on Commit {\n author {\n name\n avatarUrl\n user {\n login\n id\n }\n }\n committer {\n name\n avatarUrl\n user {\n login\n id\n }\n }\n authoredByCommitter\n oid\n message\n messageHeadlineHTML\n commitUrl\n}\n\nfragment prStatusContextView_context on StatusContext {\n context\n description\n state\n targetUrl\n}\n", + "text": "query issueishDetailViewRefetchQuery(\n $repoId: ID!\n $issueishId: ID!\n $timelineCount: Int!\n $timelineCursor: String\n) {\n repository: node(id: $repoId) {\n __typename\n ...issueishDetailView_repository_3D8CP9\n id\n }\n issueish: node(id: $issueishId) {\n __typename\n ...issueishDetailView_issueish_3D8CP9\n id\n }\n}\n\nfragment issueishDetailView_repository_3D8CP9 on Repository {\n id\n name\n owner {\n __typename\n login\n id\n }\n}\n\nfragment issueishDetailView_issueish_3D8CP9 on IssueOrPullRequest {\n __typename\n ... on Node {\n id\n }\n ... on Issue {\n state\n number\n title\n bodyHTML\n author {\n __typename\n login\n avatarUrl\n ... on User {\n url\n }\n ... on Bot {\n url\n }\n ... on Node {\n id\n }\n }\n ...issueTimelineController_issue_3D8CP9\n }\n ... on PullRequest {\n ...prStatusesView_pullRequest\n state\n number\n title\n bodyHTML\n baseRefName\n headRefName\n author {\n __typename\n login\n avatarUrl\n ... on User {\n url\n }\n ... on Bot {\n url\n }\n ... on Node {\n id\n }\n }\n ...prTimelineController_pullRequest_3D8CP9\n }\n ... on UniformResourceLocatable {\n url\n }\n ... on Reactable {\n reactionGroups {\n content\n users {\n totalCount\n }\n }\n }\n}\n\nfragment issueTimelineController_issue_3D8CP9 on Issue {\n url\n timeline(first: $timelineCount, after: $timelineCursor) {\n pageInfo {\n endCursor\n hasNextPage\n }\n edges {\n cursor\n node {\n __typename\n ...commitsView_nodes\n ...issueCommentView_item\n ...crossReferencedEventsView_nodes\n ... on Node {\n id\n }\n }\n }\n }\n}\n\nfragment prStatusesView_pullRequest on PullRequest {\n id\n commits(last: 1) {\n edges {\n node {\n commit {\n status {\n state\n contexts {\n id\n state\n ...prStatusContextView_context\n }\n id\n }\n id\n }\n id\n }\n }\n }\n}\n\nfragment prTimelineController_pullRequest_3D8CP9 on PullRequest {\n url\n ...headRefForcePushedEventView_issueish\n timeline(first: $timelineCount, after: $timelineCursor) {\n pageInfo {\n endCursor\n hasNextPage\n }\n edges {\n cursor\n node {\n __typename\n ...commitsView_nodes\n ...issueCommentView_item\n ...mergedEventView_item\n ...headRefForcePushedEventView_item\n ...commitCommentThreadView_item\n ...crossReferencedEventsView_nodes\n ... on Node {\n id\n }\n }\n }\n }\n}\n\nfragment headRefForcePushedEventView_issueish on PullRequest {\n headRefName\n headRepositoryOwner {\n __typename\n login\n id\n }\n repository {\n owner {\n __typename\n login\n id\n }\n id\n }\n}\n\nfragment commitsView_nodes on Commit {\n id\n author {\n name\n user {\n login\n id\n }\n }\n ...commitView_item\n}\n\nfragment issueCommentView_item on IssueComment {\n author {\n __typename\n avatarUrl\n login\n ... on Node {\n id\n }\n }\n bodyHTML\n createdAt\n url\n}\n\nfragment mergedEventView_item on MergedEvent {\n actor {\n __typename\n avatarUrl\n login\n ... on Node {\n id\n }\n }\n commit {\n oid\n id\n }\n mergeRefName\n createdAt\n}\n\nfragment headRefForcePushedEventView_item on HeadRefForcePushedEvent {\n actor {\n __typename\n avatarUrl\n login\n ... on Node {\n id\n }\n }\n beforeCommit {\n oid\n id\n }\n afterCommit {\n oid\n id\n }\n createdAt\n}\n\nfragment commitCommentThreadView_item on CommitCommentThread {\n commit {\n oid\n id\n }\n comments(first: 100) {\n edges {\n node {\n id\n ...commitCommentView_item\n }\n }\n }\n}\n\nfragment crossReferencedEventsView_nodes on CrossReferencedEvent {\n id\n referencedAt\n isCrossRepository\n actor {\n __typename\n login\n avatarUrl\n ... on Node {\n id\n }\n }\n source {\n __typename\n ... on RepositoryNode {\n repository {\n name\n owner {\n __typename\n login\n id\n }\n id\n }\n }\n ... on Node {\n id\n }\n }\n ...crossReferencedEventView_item\n}\n\nfragment crossReferencedEventView_item on CrossReferencedEvent {\n id\n isCrossRepository\n source {\n __typename\n ... on Issue {\n number\n title\n url\n issueState: state\n }\n ... on PullRequest {\n number\n title\n url\n prState: state\n }\n ... on RepositoryNode {\n repository {\n name\n isPrivate\n owner {\n __typename\n login\n id\n }\n id\n }\n }\n ... on Node {\n id\n }\n }\n}\n\nfragment commitCommentView_item on CommitComment {\n author {\n __typename\n login\n avatarUrl\n ... on Node {\n id\n }\n }\n commit {\n oid\n id\n }\n bodyHTML\n createdAt\n path\n position\n}\n\nfragment commitView_item on Commit {\n author {\n name\n avatarUrl\n user {\n login\n id\n }\n }\n committer {\n name\n avatarUrl\n user {\n login\n id\n }\n }\n authoredByCommitter\n oid\n message\n messageHeadlineHTML\n commitUrl\n}\n\nfragment prStatusContextView_context on StatusContext {\n context\n description\n state\n targetUrl\n}\n", "metadata": {}, "fragment": { "kind": "Fragment", @@ -985,11 +987,17 @@ return { "kind": "InlineFragment", "type": "PullRequest", "selections": [ + { + "kind": "ScalarField", + "alias": null, + "name": "baseRefName", + "args": null, + "storageKey": null + }, + v11, + v12, v13, v14, - v15, - v16, - v17, { "kind": "LinkedField", "alias": null, @@ -1042,7 +1050,7 @@ return { "concreteType": "Status", "plural": false, "selections": [ - v14, + v11, { "kind": "LinkedField", "alias": null, @@ -1053,7 +1061,7 @@ return { "plural": true, "selections": [ v5, - v14, + v11, { "kind": "ScalarField", "alias": null, @@ -1097,6 +1105,7 @@ return { "args": null, "storageKey": null }, + v17, { "kind": "LinkedField", "alias": null, @@ -1203,7 +1212,7 @@ return { "selections": v21 }, v25, - v17, + v14, v26, { "kind": "ScalarField", @@ -1294,11 +1303,11 @@ return { "kind": "InlineFragment", "type": "Issue", "selections": [ + v11, + v12, + v13, v14, - v15, - v16, v17, - v13, { "kind": "LinkedField", "alias": null, diff --git a/lib/views/__generated__/issueishDetailView_issueish.graphql.js b/lib/views/__generated__/issueishDetailView_issueish.graphql.js index 6872a99a9e..a28361de4a 100644 --- a/lib/views/__generated__/issueishDetailView_issueish.graphql.js +++ b/lib/views/__generated__/issueishDetailView_issueish.graphql.js @@ -35,6 +35,8 @@ export type issueishDetailView_issueish = {| +avatarUrl: any, +url?: any, |}, + +baseRefName?: string, + +headRefName?: string, +$fragmentRefs: issueTimelineController_issue$ref & prStatusesView_pullRequest$ref & prTimelineController_pullRequest$ref, +$refType: issueishDetailView_issueish$ref, |}; @@ -213,6 +215,20 @@ return { v2, v3, v4, + { + "kind": "ScalarField", + "alias": null, + "name": "baseRefName", + "args": null, + "storageKey": null + }, + { + "kind": "ScalarField", + "alias": null, + "name": "headRefName", + "args": null, + "storageKey": null + }, v6, { "kind": "FragmentSpread", @@ -241,5 +257,5 @@ return { }; })(); // prettier-ignore -(node/*: any*/).hash = '5b78b0edbca6e2259feed1ebbed3bfcf'; +(node/*: any*/).hash = '66ea55633a5c3dad06211d4a83154181'; module.exports = node; diff --git a/lib/views/issueish-detail-view.js b/lib/views/issueish-detail-view.js index b76f373d9c..55007ac8e5 100644 --- a/lib/views/issueish-detail-view.js +++ b/lib/views/issueish-detail-view.js @@ -130,6 +130,16 @@ export class BareIssueishDetailView extends React.Component { />
+ {isPr &&
+ + {issueish.author.login} wants to merge{' '} + 2 commits and{' '} + 3 changed files into{' '} + {issueish.baseRefName} from{' '} + {issueish.headRefName} + +
}
+ + {isPr &&
} @@ -286,7 +298,7 @@ export default createRefetchContainer(BareIssueishDetailView, { ... on PullRequest { ...prStatusesView_pullRequest - state number title bodyHTML + state number title bodyHTML baseRefName headRefName author { login avatarUrl ... on User { url } diff --git a/styles/issueish-detail-view.less b/styles/issueish-detail-view.less index 7e57da13f4..3330d97104 100644 --- a/styles/issueish-detail-view.less +++ b/styles/issueish-detail-view.less @@ -123,6 +123,18 @@ } + // Meta ------------------------ + + &-meta { + font-size: .95em; + color: @text-color-subtle; + + &Author { + color: inherit; + } + } + + // Build Status ------------------------ &-buildStatus { @@ -141,7 +153,7 @@ } - // Build Status ------------------------ + // Footer ------------------------ &-footer { margin-top: @component-padding * 3; From ee38d14098096868c2cda9e75449b0be570e66d3 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 24 Jul 2018 12:49:58 -0400 Subject: [PATCH 0028/4252] Use a Gutter component as a bridge to the addGutter API --- lib/atom/gutter.js | 110 +++++++++++++++++++++++++++++++++++++++ test/atom/gutter.test.js | 84 ++++++++++++++++++++++++++++++ 2 files changed, 194 insertions(+) create mode 100644 lib/atom/gutter.js create mode 100644 test/atom/gutter.test.js diff --git a/lib/atom/gutter.js b/lib/atom/gutter.js new file mode 100644 index 0000000000..34a29873cc --- /dev/null +++ b/lib/atom/gutter.js @@ -0,0 +1,110 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import {Disposable} from 'event-kit'; + +import {autobind, extractProps} from '../helpers'; +import {RefHolderPropType} from '../prop-types'; +import {TextEditorContext} from './atom-text-editor'; +import RefHolder from '../models/ref-holder'; + +const gutterProps = { + name: PropTypes.string.isRequired, + priority: PropTypes.number.isRequired, + visible: PropTypes.bool, + type: PropTypes.oneOf(['line-number', 'decorated']), + labelFn: PropTypes.func, + onMouseDown: PropTypes.func, + onMouseMove: PropTypes.func, +}; + +class BareGutter extends React.Component { + static propTypes = { + editorHolder: RefHolderPropType.isRequired, + className: PropTypes.string, + ...gutterProps, + } + + static defaultProps = { + visible: true, + type: 'decorated', + labelFn: () => {}, + } + + constructor(props) { + super(props); + autobind(this, 'observeEditor', 'forceUpdate'); + + this.state = { + gutter: null, + editorComponent: null, + }; + + this.sub = new Disposable(); + } + + componentDidMount() { + this.sub = this.props.editorHolder.observe(this.observeEditor); + } + + componentDidUpdate(prevProps) { + if (this.props.editorHolder !== prevProps.editorHolder) { + this.sub.dispose(); + this.sub = this.props.editorHolder.observe(this.observeEditor); + } + } + + componentWillUnmount() { + if (this.state.gutter !== null) { + this.state.gutter.destroy(); + } + this.sub.dispose(); + } + + render() { + return null; + } + + observeEditor(editor) { + this.setState((prevState, props) => { + if (prevState.gutter !== null) { + prevState.gutter.destroy(); + } + + const options = extractProps(props, gutterProps); + options.class = props.className; + return {gutter: editor.addGutter(options)}; + }); + } +} + +export default class Gutter extends React.Component { + static propTypes = { + editor: PropTypes.object, + } + + constructor(props) { + super(props); + this.state = { + editorHolder: RefHolder.on(this.props.editor), + }; + } + + static getDerivedStateFromProps(props, state) { + const editorChanged = state.editorHolder.map(editor => editor !== props.editor).getOr(props.editor !== undefined); + return editorChanged ? RefHolder.on(props.editor) : null; + } + + render() { + if (!this.state.editorHolder.isEmpty()) { + return ; + } + + return ( + + {editorHolder => ( + + )} + + ); + } +} diff --git a/test/atom/gutter.test.js b/test/atom/gutter.test.js new file mode 100644 index 0000000000..a97f5236b8 --- /dev/null +++ b/test/atom/gutter.test.js @@ -0,0 +1,84 @@ +import React from 'react'; +import {mount} from 'enzyme'; + +import AtomTextEditor from '../../lib/atom/atom-text-editor'; +import Gutter from '../../lib/atom/gutter'; + +describe('Gutter', function() { + let atomEnv, domRoot; + + beforeEach(function() { + atomEnv = global.buildAtomEnvironment(); + + domRoot = document.createElement('div'); + domRoot.id = 'github-Gutter-test'; + document.body.appendChild(domRoot); + + const workspaceElement = atomEnv.workspace.getElement(); + domRoot.appendChild(workspaceElement); + }); + + afterEach(function() { + atomEnv.destroy(); + document.body.removeChild(domRoot); + }); + + it('adds a custom gutter to an editor supplied by prop', async function() { + const editor = await atomEnv.workspace.open(__filename); + + const app = ( + + ); + const wrapper = mount(app); + + const gutter = editor.gutterWithName('aaa'); + assert.isNotNull(gutter); + assert.isTrue(gutter.isVisible()); + assert.strictEqual(gutter.priority, 10); + + wrapper.unmount(); + + assert.isNull(editor.gutterWithName('aaa')); + }); + + it('adds a custom gutter to an editor from a context', function() { + const app = ( + + + + ); + const wrapper = mount(app); + + const editor = wrapper.instance().getModel(); + const gutter = editor.gutterWithName('bbb'); + assert.isNotNull(gutter); + assert.isTrue(gutter.isVisible()); + assert.strictEqual(gutter.priority, 20); + }); + + it('uses a function to derive number labels', async function() { + const text = '000\n111\n222\n333\n444\n555\n666\n777\n888\n999\n'; + const labelFn = ({bufferRow, screenRow}) => `custom ${bufferRow} ${screenRow}`; + + const app = ( + + + + ); + const wrapper = mount(app, {attachTo: domRoot}); + + const editorRoot = wrapper.getDOMNode(); + await assert.async.lengthOf(editorRoot.querySelectorAll('.yyy .line-number'), 12); + + const lineNumbers = editorRoot.querySelectorAll('.yyy .line-number'); + assert.strictEqual(lineNumbers[1].innerText, 'custom 0 0'); + assert.strictEqual(lineNumbers[2].innerText, 'custom 1 1'); + assert.strictEqual(lineNumbers[3].innerText, 'custom 2 2'); + }); +}); From 149b250a434b36a55c56c9112bc93fc3ac467b38 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 24 Jul 2018 14:26:17 -0400 Subject: [PATCH 0029/4252] CSS to get the FilePatchItem editor to scroll properly --- lib/views/file-patch-view.js | 6 +++++- styles/file-patch-view.less | 8 +++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/views/file-patch-view.js b/lib/views/file-patch-view.js index b1ecb833e4..3bda3fa743 100644 --- a/lib/views/file-patch-view.js +++ b/lib/views/file-patch-view.js @@ -75,7 +75,11 @@ export default class FilePatchView extends React.Component { />
- + diff --git a/styles/file-patch-view.less b/styles/file-patch-view.less index 0c0dd50899..0b86273d43 100644 --- a/styles/file-patch-view.less +++ b/styles/file-patch-view.less @@ -10,6 +10,7 @@ cursor: default; flex: 1; min-width: 0; + height: 100%; &-header { display: flex; @@ -39,8 +40,8 @@ &-container { flex: 1; - overflow-y: auto; display: flex; + height: 100%; flex-direction: column; .is-blank, @@ -57,6 +58,11 @@ .large-file-patch { flex-direction: column; } + + atom-text-editor { + width: 100%; + height: 100%; + } } From 2191fbf974b3147c8f7ef82237e0b114ea157874 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 24 Jul 2018 14:26:33 -0400 Subject: [PATCH 0030/4252] Render old and new line numbers --- lib/models/presented-file-patch.js | 24 ++++++++++++++++++++ lib/views/file-patch-view.js | 36 ++++++++++++++++++++++++++++++ styles/file-patch-view.less | 16 +++++++++++++ 3 files changed, 76 insertions(+) diff --git a/lib/models/presented-file-patch.js b/lib/models/presented-file-patch.js index aef7b69b34..9fcddde44c 100644 --- a/lib/models/presented-file-patch.js +++ b/lib/models/presented-file-patch.js @@ -11,6 +11,9 @@ export default class PresentedFilePatch { deleted: [], nonewline: [], }; + this.oldLineNumbers = new Map(); + this.newLineNumbers = new Map(); + this.maxLineNumberWidth = 0; let bufferLine = 0; this.text = filePatch.getHunks().reduce((str, hunk) => { @@ -18,6 +21,15 @@ export default class PresentedFilePatch { return hunk.getLines().reduce((hunkStr, line) => { hunkStr += line.getText() + '\n'; + this.oldLineNumbers.set(bufferLine, line.getOldLineNumber()); + this.newLineNumbers.set(bufferLine, line.getNewLineNumber()); + + if (line.getOldLineNumber().toString().length > this.maxLineNumberWidth) { + this.maxLineNumberWidth = line.getOldLineNumber().toString().length; + } + if (line.getNewLineNumber().toString().length > this.maxLineNumberWidth) { + this.maxLineNumberWidth = line.getNewLineNumber().toString().length; + } this.bufferPositions[line.getStatus()].push( new Point(bufferLine, 0), @@ -56,4 +68,16 @@ export default class PresentedFilePatch { getNoNewlineBufferPositions() { return this.bufferPositions.nonewline; } + + getOldLineNumberAt(bufferRow) { + return this.oldLineNumbers.get(bufferRow); + } + + getNewLineNumberAt(bufferRow) { + return this.newLineNumbers.get(bufferRow); + } + + getMaxLineNumberWidth() { + return this.maxLineNumberWidth; + } } diff --git a/lib/views/file-patch-view.js b/lib/views/file-patch-view.js index 3bda3fa743..aa8acc1004 100644 --- a/lib/views/file-patch-view.js +++ b/lib/views/file-patch-view.js @@ -2,11 +2,13 @@ import React, {Fragment} from 'react'; import PropTypes from 'prop-types'; import cx from 'classnames'; +import {autobind} from '../helpers'; import FilePatchSelection from '../models/file-patch-selection'; import AtomTextEditor from '../atom/atom-text-editor'; import Marker from '../atom/marker'; import MarkerLayer from '../atom/marker-layer'; import Decoration from '../atom/decoration'; +import Gutter from '../atom/gutter'; import FilePatchHeaderView from './file-patch-header-view'; import FilePatchMetaView from './file-patch-meta-view'; import HunkHeaderView from './hunk-header-view'; @@ -16,6 +18,8 @@ const executableText = { 100755: 'executable', }; +const NBSP_CHARACTER = '\u00a0'; + export default class FilePatchView extends React.Component { static propTypes = { relPath: PropTypes.string.isRequired, @@ -36,6 +40,7 @@ export default class FilePatchView extends React.Component { constructor(props) { super(props); + autobind(this, 'oldLineNumberLabel', 'newLineNumberLabel'); this.state = { selection: new FilePatchSelection(this.props.filePatch.getHunks()), @@ -80,6 +85,20 @@ export default class FilePatchView extends React.Component { lineNumberGutterVisible={false} autoWidth={false} autoHeight={false}> + + @@ -276,4 +295,21 @@ export default class FilePatchView extends React.Component { ); } + + oldLineNumberLabel({bufferRow}) { + return this.pad(this.state.presentedFilePatch.getOldLineNumberAt(bufferRow)); + } + + newLineNumberLabel({bufferRow}) { + return this.pad(this.state.presentedFilePatch.getNewLineNumberAt(bufferRow)); + } + + pad(num) { + const maxDigits = this.state.presentedFilePatch.getMaxLineNumberWidth(); + if (num === undefined || num === -1) { + return NBSP_CHARACTER.repeat(maxDigits); + } else { + return NBSP_CHARACTER.repeat(maxDigits - num.toString().length) + num.toString(); + } + } } diff --git a/styles/file-patch-view.less b/styles/file-patch-view.less index 0b86273d43..1b5ed499eb 100644 --- a/styles/file-patch-view.less +++ b/styles/file-patch-view.less @@ -163,3 +163,19 @@ } } } + +.gutter.old { + width: 5em; + + .line-number { + opacity: 1; + } +} + +.gutter.new { + width: 5em; + + .line-number { + opacity: 1; + } +} From 90b2af315b4e366f30a6095dcf2e36c5239a06e7 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 25 Jul 2018 09:55:20 -0400 Subject: [PATCH 0031/4252] Move controller methods over --- lib/containers/file-patch-container.js | 4 + lib/controllers/file-patch-controller.js | 211 ++++++++++++++++++++++- lib/controllers/root-controller.js | 4 +- lib/items/file-patch-item.js | 8 + lib/models/hunk-line.js | 4 + lib/models/presented-file-patch.js | 32 +++- lib/views/file-patch-view.js | 121 +++++++++++-- lib/views/hunk-header-view.js | 7 + 8 files changed, 370 insertions(+), 21 deletions(-) diff --git a/lib/containers/file-patch-container.js b/lib/containers/file-patch-container.js index 5375d24f72..8bb5f11645 100644 --- a/lib/containers/file-patch-container.js +++ b/lib/containers/file-patch-container.js @@ -13,7 +13,11 @@ export default class FilePatchContainer extends React.Component { stagingStatus: PropTypes.oneOf(['staged', 'unstaged']), relPath: PropTypes.string.isRequired, + workspace: PropTypes.object.isRequired, tooltips: PropTypes.object.isRequired, + + destroy: PropTypes.func.isRequired, + undoLastDiscard: PropTypes.func.isRequired, } constructor(props) { diff --git a/lib/controllers/file-patch-controller.js b/lib/controllers/file-patch-controller.js index 3df858ab0a..ba20e3d24e 100644 --- a/lib/controllers/file-patch-controller.js +++ b/lib/controllers/file-patch-controller.js @@ -1,9 +1,218 @@ import React from 'react'; +import PropTypes from 'prop-types'; +import {Point} from 'atom'; +import path from 'path'; +import {autobind} from '../helpers'; +import FilePatchSelection from '../models/file-patch-selection'; +import FilePatchItem from '../items/file-patch-item'; import FilePatchView from '../views/file-patch-view'; export default class FilePatchController extends React.Component { + static propTypes = { + repository: PropTypes.object.isRequired, + stagingStatus: PropTypes.oneOf(['staged', 'unstaged']), + relPath: PropTypes.string.isRequired, + filePatch: PropTypes.object.isRequired, + + workspace: PropTypes.object.isRequired, + tooltips: PropTypes.object.isRequired, + + destroy: PropTypes.func.isRequired, + undoLastDiscard: PropTypes.func.isRequired, + } + + constructor(props) { + super(props); + autobind( + this, + 'mouseDownOnHeader', 'mouseDownOnLineNumber', 'mouseMoveOnLineNumber', 'mouseUp', + 'undoLastDiscard', 'diveIntoMirrorPatch', 'openFile', 'toggleFile', 'toggleModeChange', 'toggleSymlinkChange', + ); + + this.state = { + selection: new FilePatchSelection(this.props.filePatch.getHunks()), + }; + + this.mouseSelectionInProgress = false; + } + render() { - return ; + return ( + + ); + } + + mouseDownOnHeader(event, hunk) { + if (event.button !== 0) { return; } + const windows = process.platform === 'win32'; + if (event.ctrlKey && !windows) { return; } // simply open context menu + + this.mouseSelectionInProgress = true; + event.persist && event.persist(); + + this.setState(prevState => { + let selection = prevState.selection; + if (event.metaKey || (event.ctrlKey && windows)) { + if (selection.getMode() === 'hunk') { + selection = selection.addOrSubtractHunkSelection(hunk); + } else { + // TODO: optimize + selection = hunk.getLines().reduce( + (current, line) => current.addOrSubtractLineSelection(line).coalesce(), + selection, + ); + } + } else if (event.shiftKey) { + if (selection.getMode() === 'hunk') { + selection = selection.selectHunk(hunk, true); + } else { + const hunkLines = hunk.getLines(); + const tailIndex = selection.getLineSelectionTailIndex(); + const selectedHunkAfterTail = tailIndex < hunkLines[0].diffLineNumber; + if (selectedHunkAfterTail) { + selection = selection.selectLine(hunkLines[hunkLines.length - 1], true); + } else { + selection = selection.selectLine(hunkLines[0], true); + } + } + } else { + selection = selection.selectHunk(hunk, false); + } + + return {selection}; + }); + } + + mouseDownOnLineNumber(event, hunk, line) { + if (event.button !== 0) { return; } + const windows = process.platform === 'win32'; + if (event.ctrlKey && !windows) { return; } // simply open context menu + + this.mouseSelectionInProgress = true; + event.persist && event.persist(); + + this.setState(prevState => { + let selection = prevState.selection; + + if (event.metaKey || (event.ctrlKey && windows)) { + if (selection.getMode() === 'hunk') { + selection = selection.addOrSubtractHunkSelection(hunk); + } else { + selection = selection.addOrSubtractLineSelection(line); + } + } else if (event.shiftKey) { + if (selection.getMode() === 'hunk') { + selection = selection.selectHunk(hunk, true); + } else { + selection = selection.selectLine(line, true); + } + } else if (event.detail === 1) { + selection = selection.selectLine(line, false); + } else if (event.detail === 2) { + selection = selection.selectHunk(hunk, false); + } + + return {selection}; + }); + } + + mouseMoveOnLineNumber(event, hunk, line) { + if (!this.mouseSelectionInProgress) { return; } + + this.setState(prevState => { + let selection = null; + if (prevState.selection.getMode() === 'hunk') { + selection = prevState.selection.selectHunk(hunk, true); + } else { + selection = prevState.selection.selectLine(line, true); + } + return {selection}; + }); + } + + mouseUp() { + this.mouseSelectionInProgress = false; + this.setState(prevState => { + return {selection: prevState.selection.coalesce()}; + }); + } + + undoLastDiscard() { + return this.props.undoLastDiscard(this.props.relPath, this.props.repository); + } + + async diveIntoMirrorPatch() { + const mirrorStatus = this.withStagingStatus({staged: 'unstaged', unstaged: 'staged'}); + const workingDirectory = this.props.repository.getWorkingDirectoryPath(); + const uri = FilePatchItem.buildURI(this.props.relPath, workingDirectory, mirrorStatus); + + this.destroy(); + await this.props.workspace.open(uri); + } + + async openFile(lineNumber = null) { + const absolutePath = path.join(this.props.repository.getWorkingDirectoryPath(), this.props.relPath); + const editor = await this.props.workspace.open(absolutePath, {pending: true}); + const position = new Point(lineNumber ? lineNumber - 1 : 0, 0); + editor.scrollToBufferPosition(position, {center: true}); + editor.setCursorBufferPosition(position); + } + + toggleFile() { + const methodName = this.withStagingStatus({staged: 'unstageFile', unstaged: 'stageFile'}); + return this.props.repository[methodName]([this.props.relPath]); + } + + toggleModeChange() { + const targetMode = this.withStagingStatus({ + unstaged: this.props.filePatch.getNewMode(), + staged: this.props.filePatch.getOldMode(), + }); + return this.props.repository.stageFileModeChange(this.props.relPath, targetMode); + } + + toggleSymlinkChange() { + const {filePatch, relPath, repository} = this.props; + return this.withStagingStatus({ + unstaged: () => { + if (filePatch.hasTypechange() && filePatch.getStatus() === 'added') { + return repository.stageFileSymlinkChange(relPath); + } + + return repository.stageFiles([relPath]); + }, + staged: () => { + if (filePatch.hasTypechange() && filePatch.getStatus() === 'deleted') { + return repository.stageFileSymlinkChange(relPath); + } + + return repository.unstageFiles([relPath]); + }, + }); + } + + withStagingStatus(callbacks) { + const callback = callbacks[this.props.stagingStatus]; + if (!callback) { + throw new Error(`Unknown staging status: ${this.props.stagingStatus}`); + } + return callback instanceof Function ? callback() : callback; } } diff --git a/lib/controllers/root-controller.js b/lib/controllers/root-controller.js index 2d6c4814ef..a0a373e63b 100644 --- a/lib/controllers/root-controller.js +++ b/lib/controllers/root-controller.js @@ -40,7 +40,6 @@ export default class RootController extends React.Component { project: PropTypes.object.isRequired, loginModel: PropTypes.object.isRequired, confirm: PropTypes.func.isRequired, - workdirContextPool: WorkdirContextPoolPropType.isRequired, getRepositoryForWorkdir: PropTypes.func.isRequired, createRepositoryForProjectPath: PropTypes.func, cloneRepositoryForProjectPath: PropTypes.func, @@ -316,6 +315,9 @@ export default class RootController extends React.Component { stagingStatus={params.stagingStatus} tooltips={this.props.tooltips} + workspace={this.props.workspace} + + undoLastDiscard={this.undoLastDiscard} /> )} diff --git a/lib/items/file-patch-item.js b/lib/items/file-patch-item.js index 3ab27842fc..b8b108e354 100644 --- a/lib/items/file-patch-item.js +++ b/lib/items/file-patch-item.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import {Emitter} from 'event-kit'; import {WorkdirContextPoolPropType} from '../prop-types'; +import {autobind} from '../helpers'; import FilePatchContainer from '../containers/file-patch-container'; export default class FilePatchItem extends React.Component { @@ -12,6 +13,11 @@ export default class FilePatchItem extends React.Component { relPath: PropTypes.string.isRequired, workingDirectory: PropTypes.string.isRequired, stagingStatus: PropTypes.oneOf(['staged', 'unstaged']), + + workspace: PropTypes.object.isRequired, + tooltips: PropTypes.object.isRequired, + + undoLastDiscard: PropTypes.func.isRequired, } static uriPattern = 'atom-github://file-patch/{relPath...}?workdir={workingDirectory}&stagingStatus={stagingStatus}' @@ -25,6 +31,7 @@ export default class FilePatchItem extends React.Component { constructor(props) { super(props); + autobind(this, 'destroy'); this.emitter = new Emitter(); this.isDestroyed = false; @@ -66,6 +73,7 @@ export default class FilePatchItem extends React.Component { return ( ); diff --git a/lib/models/hunk-line.js b/lib/models/hunk-line.js index fcf4826e17..8ab430836e 100644 --- a/lib/models/hunk-line.js +++ b/lib/models/hunk-line.js @@ -35,6 +35,10 @@ export default class HunkLine { return this.newLineNumber; } + getDiffLineNumber() { + return this.diffLineNumber; + } + getStatus() { return this.status; } diff --git a/lib/models/presented-file-patch.js b/lib/models/presented-file-patch.js index 9fcddde44c..02a5b2ba09 100644 --- a/lib/models/presented-file-patch.js +++ b/lib/models/presented-file-patch.js @@ -11,8 +11,9 @@ export default class PresentedFilePatch { deleted: [], nonewline: [], }; - this.oldLineNumbers = new Map(); - this.newLineNumbers = new Map(); + this.hunkIndex = new Map(); + this.lineIndex = new Map(); + this.lineReverseIndex = new Map(); this.maxLineNumberWidth = 0; let bufferLine = 0; @@ -21,8 +22,9 @@ export default class PresentedFilePatch { return hunk.getLines().reduce((hunkStr, line) => { hunkStr += line.getText() + '\n'; - this.oldLineNumbers.set(bufferLine, line.getOldLineNumber()); - this.newLineNumbers.set(bufferLine, line.getNewLineNumber()); + this.hunkIndex.set(bufferLine, hunk); + this.lineIndex.set(bufferLine, line); + this.lineReverseIndex.set(line, bufferLine); if (line.getOldLineNumber().toString().length > this.maxLineNumberWidth) { this.maxLineNumberWidth = line.getOldLineNumber().toString().length; @@ -69,12 +71,30 @@ export default class PresentedFilePatch { return this.bufferPositions.nonewline; } + getHunkAt(bufferRow) { + return this.hunkIndex.get(bufferRow); + } + + getLineAt(bufferRow) { + return this.lineIndex.get(bufferRow); + } + getOldLineNumberAt(bufferRow) { - return this.oldLineNumbers.get(bufferRow); + const line = this.getLineAt(bufferRow); + return line ? line.getOldLineNumber() : -1; } getNewLineNumberAt(bufferRow) { - return this.newLineNumbers.get(bufferRow); + const line = this.getLineAt(bufferRow); + return line ? line.getNewLineNumber() : -1; + } + + getPositionForLine(line) { + const bufferRow = this.lineReverseIndex.get(line); + if (bufferRow === undefined) { + return null; + } + return new Point(bufferRow, 0); } getMaxLineNumberWidth() { diff --git a/lib/views/file-patch-view.js b/lib/views/file-patch-view.js index aa8acc1004..0297545c14 100644 --- a/lib/views/file-patch-view.js +++ b/lib/views/file-patch-view.js @@ -3,7 +3,6 @@ import PropTypes from 'prop-types'; import cx from 'classnames'; import {autobind} from '../helpers'; -import FilePatchSelection from '../models/file-patch-selection'; import AtomTextEditor from '../atom/atom-text-editor'; import Marker from '../atom/marker'; import MarkerLayer from '../atom/marker-layer'; @@ -26,10 +25,16 @@ export default class FilePatchView extends React.Component { stagingStatus: PropTypes.oneOf(['staged', 'unstaged']).isRequired, isPartiallyStaged: PropTypes.bool.isRequired, filePatch: PropTypes.object.isRequired, + selection: PropTypes.object.isRequired, repository: PropTypes.object.isRequired, tooltips: PropTypes.object.isRequired, + mouseDownOnHeader: PropTypes.func.isRequired, + mouseDownOnLineNumber: PropTypes.func.isRequired, + mouseMoveOnLineNumber: PropTypes.func.isRequired, + mouseUp: PropTypes.func.isRequired, + undoLastDiscard: PropTypes.func.isRequired, diveIntoMirrorPatch: PropTypes.func.isRequired, openFile: PropTypes.func.isRequired, @@ -40,22 +45,52 @@ export default class FilePatchView extends React.Component { constructor(props) { super(props); - autobind(this, 'oldLineNumberLabel', 'newLineNumberLabel'); + autobind( + this, + 'didMouseDownOnLineNumber', 'didMouseMoveOnLineNumber', 'didMouseUp', 'oldLineNumberLabel', 'newLineNumberLabel', + ); + const presentedFilePatch = this.props.filePatch.present(); + const selectedLines = this.props.selection.getSelectedLines(); this.state = { - selection: new FilePatchSelection(this.props.filePatch.getHunks()), - presentedFilePatch: this.props.filePatch.present(), + lastSelection: this.props.selection, + selectedHunks: this.props.selection.getSelectedHunks(), + selectedLines, + presentedFilePatch, + selectedLinePositions: Array.from(selectedLines, line => presentedFilePatch.getPositionForLine(line)), }; + + this.lastMouseMoveLine = null; } static getDerivedStateFromProps(props, state) { + const nextState = {}; + let currentPresentedFilePatch = state.presentedFilePatch; + if (props.filePatch !== state.presentedFilePatch.getFilePatch()) { - return { - presentedFilePatch: props.filePatch.present(), - }; + currentPresentedFilePatch = props.filePatch.present(); + nextState.presentedFilePatch = currentPresentedFilePatch; } - return null; + if (props.selection !== state.lastSelection) { + nextState.lastSelection = props.selection; + nextState.selectedHunks = props.selection.getSelectedHunks(); + nextState.selectedLines = props.selection.getSelectedLines(); + + nextState.selectedLinePositions = Array.from(nextState.selectedLines, line => { + return currentPresentedFilePatch.getPositionForLine(line); + }); + } + + return nextState; + } + + componentDidMount() { + window.addEventListener('mouseup', this.didMouseUp); + } + + componentWillUnmount() { + window.removeEventListener('mouseup', this.didMouseUp); } render() { @@ -91,6 +126,8 @@ export default class FilePatchView extends React.Component { className="old" type="line-number" labelFn={this.oldLineNumberLabel} + onMouseDown={this.didMouseDownOnLineNumber} + onMouseMove={this.didMouseMoveOnLineNumber} /> @@ -111,17 +150,25 @@ export default class FilePatchView extends React.Component { {this.renderHunkHeaders()} + {this.renderLineDecorations( + this.state.selectedLinePositions, + 'github-FilePatchView-line--selected', + {gutter: true}, + )} {this.renderLineDecorations( this.state.presentedFilePatch.getAddedBufferPositions(), 'github-FilePatchView-line--added', + {line: true}, )} {this.renderLineDecorations( this.state.presentedFilePatch.getDeletedBufferPositions(), 'github-FilePatchView-line--deleted', + {line: true}, )} {this.renderLineDecorations( this.state.presentedFilePatch.getNoNewlineBufferPositions(), 'github-FilePatchView-line--nonewline', + {line: true}, )} @@ -248,8 +295,8 @@ export default class FilePatchView extends React.Component { } renderHunkHeaders() { - const selectedHunks = this.state.selection.getSelectedHunks(); - const isHunkSelectionMode = this.state.selection.getMode() === 'hunk'; + const selectedHunks = this.state.selectedHunks; + const isHunkSelectionMode = this.props.selection.getMode() === 'hunk'; const toggleVerb = this.props.stagingStatus === 'unstaged' ? 'Stage' : 'Unstage'; return this.props.filePatch.getHunks().map((hunk, index) => { @@ -269,7 +316,7 @@ export default class FilePatchView extends React.Component { hunk={hunk} isSelected={isSelected} stagingStatus={this.props.stagingStatus} - selectionMode={this.state.selection.getMode()} + selectionMode={this.props.selection.getMode()} toggleSelectionLabel={toggleSelectionLabel} discardSelectionLabel={discardSelectionLabel} @@ -277,6 +324,7 @@ export default class FilePatchView extends React.Component { toggleSelection={() => {}} discardSelection={() => {}} + mouseDown={this.props.mouseDownOnHeader} /> @@ -284,18 +332,65 @@ export default class FilePatchView extends React.Component { }); } - renderLineDecorations(positions, lineClass) { + renderSelectedLines() { + return ( + + {Array.from(this.state.selectedLines, line => { + const position = this.state.presentedFilePatch.getPositionForLine(line); + return ; + })} + + ); + } + + renderLineDecorations(positions, lineClass, {line, gutter}) { return ( {positions.map((position, index) => { return ; })} - + {line && } + {gutter && ( + + + + + )} ); } + didMouseDownOnLineNumber(event) { + const line = this.state.presentedFilePatch.getLineAt(event.bufferRow); + const hunk = this.state.presentedFilePatch.getHunkAt(event.bufferRow); + + if (line === undefined || hunk === undefined) { + return; + } + + this.props.mouseDownOnLineNumber(event.domEvent, hunk, line); + } + + didMouseMoveOnLineNumber(event) { + const line = this.state.presentedFilePatch.getLineAt(event.bufferRow); + if (this.lastMouseMoveLine === line || line === undefined) { + return; + } + this.lastMouseMoveLine = line; + + const hunk = this.state.presentedFilePatch.getHunkAt(event.bufferRow); + if (hunk === undefined) { + return; + } + + this.props.mouseMoveOnLineNumber(event.domEvent, hunk, line); + } + + didMouseUp() { + this.props.mouseUp(); + } + oldLineNumberLabel({bufferRow}) { return this.pad(this.state.presentedFilePatch.getOldLineNumberAt(bufferRow)); } diff --git a/lib/views/hunk-header-view.js b/lib/views/hunk-header-view.js index e0ba9f9b46..ca725f9bd4 100644 --- a/lib/views/hunk-header-view.js +++ b/lib/views/hunk-header-view.js @@ -2,6 +2,7 @@ import React, {Fragment} from 'react'; import PropTypes from 'prop-types'; import cx from 'classnames'; +import {autobind} from '../helpers'; import RefHolder from '../models/ref-holder'; import Tooltip from '../atom/tooltip'; @@ -18,10 +19,12 @@ export default class HunkHeaderView extends React.Component { toggleSelection: PropTypes.func.isRequired, discardSelection: PropTypes.func.isRequired, + mouseDown: PropTypes.func.isRequired, }; constructor(props) { super(props); + autobind(this, 'didMouseDown'); this.refDiscardButton = new RefHolder(); } @@ -57,4 +60,8 @@ export default class HunkHeaderView extends React.Component {
); } + + didMouseDown(event) { + return this.props.mouseDown(event, this.props.hunk); + } } From 6e085366d0657241ad6295a041e694ac1e72d8f8 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 25 Jul 2018 09:57:03 -0400 Subject: [PATCH 0032/4252] Some inexpert styling :art: --- lib/views/hunk-header-view.js | 6 +++--- styles/file-patch-view.less | 15 ++++++++------- styles/hunk-header-view.less | 34 +++++++++++++++++----------------- 3 files changed, 28 insertions(+), 27 deletions(-) diff --git a/lib/views/hunk-header-view.js b/lib/views/hunk-header-view.js index ca725f9bd4..f943c048e6 100644 --- a/lib/views/hunk-header-view.js +++ b/lib/views/hunk-header-view.js @@ -31,12 +31,12 @@ export default class HunkHeaderView extends React.Component { render() { const conditional = { - 'is-selected': this.props.isSelected, - 'is-hunkMode': this.props.selectionMode === 'hunk', + 'github-HunkHeaderView--isSelected': this.props.isSelected, + 'github-HunkHeaderView--isHunkMode': this.props.selectionMode === 'hunk', }; return ( -
+
{this.props.hunk.getHeader().trim()} {this.props.hunk.getSectionHeading().trim()} diff --git a/styles/file-patch-view.less b/styles/file-patch-view.less index 1b5ed499eb..193a97d759 100644 --- a/styles/file-patch-view.less +++ b/styles/file-patch-view.less @@ -164,18 +164,19 @@ } } -.gutter.old { +.gutter.old, .gutter.new { width: 5em; - .line-number { - opacity: 1; + div { + width: 100%; } -} - -.gutter.new { - width: 5em; .line-number { opacity: 1; + + &.github-FilePatchView-line--selected { + color: contrast(@button-background-color-selected); + background: @button-background-color-selected; + } } } diff --git a/styles/hunk-header-view.less b/styles/hunk-header-view.less index a1e1e19449..253c0c2075 100644 --- a/styles/hunk-header-view.less +++ b/styles/hunk-header-view.less @@ -11,6 +11,7 @@ font-size: .9em; background-color: @panel-heading-background-color; border-bottom: 1px solid @panel-heading-border-color; + cursor: default; &-title { flex: 1; @@ -81,29 +82,28 @@ &-lineText { display: inline-block; text-indent: 0; - } + } } - // // States // ------------------------------- -.github-HunkView.is-selected.is-hunkMode .github-HunkView-header { - background-color: @background-color-selected; - .github-HunkView-title { - color: @text-color; +.github-HunkHeaderView--isSelected.github-HunkHeaderView--isHunkMode { + background-color: @button-background-color-selected; + .github-HunkHeaderView-title { + color: contrast(@button-background-color-selected); } - .github-HunkView-stageButton, .github-HunkView-discardButton { + .github-HunkHeaderView-stageButton, .github-HunkHeaderView-discardButton { border-color: mix(@text-color, @background-color-selected, 25%); } } -.github-HunkView-title:hover { +.github-HunkHeaderView-title:hover { color: @text-color-highlight; } -.github-HunkView-line { +.github-HunkHeaderView-line { // mixin .hunk-line-mixin(@fg; @bg) { @@ -114,12 +114,12 @@ color: @text-color; background-color: @background-color-selected; } - .github-HunkView-lineContent { + .github-HunkHeaderView-lineContent { color: saturate( mix(@fg, @text-color-highlight, 20%), 20%); background-color: saturate( mix(@bg, @hunk-bg-color, 15%), 20%); } // hightlight when focused + selected - .github-FilePatchView:focus &.is-selected .github-HunkView-lineContent { + .github-FilePatchView:focus &.is-selected .github-HunkHeaderView-lineContent { color: saturate( mix(@fg, @text-color-highlight, 10%), 10%); background-color: saturate( mix(@bg, @hunk-bg-color, 25%), 10%); } @@ -134,7 +134,7 @@ } // divider line between added and deleted lines - &.is-deleted + .is-added .github-HunkView-lineContent { + &.is-deleted + .is-added .github-HunkHeaderView-lineContent { box-shadow: 0 -1px 0 hsla(0,0%,50%,.1); } @@ -142,16 +142,16 @@ // focus colors .github-FilePatchView:focus { - .github-HunkView.is-selected.is-hunkMode .github-HunkView-title, - .github-HunkView.is-selected.is-hunkMode .github-HunkView-header, - .github-HunkView-line.is-selected .github-HunkView-lineNumber { + .github-HunkHeaderView.is-selected.is-hunkMode .github-HunkHeaderView-title, + .github-HunkHeaderView.is-selected.is-hunkMode .github-HunkHeaderView-header, + .github-HunkHeaderView-line.is-selected .github-HunkHeaderView-lineNumber { color: contrast(@button-background-color-selected); background: @button-background-color-selected; } - .github-HunkView-line.is-selected .github-HunkView-lineNumber { + .github-HunkHeaderView-line.is-selected .github-HunkHeaderView-lineNumber { border-color: mix(@button-border-color, @button-background-color-selected, 25%); } - .github-HunkView.is-selected.is-hunkMode .github-HunkView { + .github-HunkHeaderView.is-selected.is-hunkMode .github-HunkHeaderView { &-stageButton, &-discardButton { border-color: mix(@hunk-bg-color, @button-background-color-selected, 30%); From b5c7619be436fd4d03c2492394eed66e4ba029c3 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 25 Jul 2018 10:22:00 -0400 Subject: [PATCH 0033/4252] :scissors: unused code --- lib/views/file-patch-view.js | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/lib/views/file-patch-view.js b/lib/views/file-patch-view.js index 0297545c14..824816fb3a 100644 --- a/lib/views/file-patch-view.js +++ b/lib/views/file-patch-view.js @@ -332,17 +332,6 @@ export default class FilePatchView extends React.Component { }); } - renderSelectedLines() { - return ( - - {Array.from(this.state.selectedLines, line => { - const position = this.state.presentedFilePatch.getPositionForLine(line); - return ; - })} - - ); - } - renderLineDecorations(positions, lineClass, {line, gutter}) { return ( From 963e303b92a08ba3d9d2e67f9e83e0ff79089a4c Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 25 Jul 2018 10:22:15 -0400 Subject: [PATCH 0034/4252] Use the position as a key for Marker components --- lib/views/file-patch-view.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/views/file-patch-view.js b/lib/views/file-patch-view.js index 824816fb3a..3c271e6858 100644 --- a/lib/views/file-patch-view.js +++ b/lib/views/file-patch-view.js @@ -335,8 +335,8 @@ export default class FilePatchView extends React.Component { renderLineDecorations(positions, lineClass, {line, gutter}) { return ( - {positions.map((position, index) => { - return ; + {positions.map(position => { + return ; })} {line && } From 6677a6d4bc8beb9277876c1c193dc91d607d7b60 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 25 Jul 2018 13:54:12 -0400 Subject: [PATCH 0035/4252] Staging and discard actions --- lib/controllers/file-patch-controller.js | 134 +++++++++++++++++++---- lib/controllers/root-controller.js | 1 + lib/items/file-patch-item.js | 1 + lib/views/file-patch-view.js | 29 ++++- lib/views/hunk-header-view.js | 10 +- 5 files changed, 147 insertions(+), 28 deletions(-) diff --git a/lib/controllers/file-patch-controller.js b/lib/controllers/file-patch-controller.js index ba20e3d24e..3b6741a218 100644 --- a/lib/controllers/file-patch-controller.js +++ b/lib/controllers/file-patch-controller.js @@ -19,6 +19,7 @@ export default class FilePatchController extends React.Component { tooltips: PropTypes.object.isRequired, destroy: PropTypes.func.isRequired, + discardLines: PropTypes.func.isRequired, undoLastDiscard: PropTypes.func.isRequired, } @@ -27,14 +28,42 @@ export default class FilePatchController extends React.Component { autobind( this, 'mouseDownOnHeader', 'mouseDownOnLineNumber', 'mouseMoveOnLineNumber', 'mouseUp', - 'undoLastDiscard', 'diveIntoMirrorPatch', 'openFile', 'toggleFile', 'toggleModeChange', 'toggleSymlinkChange', + 'undoLastDiscard', 'diveIntoMirrorPatch', 'openFile', + 'toggleFile', 'selectAndToggleHunk', 'toggleLines', 'toggleModeChange', 'toggleSymlinkChange', + 'discardLines', 'selectAndDiscardHunk', ); this.state = { + lastFilePatch: this.props.filePatch, selection: new FilePatchSelection(this.props.filePatch.getHunks()), }; this.mouseSelectionInProgress = false; + this.stagingOperationInProgress = false; + + this.patchChangePromise = new Promise(resolve => { + this.resolvePatchChangePromise = resolve; + }); + } + + static getDerivedStateFromProps(props, state) { + if (props.filePatch !== state.lastFilePatch) { + return { + selection: state.selection.updateHunks(props.filePatch.getHunks()), + lastFilePatch: props.filePatch, + }; + } + + return null; + } + + componentDidUpdate(prevProps) { + if (prevProps.filePatch !== this.props.filePatch) { + this.resolvePatchChangePromise(); + this.patchChangePromise = new Promise(resolve => { + this.resolvePatchChangePromise = resolve; + }); + } } render() { @@ -49,12 +78,16 @@ export default class FilePatchController extends React.Component { mouseMoveOnLineNumber={this.mouseMoveOnLineNumber} mouseUp={this.mouseUp} - undoLastDiscard={this.undoLastDiscard} diveIntoMirrorPatch={this.diveIntoMirrorPatch} openFile={this.openFile} toggleFile={this.toggleFile} + selectAndToggleHunk={this.selectAndToggleHunk} + toggleLines={this.toggleLines} toggleModeChange={this.toggleModeChange} toggleSymlinkChange={this.toggleSymlinkChange} + undoLastDiscard={this.undoLastDiscard} + discardLines={this.discardLines} + selectAndDiscardHunk={this.selectAndDiscardHunk} /> ); } @@ -163,7 +196,7 @@ export default class FilePatchController extends React.Component { const workingDirectory = this.props.repository.getWorkingDirectoryPath(); const uri = FilePatchItem.buildURI(this.props.relPath, workingDirectory, mirrorStatus); - this.destroy(); + this.props.destroy(); await this.props.workspace.open(uri); } @@ -176,35 +209,78 @@ export default class FilePatchController extends React.Component { } toggleFile() { - const methodName = this.withStagingStatus({staged: 'unstageFile', unstaged: 'stageFile'}); - return this.props.repository[methodName]([this.props.relPath]); + return this.stagingOperation(() => { + const methodName = this.withStagingStatus({staged: 'unstageFile', unstaged: 'stageFile'}); + return this.props.repository[methodName]([this.props.relPath]); + }); + } + + async selectAndToggleHunk(hunk) { + await this.selectHunk(hunk); + + return this.stagingOperation(() => { + const patch = this.withStagingStatus({ + staged: () => this.props.filePatch.getUnstagePatchForHunk(hunk), + unstaged: () => this.props.filePatch.getStagePatchForHunk(hunk), + }); + return this.props.repository.applyPatchToIndex(patch); + }); + } + + toggleLines(lines) { + return this.stagingOperation(() => { + const patch = this.withStagingStatus({ + staged: () => this.props.filePatch.getUnstagePatchForLines(lines), + unstaged: () => this.props.filePatch.getStagePatchForLines(lines), + }); + return this.props.repository.applyPatchToIndex(patch); + }); } toggleModeChange() { - const targetMode = this.withStagingStatus({ - unstaged: this.props.filePatch.getNewMode(), - staged: this.props.filePatch.getOldMode(), + return this.stagingOperation(() => { + const targetMode = this.withStagingStatus({ + unstaged: this.props.filePatch.getNewMode(), + staged: this.props.filePatch.getOldMode(), + }); + return this.props.repository.stageFileModeChange(this.props.relPath, targetMode); }); - return this.props.repository.stageFileModeChange(this.props.relPath, targetMode); } toggleSymlinkChange() { - const {filePatch, relPath, repository} = this.props; - return this.withStagingStatus({ - unstaged: () => { - if (filePatch.hasTypechange() && filePatch.getStatus() === 'added') { - return repository.stageFileSymlinkChange(relPath); - } + return this.stagingOperation(() => { + const {filePatch, relPath, repository} = this.props; + return this.withStagingStatus({ + unstaged: () => { + if (filePatch.hasTypechange() && filePatch.getStatus() === 'added') { + return repository.stageFileSymlinkChange(relPath); + } - return repository.stageFiles([relPath]); - }, - staged: () => { - if (filePatch.hasTypechange() && filePatch.getStatus() === 'deleted') { - return repository.stageFileSymlinkChange(relPath); - } + return repository.stageFiles([relPath]); + }, + staged: () => { + if (filePatch.hasTypechange() && filePatch.getStatus() === 'deleted') { + return repository.stageFileSymlinkChange(relPath); + } - return repository.unstageFiles([relPath]); - }, + return repository.unstageFiles([relPath]); + }, + }); + }); + } + + discardLines(lines) { + return this.props.discardLines(this.props.filePatch, lines, this.props.repository); + } + + async selectAndDiscardHunk(hunk) { + await this.selectHunk(hunk); + return this.discardLines(hunk.getLines()); + } + + selectHunk(hunk) { + return new Promise(resolve => { + this.setState(prevState => ({selection: prevState.selection.selectHunk(hunk)}), resolve); }); } @@ -215,4 +291,16 @@ export default class FilePatchController extends React.Component { } return callback instanceof Function ? callback() : callback; } + + stagingOperation(fn) { + if (this.stagingOperationInProgress) { + return null; + } + this.stagingOperationInProgress = true; + this.patchChangePromise.then(() => { + this.stagingOperationInProgress = false; + }); + + return fn(); + } } diff --git a/lib/controllers/root-controller.js b/lib/controllers/root-controller.js index a0a373e63b..85cb421385 100644 --- a/lib/controllers/root-controller.js +++ b/lib/controllers/root-controller.js @@ -317,6 +317,7 @@ export default class RootController extends React.Component { tooltips={this.props.tooltips} workspace={this.props.workspace} + discardLines={this.discardLines} undoLastDiscard={this.undoLastDiscard} /> )} diff --git a/lib/items/file-patch-item.js b/lib/items/file-patch-item.js index b8b108e354..65e23e70ce 100644 --- a/lib/items/file-patch-item.js +++ b/lib/items/file-patch-item.js @@ -17,6 +17,7 @@ export default class FilePatchItem extends React.Component { workspace: PropTypes.object.isRequired, tooltips: PropTypes.object.isRequired, + discardLines: PropTypes.func.isRequired, undoLastDiscard: PropTypes.func.isRequired, } diff --git a/lib/views/file-patch-view.js b/lib/views/file-patch-view.js index 3c271e6858..2a879728aa 100644 --- a/lib/views/file-patch-view.js +++ b/lib/views/file-patch-view.js @@ -35,19 +35,24 @@ export default class FilePatchView extends React.Component { mouseMoveOnLineNumber: PropTypes.func.isRequired, mouseUp: PropTypes.func.isRequired, - undoLastDiscard: PropTypes.func.isRequired, diveIntoMirrorPatch: PropTypes.func.isRequired, openFile: PropTypes.func.isRequired, toggleFile: PropTypes.func.isRequired, + selectAndToggleHunk: PropTypes.func.isRequired, + toggleLines: PropTypes.func.isRequired, toggleModeChange: PropTypes.func.isRequired, toggleSymlinkChange: PropTypes.func.isRequired, + undoLastDiscard: PropTypes.func.isRequired, + discardLines: PropTypes.func.isRequired, + selectAndDiscardHunk: PropTypes.func.isRequired, } constructor(props) { super(props); autobind( this, - 'didMouseDownOnLineNumber', 'didMouseMoveOnLineNumber', 'didMouseUp', 'oldLineNumberLabel', 'newLineNumberLabel', + 'didMouseDownOnLineNumber', 'didMouseMoveOnLineNumber', 'didMouseUp', + 'oldLineNumberLabel', 'newLineNumberLabel', ); const presentedFilePatch = this.props.filePatch.present(); @@ -322,8 +327,8 @@ export default class FilePatchView extends React.Component { tooltips={this.props.tooltips} - toggleSelection={() => {}} - discardSelection={() => {}} + toggleSelection={() => this.toggleSelection(hunk)} + discardSelection={() => this.discardSelection(hunk)} mouseDown={this.props.mouseDownOnHeader} /> @@ -388,6 +393,22 @@ export default class FilePatchView extends React.Component { return this.pad(this.state.presentedFilePatch.getNewLineNumberAt(bufferRow)); } + toggleSelection(hunk) { + if (this.state.selectedHunks.has(hunk)) { + return this.props.toggleLines(this.state.selectedLines); + } else { + return this.props.selectAndToggleHunk(hunk); + } + } + + discardSelection(hunk) { + if (this.state.selectedHunks.has(hunk)) { + return this.props.discardLines(this.state.selectedLines); + } else { + return this.props.selectAndDiscardHunk(hunk); + } + } + pad(num) { const maxDigits = this.state.presentedFilePatch.getMaxLineNumberWidth(); if (num === undefined || num === -1) { diff --git a/lib/views/hunk-header-view.js b/lib/views/hunk-header-view.js index f943c048e6..ec92b92c96 100644 --- a/lib/views/hunk-header-view.js +++ b/lib/views/hunk-header-view.js @@ -6,6 +6,10 @@ import {autobind} from '../helpers'; import RefHolder from '../models/ref-holder'; import Tooltip from '../atom/tooltip'; +function theBuckStopsHere(event) { + event.stopPropagation(); +} + export default class HunkHeaderView extends React.Component { static propTypes = { hunk: PropTypes.object.isRequired, @@ -40,7 +44,10 @@ export default class HunkHeaderView extends React.Component { {this.props.hunk.getHeader().trim()} {this.props.hunk.getSectionHeading().trim()} - {this.props.stagingStatus === 'unstaged' && ( @@ -49,6 +56,7 @@ export default class HunkHeaderView extends React.Component { ref={this.refDiscardButton.setter} className="icon-trashcan github-HunkHeaderView-discardButton" onClick={this.props.discardSelection} + onMouseDown={theBuckStopsHere} /> Date: Wed, 25 Jul 2018 13:54:20 -0400 Subject: [PATCH 0036/4252] Let's put HunkView to the side --- lib/views/{hunk-view.js => hunk-view.old.js} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename lib/views/{hunk-view.js => hunk-view.old.js} (100%) diff --git a/lib/views/hunk-view.js b/lib/views/hunk-view.old.js similarity index 100% rename from lib/views/hunk-view.js rename to lib/views/hunk-view.old.js From 4d71aed29a1cbde92f4ff6c38915aa3d18fabd97 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 25 Jul 2018 14:04:21 -0400 Subject: [PATCH 0037/4252] Use diff to... generate diffs --- package-lock.json | 5 ++--- package.json | 1 + 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 210339e328..191f531911 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2360,8 +2360,7 @@ "diff": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", - "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", - "dev": true + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==" }, "discontinuous-range": { "version": "1.0.0", @@ -5488,7 +5487,7 @@ }, "lru-cache": { "version": "4.1.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.3.tgz", "integrity": "sha512-fFEhvcgzuIoJVUF8fYr5KR0YqxD238zgObTps31YdADwPPAp82a4M8TrckkWyx7ekNlf9aBcVn81cFwwXngrJA==", "requires": { "pseudomap": "^1.0.2", diff --git a/package.json b/package.json index d6737e4946..8e3f3fb420 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "bytes": "^3.0.0", "classnames": "2.2.6", "compare-sets": "1.0.1", + "diff": "3.5.0", "dugite": "^1.66.0", "event-kit": "2.5.0", "fs-extra": "4.0.3", From fe8d0f7eb11575ede0c3d0bf47a6108dedeac9e2 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 25 Jul 2018 14:18:25 -0400 Subject: [PATCH 0038/4252] On second thought don't --- package-lock.json | 3 ++- package.json | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 191f531911..167e2c0681 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2360,7 +2360,8 @@ "diff": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", - "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==" + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "dev": true }, "discontinuous-range": { "version": "1.0.0", diff --git a/package.json b/package.json index 8e3f3fb420..d6737e4946 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,6 @@ "bytes": "^3.0.0", "classnames": "2.2.6", "compare-sets": "1.0.1", - "diff": "3.5.0", "dugite": "^1.66.0", "event-kit": "2.5.0", "fs-extra": "4.0.3", From 40b225e1a4b96fbc56e815528a760f22b449778a Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 26 Jul 2018 08:39:58 -0400 Subject: [PATCH 0039/4252] Allow TextEditor text to be set with setTextViaDiff to preserve markers --- lib/atom/atom-text-editor.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/atom/atom-text-editor.js b/lib/atom/atom-text-editor.js index b2b77c276e..6f2665a032 100644 --- a/lib/atom/atom-text-editor.js +++ b/lib/atom/atom-text-editor.js @@ -43,6 +43,7 @@ export default class AtomTextEditor extends React.PureComponent { text: PropTypes.string, didChange: PropTypes.func, didChangeCursorPosition: PropTypes.func, + preserveMarkers: PropTypes.bool, children: PropTypes.node, } @@ -103,7 +104,12 @@ export default class AtomTextEditor extends React.PureComponent { quietlySetText(text) { this.suppressChange = true; try { - this.getModel().setText(text); + const editor = this.getModel(); + if (this.props.preserveMarkers) { + editor.getBuffer().setTextViaDiff(text); + } else { + editor.setText(text); + } } finally { this.suppressChange = false; } From 13950a933e5401d4f37dd19ccffc3aca22e195a9 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 26 Jul 2018 08:40:17 -0400 Subject: [PATCH 0040/4252] Accept a nameMap in extractProps to modify names --- lib/helpers.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/helpers.js b/lib/helpers.js index 5e7c020f11..cc572630ed 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -37,10 +37,11 @@ export function autobind(self, ...methods) { // } // } // ``` -export function extractProps(props, propTypes) { +export function extractProps(props, propTypes, nameMap = {}) { return Object.keys(propTypes).reduce((opts, propName) => { if (props[propName] !== undefined) { - opts[propName] = props[propName]; + const destPropName = nameMap[propName] || propName; + opts[destPropName] = props[propName]; } return opts; }, {}); From 3960cab7fba3fdb867af2636223c82052e57693e Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 26 Jul 2018 08:40:28 -0400 Subject: [PATCH 0041/4252] Remove unused state --- lib/atom/gutter.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/atom/gutter.js b/lib/atom/gutter.js index 34a29873cc..713a337d92 100644 --- a/lib/atom/gutter.js +++ b/lib/atom/gutter.js @@ -36,7 +36,6 @@ class BareGutter extends React.Component { this.state = { gutter: null, - editorComponent: null, }; this.sub = new Disposable(); From c7186fe777506698a88657ed1bb3a161fb20b1d1 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 26 Jul 2018 08:40:44 -0400 Subject: [PATCH 0042/4252] Update Decorations and Markers in place --- lib/atom/decoration.js | 59 ++++++++++++++++++++++++++---------------- lib/atom/marker.js | 55 ++++++++++++++++++++++++++++++++++----- 2 files changed, 85 insertions(+), 29 deletions(-) diff --git a/lib/atom/decoration.js b/lib/atom/decoration.js index 08ec2b901a..c9bf333a87 100644 --- a/lib/atom/decoration.js +++ b/lib/atom/decoration.js @@ -3,29 +3,37 @@ import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; import {Disposable} from 'event-kit'; -import {createItem, autobind} from '../helpers'; +import {createItem, autobind, extractProps} from '../helpers'; import {RefHolderPropType} from '../prop-types'; import {TextEditorContext} from './atom-text-editor'; import {DecorableContext} from './marker'; import RefHolder from '../models/ref-holder'; -class WrappedDecoration extends React.Component { +const decorationPropTypes = { + type: PropTypes.oneOf(['line', 'line-number', 'highlight', 'overlay', 'gutter', 'block']).isRequired, + className: PropTypes.string, + style: PropTypes.string, + onlyHead: PropTypes.bool, + onlyEmpty: PropTypes.bool, + onlyNonEmpty: PropTypes.bool, + omitEmptyLastRow: PropTypes.bool, + position: PropTypes.oneOf(['head', 'tail', 'before', 'after']), + avoidOverflow: PropTypes.bool, + gutterName: PropTypes.string, +}; + +class BareDecoration extends React.Component { static propTypes = { editorHolder: RefHolderPropType.isRequired, markerHolder: RefHolderPropType.isRequired, decorateMethod: PropTypes.oneOf(['decorateMarker', 'decorateMarkerLayer']), - type: PropTypes.oneOf(['line', 'line-number', 'highlight', 'overlay', 'gutter', 'block']).isRequired, - position: PropTypes.oneOf(['head', 'tail', 'before', 'after']), - className: PropTypes.string, - children: PropTypes.node, itemHolder: RefHolderPropType, - options: PropTypes.object, + children: PropTypes.node, + ...decorationPropTypes, } static defaultProps = { decorateMethod: 'decorateMarker', - options: {}, - position: 'head', } constructor(props, context) { @@ -64,6 +72,13 @@ class WrappedDecoration extends React.Component { this.markerSub.dispose(); this.markerSub = this.state.markerHolder.observe(this.observeParents); } + + if ( + Object.keys(decorationPropTypes).some(key => this.props[key] !== prevProps[key]) + ) { + const opts = this.getDecorationOpts(this.props); + this.decorationHolder.map(decoration => decoration.setProperties(opts)); + } } render() { @@ -78,6 +93,8 @@ class WrappedDecoration extends React.Component { } observeParents() { + this.decorationHolder.map(decoration => decoration.destroy()); + if (this.props.editorHolder.isEmpty() || this.props.markerHolder.isEmpty()) { return; } @@ -86,25 +103,16 @@ class WrappedDecoration extends React.Component { } createDecoration() { - this.decorationHolder.map(decoration => decoration.destroy()); - if (!this.item) { this.item = createItem(this.domNode, this.props.itemHolder); } - const options = { - ...this.props.options, - type: this.props.type, - position: this.props.position, - class: this.props.className, - item: this.item, - }; - + const opts = this.getDecorationOpts(this.props); const editor = this.props.editorHolder.get(); const marker = this.props.markerHolder.get(); this.decorationHolder.setter( - editor[this.props.decorateMethod](marker, options), + editor[this.props.decorateMethod](marker, opts), ); } @@ -113,6 +121,13 @@ class WrappedDecoration extends React.Component { this.editorSub.dispose(); this.markerSub.dispose(); } + + getDecorationOpts(props) { + return { + ...extractProps(props, decorationPropTypes, {className: 'class'}), + item: this.item, + }; + } } export default class Decoration extends React.Component { @@ -151,7 +166,7 @@ export default class Decoration extends React.Component { render() { if (!this.state.editorHolder.isEmpty() && !this.state.markerHolder.isEmpty()) { return ( - ( {({holder, decorateMethod}) => ( - prevProps[key] !== this.props[key])) { + this.markerHolder.map(marker => marker.setProperties(extractProps(this.props, markerProps))); } } @@ -124,6 +145,28 @@ class WrappedMarker extends React.Component { this.markerHolder.map(marker => this.props.handleID(marker.id)); } + + updateMarkerPosition() { + this.markerHolder.map(marker => { + if (this.props.bufferRange) { + return marker.setBufferRange(this.props.bufferRange); + } + + if (this.props.screenRange) { + return marker.setScreenRange(this.props.screenRange); + } + + if (this.props.bufferPosition) { + return marker.setBufferRange(new Range(this.props.bufferPosition, this.props.bufferPosition)); + } + + if (this.props.screenPosition) { + return marker.setScreenRange(new Range(this.props.screenPosition, this.props.screenPosition)); + } + + throw new Error('Expected one of bufferRange, screenRange, bufferPosition, or screenPosition to be set'); + }); + } } export default class Marker extends React.Component { @@ -154,25 +197,23 @@ export default class Marker extends React.Component { render() { if (!this.state.markableHolder.isEmpty()) { - return ; + return ; } - /* eslint-disable react/jsx-key */ return ( {layerHolder => { if (layerHolder) { - return ; + return ; } else { return ( - {editorHolder => } + {editorHolder => } ); } }} ); - /* eslint-enable react/jsx-key */ } } From 401d662114d577addb6227c4b40b9a61bc7b9d75 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 26 Jul 2018 08:41:33 -0400 Subject: [PATCH 0043/4252] Update FilePatchView uses of Marker, AtomTextEditor, and Decoration --- lib/views/file-patch-view.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/views/file-patch-view.js b/lib/views/file-patch-view.js index 2a879728aa..fa9bdfeae4 100644 --- a/lib/views/file-patch-view.js +++ b/lib/views/file-patch-view.js @@ -122,9 +122,11 @@ export default class FilePatchView extends React.Component {
+ + + {positions.map(position => { return ; })} @@ -347,8 +349,8 @@ export default class FilePatchView extends React.Component { {line && } {gutter && ( - - + + )} From 3ead0884c127d7cbec7673eaa3122a3a8ad7bab2 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 26 Jul 2018 11:29:21 -0400 Subject: [PATCH 0044/4252] Update AtomTextEditor's text before children render --- lib/atom/atom-text-editor.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/lib/atom/atom-text-editor.js b/lib/atom/atom-text-editor.js index 6f2665a032..b2f07a5221 100644 --- a/lib/atom/atom-text-editor.js +++ b/lib/atom/atom-text-editor.js @@ -65,6 +65,8 @@ export default class AtomTextEditor extends React.PureComponent { } render() { + this.refModel.map(() => this.quietlySetText(this.props.text)); + return ( @@ -80,6 +82,7 @@ export default class AtomTextEditor extends React.PureComponent { this.refElement.map(element => { const editor = element.getModel(); + editor.setText(this.props.text); this.refModel.setter(editor); @@ -105,6 +108,10 @@ export default class AtomTextEditor extends React.PureComponent { this.suppressChange = true; try { const editor = this.getModel(); + if (editor.getText() === text) { + return; + } + if (this.props.preserveMarkers) { editor.getBuffer().setTextViaDiff(text); } else { @@ -117,13 +124,7 @@ export default class AtomTextEditor extends React.PureComponent { setAttributesOnElement(theProps) { const modelProps = extractProps(this.props, editorProps); - - const editor = this.getModel(); - editor.update(modelProps); - - if (editor.getText() !== theProps.text) { - this.quietlySetText(theProps.text); - } + this.getModel().update(modelProps); } didChange() { From 54010cda45d1794a2f1eb6826d8756e30e5d9a62 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 26 Jul 2018 11:30:53 -0400 Subject: [PATCH 0045/4252] Update component naming for consistency --- lib/atom/marker-layer.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/atom/marker-layer.js b/lib/atom/marker-layer.js index 8793c3ee50..9c483d7582 100644 --- a/lib/atom/marker-layer.js +++ b/lib/atom/marker-layer.js @@ -14,7 +14,7 @@ const markerLayerProps = { export const MarkerLayerContext = React.createContext(); -class WrappedMarkerLayer extends React.Component { +class BareMarkerLayer extends React.Component { static propTypes = { ...markerLayerProps, editor: PropTypes.object, @@ -88,7 +88,6 @@ class WrappedMarkerLayer extends React.Component { const options = extractProps(this.props, markerLayerProps); - this.layerHolder.setter( this.state.editorHolder.map(editor => editor.addMarkerLayer(options)).getOr(null), ); @@ -104,7 +103,7 @@ export default class MarkerLayer extends React.Component { render() { return ( - {editor => } + {editor => } ); } From 00bb1fc6a4426e97667a2e04a6759b303233c033 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 26 Jul 2018 11:31:11 -0400 Subject: [PATCH 0046/4252] Destroy empty MarkerLayers --- lib/views/file-patch-view.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/views/file-patch-view.js b/lib/views/file-patch-view.js index fa9bdfeae4..8bf69343e7 100644 --- a/lib/views/file-patch-view.js +++ b/lib/views/file-patch-view.js @@ -340,6 +340,10 @@ export default class FilePatchView extends React.Component { } renderLineDecorations(positions, lineClass, {line, gutter}) { + if (positions.length === 0) { + return null; + } + return ( {positions.map(position => { From 5b9cd7d3f7f25cab5d7a54ff7c1e593e4654ad58 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 26 Jul 2018 11:31:37 -0400 Subject: [PATCH 0047/4252] Use Constructor.fromObject instead of new Constructor --- lib/atom/marker.js | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/lib/atom/marker.js b/lib/atom/marker.js index 1e1fcad17a..1324cb1b06 100644 --- a/lib/atom/marker.js +++ b/lib/atom/marker.js @@ -35,6 +35,17 @@ export const MarkerContext = React.createContext(); export const DecorableContext = React.createContext(); +// Compare Range or Point compatible props +function changed(Constructor, from, to) { + if (from == null && to == null) { + return false; + } else if (from == null || to == null) { + return true; + } else { + return !Constructor.fromObject(from).isEqual(to); + } +} + class BareMarker extends React.Component { static propTypes = { ...markerProps, @@ -80,16 +91,6 @@ class BareMarker extends React.Component { } componentDidUpdate(prevProps) { - function changed(Constructor, from, to) { - if (!from && !to) { - return false; - } else if (!from || !to) { - return true; - } else { - return !(new Constructor(from).isEqual(to)); - } - } - if (prevProps.markableHolder !== this.props.markableHolder) { this.observeMarkable(); } else if ( From 2bfe0d6f2c3b5a8f9110394d869bf186d2f6c4c1 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 27 Jul 2018 09:42:33 -0400 Subject: [PATCH 0048/4252] Style the selected cursor line --- styles/file-patch-view.less | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/styles/file-patch-view.less b/styles/file-patch-view.less index 193a97d759..589dae1269 100644 --- a/styles/file-patch-view.less +++ b/styles/file-patch-view.less @@ -152,6 +152,11 @@ .hunk-line-mixin(@fg; @bg) { color: saturate( mix(@fg, @text-color-highlight, 20%), 20%); background-color: saturate( mix(@bg, @hunk-bg-color, 15%), 20%); + + &.line.cursor-line { + color: saturate( mix(@fg, @text-color-highlight, 20%), 80%); + background-color: saturate( mix(@bg, @hunk-bg-color, 15%), 80%); + } } &--deleted { From a1e76166bc39b7ec0884262fd6259d0190cf8bf5 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 27 Jul 2018 09:47:09 -0400 Subject: [PATCH 0049/4252] MarkerPosition model for different ways markers can be positioned --- lib/models/marker-position.js | 155 ++++++++++++++++++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 lib/models/marker-position.js diff --git a/lib/models/marker-position.js b/lib/models/marker-position.js new file mode 100644 index 0000000000..7e9989f1ca --- /dev/null +++ b/lib/models/marker-position.js @@ -0,0 +1,155 @@ +import {Range} from 'atom'; + +class Position { + markOn(markable, options) { + throw new Error('markOn not overridden'); + } + + setIn(marker) { + throw new Error('setIn not overridden'); + } + + matches(other) { + throw new Error('matches not overridden'); + } + + matchFromBufferRange(other) { + return false; + } + + matchFromScreenRange(other) { + return false; + } +} + +class BufferRangePosition extends Position { + constructor(bufferRange) { + super(); + this.bufferRange = Range.fromObject(bufferRange); + } + + markOn(markable, options) { + return markable.markBufferRange(this.bufferRange, options); + } + + setIn(marker) { + return marker.setBufferRange(this.bufferRange); + } + + matches(other) { + return other.matchFromBufferRange(this); + } + + matchFromBufferRange(other) { + return other.bufferRange.isEqual(this.bufferRange); + } + + toString() { + return `buffer(${this.bufferRange.toString()})`; + } +} + +class ScreenRangePosition extends Position { + constructor(screenRange) { + super(); + this.screenRange = screenRange; + } + + markOn(markable, options) { + return markable.markScreenRange(this.screenRange, options); + } + + setIn(marker) { + return marker.setScreenRange(this.screenRange); + } + + matches(other) { + return other.matchFromScreenRange(this); + } + + matchFromScreenRange(other) { + return other.screenRange.isEqual(this.screenRange); + } + + toString() { + return `screen(${this.screenRange.toString()})`; + } +} + +class BufferOrScreenRangePosition extends Position { + constructor(bufferRange, screenRange) { + super(); + this.bufferRange = bufferRange; + this.screenRange = screenRange; + } + + markOn(markable, options) { + return markable.markBufferRange(this.bufferRange); + } + + setIn(marker) { + return marker.setBufferRange(this.bufferRange); + } + + matches(other) { + return other.matchFromBufferRange(this) || other.matchFromScreenRange(this); + } + + matchFromBufferRange(other) { + return other.bufferRange.isEqual(this.bufferRange); + } + + matchFromScreenRange(other) { + return other.screenRange.isEqual(this.screenRange); + } + + toString() { + return `either(b${this.bufferRange.toString()}/s${this.screenRange.toString()})`; + } +} + +export function fromBufferRange(bufferRange) { + return new BufferRangePosition(Range.fromObject(bufferRange)); +} + +export function fromBufferPosition(bufferPoint) { + return new BufferRangePosition(new Range(bufferPoint, bufferPoint)); +} + +export function fromScreenRange(screenRange) { + return new ScreenRangePosition(Range.fromObject(screenRange)); +} + +export function fromScreenPosition(screenPoint) { + return new ScreenRangePosition(new Range(screenPoint, screenPoint)); +} + +export function fromMarker(marker) { + return new BufferOrScreenRangePosition( + marker.getBufferRange(), + marker.getScreenRange(), + ); +} + +export function fromChangeEvent(event, reversed = false) { + const oldBufferStartPosition = reversed ? event.oldHeadBufferPosition : event.oldTailBufferPosition; + const oldBufferEndPosition = reversed ? event.oldTailBufferPosition : event.oldHeadBufferPosition; + const oldScreenStartPosition = reversed ? event.oldHeadScreenPosition : event.oldTailScreenPosition; + const oldScreenEndPosition = reversed ? event.oldTailScreenPosition : event.oldHeadScreenPosition; + + const newBufferStartPosition = reversed ? event.newHeadBufferPosition : event.newTailBufferPosition; + const newBufferEndPosition = reversed ? event.newTailBufferPosition : event.newHeadBufferPosition; + const newScreenStartPosition = reversed ? event.newHeadScreenPosition : event.newTailScreenPosition; + const newScreenEndPosition = reversed ? event.newTailScreenPosition : event.newHeadScreenPosition; + + return { + oldPosition: new BufferOrScreenRangePosition( + new Range(oldBufferStartPosition, oldBufferEndPosition), + new Range(oldScreenStartPosition, oldScreenEndPosition), + ), + newPosition: new BufferOrScreenRangePosition( + new Range(newBufferStartPosition, newBufferEndPosition), + new Range(newScreenStartPosition, newScreenEndPosition), + ), + }; +} From d760888c3027f9cc714725a74274a50d033397fc Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 27 Jul 2018 09:47:30 -0400 Subject: [PATCH 0050/4252] Consume MarkerPosition in --- lib/atom/marker.js | 124 +++++++++++++++++---------------------------- lib/prop-types.js | 6 +++ 2 files changed, 52 insertions(+), 78 deletions(-) diff --git a/lib/atom/marker.js b/lib/atom/marker.js index 1324cb1b06..f6ba3819fe 100644 --- a/lib/atom/marker.js +++ b/lib/atom/marker.js @@ -1,11 +1,11 @@ import React from 'react'; import PropTypes from 'prop-types'; -import {Disposable} from 'event-kit'; -import {Range, Point} from 'atom'; +import {CompositeDisposable} from 'event-kit'; import {autobind, extractProps} from '../helpers'; -import {RefHolderPropType} from '../prop-types'; +import {RefHolderPropType, MarkerPositionPropType} from '../prop-types'; import RefHolder from '../models/ref-holder'; +import {fromChangeEvent, fromMarker} from '../models/marker-position'; import {TextEditorContext} from './atom-text-editor'; import {MarkerLayerContext} from './marker-layer'; @@ -16,16 +16,6 @@ const MarkablePropType = PropTypes.shape({ markScreenPosition: PropTypes.func.isRequired, }); -const RangePropType = PropTypes.oneOfType([ - PropTypes.array, - PropTypes.instanceOf(Range), -]); - -const PointPropType = PropTypes.oneOfType([ - PropTypes.array, - PropTypes.instanceOf(Point), -]); - const markerProps = { reversed: PropTypes.bool, invalidate: PropTypes.oneOf(['never', 'surround', 'overlap', 'inside', 'touch']), @@ -35,39 +25,27 @@ export const MarkerContext = React.createContext(); export const DecorableContext = React.createContext(); -// Compare Range or Point compatible props -function changed(Constructor, from, to) { - if (from == null && to == null) { - return false; - } else if (from == null || to == null) { - return true; - } else { - return !Constructor.fromObject(from).isEqual(to); - } -} - class BareMarker extends React.Component { static propTypes = { ...markerProps, - bufferRange: RangePropType, - bufferPosition: PointPropType, - screenRange: RangePropType, - screenPosition: PointPropType, + position: MarkerPositionPropType.isRequired, markableHolder: RefHolderPropType, children: PropTypes.node, + onDidChange: PropTypes.func, handleID: PropTypes.func, } static defaultProps = { + onDidChange: () => {}, handleID: () => {}, } constructor(props) { super(props); - autobind(this, 'createMarker'); + autobind(this, 'createMarker', 'didChange'); - this.sub = new Disposable(); + this.subs = new CompositeDisposable(); this.markerHolder = new RefHolder(); this.decorable = { @@ -81,6 +59,7 @@ class BareMarker extends React.Component { } render() { + this.updateMarkerPosition(); return ( @@ -93,13 +72,6 @@ class BareMarker extends React.Component { componentDidUpdate(prevProps) { if (prevProps.markableHolder !== this.props.markableHolder) { this.observeMarkable(); - } else if ( - changed(Range, prevProps.bufferRange, this.props.bufferRange) || - changed(Point, prevProps.bufferPosition, this.props.bufferPosition) || - changed(Range, prevProps.screenRange, this.props.screenRange) || - changed(Point, prevProps.screenPosition, this.props.screenPosition) - ) { - this.updateMarkerPosition(); } if (Object.keys(markerProps).some(key => prevProps[key] !== this.props[key])) { @@ -108,64 +80,60 @@ class BareMarker extends React.Component { } componentWillUnmount() { + this.markerHolder.map(marker => console.log(`destroying marker ${marker.id} due to unmount`)); this.markerHolder.map(marker => marker.destroy()); - this.sub.dispose(); + this.subs.dispose(); } observeMarkable() { - this.sub.dispose(); - this.sub = this.props.markableHolder.observe(this.createMarker); + this.subs.dispose(); + this.subs = new CompositeDisposable( + this.props.markableHolder.observe(this.createMarker), + ); } createMarker() { + this.markerHolder.map(marker => console.log(`destroying marker ${marker.id} to create a new one`)); this.markerHolder.map(marker => marker.destroy()); const options = extractProps(this.props, markerProps); - this.markerHolder.setter( - this.props.markableHolder.map(markable => { - if (this.props.bufferRange) { - return markable.markBufferRange(this.props.bufferRange, options); - } - - if (this.props.screenRange) { - return markable.markScreenRange(this.props.screenRange, options); - } - - if (this.props.bufferPosition) { - return markable.markBufferPosition(this.props.bufferPosition, options); - } - - if (this.props.screenPosition) { - return markable.markScreenPosition(this.props.screenPosition, options); - } - - throw new Error('Expected one of bufferRange, screenRange, bufferPosition, or screenPosition to be set'); - }).getOr(null), - ); - - this.markerHolder.map(marker => this.props.handleID(marker.id)); + this.props.markableHolder.map(markable => { + const marker = this.props.position.markOn(markable, options); + const markableKind = markable.constructor.name.toLowerCase().replace(/^display/, ''); + console.log(`created marker ${marker.id} on ${markableKind} ${markable.id}`); + + this.subs.add( + marker.onDidChange(this.didChange), + marker.onDidChange(() => { + console.log(`marker ${marker.id} was changed to ${marker.getBufferRange().toString()}`); + return null; + }), + ); + this.markerHolder.setter(marker); + this.props.handleID(marker.id); + return null; + }); } updateMarkerPosition() { this.markerHolder.map(marker => { - if (this.props.bufferRange) { - return marker.setBufferRange(this.props.bufferRange); - } - - if (this.props.screenRange) { - return marker.setScreenRange(this.props.screenRange); - } - - if (this.props.bufferPosition) { - return marker.setBufferRange(new Range(this.props.bufferPosition, this.props.bufferPosition)); - } - - if (this.props.screenPosition) { - return marker.setScreenRange(new Range(this.props.screenPosition, this.props.screenPosition)); + const before = fromMarker(marker); + const after = this.props.position; + if (!before.matches(after)) { + console.log( + `updating marker ${marker.id} from ${before.toString()} to ${after.toString()}`, + ); } + this.props.position.setIn(marker); + return null; + }); + } - throw new Error('Expected one of bufferRange, screenRange, bufferPosition, or screenPosition to be set'); + didChange(event) { + this.props.onDidChange({ + ...fromChangeEvent(event), + ...event, }); } } diff --git a/lib/prop-types.js b/lib/prop-types.js index 5ea4e20523..d8fa6c1ca6 100644 --- a/lib/prop-types.js +++ b/lib/prop-types.js @@ -89,6 +89,12 @@ export const RefHolderPropType = PropTypes.shape({ observe: PropTypes.func.isRequired, }); +export const MarkerPositionPropType = PropTypes.shape({ + markOn: PropTypes.func.isRequired, + setIn: PropTypes.func.isRequired, + matches: PropTypes.func.isRequired, +}); + export const EnableableOperationPropType = PropTypes.shape({ isEnabled: PropTypes.func.isRequired, run: PropTypes.func.isRequired, From b8b66937c1ad7a16d0a2de1f2898dab39d4e225d Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 27 Jul 2018 09:47:56 -0400 Subject: [PATCH 0051/4252] Update callsites --- lib/models/presented-file-patch.js | 7 ++++--- test/atom/marker.test.js | 13 +++++++------ 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/lib/models/presented-file-patch.js b/lib/models/presented-file-patch.js index 02a5b2ba09..871190dfc2 100644 --- a/lib/models/presented-file-patch.js +++ b/lib/models/presented-file-patch.js @@ -1,4 +1,5 @@ import {Point} from 'atom'; +import {fromBufferPosition} from './marker-position'; export default class PresentedFilePatch { constructor(filePatch) { @@ -18,7 +19,7 @@ export default class PresentedFilePatch { let bufferLine = 0; this.text = filePatch.getHunks().reduce((str, hunk) => { - this.hunkStartPositions.push(new Point(bufferLine, 0)); + this.hunkStartPositions.push(fromBufferPosition(new Point(bufferLine, 0))); return hunk.getLines().reduce((hunkStr, line) => { hunkStr += line.getText() + '\n'; @@ -34,7 +35,7 @@ export default class PresentedFilePatch { } this.bufferPositions[line.getStatus()].push( - new Point(bufferLine, 0), + fromBufferPosition(new Point(bufferLine, 0)), ); bufferLine++; @@ -94,7 +95,7 @@ export default class PresentedFilePatch { if (bufferRow === undefined) { return null; } - return new Point(bufferRow, 0); + return new fromBufferPosition(new Point(bufferRow, 0)); } getMaxLineNumberWidth() { diff --git a/test/atom/marker.test.js b/test/atom/marker.test.js index b965b0018d..5ca555f438 100644 --- a/test/atom/marker.test.js +++ b/test/atom/marker.test.js @@ -4,6 +4,7 @@ import {mount} from 'enzyme'; import Marker from '../../lib/atom/marker'; import AtomTextEditor from '../../lib/atom/atom-text-editor'; import MarkerLayer from '../../lib/atom/marker-layer'; +import {fromBufferRange, fromScreenRange, fromBufferPosition} from '../../lib/models/marker-position'; describe('Marker', function() { let atomEnv, editor, markerID; @@ -23,7 +24,7 @@ describe('Marker', function() { it('adds its marker on mount with default properties', function() { mount( - , + , ); const marker = editor.getMarker(markerID); @@ -37,7 +38,7 @@ describe('Marker', function() { , ); @@ -67,7 +68,7 @@ describe('Marker', function() { }); it('destroys its marker on unmount', function() { - const wrapper = mount(); + const wrapper = mount(); assert.isDefined(editor.getMarker(markerID)); wrapper.unmount(); @@ -77,7 +78,7 @@ describe('Marker', function() { it('marks an editor from a parent node', function() { const wrapper = mount( - + , ); @@ -91,7 +92,7 @@ describe('Marker', function() { const wrapper = mount( { layerID = id; }}> - + , ); From 44a650133c7c8427478941005f7c0a4f03ef8fa4 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 27 Jul 2018 09:52:16 -0400 Subject: [PATCH 0052/4252] In which invalidate="never" solves all of my problems forever --- lib/views/file-patch-view.js | 34 ++++++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/lib/views/file-patch-view.js b/lib/views/file-patch-view.js index 8bf69343e7..9ba6dd3bd2 100644 --- a/lib/views/file-patch-view.js +++ b/lib/views/file-patch-view.js @@ -11,6 +11,7 @@ import Gutter from '../atom/gutter'; import FilePatchHeaderView from './file-patch-header-view'; import FilePatchMetaView from './file-patch-meta-view'; import HunkHeaderView from './hunk-header-view'; +import {fromBufferPosition} from '../models/marker-position'; const executableText = { 100644: 'non executable', @@ -146,7 +147,7 @@ export default class FilePatchView extends React.Component { onMouseMove={this.didMouseMoveOnLineNumber} /> - + {this.renderExecutableModeChangeMeta()} @@ -305,6 +306,7 @@ export default class FilePatchView extends React.Component { const selectedHunks = this.state.selectedHunks; const isHunkSelectionMode = this.props.selection.getMode() === 'hunk'; const toggleVerb = this.props.stagingStatus === 'unstaged' ? 'Stage' : 'Unstage'; + const hunkStartPositions = this.state.presentedFilePatch.getHunkStartPositions(); return this.props.filePatch.getHunks().map((hunk, index) => { const isSelected = selectedHunks.has(hunk); @@ -314,10 +316,18 @@ export default class FilePatchView extends React.Component { } const toggleSelectionLabel = `${toggleVerb}${buttonSuffix}`; const discardSelectionLabel = `Discard${buttonSuffix}`; - const bufferPosition = this.state.presentedFilePatch.getHunkStartPositions()[index]; + + const onDidChange = event => { + hunkStartPositions[index] = event.newPosition; + }; return ( - + + + ); }); @@ -345,9 +356,20 @@ export default class FilePatchView extends React.Component { } return ( - - {positions.map(position => { - return ; + + {positions.map((position, index) => { + const onDidChange = event => { + positions[index] = event.newPosition; + }; + + return ( + + ); })} {line && } From bde08098d2089769c4206363007a43d17ad57410 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 27 Jul 2018 09:52:26 -0400 Subject: [PATCH 0053/4252] Use MarkerPositions in Decoration tests --- test/atom/decoration.test.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/atom/decoration.test.js b/test/atom/decoration.test.js index 441c5da836..6be94ab9ff 100644 --- a/test/atom/decoration.test.js +++ b/test/atom/decoration.test.js @@ -6,6 +6,7 @@ import Decoration from '../../lib/atom/decoration'; import AtomTextEditor from '../../lib/atom/atom-text-editor'; import Marker from '../../lib/atom/marker'; import MarkerLayer from '../../lib/atom/marker-layer'; +import {fromBufferRange, fromBufferPosition} from '../../lib/models/marker-position'; describe('Decoration', function() { let atomEnv, editor, marker; @@ -118,7 +119,7 @@ describe('Decoration', function() { it('decorates a parent Marker', function() { const wrapper = mount( - + , @@ -132,7 +133,7 @@ describe('Decoration', function() { mount( - + , From 7a5fe5a1125a549293a346381fac19f85ab628a0 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 27 Jul 2018 10:23:01 -0400 Subject: [PATCH 0054/4252] :fire: logging in --- lib/atom/marker.js | 28 ++++------------------------ 1 file changed, 4 insertions(+), 24 deletions(-) diff --git a/lib/atom/marker.js b/lib/atom/marker.js index f6ba3819fe..5bf575ba8f 100644 --- a/lib/atom/marker.js +++ b/lib/atom/marker.js @@ -5,7 +5,7 @@ import {CompositeDisposable} from 'event-kit'; import {autobind, extractProps} from '../helpers'; import {RefHolderPropType, MarkerPositionPropType} from '../prop-types'; import RefHolder from '../models/ref-holder'; -import {fromChangeEvent, fromMarker} from '../models/marker-position'; +import {fromChangeEvent} from '../models/marker-position'; import {TextEditorContext} from './atom-text-editor'; import {MarkerLayerContext} from './marker-layer'; @@ -80,7 +80,6 @@ class BareMarker extends React.Component { } componentWillUnmount() { - this.markerHolder.map(marker => console.log(`destroying marker ${marker.id} due to unmount`)); this.markerHolder.map(marker => marker.destroy()); this.subs.dispose(); } @@ -93,23 +92,14 @@ class BareMarker extends React.Component { } createMarker() { - this.markerHolder.map(marker => console.log(`destroying marker ${marker.id} to create a new one`)); this.markerHolder.map(marker => marker.destroy()); const options = extractProps(this.props, markerProps); this.props.markableHolder.map(markable => { const marker = this.props.position.markOn(markable, options); - const markableKind = markable.constructor.name.toLowerCase().replace(/^display/, ''); - console.log(`created marker ${marker.id} on ${markableKind} ${markable.id}`); - - this.subs.add( - marker.onDidChange(this.didChange), - marker.onDidChange(() => { - console.log(`marker ${marker.id} was changed to ${marker.getBufferRange().toString()}`); - return null; - }), - ); + + this.subs.add(marker.onDidChange(this.didChange)); this.markerHolder.setter(marker); this.props.handleID(marker.id); return null; @@ -117,17 +107,7 @@ class BareMarker extends React.Component { } updateMarkerPosition() { - this.markerHolder.map(marker => { - const before = fromMarker(marker); - const after = this.props.position; - if (!before.matches(after)) { - console.log( - `updating marker ${marker.id} from ${before.toString()} to ${after.toString()}`, - ); - } - this.props.position.setIn(marker); - return null; - }); + this.markerHolder.map(marker => this.props.position.setIn(marker)); } didChange(event) { From 751f2d8b9e44f830d6859e76b8be0039e8e89071 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 27 Jul 2018 11:14:30 -0400 Subject: [PATCH 0055/4252] Propagate getRealItemPromise through StubItems and createItems --- lib/items/stub-item.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/items/stub-item.js b/lib/items/stub-item.js index 7876428134..8dd42e1a89 100644 --- a/lib/items/stub-item.js +++ b/lib/items/stub-item.js @@ -51,7 +51,13 @@ export default class StubItem { setRealItem(item) { this.realItem = item; - this.resolveRealItemPromise(); + + if (this.realItem.getRealItemPromise) { + this.realItem.getRealItemPromise().then(this.resolveRealItemPromise); + } else { + this.resolveRealItemPromise(this.realItem); + } + this.emitter.emit('did-change-title'); this.emitter.emit('did-change-icon'); @@ -108,6 +114,7 @@ export default class StubItem { } destroy() { + this.resolveRealItemPromise(null); this.subscriptions.dispose(); this.emitter.dispose(); if (this.realItem) { From 020bff318778f77ee946838dcc45a7a7f4812409 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 27 Jul 2018 11:14:58 -0400 Subject: [PATCH 0056/4252] Default workingDirectoryPath to null --- lib/containers/git-tab-container.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/containers/git-tab-container.js b/lib/containers/git-tab-container.js index b4414813e1..e8ddef3cf7 100644 --- a/lib/containers/git-tab-container.js +++ b/lib/containers/git-tab-container.js @@ -18,7 +18,7 @@ const DEFAULT_REPO_DATA = { unstagedChanges: [], stagedChanges: [], mergeConflicts: [], - workingDirectoryPath: '', + workingDirectoryPath: null, mergeMessage: null, fetchInProgress: true, }; From 157d19837c7fdc04f45f3b230c1f9f217747a48c Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 27 Jul 2018 11:15:47 -0400 Subject: [PATCH 0057/4252] Detect activated patch items through stubs and proxies --- lib/items/file-patch-item.js | 6 +++++- lib/views/staging-view.js | 11 ++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/lib/items/file-patch-item.js b/lib/items/file-patch-item.js index 65e23e70ce..d7d735500a 100644 --- a/lib/items/file-patch-item.js +++ b/lib/items/file-patch-item.js @@ -83,7 +83,7 @@ export default class FilePatchItem extends React.Component { serialize() { return { deserializer: 'FilePatchControllerStub', - uri: this.getURI(), + uri: FilePatchItem.buildURI(this.props.relPath, this.props.workingDirectory, this.props.stagingStatus), }; } @@ -98,4 +98,8 @@ export default class FilePatchItem extends React.Component { getWorkingDirectory() { return this.props.workingDirectory; } + + isFilePatchItem() { + return true; + } } diff --git a/lib/views/staging-view.js b/lib/views/staging-view.js index 1b563d77d6..f40407c04d 100644 --- a/lib/views/staging-view.js +++ b/lib/views/staging-view.js @@ -530,21 +530,22 @@ export default class StagingView extends React.Component { } } - syncWithWorkspace() { + async syncWithWorkspace() { const item = this.props.workspace.getActivePaneItem(); if (!item) { return; } - const realItem = item.getRealItem && item.getRealItem(); + const realItemPromise = item.getRealItemPromise && item.getRealItemPromise(); + const realItem = await realItemPromise; if (!realItem) { return; } - const isFilePatchController = realItem instanceof FilePatchItem; - const isMatch = realItem.getWorkingDirectory && item.getWorkingDirectory() === this.props.workingDirectoryPath; + const isFilePatchItem = realItem.isFilePatchItem && realItem.isFilePatchItem(); + const isMatch = realItem.getWorkingDirectory && realItem.getWorkingDirectory() === this.props.workingDirectoryPath; - if (isFilePatchController && isMatch) { + if (isFilePatchItem && isMatch) { this.quietlySelectItem(realItem.getFilePath(), realItem.getStagingStatus()); } } From dc11bcf6cdbd65e75468f03f79efa8688e4b6cbc Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 27 Jul 2018 11:16:22 -0400 Subject: [PATCH 0058/4252] Sync StagingView with workspace the first time it is populated --- lib/views/staging-view.js | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/lib/views/staging-view.js b/lib/views/staging-view.js index f40407c04d..b3081339a0 100644 --- a/lib/views/staging-view.js +++ b/lib/views/staging-view.js @@ -150,6 +150,10 @@ export default class StagingView extends React.Component { this.syncWithWorkspace(); }), ); + + if (this.isPopulated(this.props)) { + this.syncWithWorkspace(); + } } componentDidUpdate(prevProps, prevState) { @@ -170,6 +174,10 @@ export default class StagingView extends React.Component { element.scrollIntoViewIfNeeded(); } } + + if (!this.isPopulated(prevProps) && this.isPopulated(this.props)) { + this.syncWithWorkspace(); + } } render() { @@ -600,7 +608,7 @@ export default class StagingView extends React.Component { quietlySelectItem(filePath, stagingStatus) { return new Promise(resolve => { this.setState(prevState => { - const item = this.state.selection.findItem((each, key) => each.filePath === filePath && key === stagingStatus); + const item = prevState.selection.findItem((each, key) => each.filePath === filePath && key === stagingStatus); if (!item) { // FIXME: make staging view display no selected item // eslint-disable-next-line no-console @@ -821,4 +829,12 @@ export default class StagingView extends React.Component { hasFocus() { return this.refRoot.contains(document.activeElement); } + + isPopulated(props) { + return props.workingDirectoryPath != null && ( + props.unstagedChanges.length > 0 || + props.mergeConflicts.length > 0 || + props.stagedChanges.length > 0 + ); + } } From 537753a48f379280b0c30f39fa8c4a9509cb4d3e Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 27 Jul 2018 13:36:43 -0400 Subject: [PATCH 0059/4252] Center the "no content" message --- lib/containers/file-patch-container.js | 4 ++-- styles/file-patch-view.less | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/containers/file-patch-container.js b/lib/containers/file-patch-container.js index 8bb5f11645..a5040a61db 100644 --- a/lib/containers/file-patch-container.js +++ b/lib/containers/file-patch-container.js @@ -61,8 +61,8 @@ export default class FilePatchContainer extends React.Component { renderEmptyPatchMessage() { return ( -
- No changes to display +
+

No changes to display

); } diff --git a/styles/file-patch-view.less b/styles/file-patch-view.less index 589dae1269..4e4b4ff3e6 100644 --- a/styles/file-patch-view.less +++ b/styles/file-patch-view.less @@ -12,6 +12,11 @@ min-width: 0; height: 100%; + &.is-blank { + text-align: center; + justify-content: center; + } + &-header { display: flex; justify-content: space-between; From 5d7dfbf104785c930b25c7d3afad859bdbb9afb7 Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Mon, 30 Jul 2018 01:45:45 +0000 Subject: [PATCH 0060/4252] chore(package): update relay-compiler to version 1.6.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fc5ec8198b..89296e4c5d 100644 --- a/package.json +++ b/package.json @@ -88,7 +88,7 @@ "mocha-stress": "1.0.0", "node-fetch": "2.2.0", "nyc": "13.0.0", - "relay-compiler": "1.6.0", + "relay-compiler": "1.6.1", "sinon": "6.0.1", "test-until": "1.1.1" }, From 56eef351c4a167f34158cca9f19addfbe3fb5de1 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 30 Jul 2018 08:54:59 -0400 Subject: [PATCH 0061/4252] :lock: --- package-lock.json | 130 +++++++++++++++++++++++++++++++--------------- 1 file changed, 88 insertions(+), 42 deletions(-) diff --git a/package-lock.json b/package-lock.json index 76b879b8b3..ceebc1c79e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -178,13 +178,19 @@ "@mrmlnc/readdir-enhanced": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz", - "integrity": "sha1-UkryQNGjYFJ7cwR17PoTRKpUDd4=", + "integrity": "sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g==", "dev": true, "requires": { "call-me-maybe": "^1.0.1", "glob-to-regexp": "^0.3.0" } }, + "@nodelib/fs.stat": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.0.tgz", + "integrity": "sha512-LAQ1d4OPfSJ/BMbI2DuizmYrrkD9JMaTdi2hQTlI53lQ4kRQPyZQRS4CYQ7O66bnBBnP/oYdRxbk++X0xuFU6A==", + "dev": true + }, "@sinonjs/formatio": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-2.0.0.tgz", @@ -1407,9 +1413,9 @@ } }, "babel-preset-fbjs": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/babel-preset-fbjs/-/babel-preset-fbjs-2.1.4.tgz", - "integrity": "sha1-IvNY5mVAc6z2HkegUqd317zPA68=", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-fbjs/-/babel-preset-fbjs-2.2.0.tgz", + "integrity": "sha512-jj0KFJDioYZMtPtZf77dQuU+Ad/1BtN0UnAYlHDa8J8f4tGXr3YrPoJImD5MdueaOPeN/jUdrCgu330EfXr0XQ==", "dev": true, "requires": { "babel-plugin-check-es2015-constants": "^6.8.0", @@ -3094,12 +3100,13 @@ "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=" }, "fast-glob": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-2.2.1.tgz", - "integrity": "sha512-wSyW1TBK3ia5V+te0rGPXudeMHoUQW6O5Y9oATiaGhpENmEifPDlOdhpsnlj5HoG6ttIvGiY1DdCmI9X2xGMhg==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-2.2.2.tgz", + "integrity": "sha512-TR6zxCKftDQnUAPvkrCWdBgDq/gbqx8A3ApnBrR5rMvpp6+KMJI0Igw7fkWPgeVK0uhRXTXdvO3O+YP0CaUX2g==", "dev": true, "requires": { "@mrmlnc/readdir-enhanced": "^2.2.1", + "@nodelib/fs.stat": "^1.0.1", "glob-parent": "^3.1.0", "is-glob": "^4.0.0", "merge2": "^1.2.1", @@ -3514,9 +3521,9 @@ } }, "graphql-compiler": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/graphql-compiler/-/graphql-compiler-1.6.0.tgz", - "integrity": "sha1-JPFGzfiLgOBFMipXKhpdhLnCqdU=", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/graphql-compiler/-/graphql-compiler-1.6.1.tgz", + "integrity": "sha512-pDZkKjNHqp63qKxrooqv312DVk/wToClXWduHis+YOl1p362keKQClfYRjaswIL+R49jPjr7cC2Y5iv6vle/sA==", "dev": true, "requires": { "chalk": "^1.1.1", @@ -4927,9 +4934,9 @@ } }, "merge2": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.2.1.tgz", - "integrity": "sha512-wUqcG5pxrAcaFI1lkqkMnk3Q7nUxV/NWfpAFSeWUwG9TRODnBDCUHa75mi3o3vLWQ5N4CQERWCauSlP0I3ZqUg==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.2.2.tgz", + "integrity": "sha512-bgM8twH86rWni21thii6WCMQMRMmwqqdW3sGWi9IipnVAszdLXRjwDwAnyrVXo6DuP3AjRMMttZKUB48QWIFGg==", "dev": true }, "micromatch": { @@ -6373,50 +6380,56 @@ "dev": true }, "relay-compiler": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/relay-compiler/-/relay-compiler-1.6.0.tgz", - "integrity": "sha1-ChvI0owc8x2JhRCKdhumwNtI1KE=", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/relay-compiler/-/relay-compiler-1.6.1.tgz", + "integrity": "sha512-biugFmjBpiF5mvVXUIV3DzEgnuJTonZaTN8ASH6WOJSY0lyQ0eGJ2m6RFm7c9a2A/fpAQzduj1udY0bMrJJSWg==", "dev": true, "requires": { - "@babel/generator": "7.0.0-beta.40", - "@babel/types": "7.0.0-beta.40", + "@babel/generator": "7.0.0-beta.54", + "@babel/parser": "7.0.0-beta.54", + "@babel/types": "7.0.0-beta.54", "babel-polyfill": "^6.20.0", - "babel-preset-fbjs": "^2.1.4", + "babel-preset-fbjs": "2.2.0", "babel-runtime": "^6.23.0", "babel-traverse": "^6.26.0", - "babylon": "7.0.0-beta.40", "chalk": "^1.1.1", - "fast-glob": "^2.0.0", + "fast-glob": "^2.2.2", "fb-watchman": "^2.0.0", - "fbjs": "^0.8.14", - "graphql-compiler": "1.6.0", + "fbjs": "0.8.17", + "graphql-compiler": "1.6.1", "immutable": "~3.7.6", - "relay-runtime": "1.6.0", + "relay-runtime": "1.6.1", "signedsource": "^1.0.0", "yargs": "^9.0.0" }, "dependencies": { "@babel/generator": { - "version": "7.0.0-beta.40", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.0.0-beta.40.tgz", - "integrity": "sha1-q2H5VW9PcdvRE4lJx5W7miHjAuo=", + "version": "7.0.0-beta.54", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.0.0-beta.54.tgz", + "integrity": "sha1-wEPH7r7r/X5mXZXCgaSq/IPU4ck=", "dev": true, "requires": { - "@babel/types": "7.0.0-beta.40", + "@babel/types": "7.0.0-beta.54", "jsesc": "^2.5.1", - "lodash": "^4.2.0", + "lodash": "^4.17.5", "source-map": "^0.5.0", "trim-right": "^1.0.1" } }, + "@babel/parser": { + "version": "7.0.0-beta.54", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.0.0-beta.54.tgz", + "integrity": "sha1-wBqmO1fJyNzodEeWyB2d8SHyDbQ=", + "dev": true + }, "@babel/types": { - "version": "7.0.0-beta.40", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.0.0-beta.40.tgz", - "integrity": "sha1-JcPXquFBJqvgX8sJjGWma21rjBQ=", + "version": "7.0.0-beta.54", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.0.0-beta.54.tgz", + "integrity": "sha1-AlrWhJL+1ULBPxTFeaRMhI5TEGM=", "dev": true, "requires": { "esutils": "^2.0.2", - "lodash": "^4.2.0", + "lodash": "^4.17.5", "to-fast-properties": "^2.0.0" } }, @@ -6446,12 +6459,6 @@ "core-js": "^2.4.0", "regenerator-runtime": "^0.11.0" } - }, - "babylon": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", - "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==", - "dev": true } } }, @@ -6486,11 +6493,34 @@ } }, "babylon": { - "version": "7.0.0-beta.40", - "resolved": "https://registry.npmjs.org/babylon/-/babylon-7.0.0-beta.40.tgz", - "integrity": "sha1-kfyM1W1euYso5v3kEEXylXd5lAo=", + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", + "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==", "dev": true }, + "fbjs": { + "version": "0.8.17", + "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.17.tgz", + "integrity": "sha1-xNWY6taUkRJlPWWIsBpc3Nn5D90=", + "dev": true, + "requires": { + "core-js": "^1.0.0", + "isomorphic-fetch": "^2.1.1", + "loose-envify": "^1.0.0", + "object-assign": "^4.1.0", + "promise": "^7.1.1", + "setimmediate": "^1.0.5", + "ua-parser-js": "^0.7.18" + }, + "dependencies": { + "core-js": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz", + "integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=", + "dev": true + } + } + }, "jsesc": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.1.tgz", @@ -6502,6 +6532,22 @@ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", "dev": true + }, + "relay-runtime": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/relay-runtime/-/relay-runtime-1.6.1.tgz", + "integrity": "sha512-qBN8HYEsA+zqJMvNcULNeBJukIOqcuAQ5ivN0nV4OI2SbR/hHz8vryTx02Drh85uk/bbEZe+6q2gw39hFSx3/Q==", + "dev": true, + "requires": { + "babel-runtime": "^6.23.0", + "fbjs": "0.8.17" + } + }, + "ua-parser-js": { + "version": "0.7.18", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.18.tgz", + "integrity": "sha512-LtzwHlVHwFGTptfNSgezHp7WUlwiqb0gA9AALRbKaERfxwJoiX0A73QbTToxteIAuIaFshhgIZfqK8s7clqgnA==", + "dev": true } } }, From a749da1dcc331bee4d7da730f9d220c2ac682708 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 30 Jul 2018 09:04:54 -0400 Subject: [PATCH 0062/4252] Regenerate GraphQL queries --- ...urrentPullRequestContainerQuery.graphql.js | 6 +- .../issueishDetailContainerQuery.graphql.js | 6 +- .../issueishSearchContainerQuery.graphql.js | 6 +- .../remoteContainerQuery.graphql.js | 6 +- .../issueTimelineControllerQuery.graphql.js | 86 ++++++------ .../prTimelineControllerQuery.graphql.js | 132 ++++++++---------- .../issueishTooltipItemQuery.graphql.js | 6 +- .../userMentionTooltipItemQuery.graphql.js | 6 +- .../issueishDetailViewRefetchQuery.graphql.js | 6 +- .../prStatusesViewRefetchQuery.graphql.js | 6 +- 10 files changed, 141 insertions(+), 125 deletions(-) diff --git a/lib/containers/__generated__/currentPullRequestContainerQuery.graphql.js b/lib/containers/__generated__/currentPullRequestContainerQuery.graphql.js index 3bc8e25975..a284aa3c5f 100644 --- a/lib/containers/__generated__/currentPullRequestContainerQuery.graphql.js +++ b/lib/containers/__generated__/currentPullRequestContainerQuery.graphql.js @@ -1,6 +1,6 @@ /** * @flow - * @relayHash 40b7bc7d6c17a665a1fe95116c6f6719 + * @relayHash 5821e0667d3f593dc75d2c4ac34985ca */ /* eslint-disable */ @@ -28,6 +28,10 @@ export type currentPullRequestContainerQueryResponse = {| |} |} |}; +export type currentPullRequestContainerQuery = {| + variables: currentPullRequestContainerQueryVariables, + response: currentPullRequestContainerQueryResponse, +|}; */ diff --git a/lib/containers/__generated__/issueishDetailContainerQuery.graphql.js b/lib/containers/__generated__/issueishDetailContainerQuery.graphql.js index 1af06e1212..f2fb306821 100644 --- a/lib/containers/__generated__/issueishDetailContainerQuery.graphql.js +++ b/lib/containers/__generated__/issueishDetailContainerQuery.graphql.js @@ -1,6 +1,6 @@ /** * @flow - * @relayHash fd2fb50c17cd6fc965d556a7247314a9 + * @relayHash d9bc206d6bd62b978d889af219ec648d */ /* eslint-disable */ @@ -22,6 +22,10 @@ export type issueishDetailContainerQueryResponse = {| +$fragmentRefs: issueishDetailController_repository$ref |} |}; +export type issueishDetailContainerQuery = {| + variables: issueishDetailContainerQueryVariables, + response: issueishDetailContainerQueryResponse, +|}; */ diff --git a/lib/containers/__generated__/issueishSearchContainerQuery.graphql.js b/lib/containers/__generated__/issueishSearchContainerQuery.graphql.js index b1b78a6de8..a23ceb7401 100644 --- a/lib/containers/__generated__/issueishSearchContainerQuery.graphql.js +++ b/lib/containers/__generated__/issueishSearchContainerQuery.graphql.js @@ -1,6 +1,6 @@ /** * @flow - * @relayHash 39c0b6593d96d2f8436bff9ca0d239c4 + * @relayHash 4f7294887e96b0a41dd57f0f3164f765 */ /* eslint-disable */ @@ -22,6 +22,10 @@ export type issueishSearchContainerQueryResponse = {| |}>, |} |}; +export type issueishSearchContainerQuery = {| + variables: issueishSearchContainerQueryVariables, + response: issueishSearchContainerQueryResponse, +|}; */ diff --git a/lib/containers/__generated__/remoteContainerQuery.graphql.js b/lib/containers/__generated__/remoteContainerQuery.graphql.js index f1fd34d3b6..a5ac465623 100644 --- a/lib/containers/__generated__/remoteContainerQuery.graphql.js +++ b/lib/containers/__generated__/remoteContainerQuery.graphql.js @@ -1,6 +1,6 @@ /** * @flow - * @relayHash 7fbd7496eff90270ea34a304108aa100 + * @relayHash 982797d241da7600c6e95299dff83585 */ /* eslint-disable */ @@ -22,6 +22,10 @@ export type remoteContainerQueryResponse = {| |}, |} |}; +export type remoteContainerQuery = {| + variables: remoteContainerQueryVariables, + response: remoteContainerQueryResponse, +|}; */ diff --git a/lib/controllers/__generated__/issueTimelineControllerQuery.graphql.js b/lib/controllers/__generated__/issueTimelineControllerQuery.graphql.js index 8493f37921..494f483b3b 100644 --- a/lib/controllers/__generated__/issueTimelineControllerQuery.graphql.js +++ b/lib/controllers/__generated__/issueTimelineControllerQuery.graphql.js @@ -1,6 +1,6 @@ /** * @flow - * @relayHash 11a080534b1e9e618daa153d559f5efc + * @relayHash f743b45dcda2d5c1204b416ba482392d */ /* eslint-disable */ @@ -20,6 +20,10 @@ export type issueTimelineControllerQueryResponse = {| +$fragmentRefs: issueTimelineController_issue$ref |} |}; +export type issueTimelineControllerQuery = {| + variables: issueTimelineControllerQueryVariables, + response: issueTimelineControllerQueryResponse, +|}; */ @@ -230,42 +234,56 @@ v4 = { "args": null, "storageKey": null }, -v5 = { +v5 = [ + { + "kind": "Variable", + "name": "after", + "variableName": "timelineCursor", + "type": "String" + }, + { + "kind": "Variable", + "name": "first", + "variableName": "timelineCount", + "type": "Int" + } +], +v6 = { "kind": "ScalarField", "alias": null, "name": "login", "args": null, "storageKey": null }, -v6 = { +v7 = { "kind": "ScalarField", "alias": null, "name": "avatarUrl", "args": null, "storageKey": null }, -v7 = { +v8 = { "kind": "ScalarField", "alias": null, "name": "name", "args": null, "storageKey": null }, -v8 = { +v9 = { "kind": "ScalarField", "alias": null, "name": "number", "args": null, "storageKey": null }, -v9 = { +v10 = { "kind": "ScalarField", "alias": null, "name": "title", "args": null, "storageKey": null }, -v10 = { +v11 = { "kind": "LinkedField", "alias": null, "name": "user", @@ -274,7 +292,7 @@ v10 = { "concreteType": "User", "plural": false, "selections": [ - v5, + v6, v3 ] }; @@ -355,20 +373,7 @@ return { "alias": null, "name": "timeline", "storageKey": null, - "args": [ - { - "kind": "Variable", - "name": "after", - "variableName": "timelineCursor", - "type": "String" - }, - { - "kind": "Variable", - "name": "first", - "variableName": "timelineCount", - "type": "Int" - } - ], + "args": v5, "concreteType": "IssueTimelineConnection", "plural": false, "selections": [ @@ -452,8 +457,8 @@ return { "plural": false, "selections": [ v2, - v5, v6, + v7, v3 ] }, @@ -476,7 +481,7 @@ return { "concreteType": "Repository", "plural": false, "selections": [ - v7, + v8, { "kind": "LinkedField", "alias": null, @@ -487,7 +492,7 @@ return { "plural": false, "selections": [ v2, - v5, + v6, v3 ] }, @@ -506,8 +511,8 @@ return { "kind": "InlineFragment", "type": "PullRequest", "selections": [ - v8, v9, + v10, v4, { "kind": "ScalarField", @@ -522,8 +527,8 @@ return { "kind": "InlineFragment", "type": "Issue", "selections": [ - v8, v9, + v10, v4, { "kind": "ScalarField", @@ -552,8 +557,8 @@ return { "plural": false, "selections": [ v2, + v7, v6, - v5, v3 ] }, @@ -587,9 +592,9 @@ return { "concreteType": "GitActor", "plural": false, "selections": [ - v7, - v10, - v6 + v8, + v11, + v7 ] }, { @@ -601,9 +606,9 @@ return { "concreteType": "GitActor", "plural": false, "selections": [ + v8, v7, - v6, - v10 + v11 ] }, { @@ -653,20 +658,7 @@ return { "kind": "LinkedHandle", "alias": null, "name": "timeline", - "args": [ - { - "kind": "Variable", - "name": "after", - "variableName": "timelineCursor", - "type": "String" - }, - { - "kind": "Variable", - "name": "first", - "variableName": "timelineCount", - "type": "Int" - } - ], + "args": v5, "handle": "connection", "key": "IssueTimelineController_timeline", "filters": null diff --git a/lib/controllers/__generated__/prTimelineControllerQuery.graphql.js b/lib/controllers/__generated__/prTimelineControllerQuery.graphql.js index 12ce6837e0..75012a0b0e 100644 --- a/lib/controllers/__generated__/prTimelineControllerQuery.graphql.js +++ b/lib/controllers/__generated__/prTimelineControllerQuery.graphql.js @@ -1,6 +1,6 @@ /** * @flow - * @relayHash af853f1ab5f7d7727fafd178050ec61a + * @relayHash 1a3787f5d7fae81fbb3897db8f0bae46 */ /* eslint-disable */ @@ -20,6 +20,10 @@ export type prTimelineControllerQueryResponse = {| +$fragmentRefs: prTimelineController_pullRequest$ref |} |}; +export type prTimelineControllerQuery = {| + variables: prTimelineControllerQueryVariables, + response: prTimelineControllerQueryResponse, +|}; */ @@ -344,52 +348,66 @@ v7 = { "plural": false, "selections": v6 }, -v8 = { +v8 = [ + { + "kind": "Variable", + "name": "after", + "variableName": "timelineCursor", + "type": "String" + }, + { + "kind": "Variable", + "name": "first", + "variableName": "timelineCount", + "type": "Int" + } +], +v9 = { "kind": "ScalarField", "alias": null, "name": "avatarUrl", "args": null, "storageKey": null }, -v9 = [ +v10 = [ v2, v5, - v8, + v9, v3 ], -v10 = { +v11 = { "kind": "ScalarField", "alias": null, "name": "name", "args": null, "storageKey": null }, -v11 = { +v12 = { "kind": "ScalarField", "alias": null, "name": "number", "args": null, "storageKey": null }, -v12 = { +v13 = { "kind": "ScalarField", "alias": null, "name": "title", "args": null, "storageKey": null }, -v13 = { +v14 = { "kind": "ScalarField", "alias": null, "name": "oid", "args": null, "storageKey": null }, -v14 = [ - v13, +v15 = [ + v14, v3 ], -v15 = { +v16 = { "kind": "LinkedField", "alias": null, "name": "commit", @@ -397,29 +415,29 @@ v15 = { "args": null, "concreteType": "Commit", "plural": false, - "selections": v14 + "selections": v15 }, -v16 = { +v17 = { "kind": "ScalarField", "alias": null, "name": "bodyHTML", "args": null, "storageKey": null }, -v17 = { +v18 = { "kind": "ScalarField", "alias": null, "name": "createdAt", "args": null, "storageKey": null }, -v18 = [ +v19 = [ v2, - v8, + v9, v5, v3 ], -v19 = { +v20 = { "kind": "LinkedField", "alias": null, "name": "actor", @@ -427,9 +445,9 @@ v19 = { "args": null, "concreteType": null, "plural": false, - "selections": v18 + "selections": v19 }, -v20 = { +v21 = { "kind": "LinkedField", "alias": null, "name": "user", @@ -549,20 +567,7 @@ return { "alias": null, "name": "timeline", "storageKey": null, - "args": [ - { - "kind": "Variable", - "name": "after", - "variableName": "timelineCursor", - "type": "String" - }, - { - "kind": "Variable", - "name": "first", - "variableName": "timelineCount", - "type": "Int" - } - ], + "args": v8, "concreteType": "PullRequestTimelineConnection", "plural": false, "selections": [ @@ -644,7 +649,7 @@ return { "args": null, "concreteType": null, "plural": false, - "selections": v9 + "selections": v10 }, { "kind": "LinkedField", @@ -665,7 +670,7 @@ return { "concreteType": "Repository", "plural": false, "selections": [ - v10, + v11, v7, v3, { @@ -682,8 +687,8 @@ return { "kind": "InlineFragment", "type": "PullRequest", "selections": [ - v11, v12, + v13, v4, { "kind": "ScalarField", @@ -698,8 +703,8 @@ return { "kind": "InlineFragment", "type": "Issue", "selections": [ - v11, v12, + v13, v4, { "kind": "ScalarField", @@ -718,7 +723,7 @@ return { "kind": "InlineFragment", "type": "CommitCommentThread", "selections": [ - v15, + v16, { "kind": "LinkedField", "alias": null, @@ -762,11 +767,11 @@ return { "args": null, "concreteType": null, "plural": false, - "selections": v9 + "selections": v10 }, - v15, v16, v17, + v18, { "kind": "ScalarField", "alias": null, @@ -793,7 +798,7 @@ return { "kind": "InlineFragment", "type": "HeadRefForcePushedEvent", "selections": [ - v19, + v20, { "kind": "LinkedField", "alias": null, @@ -802,7 +807,7 @@ return { "args": null, "concreteType": "Commit", "plural": false, - "selections": v14 + "selections": v15 }, { "kind": "LinkedField", @@ -812,17 +817,17 @@ return { "args": null, "concreteType": "Commit", "plural": false, - "selections": v14 + "selections": v15 }, - v17 + v18 ] }, { "kind": "InlineFragment", "type": "MergedEvent", "selections": [ - v19, - v15, + v20, + v16, { "kind": "ScalarField", "alias": null, @@ -830,7 +835,7 @@ return { "args": null, "storageKey": null }, - v17 + v18 ] }, { @@ -845,10 +850,10 @@ return { "args": null, "concreteType": null, "plural": false, - "selections": v18 + "selections": v19 }, - v16, v17, + v18, v4 ] }, @@ -865,9 +870,9 @@ return { "concreteType": "GitActor", "plural": false, "selections": [ - v10, - v20, - v8 + v11, + v21, + v9 ] }, { @@ -879,9 +884,9 @@ return { "concreteType": "GitActor", "plural": false, "selections": [ - v10, - v8, - v20 + v11, + v9, + v21 ] }, { @@ -891,7 +896,7 @@ return { "args": null, "storageKey": null }, - v13, + v14, { "kind": "ScalarField", "alias": null, @@ -925,20 +930,7 @@ return { "kind": "LinkedHandle", "alias": null, "name": "timeline", - "args": [ - { - "kind": "Variable", - "name": "after", - "variableName": "timelineCursor", - "type": "String" - }, - { - "kind": "Variable", - "name": "first", - "variableName": "timelineCount", - "type": "Int" - } - ], + "args": v8, "handle": "connection", "key": "prTimelineContainer_timeline", "filters": null diff --git a/lib/items/__generated__/issueishTooltipItemQuery.graphql.js b/lib/items/__generated__/issueishTooltipItemQuery.graphql.js index bac3dfae3e..e72d52912c 100644 --- a/lib/items/__generated__/issueishTooltipItemQuery.graphql.js +++ b/lib/items/__generated__/issueishTooltipItemQuery.graphql.js @@ -1,6 +1,6 @@ /** * @flow - * @relayHash e8e09ef59fd58c211220126ee7dcd2a5 + * @relayHash ec7add21f4125e294e4679a0fed3dfc9 */ /* eslint-disable */ @@ -18,6 +18,10 @@ export type issueishTooltipItemQueryResponse = {| +$fragmentRefs: issueishTooltipContainer_resource$ref |} |}; +export type issueishTooltipItemQuery = {| + variables: issueishTooltipItemQueryVariables, + response: issueishTooltipItemQueryResponse, +|}; */ diff --git a/lib/items/__generated__/userMentionTooltipItemQuery.graphql.js b/lib/items/__generated__/userMentionTooltipItemQuery.graphql.js index deff0ebf3f..85f27f9e43 100644 --- a/lib/items/__generated__/userMentionTooltipItemQuery.graphql.js +++ b/lib/items/__generated__/userMentionTooltipItemQuery.graphql.js @@ -1,6 +1,6 @@ /** * @flow - * @relayHash 65b45175c8b30ca98da00fa016b8a0dc + * @relayHash 618ef28f16a644f2aa8240535a6b5ea4 */ /* eslint-disable */ @@ -18,6 +18,10 @@ export type userMentionTooltipItemQueryResponse = {| +$fragmentRefs: userMentionTooltipContainer_repositoryOwner$ref |} |}; +export type userMentionTooltipItemQuery = {| + variables: userMentionTooltipItemQueryVariables, + response: userMentionTooltipItemQueryResponse, +|}; */ diff --git a/lib/views/__generated__/issueishDetailViewRefetchQuery.graphql.js b/lib/views/__generated__/issueishDetailViewRefetchQuery.graphql.js index 212b2badeb..2094215ae1 100644 --- a/lib/views/__generated__/issueishDetailViewRefetchQuery.graphql.js +++ b/lib/views/__generated__/issueishDetailViewRefetchQuery.graphql.js @@ -1,6 +1,6 @@ /** * @flow - * @relayHash e59fe378fd0cacef7e695703a6cb0022 + * @relayHash 7c0400f69539fb35f8ee2107122c5baf */ /* eslint-disable */ @@ -25,6 +25,10 @@ export type issueishDetailViewRefetchQueryResponse = {| +$fragmentRefs: issueishDetailView_issueish$ref |}, |}; +export type issueishDetailViewRefetchQuery = {| + variables: issueishDetailViewRefetchQueryVariables, + response: issueishDetailViewRefetchQueryResponse, +|}; */ diff --git a/lib/views/__generated__/prStatusesViewRefetchQuery.graphql.js b/lib/views/__generated__/prStatusesViewRefetchQuery.graphql.js index aa3ba531f6..ff57c78c6e 100644 --- a/lib/views/__generated__/prStatusesViewRefetchQuery.graphql.js +++ b/lib/views/__generated__/prStatusesViewRefetchQuery.graphql.js @@ -1,6 +1,6 @@ /** * @flow - * @relayHash 785a8d24aba2ef8a8145215f0bff78a0 + * @relayHash 5b2061918400a59074629fc3e4800b18 */ /* eslint-disable */ @@ -18,6 +18,10 @@ export type prStatusesViewRefetchQueryResponse = {| +$fragmentRefs: prStatusesView_pullRequest$ref |} |}; +export type prStatusesViewRefetchQuery = {| + variables: prStatusesViewRefetchQueryVariables, + response: prStatusesViewRefetchQueryResponse, +|}; */ From 9fc9622b3ae6f8c9c477cb9f6214c42c2764f1a4 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 31 Jul 2018 15:26:36 -0400 Subject: [PATCH 0063/4252] MarkerPosition tests --- lib/models/marker-position.js | 124 ++++++++++- test/models/marker-position.test.js | 310 ++++++++++++++++++++++++++++ 2 files changed, 423 insertions(+), 11 deletions(-) create mode 100644 test/models/marker-position.test.js diff --git a/lib/models/marker-position.js b/lib/models/marker-position.js index 7e9989f1ca..dacb9e44e7 100644 --- a/lib/models/marker-position.js +++ b/lib/models/marker-position.js @@ -1,14 +1,22 @@ import {Range} from 'atom'; class Position { + constructor(bufferRange, screenRange) { + this.bufferRange = bufferRange && Range.fromObject(bufferRange); + this.screenRange = screenRange && Range.fromObject(screenRange); + } + + /* istanbul ignore next */ markOn(markable, options) { throw new Error('markOn not overridden'); } + /* istanbul ignore next */ setIn(marker) { throw new Error('setIn not overridden'); } + /* istanbul ignore next */ matches(other) { throw new Error('matches not overridden'); } @@ -20,12 +28,57 @@ class Position { matchFromScreenRange(other) { return false; } + + bufferStartRow() { + return this.bufferRange !== null ? this.bufferRange.start.row : -1; + } + + bufferRowCount() { + return this.bufferRange !== null ? this.bufferRange.getRowCount() : 0; + } + + intersectRows(rowSet) { + if (this.bufferRange === null) { + return []; + } + + const intersections = []; + let currentRangeStart = null; + + let row = this.bufferRange.start.row; + while (row <= this.bufferRange.end.row) { + if (rowSet.has(row) && currentRangeStart === null) { + currentRangeStart = row; + } else if (!rowSet.has(row) && currentRangeStart !== null) { + intersections.push( + fromBufferRange([[currentRangeStart, 0], [row - 1, 0]]), + ); + currentRangeStart = null; + } + row++; + } + if (currentRangeStart !== null) { + intersections.push( + fromBufferRange([[currentRangeStart, 0], this.bufferRange.end]), + ); + } + + return intersections; + } + + /* istanbul ignore next */ + serialize() { + throw new Error('serialize not overridden'); + } + + isPresent() { + return true; + } } class BufferRangePosition extends Position { constructor(bufferRange) { - super(); - this.bufferRange = Range.fromObject(bufferRange); + super(bufferRange, null); } markOn(markable, options) { @@ -44,6 +97,10 @@ class BufferRangePosition extends Position { return other.bufferRange.isEqual(this.bufferRange); } + serialize() { + return this.bufferRange.serialize(); + } + toString() { return `buffer(${this.bufferRange.toString()})`; } @@ -51,8 +108,7 @@ class BufferRangePosition extends Position { class ScreenRangePosition extends Position { constructor(screenRange) { - super(); - this.screenRange = screenRange; + super(null, screenRange); } markOn(markable, options) { @@ -71,20 +127,18 @@ class ScreenRangePosition extends Position { return other.screenRange.isEqual(this.screenRange); } + serialize() { + return this.screenRange.serialize(); + } + toString() { return `screen(${this.screenRange.toString()})`; } } class BufferOrScreenRangePosition extends Position { - constructor(bufferRange, screenRange) { - super(); - this.bufferRange = bufferRange; - this.screenRange = screenRange; - } - markOn(markable, options) { - return markable.markBufferRange(this.bufferRange); + return markable.markBufferRange(this.bufferRange, options); } setIn(marker) { @@ -103,11 +157,59 @@ class BufferOrScreenRangePosition extends Position { return other.screenRange.isEqual(this.screenRange); } + serialize() { + return this.bufferRange.serialize(); + } + toString() { return `either(b${this.bufferRange.toString()}/s${this.screenRange.toString()})`; } } +export const nullPosition = { + markOn() { + return null; + }, + + setIn() {}, + + matches(other) { + return other === this; + }, + + matchFromBufferRange() { + return false; + }, + + matchFromScreenRange() { + return false; + }, + + bufferStartRow() { + return -1; + }, + + bufferRowCount() { + return 0; + }, + + intersectRows() { + return []; + }, + + serialize() { + return null; + }, + + isPresent() { + return false; + }, + + toString() { + return 'null'; + }, +}; + export function fromBufferRange(bufferRange) { return new BufferRangePosition(Range.fromObject(bufferRange)); } diff --git a/test/models/marker-position.test.js b/test/models/marker-position.test.js new file mode 100644 index 0000000000..3f5b069eec --- /dev/null +++ b/test/models/marker-position.test.js @@ -0,0 +1,310 @@ +import { + nullPosition, fromBufferRange, fromBufferPosition, fromScreenRange, fromScreenPosition, fromMarker, fromChangeEvent, +} from '../../lib/models/marker-position'; + +describe('MarkerPosition', function() { + let atomEnv, editor; + + beforeEach(async function() { + atomEnv = global.buildAtomEnvironment(); + + editor = await atomEnv.workspace.open(__filename); + }); + + afterEach(function() { + atomEnv.destroy(); + }); + + describe('markOn', function() { + it('marks a buffer range', function() { + const position = fromBufferRange([[1, 0], [4, 0]]); + const marker = position.markOn(editor, {invalidate: 'never'}); + + assert.deepEqual(marker.getBufferRange().serialize(), [[1, 0], [4, 0]]); + assert.strictEqual(marker.getInvalidationStrategy(), 'never'); + }); + + it('marks a buffer position', function() { + const position = fromBufferPosition([2, 0]); + const marker = position.markOn(editor, {invalidate: 'never'}); + + assert.deepEqual(marker.getBufferRange().serialize(), [[2, 0], [2, 0]]); + assert.strictEqual(marker.getInvalidationStrategy(), 'never'); + }); + + it('marks a screen range', function() { + const position = fromScreenRange([[2, 0], [5, 0]]); + const marker = position.markOn(editor, {invalidate: 'never'}); + + assert.deepEqual(marker.getBufferRange().serialize(), [[2, 0], [5, 0]]); + assert.strictEqual(marker.getInvalidationStrategy(), 'never'); + }); + + it('marks a screen position', function() { + const position = fromScreenPosition([3, 0]); + const marker = position.markOn(editor, {invalidate: 'never'}); + + assert.deepEqual(marker.getBufferRange().serialize(), [[3, 0], [3, 0]]); + assert.strictEqual(marker.getInvalidationStrategy(), 'never'); + }); + + it('marks a combination position', function() { + const marker0 = editor.markBufferRange([[0, 0], [2, 0]]); + const position = fromMarker(marker0); + + const marker1 = position.markOn(editor, {invalidate: 'never'}); + assert.deepEqual(marker1.getBufferRange().serialize(), [[0, 0], [2, 0]]); + assert.strictEqual(marker1.getInvalidationStrategy(), 'never'); + }); + + it('does nothing with a nullPosition', function() { + assert.isNull(nullPosition.markOn(editor, {})); + assert.lengthOf(editor.findMarkers({}), 0); + }); + }); + + describe('setIn', function() { + let marker; + + beforeEach(function() { + marker = editor.markBufferRange([[1, 0], [3, 0]]); + }); + + it('updates an existing marker by buffer range', function() { + fromBufferRange([[2, 0], [4, 0]]).setIn(marker); + assert.deepEqual(marker.getBufferRange().serialize(), [[2, 0], [4, 0]]); + }); + + it('updates an existing marker by screen range', function() { + fromScreenRange([[6, 0], [7, 0]]).setIn(marker); + assert.deepEqual(marker.getBufferRange().serialize(), [[6, 0], [7, 0]]); + }); + + it('updates with a combination position', function() { + const other = editor.markBufferRange([[2, 0], [4, 0]]); + fromMarker(other).setIn(marker); + assert.deepEqual(marker.getBufferRange().serialize(), [[2, 0], [4, 0]]); + }); + + it('does nothing with a nullPosition', function() { + nullPosition.setIn(marker); + assert.deepEqual(marker.getBufferRange().serialize(), [[1, 0], [3, 0]]); + }); + }); + + describe('matches', function() { + let marker0, marker1; + + beforeEach(function() { + marker0 = editor.markBufferRange([[2, 0], [4, 0]]); + marker1 = editor.markBufferRange([[1, 0], [3, 0]]); + }); + + it('a buffer range', function() { + const position = fromBufferRange([[2, 0], [4, 0]]); + assert.isTrue(position.matches(fromBufferRange([[2, 0], [4, 0]]))); + assert.isFalse(position.matches(fromBufferRange([[2, 0], [5, 0]]))); + assert.isFalse(position.matches(fromScreenRange([[2, 0], [4, 0]]))); + assert.isTrue(position.matches(fromMarker(marker0))); + assert.isFalse(position.matches(fromMarker(marker1))); + assert.isFalse(position.matches(nullPosition)); + }); + + it('a screen range', function() { + const position = fromScreenRange([[1, 0], [3, 0]]); + assert.isTrue(position.matches(fromScreenRange([[1, 0], [3, 0]]))); + assert.isFalse(position.matches(fromScreenRange([[2, 0], [4, 0]]))); + assert.isFalse(position.matches(fromBufferRange([[1, 0], [3, 0]]))); + assert.isFalse(position.matches(fromMarker(marker0))); + assert.isTrue(position.matches(fromMarker(marker1))); + assert.isFalse(position.matches(nullPosition)); + }); + + it('a combination range', function() { + const position = fromMarker(marker0); + assert.isTrue(position.matches(fromMarker(marker0))); + assert.isFalse(position.matches(fromMarker(marker1))); + assert.isTrue(position.matches(fromBufferRange([[2, 0], [4, 0]]))); + assert.isFalse(position.matches(fromBufferRange([[1, 0], [3, 0]]))); + assert.isTrue(position.matches(fromScreenRange([[2, 0], [4, 0]]))); + assert.isFalse(position.matches(fromScreenRange([[1, 0], [3, 0]]))); + assert.isFalse(position.matches(nullPosition)); + }); + + it('a null position', function() { + assert.isTrue(nullPosition.matches(nullPosition)); + assert.isFalse(nullPosition.matches(fromBufferRange([[1, 0], [2, 0]]))); + assert.isFalse(nullPosition.matches(fromScreenRange([[1, 0], [2, 0]]))); + assert.isFalse(nullPosition.matches(fromMarker(marker0))); + }); + }); + + describe('bufferStartRow()', function() { + it('retrieves the first row from a buffer range', function() { + assert.strictEqual(fromBufferRange([[2, 0], [3, 4]]).bufferStartRow(), 2); + }); + + it('retrieves the first row from a combination range', function() { + const marker = editor.markBufferRange([[3, 0], [5, 0]]); + assert.strictEqual(fromMarker(marker).bufferStartRow(), 3); + }); + + it('returns -1 from a screen range', function() { + assert.strictEqual(fromScreenRange([[1, 0], [2, 0]]).bufferStartRow(), -1); + }); + + it('returns -1 from a null position', function() { + assert.strictEqual(nullPosition.bufferStartRow(), -1); + }); + }); + + describe('bufferRowCount()', function() { + it('counts the rows in a buffer range', function() { + assert.strictEqual(fromBufferRange([[1, 0], [4, 0]]).bufferRowCount(), 4); + assert.strictEqual(fromBufferPosition([2, 0]).bufferRowCount(), 1); + }); + + it('counts the rows in a combination range', function() { + const marker = editor.markBufferRange([[2, 0], [6, 0]]); + assert.strictEqual(fromMarker(marker).bufferRowCount(), 5); + }); + + it('returns 0 for screen or null ranges', function() { + assert.strictEqual(fromScreenRange([[1, 0], [2, 0]]).bufferRowCount(), 0); + assert.strictEqual(nullPosition.bufferRowCount(), 0); + }); + }); + + describe('intersectRows()', function() { + it('returns an empty array with no intersection rows', function() { + assert.deepEqual(fromBufferRange([[1, 0], [3, 0]]).intersectRows(new Set([0, 5, 6])), []); + }); + + it('detects an intersection at the beginning of the range', function() { + const position = fromBufferRange([[2, 0], [6, 0]]); + const rowSet = new Set([0, 1, 2, 3]); + + assert.deepEqual(position.intersectRows(rowSet).map(i => i.serialize()), [ + [[2, 0], [3, 0]], + ]); + }); + + it('detects an intersection in the middle of the range', function() { + const position = fromBufferRange([[2, 0], [6, 0]]); + const rowSet = new Set([0, 3, 4, 8, 9]); + + assert.deepEqual(position.intersectRows(rowSet).map(i => i.serialize()), [ + [[3, 0], [4, 0]], + ]); + }); + + it('detects an intersection at the end of the range', function() { + const position = fromBufferRange([[2, 0], [6, 0]]); + const rowSet = new Set([4, 5, 6, 7, 10, 11]); + + assert.deepEqual(position.intersectRows(rowSet).map(i => i.serialize()), [ + [[4, 0], [6, 0]], + ]); + }); + + it('detects multiple intersections', function() { + const position = fromBufferRange([[2, 0], [8, 0]]); + const rowSet = new Set([0, 3, 4, 6, 7, 10]); + + assert.deepEqual(position.intersectRows(rowSet).map(i => i.serialize()), [ + [[3, 0], [4, 0]], + [[6, 0], [7, 0]], + ]); + }); + + it('returns an empty array for screen or null ranges', function() { + assert.deepEqual(fromScreenRange([[1, 0], [4, 0]]).intersectRows(new Set()), []); + assert.deepEqual(nullPosition.intersectRows(new Set()), []); + }); + }); + + describe('isPresent()', function() { + it('returns true on non-null positions', function() { + assert.isTrue(fromBufferRange([[1, 0], [2, 0]]).isPresent()); + assert.isTrue(fromScreenRange([[1, 0], [2, 0]]).isPresent()); + + const marker = editor.markBufferRange([[1, 0], [2, 0]]); + assert.isTrue(fromMarker(marker).isPresent()); + }); + + it('returns false on null positions', function() { + assert.isFalse(nullPosition.isPresent()); + }); + }); + + describe('serialize()', function() { + it('produces an array', function() { + assert.deepEqual(fromBufferRange([[0, 0], [1, 1]]).serialize(), [[0, 0], [1, 1]]); + assert.deepEqual(fromScreenRange([[0, 0], [1, 1]]).serialize(), [[0, 0], [1, 1]]); + + const marker = editor.markBufferRange([[2, 2], [3, 0]]); + assert.deepEqual(fromMarker(marker).serialize(), [[2, 2], [3, 0]]); + }); + + it('serializes a null position as null', function() { + assert.isNull(nullPosition.serialize()); + }); + }); + + describe('toString()', function() { + it('pretty-prints buffer ranges', function() { + assert.strictEqual(fromBufferRange([[0, 0], [2, 0]]).toString(), 'buffer([(0, 0) - (2, 0)])'); + }); + + it('pretty-prints screen ranges', function() { + assert.strictEqual(fromScreenRange([[3, 0], [7, 0]]).toString(), 'screen([(3, 0) - (7, 0)])'); + }); + + it('pretty-prints combination ranges', function() { + const marker = editor.markBufferRange([[1, 0], [3, 0]]); + assert.strictEqual(fromMarker(marker).toString(), 'either(b[(1, 0) - (3, 0)]/s[(1, 0) - (3, 0)])'); + }); + + it('pretty-prints a null position', function() { + assert.strictEqual(nullPosition.toString(), 'null'); + }); + }); + + describe('fromChangeEvent()', function() { + it('produces positions from a non-reversed marker change', function() { + const {oldPosition, newPosition} = fromChangeEvent({ + oldTailBufferPosition: [0, 0], + oldHeadBufferPosition: [1, 1], + oldTailScreenPosition: [2, 2], + oldHeadScreenPosition: [3, 3], + newTailBufferPosition: [4, 4], + newHeadBufferPosition: [5, 5], + newTailScreenPosition: [6, 6], + newHeadScreenPosition: [7, 7], + }); + + assert.isTrue(oldPosition.matches(fromBufferRange([[0, 0], [1, 1]]))); + assert.isTrue(oldPosition.matches(fromScreenRange([[2, 2], [3, 3]]))); + assert.isTrue(newPosition.matches(fromBufferRange([[4, 4], [5, 5]]))); + assert.isTrue(newPosition.matches(fromScreenRange([[6, 6], [7, 7]]))); + }); + + it('produces positions from a reversed marker change', function() { + const {oldPosition, newPosition} = fromChangeEvent({ + oldTailBufferPosition: [0, 0], + oldHeadBufferPosition: [1, 1], + oldTailScreenPosition: [2, 2], + oldHeadScreenPosition: [3, 3], + newTailBufferPosition: [4, 4], + newHeadBufferPosition: [5, 5], + newTailScreenPosition: [6, 6], + newHeadScreenPosition: [7, 7], + }, true); + + assert.isTrue(oldPosition.matches(fromBufferRange([[1, 1], [0, 0]]))); + assert.isTrue(oldPosition.matches(fromScreenRange([[3, 3], [2, 2]]))); + assert.isTrue(newPosition.matches(fromBufferRange([[5, 5], [4, 4]]))); + assert.isTrue(newPosition.matches(fromScreenRange([[7, 7], [6, 6]]))); + }); + }); +}); From 0f3f1eccd43285c497cb0e4b62c139d0df60ed3f Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Wed, 1 Aug 2018 02:45:08 +0000 Subject: [PATCH 0064/4252] chore(package): update relay-compiler to version 1.6.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 89296e4c5d..2e41994a2c 100644 --- a/package.json +++ b/package.json @@ -88,7 +88,7 @@ "mocha-stress": "1.0.0", "node-fetch": "2.2.0", "nyc": "13.0.0", - "relay-compiler": "1.6.1", + "relay-compiler": "1.6.2", "sinon": "6.0.1", "test-until": "1.1.1" }, From 6e8ae370323c998020548ccc5597b81b0f90eedb Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 1 Aug 2018 09:10:25 -0400 Subject: [PATCH 0065/4252] Model a MarkerPosition plus an offset range as a Change --- lib/models/patch/change.js | 77 ++++++++++++++++++++++++++++++++ test/models/patch/change.test.js | 66 +++++++++++++++++++++++++++ 2 files changed, 143 insertions(+) create mode 100644 lib/models/patch/change.js create mode 100644 test/models/patch/change.test.js diff --git a/lib/models/patch/change.js b/lib/models/patch/change.js new file mode 100644 index 0000000000..f1eb504e39 --- /dev/null +++ b/lib/models/patch/change.js @@ -0,0 +1,77 @@ +// A contiguous region of additions or deletions within a {Hunk}. +// +// The Change's position is a {MarkerPosition} containing a {Range} of the change rows within the diff buffer. The +// calculated offsets delimit a half-open interval of {String} code point offsets within the diff buffer, such that +// `buffer.slice(startOffset, endOffset)` returns the exact contents of the changed lines, including the last line +// and its line-end character. +export default class Change { + constructor({position, startOffset, endOffset}) { + this.position = position; + this.startOffset = startOffset; + this.endOffset = endOffset; + } + + markOn(markable, options) { + return this.position.markOn(markable, options); + } + + setIn(marker) { + return this.position.setIn(marker); + } + + toStringIn(buffer, origin) { + let str = ''; + for (let offset = this.startOffset; offset < this.endOffset; offset++) { + const ch = buffer[offset]; + if (offset === this.startOffset) { + str += origin; + } + str += ch; + if (ch === '\n' && offset !== this.endOffset - 1) { + str += origin; + } + } + return str; + } + + intersectRowsIn(rowSet, buffer) { + const intPositions = this.position.intersectRows(rowSet); + const intChanges = []; + let intIndex = 0; + let currentRow = this.position.bufferStartRow(); + let currentOffset = this.startOffset; + let nextStartOffset = null; + + while (intIndex < intPositions.length && currentOffset < this.endOffset) { + const currentInt = intPositions[intIndex]; + + if (currentRow === currentInt.bufferStartRow()) { + nextStartOffset = currentOffset; + } + + if (currentRow === currentInt.bufferEndRow() + 1) { + intChanges.push(new this.constructor({ + position: currentInt, + startOffset: nextStartOffset, + endOffset: currentOffset, + })); + + intIndex++; + nextStartOffset = null; + } + + currentOffset = buffer.indexOf('\n', currentOffset) + 1; + currentRow++; + } + + if (intIndex < intPositions.length && nextStartOffset !== null) { + intChanges.push(new this.constructor({ + position: intPositions[intIndex], + startOffset: nextStartOffset, + endOffset: currentOffset, + })); + } + + return intChanges; + } +} diff --git a/test/models/patch/change.test.js b/test/models/patch/change.test.js new file mode 100644 index 0000000000..29dde58e80 --- /dev/null +++ b/test/models/patch/change.test.js @@ -0,0 +1,66 @@ +import Change from '../../../lib/models/patch/change'; +import {fromBufferRange} from '../../../lib/models/marker-position'; + +describe('Change', function() { + it('delegates methods to its MarkerPosition', function() { + const ch = new Change({ + position: fromBufferRange([[0, 0], [1, 0]]), + startOffset: 0, + endOffset: 10, + }); + + const markable = {markBufferRange: sinon.stub().returns(0)}; + assert.strictEqual(ch.markOn(markable, {}), 0); + assert.deepEqual(markable.markBufferRange.firstCall.args[0].serialize(), [[0, 0], [1, 0]]); + + const marker = {setBufferRange: sinon.stub().returns(1)}; + assert.strictEqual(ch.setIn(marker), 1); + assert.deepEqual(marker.setBufferRange.firstCall.args[0].serialize(), [[0, 0], [1, 0]]); + }); + + it('extracts its offset range from buffer text with toStringIn()', function() { + const buffer = '0000\n1111\n2222\n3333\n4444\n5555\n'; + const ch = new Change({ + position: fromBufferRange([[1, 0], [2, 0]]), + startOffset: 5, + endOffset: 25, + }); + + assert.strictEqual(ch.toStringIn(buffer, '+'), '+1111\n+2222\n+3333\n+4444\n'); + assert.strictEqual(ch.toStringIn(buffer, '-'), '-1111\n-2222\n-3333\n-4444\n'); + }); + + it('returns Changes corresponding to intersecting buffer rows', function() { + const buffer = '0000\n1111\n2222\n3333\n4444\n5555\n6666\n7777\n8888\n9999'; + const ch = new Change({ + position: fromBufferRange([[1, 0], [8, 0]]), + startOffset: 5, + endOffset: 45, + }); + + const intersections = ch.intersectRowsIn(new Set([4, 5]), buffer); + assert.lengthOf(intersections, 1); + assert.strictEqual(intersections[0].toStringIn(buffer, '-'), '-4444\n-5555\n'); + }); + + it('includes a Change corresponding to an intersection at the end of the range', function() { + const buffer = '0000\n1111\n2222\n3333\n4444\n5555\n6666\n7777\n8888\n9999'; + const ch = new Change({ + position: fromBufferRange([[1, 0], [8, 0]]), + startOffset: 5, + endOffset: 45, + }); + + const intersections = ch.intersectRowsIn(new Set([1, 2, 4, 8]), buffer); + assert.lengthOf(intersections, 3); + + const int0 = intersections[0]; + assert.strictEqual(int0.toStringIn(buffer, '+'), '+1111\n+2222\n'); + + const int1 = intersections[1]; + assert.strictEqual(int1.toStringIn(buffer, '-'), '-4444\n'); + + const int2 = intersections[2]; + assert.strictEqual(int2.toStringIn(buffer, '+'), '+8888\n'); + }); +}); From 7413d6fbabc5239b21d3d467c1f7ecbbf4fb64bd Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 1 Aug 2018 09:27:17 -0400 Subject: [PATCH 0066/4252] MarkerPosition needs a bufferEndRow() accessor --- lib/models/marker-position.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/models/marker-position.js b/lib/models/marker-position.js index dacb9e44e7..f60d9987ab 100644 --- a/lib/models/marker-position.js +++ b/lib/models/marker-position.js @@ -33,6 +33,10 @@ class Position { return this.bufferRange !== null ? this.bufferRange.start.row : -1; } + bufferEndRow() { + return this.bufferRange !== null ? this.bufferRange.end.row : -1; + } + bufferRowCount() { return this.bufferRange !== null ? this.bufferRange.getRowCount() : 0; } From 6778afff7d9ff9da33774a666e8fc011c5d00978 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 1 Aug 2018 09:27:36 -0400 Subject: [PATCH 0067/4252] Delegate Change.bufferRowCount() --- lib/models/patch/change.js | 4 ++++ test/models/patch/change.test.js | 2 ++ 2 files changed, 6 insertions(+) diff --git a/lib/models/patch/change.js b/lib/models/patch/change.js index f1eb504e39..2f389bb362 100644 --- a/lib/models/patch/change.js +++ b/lib/models/patch/change.js @@ -19,6 +19,10 @@ export default class Change { return this.position.setIn(marker); } + bufferRowCount() { + return this.position.bufferRowCount(); + } + toStringIn(buffer, origin) { let str = ''; for (let offset = this.startOffset; offset < this.endOffset; offset++) { diff --git a/test/models/patch/change.test.js b/test/models/patch/change.test.js index 29dde58e80..8a34706f52 100644 --- a/test/models/patch/change.test.js +++ b/test/models/patch/change.test.js @@ -16,6 +16,8 @@ describe('Change', function() { const marker = {setBufferRange: sinon.stub().returns(1)}; assert.strictEqual(ch.setIn(marker), 1); assert.deepEqual(marker.setBufferRange.firstCall.args[0].serialize(), [[0, 0], [1, 0]]); + + assert.deepEqual(ch.bufferRowCount(), 2); }); it('extracts its offset range from buffer text with toStringIn()', function() { From a5bfa0bfe15acca84a8500059083082ee782d38e Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 1 Aug 2018 09:27:57 -0400 Subject: [PATCH 0068/4252] nullChange --- lib/models/patch/change.js | 24 ++++++++++++++++++++++++ test/models/patch/change.test.js | 12 ++++++++++-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/lib/models/patch/change.js b/lib/models/patch/change.js index 2f389bb362..f0ee9f8723 100644 --- a/lib/models/patch/change.js +++ b/lib/models/patch/change.js @@ -1,3 +1,5 @@ +import {nullPosition} from '../marker-position'; + // A contiguous region of additions or deletions within a {Hunk}. // // The Change's position is a {MarkerPosition} containing a {Range} of the change rows within the diff buffer. The @@ -79,3 +81,25 @@ export default class Change { return intChanges; } } + +export const nullChange = { + markOn(...args) { + return nullPosition.markOn(...args); + }, + + setIn(...args) { + return nullPosition.setIn(...args); + }, + + bufferRowCount() { + return nullPosition.bufferRowCount(); + }, + + toStringIn() { + return ''; + }, + + intersectRowsIn() { + return []; + }, +}; diff --git a/test/models/patch/change.test.js b/test/models/patch/change.test.js index 8a34706f52..976f9d5202 100644 --- a/test/models/patch/change.test.js +++ b/test/models/patch/change.test.js @@ -1,5 +1,5 @@ -import Change from '../../../lib/models/patch/change'; -import {fromBufferRange} from '../../../lib/models/marker-position'; +import Change, {nullChange} from '../../../lib/models/patch/change'; +import {fromBufferRange, nullPosition} from '../../../lib/models/marker-position'; describe('Change', function() { it('delegates methods to its MarkerPosition', function() { @@ -65,4 +65,12 @@ describe('Change', function() { const int2 = intersections[2]; assert.strictEqual(int2.toStringIn(buffer, '+'), '+8888\n'); }); + + it('returns appropriate values from nullChange methods', function() { + assert.deepEqual(nullChange.intersectRowsIn(new Set([0, 1, 2]), ''), []); + assert.strictEqual(nullChange.toStringIn('', '+'), ''); + assert.strictEqual(nullChange.markOn(), nullPosition.markOn()); + assert.strictEqual(nullChange.setIn(), nullPosition.setIn()); + assert.strictEqual(nullChange.bufferRowCount(), 0); + }); }); From aee4a12663eb3f5a58e2302fedad97233d1ac05e Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 1 Aug 2018 10:11:27 -0400 Subject: [PATCH 0069/4252] isPresent() methods on Changes --- lib/models/patch/change.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/models/patch/change.js b/lib/models/patch/change.js index f0ee9f8723..4cea4461d8 100644 --- a/lib/models/patch/change.js +++ b/lib/models/patch/change.js @@ -80,6 +80,10 @@ export default class Change { return intChanges; } + + isPresent() { + return true; + } } export const nullChange = { @@ -102,4 +106,8 @@ export const nullChange = { intersectRowsIn() { return []; }, + + isPresent() { + return false; + }, }; From 8756786a9272a6131445630265b5592c92d1bbd6 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 1 Aug 2018 14:16:32 -0400 Subject: [PATCH 0070/4252] Diff builder --- lib/models/patch/builder.js | 172 +++++++++++ test/models/patch/builder.test.js | 480 ++++++++++++++++++++++++++++++ 2 files changed, 652 insertions(+) create mode 100644 lib/models/patch/builder.js create mode 100644 test/models/patch/builder.test.js diff --git a/lib/models/patch/builder.js b/lib/models/patch/builder.js new file mode 100644 index 0000000000..a0808a1971 --- /dev/null +++ b/lib/models/patch/builder.js @@ -0,0 +1,172 @@ +import Hunk from './hunk'; +import File, {nullFile} from './file'; +import Patch, {nullPatch} from './patch'; +import Change, {nullChange} from './change'; +import FilePatch from './file-patch'; +import {fromBufferRange, fromBufferPosition} from '../marker-position'; + +export default function buildFilePatch(diffs) { + if (diffs.length === 0) { + return emptyDiffFilePatch(); + } else if (diffs.length === 1) { + return singleDiffFilePatch(diffs[0]); + } else if (diffs.length === 2) { + return dualDiffFilePatch(...diffs); + } else { + throw new Error(`Unexpected number of diffs: ${diffs.length}`); + } +} + +function emptyDiffFilePatch() { + return new FilePatch(nullFile, nullFile, nullPatch); +} + +function singleDiffFilePatch(diff) { + const wasSymlink = diff.oldMode === '120000'; + const isSymlink = diff.newMode === '120000'; + const [hunks, bufferText] = buildHunks(diff); + + let oldSymlink = null; + let newSymlink = null; + if (wasSymlink && !isSymlink) { + oldSymlink = diff.hunks[0].lines[0].slice(1); + } else if (!wasSymlink && isSymlink) { + newSymlink = diff.hunks[0].lines[0].slice(1); + } else if (wasSymlink && isSymlink) { + oldSymlink = diff.hunks[0].lines[0].slice(1); + newSymlink = diff.hunks[0].lines[2].slice(1); + } + + const oldFile = new File({path: diff.oldPath, mode: diff.oldMode, symlink: oldSymlink}); + const newFile = new File({path: diff.newPath, mode: diff.newMode, symlink: newSymlink}); + const patch = new Patch({status: diff.status, hunks, bufferText}); + + return new FilePatch(oldFile, newFile, patch); +} + +function dualDiffFilePatch(diff1, diff2) { + let modeChangeDiff, contentChangeDiff; + if (diff1.oldMode === '120000' || diff1.newMode === '120000') { + modeChangeDiff = diff1; + contentChangeDiff = diff2; + } else { + modeChangeDiff = diff2; + contentChangeDiff = diff1; + } + + const [hunks, bufferText] = buildHunks(contentChangeDiff); + const filePath = contentChangeDiff.oldPath || contentChangeDiff.newPath; + const symlink = modeChangeDiff.hunks[0].lines[0].slice(1); + + let status; + let oldMode, newMode; + let oldSymlink = null; + let newSymlink = null; + if (modeChangeDiff.status === 'added') { + // contents were deleted and replaced with symlink + status = 'deleted'; + oldMode = contentChangeDiff.oldMode; + newMode = modeChangeDiff.newMode; + newSymlink = symlink; + } else if (modeChangeDiff.status === 'deleted') { + // contents were added after symlink was deleted + status = 'added'; + oldMode = modeChangeDiff.oldMode; + oldSymlink = symlink; + newMode = contentChangeDiff.newMode; + } else { + throw new Error(`Invalid mode change diff status: ${modeChangeDiff.status}`); + } + + const oldFile = new File({path: filePath, mode: oldMode, symlink: oldSymlink}); + const newFile = new File({path: filePath, mode: newMode, symlink: newSymlink}); + const patch = new Patch({status, hunks, bufferText}); + + return new FilePatch(oldFile, newFile, patch); +} + +const STATUS = { + '+': 'added', + '-': 'deleted', + ' ': 'unchanged', + '\\': 'nonewline', +}; + +function buildHunks(diff) { + let bufferText = ''; + const hunks = []; + + let bufferRow = 0; + let bufferOffset = 0; + let startOffset = 0; + + for (const hunkData of diff.hunks) { + const bufferStartRow = bufferRow; + const bufferStartOffset = bufferOffset; + const additions = []; + const deletions = []; + const noNewlines = []; + + let lastStatus = null; + let currentRangeStart = bufferRow; + + const finishCurrentChange = () => { + const changes = { + added: additions, + deleted: deletions, + nonewline: noNewlines, + }[lastStatus]; + if (changes !== undefined) { + changes.push(new Change({ + position: fromBufferRange([[currentRangeStart, 0], [bufferRow - 1, 0]]), + startOffset, + endOffset: bufferOffset, + })); + } + startOffset = bufferOffset; + currentRangeStart = bufferRow; + }; + + for (const lineText of hunkData.lines) { + const bufferLine = lineText.slice(1) + '\n'; + bufferText += bufferLine; + + const status = STATUS[lineText[0]]; + if (status === undefined) { + throw new Error(`Unknown diff status character: "${lineText[0]}"`); + } + + if (status !== lastStatus && lastStatus !== null) { + finishCurrentChange(); + } + + lastStatus = status; + bufferOffset += bufferLine.length; + bufferRow++; + } + finishCurrentChange(); + + let noNewline = nullChange; + if (noNewlines.length === 1) { + noNewline = noNewlines[0]; + } else if (noNewlines.length > 1) { + throw new Error('Multiple nonewline lines encountered in diff'); + } + + hunks.push(new Hunk({ + oldStartRow: hunkData.oldStartLine, + newStartRow: hunkData.newStartLine, + oldRowCount: hunkData.oldLineCount, + newRowCount: hunkData.newLineCount, + sectionHeading: hunkData.heading, + bufferStartPosition: fromBufferPosition([bufferStartRow, 0]), + bufferStartOffset, + bufferEndRow: bufferRow, + additions, + deletions, + noNewline, + })); + } + + return [hunks, bufferText]; +} diff --git a/test/models/patch/builder.test.js b/test/models/patch/builder.test.js new file mode 100644 index 0000000000..0c8628b71c --- /dev/null +++ b/test/models/patch/builder.test.js @@ -0,0 +1,480 @@ +import {buildFilePatch} from '../../../lib/models/patch'; + +describe('buildFilePatch', function() { + let buffer; + + function assertHunkChanges(changes, expectedStrings, expectedRanges) { + const actualStrings = changes.map(change => change.toStringIn(buffer, '*')); + const actualRanges = changes.map(change => change.position.serialize()); + + assert.deepEqual( + {strings: actualStrings, ranges: actualRanges}, + {strings: expectedStrings, ranges: expectedRanges}, + ); + } + + function assertHunk(hunk, {startPosition, startOffset, header, deletions, additions, noNewline}) { + assert.deepEqual(hunk.getBufferStartPosition().serialize(), startPosition); + assert.strictEqual(hunk.getBufferStartOffset(), startOffset); + assert.strictEqual(hunk.getHeader(), header); + + assertHunkChanges(hunk.getDeletions(), deletions.strings, deletions.ranges); + assertHunkChanges(hunk.getAdditions(), additions.strings, additions.ranges); + + const noNewlineChange = hunk.getNoNewline(); + if (noNewlineChange.isPresent()) { + assertHunkChanges([noNewlineChange], [noNewline.string], [noNewline.range]); + } else { + assert.isUndefined(noNewline); + } + } + + it('returns a null patch for an empty diff list', function() { + const p = buildFilePatch([]); + assert.isFalse(p.getOldFile().isPresent()); + assert.isFalse(p.getNewFile().isPresent()); + assert.isFalse(p.getPatch().isPresent()); + }); + + describe('with a single diff', function() { + it('assembles a patch from non-symlink sides', function() { + const p = buildFilePatch([{ + oldPath: 'old/path', + oldMode: '100644', + newPath: 'new/path', + newMode: '100755', + status: 'modified', + hunks: [ + { + oldStartLine: 0, + newStartLine: 0, + oldLineCount: 7, + newLineCount: 6, + lines: [ + ' line-0', + '-line-1', + '-line-2', + '-line-3', + ' line-4', + '+line-5', + '+line-6', + ' line-7', + ' line-8', + ], + }, + { + oldStartLine: 10, + newStartLine: 11, + oldLineCount: 3, + newLineCount: 3, + lines: [ + '-line-9', + ' line-10', + ' line-11', + '+line-12', + ], + }, + { + oldStartLine: 20, + newStartLine: 21, + oldLineCount: 4, + newLineCount: 4, + lines: [ + ' line-13', + '-line-14', + '-line-15', + '+line-16', + '+line-17', + ' line-18', + ], + }, + ], + }]); + + assert.strictEqual(p.getOldPath(), 'old/path'); + assert.strictEqual(p.getOldMode(), '100644'); + assert.strictEqual(p.getNewPath(), 'new/path'); + assert.strictEqual(p.getNewMode(), '100755'); + assert.strictEqual(p.getPatch().getStatus(), 'modified'); + + buffer = + 'line-0\nline-1\nline-2\nline-3\nline-4\nline-5\nline-6\nline-7\nline-8\nline-9\nline-10\n' + + 'line-11\nline-12\nline-13\nline-14\nline-15\nline-16\nline-17\nline-18\n'; + assert.strictEqual(p.getBufferText(), buffer); + + assert.lengthOf(p.getHunks(), 3); + assertHunk(p.getHunks()[0], { + startPosition: [[0, 0], [0, 0]], + startOffset: 0, + header: '@@ -0,7 +0,6 @@\n', + deletions: { + strings: ['*line-1\n*line-2\n*line-3\n'], + ranges: [[[1, 0], [3, 0]]], + }, + additions: { + strings: ['*line-5\n*line-6\n'], + ranges: [[[5, 0], [6, 0]]], + }, + }); + + assertHunk(p.getHunks()[1], { + startPosition: [[9, 0], [9, 0]], + startOffset: 63, + header: '@@ -10,3 +11,3 @@\n', + deletions: { + strings: ['*line-9\n'], + ranges: [[[9, 0], [9, 0]]], + }, + additions: { + strings: ['*line-12\n'], + ranges: [[[12, 0], [12, 0]]], + }, + }); + + assertHunk(p.getHunks()[2], { + startPosition: [[13, 0], [13, 0]], + startOffset: 94, + header: '@@ -20,4 +21,4 @@\n', + deletions: { + strings: ['*line-14\n*line-15\n'], + ranges: [[[14, 0], [15, 0]]], + }, + additions: { + strings: ['*line-16\n*line-17\n'], + ranges: [[[16, 0], [17, 0]]], + }, + }); + }); + + it("sets the old file's symlink destination", function() { + const p = buildFilePatch([{ + oldPath: 'old/path', + oldMode: '120000', + newPath: 'new/path', + newMode: '100644', + status: 'modified', + hunks: [ + { + oldStartLine: 0, + newStartLine: 0, + oldLineCount: 0, + newLineCount: 0, + lines: [' old/destination'], + }, + ], + }]); + + assert.strictEqual(p.getOldSymlink(), 'old/destination'); + assert.isNull(p.getNewSymlink()); + }); + + it("sets the new file's symlink destination", function() { + const p = buildFilePatch([{ + oldPath: 'old/path', + oldMode: '100644', + newPath: 'new/path', + newMode: '120000', + status: 'modified', + hunks: [ + { + oldStartLine: 0, + newStartLine: 0, + oldLineCount: 0, + newLineCount: 0, + lines: [' new/destination'], + }, + ], + }]); + + assert.isNull(p.getOldSymlink()); + assert.strictEqual(p.getNewSymlink(), 'new/destination'); + }); + + it("sets both files' symlink destinations", function() { + const p = buildFilePatch([{ + oldPath: 'old/path', + oldMode: '120000', + newPath: 'new/path', + newMode: '120000', + status: 'modified', + hunks: [ + { + oldStartLine: 0, + newStartLine: 0, + oldLineCount: 0, + newLineCount: 0, + lines: [ + ' old/destination', + ' --', + ' new/destination', + ], + }, + ], + }]); + + assert.strictEqual(p.getOldSymlink(), 'old/destination'); + assert.strictEqual(p.getNewSymlink(), 'new/destination'); + }); + + it('throws an error with an unknown diff status character', function() { + assert.throws(() => { + buildFilePatch([{ + oldPath: 'old/path', + oldMode: '100644', + newPath: 'new/path', + newMode: '100644', + status: 'modified', + hunks: [{oldStartLine: 0, newStartLine: 0, oldLineCount: 1, newLineCount: 1, lines: ['xline-0']}], + }]); + }, /diff status character: "x"/); + }); + + it('parses a no-newline marker', function() { + const p = buildFilePatch([{ + oldPath: 'old/path', + oldMode: '100644', + newPath: 'new/path', + newMode: '100644', + status: 'modified', + hunks: [{oldStartLine: 0, newStartLine: 0, oldLineCount: 1, newLineCount: 1, lines: [ + '+line-0', '-line-1', '\\No newline at end of file', + ]}], + }]); + + buffer = 'line-0\nline-1\nNo newline at end of file\n'; + assert.strictEqual(p.getBufferText(), buffer); + + assert.lengthOf(p.getHunks(), 1); + assertHunk(p.getHunks()[0], { + startPosition: [[0, 0], [0, 0]], + startOffset: 0, + header: '@@ -0,1 +0,1 @@\n', + additions: {strings: ['*line-0\n'], ranges: [[[0, 0], [0, 0]]]}, + deletions: {strings: ['*line-1\n'], ranges: [[[1, 0], [1, 0]]]}, + noNewline: {string: '*No newline at end of file\n', range: [[2, 0], [2, 0]]}, + }); + }); + + it('throws an error when multiple no-newline markers are encountered', function() { + assert.throws(() => { + buildFilePatch([{ + oldPath: 'old/path', + oldMode: '100644', + newPath: 'new/path', + newMode: '100644', + status: 'modified', + hunks: [{oldStartLine: 0, newStartLine: 0, oldLineCount: 1, newLineCount: 1, lines: [ + '\\No newline at end of file', ' unchanged', '\\No newline at end of file', + ]}], + }]); + }, /Multiple nonewline/); + }); + }); + + describe('with a mode change and a content diff', function() { + it('identifies a file that was deleted and replaced by a symlink', function() { + const p = buildFilePatch([ + { + oldPath: 'the-path', + oldMode: '000000', + newPath: 'the-path', + newMode: '120000', + status: 'added', + hunks: [ + { + oldStartLine: 0, + newStartLine: 0, + oldLineCount: 0, + newLineCount: 0, + lines: [' the-destination'], + }, + ], + }, + { + oldPath: 'the-path', + oldMode: '100644', + newPath: 'the-path', + newMode: '000000', + status: 'deleted', + hunks: [ + { + oldStartLine: 0, + newStartLine: 0, + oldLineCount: 0, + newLineCount: 2, + lines: ['+line-0', '+line-1'], + }, + ], + }, + ]); + + assert.strictEqual(p.getOldPath(), 'the-path'); + assert.strictEqual(p.getOldMode(), '100644'); + assert.isNull(p.getOldSymlink()); + assert.strictEqual(p.getNewPath(), 'the-path'); + assert.strictEqual(p.getNewMode(), '120000'); + assert.strictEqual(p.getNewSymlink(), 'the-destination'); + assert.strictEqual(p.getStatus(), 'deleted'); + + buffer = 'line-0\nline-1\n'; + assert.strictEqual(p.getBufferText(), buffer); + assert.lengthOf(p.getHunks(), 1); + assertHunk(p.getHunks()[0], { + startPosition: [[0, 0], [0, 0]], + startOffset: 0, + header: '@@ -0,0 +0,2 @@\n', + deletions: {strings: [], ranges: []}, + additions: { + strings: ['*line-0\n*line-1\n'], + ranges: [[[0, 0], [1, 0]]], + }, + }); + }); + + it('identifies a symlink that was deleted and replaced by a file', function() { + const p = buildFilePatch([ + { + oldPath: 'the-path', + oldMode: '120000', + newPath: 'the-path', + newMode: '000000', + status: 'deleted', + hunks: [ + { + oldStartLine: 0, + newStartLine: 0, + oldLineCount: 0, + newLineCount: 0, + lines: [' the-destination'], + }, + ], + }, + { + oldPath: 'the-path', + oldMode: '000000', + newPath: 'the-path', + newMode: '100644', + status: 'added', + hunks: [ + { + oldStartLine: 0, + newStartLine: 0, + oldLineCount: 2, + newLineCount: 0, + lines: ['-line-0', '-line-1'], + }, + ], + }, + ]); + + assert.strictEqual(p.getOldPath(), 'the-path'); + assert.strictEqual(p.getOldMode(), '120000'); + assert.strictEqual(p.getOldSymlink(), 'the-destination'); + assert.strictEqual(p.getNewPath(), 'the-path'); + assert.strictEqual(p.getNewMode(), '100644'); + assert.isNull(p.getNewSymlink()); + assert.strictEqual(p.getStatus(), 'added'); + + buffer = 'line-0\nline-1\n'; + assert.strictEqual(p.getBufferText(), buffer); + assert.lengthOf(p.getHunks(), 1); + assertHunk(p.getHunks()[0], { + startPosition: [[0, 0], [0, 0]], + startOffset: 0, + header: '@@ -0,2 +0,0 @@\n', + deletions: { + strings: ['*line-0\n*line-1\n'], + ranges: [[[0, 0], [1, 0]]], + }, + additions: {strings: [], ranges: []}, + }); + }); + + it('is indifferent to the order of the diffs', function() { + const p = buildFilePatch([ + { + oldMode: '100644', + newPath: 'the-path', + newMode: '000000', + status: 'deleted', + hunks: [ + { + oldStartLine: 0, + newStartLine: 0, + oldLineCount: 0, + newLineCount: 2, + lines: ['+line-0', '+line-1'], + }, + ], + }, + { + oldPath: 'the-path', + oldMode: '000000', + newPath: 'the-path', + newMode: '120000', + status: 'added', + hunks: [ + { + oldStartLine: 0, + newStartLine: 0, + oldLineCount: 0, + newLineCount: 0, + lines: [' the-destination'], + }, + ], + }, + ]); + + assert.strictEqual(p.getOldPath(), 'the-path'); + assert.strictEqual(p.getOldMode(), '100644'); + assert.isNull(p.getOldSymlink()); + assert.strictEqual(p.getNewPath(), 'the-path'); + assert.strictEqual(p.getNewMode(), '120000'); + assert.strictEqual(p.getNewSymlink(), 'the-destination'); + assert.strictEqual(p.getStatus(), 'deleted'); + + buffer = 'line-0\nline-1\n'; + assert.strictEqual(p.getBufferText(), buffer); + assert.lengthOf(p.getHunks(), 1); + assertHunk(p.getHunks()[0], { + startPosition: [[0, 0], [0, 0]], + startOffset: 0, + header: '@@ -0,0 +0,2 @@\n', + deletions: {strings: [], ranges: []}, + additions: { + strings: ['*line-0\n*line-1\n'], + ranges: [[[0, 0], [1, 0]]], + }, + }); + }); + + it('throws an error on an invalid mode diff status', function() { + assert.throws(() => { + buildFilePatch([ + { + oldMode: '100644', + newPath: 'the-path', + newMode: '000000', + status: 'deleted', + hunks: [ + {oldStartLine: 0, newStartLine: 0, oldLineCount: 0, newLineCount: 2, lines: ['+line-0', '+line-1']}, + ], + }, + { + oldPath: 'the-path', + oldMode: '000000', + newMode: '120000', + status: 'modified', + hunks: [ + {oldStartLine: 0, newStartLine: 0, oldLineCount: 0, newLineCount: 0, lines: [' the-destination']}, + ], + }, + ]); + }, /mode change diff status: modified/); + }); + }); + + it('throws an error with an unexpected number of diffs', function() { + assert.throws(() => buildFilePatch([1, 2, 3]), /Unexpected number of diffs: 3/); + }); +}); From 767bdd5306a085d816779460b1507157fbdf4ec7 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 2 Aug 2018 08:49:52 -0400 Subject: [PATCH 0071/4252] YAGNI for MarkerPositions --- lib/atom/marker.js | 23 ++++++++++++++--------- lib/prop-types.js | 14 ++++++++++---- lib/views/file-patch-view.js | 6 +++--- test/atom/decoration.test.js | 6 +++--- test/atom/marker.test.js | 18 ++++++++++-------- 5 files changed, 40 insertions(+), 27 deletions(-) diff --git a/lib/atom/marker.js b/lib/atom/marker.js index 5bf575ba8f..1ebef279e6 100644 --- a/lib/atom/marker.js +++ b/lib/atom/marker.js @@ -3,17 +3,13 @@ import PropTypes from 'prop-types'; import {CompositeDisposable} from 'event-kit'; import {autobind, extractProps} from '../helpers'; -import {RefHolderPropType, MarkerPositionPropType} from '../prop-types'; +import {RefHolderPropType, RangePropType} from '../prop-types'; import RefHolder from '../models/ref-holder'; -import {fromChangeEvent} from '../models/marker-position'; import {TextEditorContext} from './atom-text-editor'; import {MarkerLayerContext} from './marker-layer'; const MarkablePropType = PropTypes.shape({ markBufferRange: PropTypes.func.isRequired, - markScreenRange: PropTypes.func.isRequired, - markBufferPosition: PropTypes.func.isRequired, - markScreenPosition: PropTypes.func.isRequired, }); const markerProps = { @@ -28,7 +24,7 @@ export const DecorableContext = React.createContext(); class BareMarker extends React.Component { static propTypes = { ...markerProps, - position: MarkerPositionPropType.isRequired, + bufferRange: RangePropType.isRequired, markableHolder: RefHolderPropType, children: PropTypes.node, onDidChange: PropTypes.func, @@ -97,7 +93,7 @@ class BareMarker extends React.Component { const options = extractProps(this.props, markerProps); this.props.markableHolder.map(markable => { - const marker = this.props.position.markOn(markable, options); + const marker = markable.markBufferRange(this.props.bufferRange, options); this.subs.add(marker.onDidChange(this.didChange)); this.markerHolder.setter(marker); @@ -107,12 +103,21 @@ class BareMarker extends React.Component { } updateMarkerPosition() { - this.markerHolder.map(marker => this.props.position.setIn(marker)); + this.markerHolder.map(marker => marker.setBufferRange(this.props.bufferRange)); } didChange(event) { + const reversed = this.markerHolder.map(marker => marker.isReversed()).getOr(false); + + const oldBufferStartPosition = reversed ? event.oldHeadBufferPosition : event.oldTailBufferPosition; + const oldBufferEndPosition = reversed ? event.oldTailBufferPosition : event.oldHeadBufferPosition; + + const newBufferStartPosition = reversed ? event.newHeadBufferPosition : event.newTailBufferPosition; + const newBufferEndPosition = reversed ? event.newTailBufferPosition : event.newHeadBufferPosition; + this.props.onDidChange({ - ...fromChangeEvent(event), + oldRange: new Range(oldBufferStartPosition, oldBufferEndPosition), + newRange: new Range(newBufferStartPosition, newBufferEndPosition), ...event, }); } diff --git a/lib/prop-types.js b/lib/prop-types.js index d8fa6c1ca6..13d0b4d394 100644 --- a/lib/prop-types.js +++ b/lib/prop-types.js @@ -89,10 +89,16 @@ export const RefHolderPropType = PropTypes.shape({ observe: PropTypes.func.isRequired, }); -export const MarkerPositionPropType = PropTypes.shape({ - markOn: PropTypes.func.isRequired, - setIn: PropTypes.func.isRequired, - matches: PropTypes.func.isRequired, +export const PointPropType = PropTypes.shape({ + row: PropTypes.number.isRequired, + column: PropTypes.number.isRequired, + isEqual: PropTypes.func.isRequired, +}); + +export const RangePropType = PropTypes.shape({ + start: PointPropType.isRequired, + end: PointPropType.isRequired, + isEqual: PropTypes.func.isRequired, }); export const EnableableOperationPropType = PropTypes.shape({ diff --git a/lib/views/file-patch-view.js b/lib/views/file-patch-view.js index 9ba6dd3bd2..dcebafb3e3 100644 --- a/lib/views/file-patch-view.js +++ b/lib/views/file-patch-view.js @@ -147,7 +147,7 @@ export default class FilePatchView extends React.Component { onMouseMove={this.didMouseMoveOnLineNumber} /> - + {this.renderExecutableModeChangeMeta()} @@ -324,7 +324,7 @@ export default class FilePatchView extends React.Component { return ( @@ -365,7 +365,7 @@ export default class FilePatchView extends React.Component { return ( diff --git a/test/atom/decoration.test.js b/test/atom/decoration.test.js index 6be94ab9ff..ee715009be 100644 --- a/test/atom/decoration.test.js +++ b/test/atom/decoration.test.js @@ -1,12 +1,12 @@ import React from 'react'; import sinon from 'sinon'; import {mount} from 'enzyme'; +import {Range} from 'atom'; import Decoration from '../../lib/atom/decoration'; import AtomTextEditor from '../../lib/atom/atom-text-editor'; import Marker from '../../lib/atom/marker'; import MarkerLayer from '../../lib/atom/marker-layer'; -import {fromBufferRange, fromBufferPosition} from '../../lib/models/marker-position'; describe('Decoration', function() { let atomEnv, editor, marker; @@ -119,7 +119,7 @@ describe('Decoration', function() { it('decorates a parent Marker', function() { const wrapper = mount( - + , @@ -133,7 +133,7 @@ describe('Decoration', function() { mount( - + , diff --git a/test/atom/marker.test.js b/test/atom/marker.test.js index 5ca555f438..9d304bd2ee 100644 --- a/test/atom/marker.test.js +++ b/test/atom/marker.test.js @@ -1,10 +1,10 @@ import React from 'react'; import {mount} from 'enzyme'; +import {Range} from 'atom'; import Marker from '../../lib/atom/marker'; import AtomTextEditor from '../../lib/atom/atom-text-editor'; import MarkerLayer from '../../lib/atom/marker-layer'; -import {fromBufferRange, fromScreenRange, fromBufferPosition} from '../../lib/models/marker-position'; describe('Marker', function() { let atomEnv, editor, markerID; @@ -24,7 +24,7 @@ describe('Marker', function() { it('adds its marker on mount with default properties', function() { mount( - , + , ); const marker = editor.getMarker(markerID); @@ -38,7 +38,7 @@ describe('Marker', function() { , ); @@ -68,7 +68,9 @@ describe('Marker', function() { }); it('destroys its marker on unmount', function() { - const wrapper = mount(); + const wrapper = mount( + , + ); assert.isDefined(editor.getMarker(markerID)); wrapper.unmount(); @@ -78,7 +80,7 @@ describe('Marker', function() { it('marks an editor from a parent node', function() { const wrapper = mount( - + , ); @@ -92,7 +94,7 @@ describe('Marker', function() { const wrapper = mount( { layerID = id; }}> - + , ); From da5c52959c14e3aede06a6f059e59d21bfcbe794 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 2 Aug 2018 10:08:05 -0400 Subject: [PATCH 0072/4252] Replace MarkerPosition and Change with IndexedRowRange --- lib/models/indexed-row-range.js | 91 ++++++++ lib/models/marker-position.js | 261 ---------------------- lib/models/patch/builder.js | 11 +- lib/models/patch/change.js | 113 ---------- test/models/indexed-row-range.test.js | 104 +++++++++ test/models/marker-position.test.js | 310 -------------------------- test/models/patch/change.test.js | 76 ------- 7 files changed, 200 insertions(+), 766 deletions(-) create mode 100644 lib/models/indexed-row-range.js delete mode 100644 lib/models/marker-position.js delete mode 100644 lib/models/patch/change.js create mode 100644 test/models/indexed-row-range.test.js delete mode 100644 test/models/marker-position.test.js delete mode 100644 test/models/patch/change.test.js diff --git a/lib/models/indexed-row-range.js b/lib/models/indexed-row-range.js new file mode 100644 index 0000000000..31bf5af749 --- /dev/null +++ b/lib/models/indexed-row-range.js @@ -0,0 +1,91 @@ +import {Range} from 'atom'; + +// A {Range} of rows within a buffer accompanied by its corresponding start and end offsets. +// +// Note that the range's columns are disregarded for purposes of offset consistency. +export default class IndexedRowRange { + constructor({bufferRange, startOffset, endOffset}) { + this.bufferRange = Range.fromObject(bufferRange); + this.startOffset = startOffset; + this.endOffset = endOffset; + } + + bufferRowCount() { + return this.bufferRange.getRowCount(); + } + + toStringIn(buffer, prefix) { + return buffer.slice(this.startOffset, this.endOffset).replace(/(^|\n)(?!$)/g, '$&' + prefix); + } + + intersectRowsIn(rowSet, buffer) { + // Identify Ranges within our bufferRange that intersect the rows in rowSet. + const intersections = []; + let nextStartRow = null; + let nextStartOffset = null; + + let currentRow = this.bufferRange.start.row; + let currentOffset = this.startOffset; + + while (currentRow <= this.bufferRange.end.row) { + if (rowSet.has(currentRow) && nextStartRow === null) { + // Start of intersecting row range + nextStartRow = currentRow; + nextStartOffset = currentOffset; + } else if (!rowSet.has(currentRow) && nextStartRow !== null) { + // One row past the end of intersecting row range + intersections.push(new IndexedRowRange({ + bufferRange: Range.fromObject([[nextStartRow, 0], [currentRow - 1, 0]]), + startOffset: nextStartOffset, + endOffset: currentOffset, + })); + + nextStartRow = null; + nextStartOffset = null; + } + + currentOffset = buffer.indexOf('\n', currentOffset) + 1; + currentRow++; + } + + if (nextStartRow !== null) { + intersections.push(new IndexedRowRange({ + bufferRange: Range.fromObject([[nextStartRow, 0], this.bufferRange.end]), + startOffset: nextStartOffset, + endOffset: currentOffset, + })); + } + + return intersections; + } + + serialize() { + return { + bufferRange: this.bufferRange.serialize(), + startOffset: this.startOffset, + endOffset: this.endOffset, + }; + } + + isPresent() { + return true; + } +} + +export const nullIndexedRowRange = { + bufferRowCount() { + return 0; + }, + + toStringIn() { + return ''; + }, + + intersectRowsIn() { + return []; + }, + + isPresent() { + return false; + }, +}; diff --git a/lib/models/marker-position.js b/lib/models/marker-position.js deleted file mode 100644 index f60d9987ab..0000000000 --- a/lib/models/marker-position.js +++ /dev/null @@ -1,261 +0,0 @@ -import {Range} from 'atom'; - -class Position { - constructor(bufferRange, screenRange) { - this.bufferRange = bufferRange && Range.fromObject(bufferRange); - this.screenRange = screenRange && Range.fromObject(screenRange); - } - - /* istanbul ignore next */ - markOn(markable, options) { - throw new Error('markOn not overridden'); - } - - /* istanbul ignore next */ - setIn(marker) { - throw new Error('setIn not overridden'); - } - - /* istanbul ignore next */ - matches(other) { - throw new Error('matches not overridden'); - } - - matchFromBufferRange(other) { - return false; - } - - matchFromScreenRange(other) { - return false; - } - - bufferStartRow() { - return this.bufferRange !== null ? this.bufferRange.start.row : -1; - } - - bufferEndRow() { - return this.bufferRange !== null ? this.bufferRange.end.row : -1; - } - - bufferRowCount() { - return this.bufferRange !== null ? this.bufferRange.getRowCount() : 0; - } - - intersectRows(rowSet) { - if (this.bufferRange === null) { - return []; - } - - const intersections = []; - let currentRangeStart = null; - - let row = this.bufferRange.start.row; - while (row <= this.bufferRange.end.row) { - if (rowSet.has(row) && currentRangeStart === null) { - currentRangeStart = row; - } else if (!rowSet.has(row) && currentRangeStart !== null) { - intersections.push( - fromBufferRange([[currentRangeStart, 0], [row - 1, 0]]), - ); - currentRangeStart = null; - } - row++; - } - if (currentRangeStart !== null) { - intersections.push( - fromBufferRange([[currentRangeStart, 0], this.bufferRange.end]), - ); - } - - return intersections; - } - - /* istanbul ignore next */ - serialize() { - throw new Error('serialize not overridden'); - } - - isPresent() { - return true; - } -} - -class BufferRangePosition extends Position { - constructor(bufferRange) { - super(bufferRange, null); - } - - markOn(markable, options) { - return markable.markBufferRange(this.bufferRange, options); - } - - setIn(marker) { - return marker.setBufferRange(this.bufferRange); - } - - matches(other) { - return other.matchFromBufferRange(this); - } - - matchFromBufferRange(other) { - return other.bufferRange.isEqual(this.bufferRange); - } - - serialize() { - return this.bufferRange.serialize(); - } - - toString() { - return `buffer(${this.bufferRange.toString()})`; - } -} - -class ScreenRangePosition extends Position { - constructor(screenRange) { - super(null, screenRange); - } - - markOn(markable, options) { - return markable.markScreenRange(this.screenRange, options); - } - - setIn(marker) { - return marker.setScreenRange(this.screenRange); - } - - matches(other) { - return other.matchFromScreenRange(this); - } - - matchFromScreenRange(other) { - return other.screenRange.isEqual(this.screenRange); - } - - serialize() { - return this.screenRange.serialize(); - } - - toString() { - return `screen(${this.screenRange.toString()})`; - } -} - -class BufferOrScreenRangePosition extends Position { - markOn(markable, options) { - return markable.markBufferRange(this.bufferRange, options); - } - - setIn(marker) { - return marker.setBufferRange(this.bufferRange); - } - - matches(other) { - return other.matchFromBufferRange(this) || other.matchFromScreenRange(this); - } - - matchFromBufferRange(other) { - return other.bufferRange.isEqual(this.bufferRange); - } - - matchFromScreenRange(other) { - return other.screenRange.isEqual(this.screenRange); - } - - serialize() { - return this.bufferRange.serialize(); - } - - toString() { - return `either(b${this.bufferRange.toString()}/s${this.screenRange.toString()})`; - } -} - -export const nullPosition = { - markOn() { - return null; - }, - - setIn() {}, - - matches(other) { - return other === this; - }, - - matchFromBufferRange() { - return false; - }, - - matchFromScreenRange() { - return false; - }, - - bufferStartRow() { - return -1; - }, - - bufferRowCount() { - return 0; - }, - - intersectRows() { - return []; - }, - - serialize() { - return null; - }, - - isPresent() { - return false; - }, - - toString() { - return 'null'; - }, -}; - -export function fromBufferRange(bufferRange) { - return new BufferRangePosition(Range.fromObject(bufferRange)); -} - -export function fromBufferPosition(bufferPoint) { - return new BufferRangePosition(new Range(bufferPoint, bufferPoint)); -} - -export function fromScreenRange(screenRange) { - return new ScreenRangePosition(Range.fromObject(screenRange)); -} - -export function fromScreenPosition(screenPoint) { - return new ScreenRangePosition(new Range(screenPoint, screenPoint)); -} - -export function fromMarker(marker) { - return new BufferOrScreenRangePosition( - marker.getBufferRange(), - marker.getScreenRange(), - ); -} - -export function fromChangeEvent(event, reversed = false) { - const oldBufferStartPosition = reversed ? event.oldHeadBufferPosition : event.oldTailBufferPosition; - const oldBufferEndPosition = reversed ? event.oldTailBufferPosition : event.oldHeadBufferPosition; - const oldScreenStartPosition = reversed ? event.oldHeadScreenPosition : event.oldTailScreenPosition; - const oldScreenEndPosition = reversed ? event.oldTailScreenPosition : event.oldHeadScreenPosition; - - const newBufferStartPosition = reversed ? event.newHeadBufferPosition : event.newTailBufferPosition; - const newBufferEndPosition = reversed ? event.newTailBufferPosition : event.newHeadBufferPosition; - const newScreenStartPosition = reversed ? event.newHeadScreenPosition : event.newTailScreenPosition; - const newScreenEndPosition = reversed ? event.newTailScreenPosition : event.newHeadScreenPosition; - - return { - oldPosition: new BufferOrScreenRangePosition( - new Range(oldBufferStartPosition, oldBufferEndPosition), - new Range(oldScreenStartPosition, oldScreenEndPosition), - ), - newPosition: new BufferOrScreenRangePosition( - new Range(newBufferStartPosition, newBufferEndPosition), - new Range(newScreenStartPosition, newScreenEndPosition), - ), - }; -} diff --git a/lib/models/patch/builder.js b/lib/models/patch/builder.js index a0808a1971..2f5b5cd34b 100644 --- a/lib/models/patch/builder.js +++ b/lib/models/patch/builder.js @@ -1,9 +1,8 @@ import Hunk from './hunk'; import File, {nullFile} from './file'; import Patch, {nullPatch} from './patch'; -import Change, {nullChange} from './change'; +import IndexedRowRange, {nullIndexedRowRange} from '../indexed-row-range'; import FilePatch from './file-patch'; -import {fromBufferRange, fromBufferPosition} from '../marker-position'; export default function buildFilePatch(diffs) { if (diffs.length === 0) { @@ -117,8 +116,8 @@ function buildHunks(diff) { nonewline: noNewlines, }[lastStatus]; if (changes !== undefined) { - changes.push(new Change({ - position: fromBufferRange([[currentRangeStart, 0], [bufferRow - 1, 0]]), + changes.push(new IndexedRowRange({ + bufferRange: Range.fromObject([[currentRangeStart, 0], [bufferRow - 1, 0]]), startOffset, endOffset: bufferOffset, })); @@ -146,7 +145,7 @@ function buildHunks(diff) { } finishCurrentChange(); - let noNewline = nullChange; + let noNewline = nullIndexedRowRange; if (noNewlines.length === 1) { noNewline = noNewlines[0]; } else if (noNewlines.length > 1) { @@ -159,7 +158,7 @@ function buildHunks(diff) { oldRowCount: hunkData.oldLineCount, newRowCount: hunkData.newLineCount, sectionHeading: hunkData.heading, - bufferStartPosition: fromBufferPosition([bufferStartRow, 0]), + bufferStartPosition: null, // fromBufferPosition([bufferStartRow, 0]), bufferStartOffset, bufferEndRow: bufferRow, additions, diff --git a/lib/models/patch/change.js b/lib/models/patch/change.js deleted file mode 100644 index 4cea4461d8..0000000000 --- a/lib/models/patch/change.js +++ /dev/null @@ -1,113 +0,0 @@ -import {nullPosition} from '../marker-position'; - -// A contiguous region of additions or deletions within a {Hunk}. -// -// The Change's position is a {MarkerPosition} containing a {Range} of the change rows within the diff buffer. The -// calculated offsets delimit a half-open interval of {String} code point offsets within the diff buffer, such that -// `buffer.slice(startOffset, endOffset)` returns the exact contents of the changed lines, including the last line -// and its line-end character. -export default class Change { - constructor({position, startOffset, endOffset}) { - this.position = position; - this.startOffset = startOffset; - this.endOffset = endOffset; - } - - markOn(markable, options) { - return this.position.markOn(markable, options); - } - - setIn(marker) { - return this.position.setIn(marker); - } - - bufferRowCount() { - return this.position.bufferRowCount(); - } - - toStringIn(buffer, origin) { - let str = ''; - for (let offset = this.startOffset; offset < this.endOffset; offset++) { - const ch = buffer[offset]; - if (offset === this.startOffset) { - str += origin; - } - str += ch; - if (ch === '\n' && offset !== this.endOffset - 1) { - str += origin; - } - } - return str; - } - - intersectRowsIn(rowSet, buffer) { - const intPositions = this.position.intersectRows(rowSet); - const intChanges = []; - let intIndex = 0; - let currentRow = this.position.bufferStartRow(); - let currentOffset = this.startOffset; - let nextStartOffset = null; - - while (intIndex < intPositions.length && currentOffset < this.endOffset) { - const currentInt = intPositions[intIndex]; - - if (currentRow === currentInt.bufferStartRow()) { - nextStartOffset = currentOffset; - } - - if (currentRow === currentInt.bufferEndRow() + 1) { - intChanges.push(new this.constructor({ - position: currentInt, - startOffset: nextStartOffset, - endOffset: currentOffset, - })); - - intIndex++; - nextStartOffset = null; - } - - currentOffset = buffer.indexOf('\n', currentOffset) + 1; - currentRow++; - } - - if (intIndex < intPositions.length && nextStartOffset !== null) { - intChanges.push(new this.constructor({ - position: intPositions[intIndex], - startOffset: nextStartOffset, - endOffset: currentOffset, - })); - } - - return intChanges; - } - - isPresent() { - return true; - } -} - -export const nullChange = { - markOn(...args) { - return nullPosition.markOn(...args); - }, - - setIn(...args) { - return nullPosition.setIn(...args); - }, - - bufferRowCount() { - return nullPosition.bufferRowCount(); - }, - - toStringIn() { - return ''; - }, - - intersectRowsIn() { - return []; - }, - - isPresent() { - return false; - }, -}; diff --git a/test/models/indexed-row-range.test.js b/test/models/indexed-row-range.test.js new file mode 100644 index 0000000000..1cd83d2a31 --- /dev/null +++ b/test/models/indexed-row-range.test.js @@ -0,0 +1,104 @@ +import IndexedRowRange, {nullIndexedRowRange} from '../../lib/models/indexed-row-range'; + +describe('IndexedRowRange', function() { + it('computes its row count', function() { + const range = new IndexedRowRange({ + bufferRange: [[0, 0], [1, 0]], + startOffset: 0, + endOffset: 10, + }); + assert.isTrue(range.isPresent()); + assert.deepEqual(range.bufferRowCount(), 2); + }); + + it('extracts its offset range from buffer text with toStringIn()', function() { + const buffer = '0000\n1111\n2222\n3333\n4444\n5555\n'; + const range = new IndexedRowRange({ + bufferRange: [[1, 0], [2, 0]], + startOffset: 5, + endOffset: 25, + }); + + assert.strictEqual(range.toStringIn(buffer, '+'), '+1111\n+2222\n+3333\n+4444\n'); + assert.strictEqual(range.toStringIn(buffer, '-'), '-1111\n-2222\n-3333\n-4444\n'); + }); + + describe('intersectRowsIn()', function() { + const buffer = '0000\n1111\n2222\n3333\n4444\n5555\n6666\n7777\n8888\n9999\n'; + // 0000.1111.2222.3333.4444.5555.6666.7777.8888.9999. + + it('returns an empty array with no intersection rows', function() { + const range = new IndexedRowRange({ + bufferRange: [[1, 0], [3, 0]], + startOffset: 5, + endOffset: 20, + }); + + assert.deepEqual(range.intersectRowsIn(new Set([0, 5, 6]), buffer), []); + }); + + it('detects an intersection at the beginning of the range', function() { + const range = new IndexedRowRange({ + bufferRange: [[2, 0], [6, 0]], + startOffset: 10, + endOffset: 35, + }); + const rowSet = new Set([0, 1, 2, 3]); + + assert.deepEqual(range.intersectRowsIn(rowSet, buffer).map(i => i.serialize()), [ + {bufferRange: [[2, 0], [3, 0]], startOffset: 10, endOffset: 20}, + ]); + }); + + it('detects an intersection in the middle of the range', function() { + const range = new IndexedRowRange({ + bufferRange: [[2, 0], [6, 0]], + startOffset: 10, + endOffset: 35, + }); + const rowSet = new Set([0, 3, 4, 8, 9]); + + assert.deepEqual(range.intersectRowsIn(rowSet, buffer).map(i => i.serialize()), [ + {bufferRange: [[3, 0], [4, 0]], startOffset: 15, endOffset: 25}, + ]); + }); + + it('detects an intersection at the end of the range', function() { + const range = new IndexedRowRange({ + bufferRange: [[2, 0], [6, 0]], + startOffset: 10, + endOffset: 35, + }); + const rowSet = new Set([4, 5, 6, 7, 10, 11]); + + assert.deepEqual(range.intersectRowsIn(rowSet, buffer).map(i => i.serialize()), [ + {bufferRange: [[4, 0], [6, 0]], startOffset: 20, endOffset: 35}, + ]); + }); + + it('detects multiple intersections', function() { + const range = new IndexedRowRange({ + bufferRange: [[2, 0], [8, 0]], + startOffset: 10, + endOffset: 45, + }); + const rowSet = new Set([0, 3, 4, 6, 7, 10]); + + assert.deepEqual(range.intersectRowsIn(rowSet, buffer).map(i => i.serialize()), [ + {bufferRange: [[3, 0], [4, 0]], startOffset: 15, endOffset: 25}, + {bufferRange: [[6, 0], [7, 0]], startOffset: 30, endOffset: 40}, + ]); + }); + + it('returns an empty array for the null range', function() { + assert.deepEqual(nullIndexedRowRange.intersectRowsIn(new Set([1, 2, 3]), buffer), []); + }); + }); + + it('returns appropriate values from nullIndexedRowRange methods', function() { + assert.deepEqual(nullIndexedRowRange.intersectRowsIn(new Set([0, 1, 2]), ''), []); + assert.strictEqual(nullIndexedRowRange.toStringIn('', '+'), ''); + assert.strictEqual(nullIndexedRowRange.bufferRowCount(), 0); + assert.isFalse(nullIndexedRowRange.isPresent()); + }); +}); diff --git a/test/models/marker-position.test.js b/test/models/marker-position.test.js deleted file mode 100644 index 3f5b069eec..0000000000 --- a/test/models/marker-position.test.js +++ /dev/null @@ -1,310 +0,0 @@ -import { - nullPosition, fromBufferRange, fromBufferPosition, fromScreenRange, fromScreenPosition, fromMarker, fromChangeEvent, -} from '../../lib/models/marker-position'; - -describe('MarkerPosition', function() { - let atomEnv, editor; - - beforeEach(async function() { - atomEnv = global.buildAtomEnvironment(); - - editor = await atomEnv.workspace.open(__filename); - }); - - afterEach(function() { - atomEnv.destroy(); - }); - - describe('markOn', function() { - it('marks a buffer range', function() { - const position = fromBufferRange([[1, 0], [4, 0]]); - const marker = position.markOn(editor, {invalidate: 'never'}); - - assert.deepEqual(marker.getBufferRange().serialize(), [[1, 0], [4, 0]]); - assert.strictEqual(marker.getInvalidationStrategy(), 'never'); - }); - - it('marks a buffer position', function() { - const position = fromBufferPosition([2, 0]); - const marker = position.markOn(editor, {invalidate: 'never'}); - - assert.deepEqual(marker.getBufferRange().serialize(), [[2, 0], [2, 0]]); - assert.strictEqual(marker.getInvalidationStrategy(), 'never'); - }); - - it('marks a screen range', function() { - const position = fromScreenRange([[2, 0], [5, 0]]); - const marker = position.markOn(editor, {invalidate: 'never'}); - - assert.deepEqual(marker.getBufferRange().serialize(), [[2, 0], [5, 0]]); - assert.strictEqual(marker.getInvalidationStrategy(), 'never'); - }); - - it('marks a screen position', function() { - const position = fromScreenPosition([3, 0]); - const marker = position.markOn(editor, {invalidate: 'never'}); - - assert.deepEqual(marker.getBufferRange().serialize(), [[3, 0], [3, 0]]); - assert.strictEqual(marker.getInvalidationStrategy(), 'never'); - }); - - it('marks a combination position', function() { - const marker0 = editor.markBufferRange([[0, 0], [2, 0]]); - const position = fromMarker(marker0); - - const marker1 = position.markOn(editor, {invalidate: 'never'}); - assert.deepEqual(marker1.getBufferRange().serialize(), [[0, 0], [2, 0]]); - assert.strictEqual(marker1.getInvalidationStrategy(), 'never'); - }); - - it('does nothing with a nullPosition', function() { - assert.isNull(nullPosition.markOn(editor, {})); - assert.lengthOf(editor.findMarkers({}), 0); - }); - }); - - describe('setIn', function() { - let marker; - - beforeEach(function() { - marker = editor.markBufferRange([[1, 0], [3, 0]]); - }); - - it('updates an existing marker by buffer range', function() { - fromBufferRange([[2, 0], [4, 0]]).setIn(marker); - assert.deepEqual(marker.getBufferRange().serialize(), [[2, 0], [4, 0]]); - }); - - it('updates an existing marker by screen range', function() { - fromScreenRange([[6, 0], [7, 0]]).setIn(marker); - assert.deepEqual(marker.getBufferRange().serialize(), [[6, 0], [7, 0]]); - }); - - it('updates with a combination position', function() { - const other = editor.markBufferRange([[2, 0], [4, 0]]); - fromMarker(other).setIn(marker); - assert.deepEqual(marker.getBufferRange().serialize(), [[2, 0], [4, 0]]); - }); - - it('does nothing with a nullPosition', function() { - nullPosition.setIn(marker); - assert.deepEqual(marker.getBufferRange().serialize(), [[1, 0], [3, 0]]); - }); - }); - - describe('matches', function() { - let marker0, marker1; - - beforeEach(function() { - marker0 = editor.markBufferRange([[2, 0], [4, 0]]); - marker1 = editor.markBufferRange([[1, 0], [3, 0]]); - }); - - it('a buffer range', function() { - const position = fromBufferRange([[2, 0], [4, 0]]); - assert.isTrue(position.matches(fromBufferRange([[2, 0], [4, 0]]))); - assert.isFalse(position.matches(fromBufferRange([[2, 0], [5, 0]]))); - assert.isFalse(position.matches(fromScreenRange([[2, 0], [4, 0]]))); - assert.isTrue(position.matches(fromMarker(marker0))); - assert.isFalse(position.matches(fromMarker(marker1))); - assert.isFalse(position.matches(nullPosition)); - }); - - it('a screen range', function() { - const position = fromScreenRange([[1, 0], [3, 0]]); - assert.isTrue(position.matches(fromScreenRange([[1, 0], [3, 0]]))); - assert.isFalse(position.matches(fromScreenRange([[2, 0], [4, 0]]))); - assert.isFalse(position.matches(fromBufferRange([[1, 0], [3, 0]]))); - assert.isFalse(position.matches(fromMarker(marker0))); - assert.isTrue(position.matches(fromMarker(marker1))); - assert.isFalse(position.matches(nullPosition)); - }); - - it('a combination range', function() { - const position = fromMarker(marker0); - assert.isTrue(position.matches(fromMarker(marker0))); - assert.isFalse(position.matches(fromMarker(marker1))); - assert.isTrue(position.matches(fromBufferRange([[2, 0], [4, 0]]))); - assert.isFalse(position.matches(fromBufferRange([[1, 0], [3, 0]]))); - assert.isTrue(position.matches(fromScreenRange([[2, 0], [4, 0]]))); - assert.isFalse(position.matches(fromScreenRange([[1, 0], [3, 0]]))); - assert.isFalse(position.matches(nullPosition)); - }); - - it('a null position', function() { - assert.isTrue(nullPosition.matches(nullPosition)); - assert.isFalse(nullPosition.matches(fromBufferRange([[1, 0], [2, 0]]))); - assert.isFalse(nullPosition.matches(fromScreenRange([[1, 0], [2, 0]]))); - assert.isFalse(nullPosition.matches(fromMarker(marker0))); - }); - }); - - describe('bufferStartRow()', function() { - it('retrieves the first row from a buffer range', function() { - assert.strictEqual(fromBufferRange([[2, 0], [3, 4]]).bufferStartRow(), 2); - }); - - it('retrieves the first row from a combination range', function() { - const marker = editor.markBufferRange([[3, 0], [5, 0]]); - assert.strictEqual(fromMarker(marker).bufferStartRow(), 3); - }); - - it('returns -1 from a screen range', function() { - assert.strictEqual(fromScreenRange([[1, 0], [2, 0]]).bufferStartRow(), -1); - }); - - it('returns -1 from a null position', function() { - assert.strictEqual(nullPosition.bufferStartRow(), -1); - }); - }); - - describe('bufferRowCount()', function() { - it('counts the rows in a buffer range', function() { - assert.strictEqual(fromBufferRange([[1, 0], [4, 0]]).bufferRowCount(), 4); - assert.strictEqual(fromBufferPosition([2, 0]).bufferRowCount(), 1); - }); - - it('counts the rows in a combination range', function() { - const marker = editor.markBufferRange([[2, 0], [6, 0]]); - assert.strictEqual(fromMarker(marker).bufferRowCount(), 5); - }); - - it('returns 0 for screen or null ranges', function() { - assert.strictEqual(fromScreenRange([[1, 0], [2, 0]]).bufferRowCount(), 0); - assert.strictEqual(nullPosition.bufferRowCount(), 0); - }); - }); - - describe('intersectRows()', function() { - it('returns an empty array with no intersection rows', function() { - assert.deepEqual(fromBufferRange([[1, 0], [3, 0]]).intersectRows(new Set([0, 5, 6])), []); - }); - - it('detects an intersection at the beginning of the range', function() { - const position = fromBufferRange([[2, 0], [6, 0]]); - const rowSet = new Set([0, 1, 2, 3]); - - assert.deepEqual(position.intersectRows(rowSet).map(i => i.serialize()), [ - [[2, 0], [3, 0]], - ]); - }); - - it('detects an intersection in the middle of the range', function() { - const position = fromBufferRange([[2, 0], [6, 0]]); - const rowSet = new Set([0, 3, 4, 8, 9]); - - assert.deepEqual(position.intersectRows(rowSet).map(i => i.serialize()), [ - [[3, 0], [4, 0]], - ]); - }); - - it('detects an intersection at the end of the range', function() { - const position = fromBufferRange([[2, 0], [6, 0]]); - const rowSet = new Set([4, 5, 6, 7, 10, 11]); - - assert.deepEqual(position.intersectRows(rowSet).map(i => i.serialize()), [ - [[4, 0], [6, 0]], - ]); - }); - - it('detects multiple intersections', function() { - const position = fromBufferRange([[2, 0], [8, 0]]); - const rowSet = new Set([0, 3, 4, 6, 7, 10]); - - assert.deepEqual(position.intersectRows(rowSet).map(i => i.serialize()), [ - [[3, 0], [4, 0]], - [[6, 0], [7, 0]], - ]); - }); - - it('returns an empty array for screen or null ranges', function() { - assert.deepEqual(fromScreenRange([[1, 0], [4, 0]]).intersectRows(new Set()), []); - assert.deepEqual(nullPosition.intersectRows(new Set()), []); - }); - }); - - describe('isPresent()', function() { - it('returns true on non-null positions', function() { - assert.isTrue(fromBufferRange([[1, 0], [2, 0]]).isPresent()); - assert.isTrue(fromScreenRange([[1, 0], [2, 0]]).isPresent()); - - const marker = editor.markBufferRange([[1, 0], [2, 0]]); - assert.isTrue(fromMarker(marker).isPresent()); - }); - - it('returns false on null positions', function() { - assert.isFalse(nullPosition.isPresent()); - }); - }); - - describe('serialize()', function() { - it('produces an array', function() { - assert.deepEqual(fromBufferRange([[0, 0], [1, 1]]).serialize(), [[0, 0], [1, 1]]); - assert.deepEqual(fromScreenRange([[0, 0], [1, 1]]).serialize(), [[0, 0], [1, 1]]); - - const marker = editor.markBufferRange([[2, 2], [3, 0]]); - assert.deepEqual(fromMarker(marker).serialize(), [[2, 2], [3, 0]]); - }); - - it('serializes a null position as null', function() { - assert.isNull(nullPosition.serialize()); - }); - }); - - describe('toString()', function() { - it('pretty-prints buffer ranges', function() { - assert.strictEqual(fromBufferRange([[0, 0], [2, 0]]).toString(), 'buffer([(0, 0) - (2, 0)])'); - }); - - it('pretty-prints screen ranges', function() { - assert.strictEqual(fromScreenRange([[3, 0], [7, 0]]).toString(), 'screen([(3, 0) - (7, 0)])'); - }); - - it('pretty-prints combination ranges', function() { - const marker = editor.markBufferRange([[1, 0], [3, 0]]); - assert.strictEqual(fromMarker(marker).toString(), 'either(b[(1, 0) - (3, 0)]/s[(1, 0) - (3, 0)])'); - }); - - it('pretty-prints a null position', function() { - assert.strictEqual(nullPosition.toString(), 'null'); - }); - }); - - describe('fromChangeEvent()', function() { - it('produces positions from a non-reversed marker change', function() { - const {oldPosition, newPosition} = fromChangeEvent({ - oldTailBufferPosition: [0, 0], - oldHeadBufferPosition: [1, 1], - oldTailScreenPosition: [2, 2], - oldHeadScreenPosition: [3, 3], - newTailBufferPosition: [4, 4], - newHeadBufferPosition: [5, 5], - newTailScreenPosition: [6, 6], - newHeadScreenPosition: [7, 7], - }); - - assert.isTrue(oldPosition.matches(fromBufferRange([[0, 0], [1, 1]]))); - assert.isTrue(oldPosition.matches(fromScreenRange([[2, 2], [3, 3]]))); - assert.isTrue(newPosition.matches(fromBufferRange([[4, 4], [5, 5]]))); - assert.isTrue(newPosition.matches(fromScreenRange([[6, 6], [7, 7]]))); - }); - - it('produces positions from a reversed marker change', function() { - const {oldPosition, newPosition} = fromChangeEvent({ - oldTailBufferPosition: [0, 0], - oldHeadBufferPosition: [1, 1], - oldTailScreenPosition: [2, 2], - oldHeadScreenPosition: [3, 3], - newTailBufferPosition: [4, 4], - newHeadBufferPosition: [5, 5], - newTailScreenPosition: [6, 6], - newHeadScreenPosition: [7, 7], - }, true); - - assert.isTrue(oldPosition.matches(fromBufferRange([[1, 1], [0, 0]]))); - assert.isTrue(oldPosition.matches(fromScreenRange([[3, 3], [2, 2]]))); - assert.isTrue(newPosition.matches(fromBufferRange([[5, 5], [4, 4]]))); - assert.isTrue(newPosition.matches(fromScreenRange([[7, 7], [6, 6]]))); - }); - }); -}); diff --git a/test/models/patch/change.test.js b/test/models/patch/change.test.js deleted file mode 100644 index 976f9d5202..0000000000 --- a/test/models/patch/change.test.js +++ /dev/null @@ -1,76 +0,0 @@ -import Change, {nullChange} from '../../../lib/models/patch/change'; -import {fromBufferRange, nullPosition} from '../../../lib/models/marker-position'; - -describe('Change', function() { - it('delegates methods to its MarkerPosition', function() { - const ch = new Change({ - position: fromBufferRange([[0, 0], [1, 0]]), - startOffset: 0, - endOffset: 10, - }); - - const markable = {markBufferRange: sinon.stub().returns(0)}; - assert.strictEqual(ch.markOn(markable, {}), 0); - assert.deepEqual(markable.markBufferRange.firstCall.args[0].serialize(), [[0, 0], [1, 0]]); - - const marker = {setBufferRange: sinon.stub().returns(1)}; - assert.strictEqual(ch.setIn(marker), 1); - assert.deepEqual(marker.setBufferRange.firstCall.args[0].serialize(), [[0, 0], [1, 0]]); - - assert.deepEqual(ch.bufferRowCount(), 2); - }); - - it('extracts its offset range from buffer text with toStringIn()', function() { - const buffer = '0000\n1111\n2222\n3333\n4444\n5555\n'; - const ch = new Change({ - position: fromBufferRange([[1, 0], [2, 0]]), - startOffset: 5, - endOffset: 25, - }); - - assert.strictEqual(ch.toStringIn(buffer, '+'), '+1111\n+2222\n+3333\n+4444\n'); - assert.strictEqual(ch.toStringIn(buffer, '-'), '-1111\n-2222\n-3333\n-4444\n'); - }); - - it('returns Changes corresponding to intersecting buffer rows', function() { - const buffer = '0000\n1111\n2222\n3333\n4444\n5555\n6666\n7777\n8888\n9999'; - const ch = new Change({ - position: fromBufferRange([[1, 0], [8, 0]]), - startOffset: 5, - endOffset: 45, - }); - - const intersections = ch.intersectRowsIn(new Set([4, 5]), buffer); - assert.lengthOf(intersections, 1); - assert.strictEqual(intersections[0].toStringIn(buffer, '-'), '-4444\n-5555\n'); - }); - - it('includes a Change corresponding to an intersection at the end of the range', function() { - const buffer = '0000\n1111\n2222\n3333\n4444\n5555\n6666\n7777\n8888\n9999'; - const ch = new Change({ - position: fromBufferRange([[1, 0], [8, 0]]), - startOffset: 5, - endOffset: 45, - }); - - const intersections = ch.intersectRowsIn(new Set([1, 2, 4, 8]), buffer); - assert.lengthOf(intersections, 3); - - const int0 = intersections[0]; - assert.strictEqual(int0.toStringIn(buffer, '+'), '+1111\n+2222\n'); - - const int1 = intersections[1]; - assert.strictEqual(int1.toStringIn(buffer, '-'), '-4444\n'); - - const int2 = intersections[2]; - assert.strictEqual(int2.toStringIn(buffer, '+'), '+8888\n'); - }); - - it('returns appropriate values from nullChange methods', function() { - assert.deepEqual(nullChange.intersectRowsIn(new Set([0, 1, 2]), ''), []); - assert.strictEqual(nullChange.toStringIn('', '+'), ''); - assert.strictEqual(nullChange.markOn(), nullPosition.markOn()); - assert.strictEqual(nullChange.setIn(), nullPosition.setIn()); - assert.strictEqual(nullChange.bufferRowCount(), 0); - }); -}); From 98ae78d74a5616ca60fabbc43991f7ffcaca42c6 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 2 Aug 2018 11:01:38 -0400 Subject: [PATCH 0073/4252] More IndexedRowRange methods I need --- lib/models/indexed-row-range.js | 13 +++++++++++++ test/models/indexed-row-range.test.js | 18 ++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/lib/models/indexed-row-range.js b/lib/models/indexed-row-range.js index 31bf5af749..6098792466 100644 --- a/lib/models/indexed-row-range.js +++ b/lib/models/indexed-row-range.js @@ -10,6 +10,15 @@ export default class IndexedRowRange { this.endOffset = endOffset; } + getBufferRows() { + return this.bufferRange.getRows(); + } + + getStartRange() { + const start = this.bufferRange.start; + return new Range(start, start); + } + bufferRowCount() { return this.bufferRange.getRowCount(); } @@ -73,6 +82,10 @@ export default class IndexedRowRange { } export const nullIndexedRowRange = { + startOffset: Infinity, + + endOffset: Infinity, + bufferRowCount() { return 0; }, diff --git a/test/models/indexed-row-range.test.js b/test/models/indexed-row-range.test.js index 1cd83d2a31..8eae81e685 100644 --- a/test/models/indexed-row-range.test.js +++ b/test/models/indexed-row-range.test.js @@ -11,6 +11,24 @@ describe('IndexedRowRange', function() { assert.deepEqual(range.bufferRowCount(), 2); }); + it('returns an array of the covered rows', function() { + const range = new IndexedRowRange({ + bufferRange: [[2, 0], [8, 0]], + startOffset: 0, + endOffset: 10, + }); + assert.sameMembers(range.getBufferRows(), [2, 3, 4, 5, 6, 7, 8]); + }); + + it('creates a Range from its first line', function() { + const range = new IndexedRowRange({ + bufferRange: [[2, 0], [8, 0]], + startOffset: 0, + endOffset: 10, + }); + assert.deepEqual(range.getStartRange().serialize(), [[2, 0], [2, 0]]); + }); + it('extracts its offset range from buffer text with toStringIn()', function() { const buffer = '0000\n1111\n2222\n3333\n4444\n5555\n'; const range = new IndexedRowRange({ From 20ede32b69645f07d6388e7f8f512eeca60a4cb6 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 2 Aug 2018 11:02:25 -0400 Subject: [PATCH 0074/4252] Remodel Hunk to use IndexedRowRanges instead of HunkLines --- lib/models/hunk-line.js | 97 ------------- lib/models/hunk.js | 83 ------------ lib/models/patch/hunk.js | 137 +++++++++++++++++++ test/models/patch/hunk.test.js | 239 +++++++++++++++++++++++++++++++++ 4 files changed, 376 insertions(+), 180 deletions(-) delete mode 100644 lib/models/hunk-line.js delete mode 100644 lib/models/hunk.js create mode 100644 lib/models/patch/hunk.js create mode 100644 test/models/patch/hunk.test.js diff --git a/lib/models/hunk-line.js b/lib/models/hunk-line.js deleted file mode 100644 index 8ab430836e..0000000000 --- a/lib/models/hunk-line.js +++ /dev/null @@ -1,97 +0,0 @@ -export default class HunkLine { - static statusMap = { - '+': 'added', - '-': 'deleted', - ' ': 'unchanged', - '\\': 'nonewline', - } - - constructor(text, status, oldLineNumber, newLineNumber, diffLineNumber) { - this.text = text; - this.status = status; - this.oldLineNumber = oldLineNumber; - this.newLineNumber = newLineNumber; - this.diffLineNumber = diffLineNumber; - } - - copy({text, status, oldLineNumber, newLineNumber} = {}) { - return new HunkLine( - text || this.getText(), - status || this.getStatus(), - oldLineNumber || this.getOldLineNumber(), - newLineNumber || this.getNewLineNumber(), - ); - } - - getText() { - return this.text; - } - - getOldLineNumber() { - return this.oldLineNumber; - } - - getNewLineNumber() { - return this.newLineNumber; - } - - getDiffLineNumber() { - return this.diffLineNumber; - } - - getStatus() { - return this.status; - } - - isChanged() { - return this.getStatus() === 'added' || this.getStatus() === 'deleted'; - } - - getOrigin() { - switch (this.getStatus()) { - case 'added': - return '+'; - case 'deleted': - return '-'; - case 'unchanged': - return ' '; - case 'nonewline': - return '\\'; - default: - return ''; - } - } - - invert() { - let invertedStatus; - switch (this.getStatus()) { - case 'added': - invertedStatus = 'deleted'; - break; - case 'deleted': - invertedStatus = 'added'; - break; - case 'unchanged': - invertedStatus = 'unchanged'; - break; - case 'nonewline': - invertedStatus = 'nonewline'; - break; - } - - return new HunkLine( - this.text, - invertedStatus, - this.newLineNumber, - this.oldLineNumber, - ); - } - - toString() { - return this.getOrigin() + (this.getStatus() === 'nonewline' ? ' ' : '') + this.getText(); - } - - getByteSize() { - return Buffer.byteLength(this.getText(), 'utf8'); - } -} diff --git a/lib/models/hunk.js b/lib/models/hunk.js deleted file mode 100644 index 600b230ae9..0000000000 --- a/lib/models/hunk.js +++ /dev/null @@ -1,83 +0,0 @@ -export default class Hunk { - constructor(oldStartRow, newStartRow, oldRowCount, newRowCount, sectionHeading, lines) { - this.oldStartRow = oldStartRow; - this.newStartRow = newStartRow; - this.oldRowCount = oldRowCount; - this.newRowCount = newRowCount; - this.sectionHeading = sectionHeading; - this.lines = lines; - } - - copy() { - return new Hunk( - this.getOldStartRow(), - this.getNewStartRow(), - this.getOldRowCount(), - this.getNewRowCount(), - this.getSectionHeading(), - this.getLines().map(l => l.copy()), - ); - } - - getOldStartRow() { - return this.oldStartRow; - } - - getNewStartRow() { - return this.newStartRow; - } - - getOldRowCount() { - return this.oldRowCount; - } - - getNewRowCount() { - return this.newRowCount; - } - - getLines() { - return this.lines; - } - - getHeader() { - return `@@ -${this.oldStartRow},${this.oldRowCount} +${this.newStartRow},${this.newRowCount} @@\n`; - } - - getSectionHeading() { - return this.sectionHeading; - } - - invert() { - const invertedLines = []; - let addedLines = []; - for (const line of this.getLines()) { - const invertedLine = line.invert(); - if (invertedLine.getStatus() === 'added') { - addedLines.push(invertedLine); - } else if (invertedLine.getStatus() === 'deleted') { - invertedLines.push(invertedLine); - } else { - invertedLines.push(...addedLines); - invertedLines.push(invertedLine); - addedLines = []; - } - } - invertedLines.push(...addedLines); - return new Hunk( - this.getNewStartRow(), - this.getOldStartRow(), - this.getNewRowCount(), - this.getOldRowCount(), - this.getSectionHeading(), - invertedLines, - ); - } - - toString() { - return this.getLines().reduce((a, b) => a + b.toString() + '\n', this.getHeader()); - } - - getByteSize() { - return this.getLines().reduce((acc, line) => acc + line.getByteSize(), 0); - } -} diff --git a/lib/models/patch/hunk.js b/lib/models/patch/hunk.js new file mode 100644 index 0000000000..fd3dc62865 --- /dev/null +++ b/lib/models/patch/hunk.js @@ -0,0 +1,137 @@ +import IndexedRowRange, {nullIndexedRowRange} from '../indexed-row-range'; + +export default class Hunk { + constructor({ + oldStartRow, + newStartRow, + oldRowCount, + newRowCount, + sectionHeading, + rowRange, + additions, + deletions, + noNewline, + }) { + this.oldStartRow = oldStartRow; + this.newStartRow = newStartRow; + this.oldRowCount = oldRowCount; + this.newRowCount = newRowCount; + this.sectionHeading = sectionHeading; + + this.rowRange = rowRange; + this.additions = additions; + this.deletions = deletions; + this.noNewline = noNewline; + } + + getOldStartRow() { + return this.oldStartRow; + } + + getNewStartRow() { + return this.newStartRow; + } + + getOldRowCount() { + return this.oldRowCount; + } + + getNewRowCount() { + return this.newRowCount; + } + + getHeader() { + return `@@ -${this.oldStartRow},${this.oldRowCount} +${this.newStartRow},${this.newRowCount} @@`; + } + + getSectionHeading() { + return this.sectionHeading; + } + + getAdditions() { + return this.additions; + } + + getDeletions() { + return this.deletions; + } + + getNoNewline() { + return this.noNewline; + } + + getStartRange() { + return this.rowRange.getStartRange(); + } + + getBufferRows() { + return this.rowRange.getBufferRows(); + } + + changedLineCount() { + return [ + this.additions, + this.deletions, + [this.noNewline], + ].reduce((count, ranges) => { + return ranges.reduce((subCount, range) => subCount + range.bufferRowCount(), count); + }, 0); + } + + invert() { + return new Hunk({ + oldStartRow: this.getNewStartRow(), + newStartRow: this.getOldStartRow(), + oldRowCount: this.getNewRowCount(), + newRowCount: this.getOldRowCount(), + sectionHeading: this.getSectionHeading(), + rowRange: this.rowRange, + additions: this.getDeletions(), + deletions: this.getAdditions(), + noNewline: this.getNoNewline(), + }); + } + + toStringIn(bufferText) { + let str = this.getHeader() + '\n'; + + let additionIndex = 0; + let deletionIndex = 0; + + const nextRange = () => { + const nextAddition = this.additions[additionIndex] || nullIndexedRowRange; + const nextDeletion = this.deletions[deletionIndex] || nullIndexedRowRange; + + const minRange = [this.noNewline, nextAddition, nextDeletion].reduce((least, range) => { + return range.startOffset < least.startOffset ? range : least; + }); + + const unchanged = minRange.startOffset === currentOffset + ? nullIndexedRowRange + : new IndexedRowRange({ + bufferRange: [[0, 0], [0, 0]], + startOffset: currentOffset, + endOffset: minRange.startOffset, + }); + + if (minRange === nextAddition) { + additionIndex++; + return {origin: '+', range: minRange, unchanged}; + } else if (minRange === nextDeletion) { + deletionIndex++; + return {origin: '-', range: minRange, unchanged}; + } else { + return {origin: '\\', range: this.noNewline, unchanged}; + } + }; + + let currentOffset = this.rowRange.startOffset; + while (currentOffset < bufferText.length) { + const {origin, range, unchanged} = nextRange(); + str += unchanged.toStringIn(bufferText, ' '); + str += range.toStringIn(bufferText, origin); + currentOffset = range.endOffset; + } + return str; + } +} diff --git a/test/models/patch/hunk.test.js b/test/models/patch/hunk.test.js new file mode 100644 index 0000000000..9dade3990f --- /dev/null +++ b/test/models/patch/hunk.test.js @@ -0,0 +1,239 @@ +import Hunk from '../../../lib/models/patch/hunk'; +import IndexedRowRange, {nullIndexedRowRange} from '../../../lib/models/indexed-row-range'; + +describe('Hunk', function() { + const attrs = { + oldStartRow: 0, + newStartRow: 0, + oldRowCount: 0, + newRowCount: 0, + sectionHeading: 'sectionHeading', + rowRange: new IndexedRowRange({ + bufferRange: [[1, 0], [10, 0]], + startOffset: 5, + endOffset: 100, + }), + additions: [ + new IndexedRowRange({bufferRange: [[1, 0], [2, 0]], startOffset: 6, endOffset: 7}), + ], + deletions: [ + new IndexedRowRange({bufferRange: [[3, 0], [4, 0]], startOffset: 8, endOffset: 9}), + new IndexedRowRange({bufferRange: [[5, 0], [6, 0]], startOffset: 10, endOffset: 11}), + ], + noNewline: nullIndexedRowRange, + }; + + it('has some basic accessors', function() { + const h = new Hunk({ + oldStartRow: 0, + newStartRow: 1, + oldRowCount: 2, + newRowCount: 3, + sectionHeading: 'sectionHeading', + rowRange: new IndexedRowRange({ + bufferRange: [[0, 0], [10, 0]], + startOffset: 0, + endOffset: 100, + }), + additions: [ + new IndexedRowRange({bufferRange: [[1, 0], [2, 0]], startOffset: 6, endOffset: 7}), + ], + deletions: [ + new IndexedRowRange({bufferRange: [[3, 0], [4, 0]], startOffset: 8, endOffset: 9}), + new IndexedRowRange({bufferRange: [[5, 0], [6, 0]], startOffset: 10, endOffset: 11}), + ], + noNewline: nullIndexedRowRange, + }); + + assert.strictEqual(h.getOldStartRow(), 0); + assert.strictEqual(h.getNewStartRow(), 1); + assert.strictEqual(h.getOldRowCount(), 2); + assert.strictEqual(h.getNewRowCount(), 3); + assert.strictEqual(h.getSectionHeading(), 'sectionHeading'); + assert.lengthOf(h.getAdditions(), 1); + assert.lengthOf(h.getDeletions(), 2); + assert.isFalse(h.getNoNewline().isPresent()); + }); + + it('creates its start range for decoration placement', function() { + const h = new Hunk({ + ...attrs, + rowRange: new IndexedRowRange({ + bufferRange: [[3, 0], [6, 0]], + startOffset: 15, + endOffset: 35, + }), + }); + + assert.deepEqual(h.getStartRange().serialize(), [[3, 0], [3, 0]]); + }); + + it('generates a patch section header', function() { + const h = new Hunk({ + ...attrs, + oldStartRow: 0, + newStartRow: 1, + oldRowCount: 2, + newRowCount: 3, + }); + + assert.strictEqual(h.getHeader(), '@@ -0,2 +1,3 @@'); + }); + + it('returns a set of covered buffer rows', function() { + const h = new Hunk({ + ...attrs, + rowRange: new IndexedRowRange({ + bufferRange: [[6, 0], [10, 0]], + startOffset: 30, + endOffset: 55, + }), + }); + assert.sameMembers(Array.from(h.getBufferRows()), [6, 7, 8, 9, 10]); + }); + + it('computes the total number of changed lines', function() { + const h0 = new Hunk({ + ...attrs, + additions: [ + new IndexedRowRange({bufferRange: [[2, 0], [4, 0]], startOffset: 0, endOffset: 0}), + new IndexedRowRange({bufferRange: [[6, 0], [6, 0]], startOffset: 0, endOffset: 0}), + ], + deletions: [ + new IndexedRowRange({bufferRange: [[7, 0], [10, 0]], startOffset: 0, endOffset: 0}), + ], + noNewline: new IndexedRowRange({bufferRange: [[12, 0], [12, 0]], startOffset: 0, endOffset: 0}), + }); + assert.strictEqual(h0.changedLineCount(), 9); + + const h1 = new Hunk({ + ...attrs, + additions: [], + deletions: [], + noNewline: nullIndexedRowRange, + }); + assert.strictEqual(h1.changedLineCount(), 0); + }); + + it('computes an inverted hunk', function() { + const original = new Hunk({ + ...attrs, + oldStartRow: 0, + newStartRow: 1, + oldRowCount: 2, + newRowCount: 3, + sectionHeading: 'the-heading', + additions: [ + new IndexedRowRange({bufferRange: [[2, 0], [4, 0]], startOffset: 0, endOffset: 0}), + new IndexedRowRange({bufferRange: [[6, 0], [6, 0]], startOffset: 0, endOffset: 0}), + ], + deletions: [ + new IndexedRowRange({bufferRange: [[7, 0], [10, 0]], startOffset: 0, endOffset: 0}), + ], + noNewline: new IndexedRowRange({bufferRange: [[12, 0], [12, 0]], startOffset: 0, endOffset: 0}), + }); + + const inverted = original.invert(); + assert.strictEqual(inverted.getOldStartRow(), 1); + assert.strictEqual(inverted.getNewStartRow(), 0); + assert.strictEqual(inverted.getOldRowCount(), 3); + assert.strictEqual(inverted.getNewRowCount(), 2); + assert.strictEqual(inverted.getSectionHeading(), 'the-heading'); + assert.lengthOf(inverted.additions, 1); + assert.lengthOf(inverted.deletions, 2); + assert.isTrue(inverted.noNewline.isPresent()); + }); + + describe('toStringIn()', function() { + it('prints its header', function() { + const h = new Hunk({ + ...attrs, + oldStartRow: 0, + newStartRow: 1, + oldRowCount: 2, + newRowCount: 3, + additions: [], + deletions: [], + noNewline: nullIndexedRowRange, + }); + + assert.strictEqual(h.toStringIn(''), '@@ -0,2 +1,3 @@\n'); + }); + + it('renders changed and unchanged lines with the appropriate origin characters', function() { + const buffer = + '0000\n0111\n0222\n0333\n0444\n0555\n0666\n0777\n0888\n0999\n' + + '1000\n1111\n1222\n' + + 'No newline at end of file\n'; + // 0000.0111.0222.0333.0444.0555.0666.0777.0888.0999.1000.1111.1222.No newline at end of file. + + const h = new Hunk({ + ...attrs, + oldStartRow: 1, + newStartRow: 1, + oldRowCount: 6, + newRowCount: 6, + rowRange: new IndexedRowRange({ + bufferRange: [[1, 0], [13, 0]], + startOffset: 5, + endOffset: 91, + }), + additions: [ + new IndexedRowRange({bufferRange: [[2, 0], [3, 0]], startOffset: 10, endOffset: 20}), + new IndexedRowRange({bufferRange: [[7, 0], [7, 0]], startOffset: 35, endOffset: 40}), + new IndexedRowRange({bufferRange: [[10, 0], [10, 0]], startOffset: 50, endOffset: 55}), + ], + deletions: [ + new IndexedRowRange({bufferRange: [[5, 0], [5, 0]], startOffset: 25, endOffset: 30}), + new IndexedRowRange({bufferRange: [[8, 0], [9, 0]], startOffset: 40, endOffset: 50}), + ], + noNewline: new IndexedRowRange({bufferRange: [[13, 0], [13, 0]], startOffset: 65, endOffset: 91}), + }); + + assert.strictEqual(h.toStringIn(buffer), [ + '@@ -1,6 +1,6 @@\n', + ' 0111\n', + '+0222\n', + '+0333\n', + ' 0444\n', + '-0555\n', + ' 0666\n', + '+0777\n', + '-0888\n', + '-0999\n', + '+1000\n', + ' 1111\n', + ' 1222\n', + '\\No newline at end of file\n', + ].join('')); + }); + + it('renders a hunk without a nonewline', function() { + const buffer = '0000\n1111\n2222\n3333\n'; + + const h = new Hunk({ + ...attrs, + oldStartRow: 1, + newStartRow: 1, + oldRowCount: 1, + newRowCount: 1, + rowRange: new IndexedRowRange({bufferRange: [[0, 0], [3, 0]], startOffset: 0, endOffset: 20}), + additions: [ + new IndexedRowRange({bufferRange: [[1, 0], [1, 0]], startOffset: 5, endOffset: 10}), + ], + deletions: [ + new IndexedRowRange({bufferRange: [[2, 0], [2, 0]], startOffset: 10, endOffset: 15}), + ], + noNewline: nullIndexedRowRange, + }); + + assert.strictEqual(h.toStringIn(buffer), [ + '@@ -1,1 +1,1 @@\n', + ' 0000\n', + '+1111\n', + '-2222\n', + ' 3333\n', + ].join('')); + }); + }); +}); From 1dc4fb8dd75754c5267633e36448cbda969be14f Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 2 Aug 2018 11:13:02 -0400 Subject: [PATCH 0075/4252] Correctly render hunks early in the buffer without noNewline --- lib/models/patch/hunk.js | 15 ++++++++++++++- test/models/patch/hunk.test.js | 2 +- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/lib/models/patch/hunk.js b/lib/models/patch/hunk.js index fd3dc62865..65062534ea 100644 --- a/lib/models/patch/hunk.js +++ b/lib/models/patch/hunk.js @@ -98,11 +98,17 @@ export default class Hunk { let additionIndex = 0; let deletionIndex = 0; + const endRange = new IndexedRowRange({ + bufferRange: [[0, 0], [0, 0]], + startOffset: this.rowRange.endOffset, + endOffset: this.rowRange.endOffset, + }); + const nextRange = () => { const nextAddition = this.additions[additionIndex] || nullIndexedRowRange; const nextDeletion = this.deletions[deletionIndex] || nullIndexedRowRange; - const minRange = [this.noNewline, nextAddition, nextDeletion].reduce((least, range) => { + const minRange = [this.noNewline, nextAddition, nextDeletion, endRange].reduce((least, range) => { return range.startOffset < least.startOffset ? range : least; }); @@ -120,6 +126,8 @@ export default class Hunk { } else if (minRange === nextDeletion) { deletionIndex++; return {origin: '-', range: minRange, unchanged}; + } else if (minRange === endRange) { + return {origin: ' ', range: minRange, unchanged}; } else { return {origin: '\\', range: this.noNewline, unchanged}; } @@ -129,6 +137,11 @@ export default class Hunk { while (currentOffset < bufferText.length) { const {origin, range, unchanged} = nextRange(); str += unchanged.toStringIn(bufferText, ' '); + + if (range === endRange) { + break; + } + str += range.toStringIn(bufferText, origin); currentOffset = range.endOffset; } diff --git a/test/models/patch/hunk.test.js b/test/models/patch/hunk.test.js index 9dade3990f..530ab39c58 100644 --- a/test/models/patch/hunk.test.js +++ b/test/models/patch/hunk.test.js @@ -209,7 +209,7 @@ describe('Hunk', function() { }); it('renders a hunk without a nonewline', function() { - const buffer = '0000\n1111\n2222\n3333\n'; + const buffer = '0000\n1111\n2222\n3333\n4444\n'; const h = new Hunk({ ...attrs, From ee6934188caf7645f15460e85e2a488ca979f0b5 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 2 Aug 2018 11:18:49 -0400 Subject: [PATCH 0076/4252] Build a Patch out of Hunks --- lib/models/patch/patch.js | 76 +++++++++++++++++++ test/models/patch/patch.test.js | 129 ++++++++++++++++++++++++++++++++ 2 files changed, 205 insertions(+) create mode 100644 lib/models/patch/patch.js create mode 100644 test/models/patch/patch.test.js diff --git a/lib/models/patch/patch.js b/lib/models/patch/patch.js new file mode 100644 index 0000000000..07ac604109 --- /dev/null +++ b/lib/models/patch/patch.js @@ -0,0 +1,76 @@ +export default class Patch { + constructor({status, hunks, bufferText}) { + this.status = status; + this.hunks = hunks; + this.bufferText = bufferText; + } + + getStatus() { + return this.status; + } + + getHunks() { + return this.hunks; + } + + getBufferText() { + return this.bufferText; + } + + getByteSize() { + return Buffer.byteLength(this.bufferText, 'utf8'); + } + + clone(opts = {}) { + return new this.constructor({ + status: opts.status !== undefined ? opts.status : this.getStatus(), + hunks: opts.hunks !== undefined ? opts.hunks : this.getHunks(), + bufferText: opts.bufferText !== undefined ? opts.bufferText : this.getBufferText(), + }); + } + + toString() { + return this.getHunks().reduce((str, hunk) => { + str += hunk.toStringIn(this.getBufferText()); + return str; + }, ''); + } + + isPresent() { + return true; + } +} + +export const nullPatch = { + getStatus() { + return null; + }, + + getHunks() { + return []; + }, + + getBufferText() { + return ''; + }, + + getByteSize() { + return 0; + }, + + clone(opts = {}) { + if (opts.status === undefined && opts.hunks === undefined && opts.bufferText === undefined) { + return this; + } else { + return new Patch({ + status: opts.status !== undefined ? opts.status : this.getStatus(), + hunks: opts.hunks !== undefined ? opts.hunks : this.getHunks(), + bufferText: opts.bufferText !== undefined ? opts.bufferText : this.getBufferText(), + }); + } + }, + + isPresent() { + return false; + }, +}; diff --git a/test/models/patch/patch.test.js b/test/models/patch/patch.test.js new file mode 100644 index 0000000000..131ae9faa1 --- /dev/null +++ b/test/models/patch/patch.test.js @@ -0,0 +1,129 @@ +import Patch, {nullPatch} from '../../../lib/models/patch/patch'; +import Hunk from '../../../lib/models/patch/hunk'; +import IndexedRowRange, {nullIndexedRowRange} from '../../../lib/models/indexed-row-range'; + +describe('Patch', function() { + it('has some standard accessors', function() { + const p = new Patch({status: 'modified', hunks: [], bufferText: 'bufferText'}); + assert.strictEqual(p.getStatus(), 'modified'); + assert.deepEqual(p.getHunks(), []); + assert.strictEqual(p.getBufferText(), 'bufferText'); + assert.isTrue(p.isPresent()); + }); + + it('computes the byte size of the total patch data', function() { + const p = new Patch({status: 'modified', hunks: [], bufferText: '\u00bd + \u00bc = \u00be'}); + assert.strictEqual(p.getByteSize(), 12); + }); + + it('clones itself with optionally overridden properties', function() { + const original = new Patch({status: 'modified', hunks: [], bufferText: 'bufferText'}); + + const dup0 = original.clone(); + assert.notStrictEqual(dup0, original); + assert.strictEqual(dup0.getStatus(), 'modified'); + assert.deepEqual(dup0.getHunks(), []); + assert.strictEqual(dup0.getBufferText(), 'bufferText'); + + const dup1 = original.clone({status: 'added'}); + assert.notStrictEqual(dup1, original); + assert.strictEqual(dup1.getStatus(), 'added'); + assert.deepEqual(dup1.getHunks(), []); + assert.strictEqual(dup1.getBufferText(), 'bufferText'); + + const hunks = [new Hunk({})]; + const dup2 = original.clone({hunks}); + assert.notStrictEqual(dup2, original); + assert.strictEqual(dup2.getStatus(), 'modified'); + assert.deepEqual(dup2.getHunks(), hunks); + assert.strictEqual(dup2.getBufferText(), 'bufferText'); + + const dup3 = original.clone({bufferText: 'changed'}); + assert.notStrictEqual(dup3, original); + assert.strictEqual(dup3.getStatus(), 'modified'); + assert.deepEqual(dup3.getHunks(), []); + assert.strictEqual(dup3.getBufferText(), 'changed'); + }); + + it('clones a nullPatch as a nullPatch', function() { + assert.strictEqual(nullPatch, nullPatch.clone()); + }); + + it('clones a nullPatch to a real Patch if properties are provided', function() { + const dup0 = nullPatch.clone({status: 'added'}); + assert.notStrictEqual(dup0, nullPatch); + assert.strictEqual(dup0.getStatus(), 'added'); + assert.deepEqual(dup0.getHunks(), []); + assert.strictEqual(dup0.getBufferText(), ''); + + const hunks = [new Hunk({})]; + const dup1 = nullPatch.clone({hunks}); + assert.notStrictEqual(dup1, nullPatch); + assert.isNull(dup1.getStatus()); + assert.deepEqual(dup1.getHunks(), hunks); + assert.strictEqual(dup1.getBufferText(), ''); + + const dup2 = nullPatch.clone({bufferText: 'changed'}); + assert.notStrictEqual(dup2, nullPatch); + assert.isNull(dup2.getStatus()); + assert.deepEqual(dup2.getHunks(), []); + assert.strictEqual(dup2.getBufferText(), 'changed'); + }); + + it('prints itself as an apply-ready string', function() { + const bufferText = '0000\n1111\n2222\n3333\n4444\n5555\n6666\n7777\n8888\n9999\n'; + // old: 0000.2222.3333.4444.5555.6666.7777.8888.9999. + // new: 0000.1111.2222.3333.4444.5555.6666.9999. + // 0000.1111.2222.3333.4444.5555.6666.7777.8888.9999. + + const hunk0 = new Hunk({ + oldStartRow: 0, + newStartRow: 0, + oldRowCount: 2, + newRowCount: 3, + sectionHeading: 'zero', + rowRange: new IndexedRowRange({bufferRange: [[0, 0], [2, 0]], startOffset: 0, endOffset: 15}), + additions: [ + new IndexedRowRange({bufferRange: [[1, 0], [1, 0]], startOffset: 5, endOffset: 10}), + ], + deletions: [], + noNewline: nullIndexedRowRange, + }); + + const hunk1 = new Hunk({ + oldStartRow: 5, + newStartRow: 6, + oldRowCount: 4, + newRowCount: 2, + sectionHeading: 'one', + rowRange: new IndexedRowRange({bufferRange: [[6, 0], [10, 0]], startOffset: 30, endOffset: 55}), + additions: [], + deletions: [ + new IndexedRowRange({bufferRange: [[7, 0], [8, 0]], startOffset: 35, endOffset: 45}), + ], + noNewline: nullIndexedRowRange, + }); + + const p = new Patch({status: 'modified', hunks: [hunk0, hunk1], bufferText}); + + assert.strictEqual(p.toString(), [ + '@@ -0,2 +0,3 @@\n', + ' 0000\n', + '+1111\n', + ' 2222\n', + '@@ -5,4 +6,2 @@\n', + ' 6666\n', + '-7777\n', + '-8888\n', + ' 9999\n', + ].join('')); + }); + + it('has a stubbed nullPatch counterpart', function() { + assert.isNull(nullPatch.getStatus()); + assert.deepEqual(nullPatch.getHunks(), []); + assert.strictEqual(nullPatch.getBufferText(), ''); + assert.strictEqual(nullPatch.getByteSize(), 0); + assert.isFalse(nullPatch.isPresent()); + }); +}); From 0161350d747696e27dc42f5cb96a795f7ed92ddc Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 2 Aug 2018 11:21:09 -0400 Subject: [PATCH 0077/4252] File model, now with tests --- lib/models/patch/file.js | 80 ++++++++++++++++++++++++++++++++++ test/models/patch/file.test.js | 73 +++++++++++++++++++++++++++++++ 2 files changed, 153 insertions(+) create mode 100644 lib/models/patch/file.js create mode 100644 test/models/patch/file.test.js diff --git a/lib/models/patch/file.js b/lib/models/patch/file.js new file mode 100644 index 0000000000..a3eda44239 --- /dev/null +++ b/lib/models/patch/file.js @@ -0,0 +1,80 @@ +export default class File { + constructor({path, mode, symlink}) { + this.path = path; + this.mode = mode; + this.symlink = symlink; + } + + getPath() { + return this.path; + } + + getMode() { + return this.mode; + } + + getSymlink() { + return this.symlink; + } + + isSymlink() { + return this.getMode() === '120000'; + } + + isRegularFile() { + return this.getMode() === '100644' || this.getMode() === '100755'; + } + + isPresent() { + return true; + } + + clone(opts = {}) { + return new File({ + path: opts.path !== undefined ? opts.path : this.path, + mode: opts.mode !== undefined ? opts.mode : this.mode, + symlink: opts.symlink !== undefined ? opts.symlink : this.symlink, + }); + } +} + +export const nullFile = { + getPath() { + /* istanbul ignore next */ + return null; + }, + + getMode() { + /* istanbul ignore next */ + return null; + }, + + getSymlink() { + /* istanbul ignore next */ + return null; + }, + + isSymlink() { + return false; + }, + + isRegularFile() { + return false; + }, + + isPresent() { + return false; + }, + + clone(opts = {}) { + if (opts.path === undefined && opts.mode === undefined && opts.symlink === undefined) { + return this; + } else { + return new File({ + path: opts.path !== undefined ? opts.path : this.getPath(), + mode: opts.mode !== undefined ? opts.mode : this.getMode(), + symlink: opts.symlink !== undefined ? opts.symlink : this.getSymlink(), + }); + } + }, +}; diff --git a/test/models/patch/file.test.js b/test/models/patch/file.test.js new file mode 100644 index 0000000000..38bb449e78 --- /dev/null +++ b/test/models/patch/file.test.js @@ -0,0 +1,73 @@ +import File, {nullFile} from '../../../lib/models/patch/file'; + +describe('File', function() { + it("detects when it's a symlink", function() { + assert.isTrue(new File({path: 'path', mode: '120000', symlink: null}).isSymlink()); + assert.isFalse(new File({path: 'path', mode: '100644', symlink: null}).isSymlink()); + assert.isFalse(nullFile.isSymlink()); + }); + + it("detects when it's a regular file", function() { + assert.isTrue(new File({path: 'path', mode: '100644', symlink: null}).isRegularFile()); + assert.isTrue(new File({path: 'path', mode: '100755', symlink: null}).isRegularFile()); + assert.isFalse(new File({path: 'path', mode: '120000', symlink: null}).isRegularFile()); + assert.isFalse(nullFile.isRegularFile()); + }); + + it('clones itself with possible overrides', function() { + const original = new File({path: 'original', mode: '100644', symlink: null}); + + const dup0 = original.clone(); + assert.notStrictEqual(original, dup0); + assert.strictEqual(dup0.getPath(), 'original'); + assert.strictEqual(dup0.getMode(), '100644'); + assert.isNull(dup0.getSymlink()); + + const dup1 = original.clone({path: 'replaced'}); + assert.notStrictEqual(original, dup1); + assert.strictEqual(dup1.getPath(), 'replaced'); + assert.strictEqual(dup1.getMode(), '100644'); + assert.isNull(dup1.getSymlink()); + + const dup2 = original.clone({mode: '100755'}); + assert.notStrictEqual(original, dup2); + assert.strictEqual(dup2.getPath(), 'original'); + assert.strictEqual(dup2.getMode(), '100755'); + assert.isNull(dup2.getSymlink()); + + const dup3 = original.clone({mode: '120000', symlink: 'destination'}); + assert.notStrictEqual(original, dup3); + assert.strictEqual(dup3.getPath(), 'original'); + assert.strictEqual(dup3.getMode(), '120000'); + assert.strictEqual(dup3.getSymlink(), 'destination'); + }); + + it('clones the null file as itself', function() { + const dup = nullFile.clone(); + assert.strictEqual(dup, nullFile); + assert.isFalse(dup.isPresent()); + }); + + it('clones the null file with new properties', function() { + const dup0 = nullFile.clone({path: 'replaced'}); + assert.notStrictEqual(nullFile, dup0); + assert.strictEqual(dup0.getPath(), 'replaced'); + assert.isNull(dup0.getMode()); + assert.isNull(dup0.getSymlink()); + assert.isTrue(dup0.isPresent()); + + const dup1 = nullFile.clone({mode: '120000'}); + assert.notStrictEqual(nullFile, dup1); + assert.isNull(dup1.getPath()); + assert.strictEqual(dup1.getMode(), '120000'); + assert.isNull(dup1.getSymlink()); + assert.isTrue(dup1.isPresent()); + + const dup2 = nullFile.clone({symlink: 'target'}); + assert.notStrictEqual(nullFile, dup2); + assert.isNull(dup2.getPath()); + assert.isNull(dup2.getMode()); + assert.strictEqual(dup2.getSymlink(), 'target'); + assert.isTrue(dup2.isPresent()); + }); +}); From 584fffc21d14ad4a78cb0948cac711ef9dfd3012 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 2 Aug 2018 11:32:45 -0400 Subject: [PATCH 0078/4252] Revisit FilePatch builder with model changes --- lib/models/patch/builder.js | 23 +++++++++-------- test/models/patch/builder.test.js | 42 +++++++++++++------------------ 2 files changed, 30 insertions(+), 35 deletions(-) diff --git a/lib/models/patch/builder.js b/lib/models/patch/builder.js index 2f5b5cd34b..eec19f6609 100644 --- a/lib/models/patch/builder.js +++ b/lib/models/patch/builder.js @@ -102,6 +102,7 @@ function buildHunks(diff) { for (const hunkData of diff.hunks) { const bufferStartRow = bufferRow; const bufferStartOffset = bufferOffset; + const additions = []; const deletions = []; const noNewlines = []; @@ -109,15 +110,15 @@ function buildHunks(diff) { let lastStatus = null; let currentRangeStart = bufferRow; - const finishCurrentChange = () => { - const changes = { + const finishCurrentRange = () => { + const ranges = { added: additions, deleted: deletions, nonewline: noNewlines, }[lastStatus]; - if (changes !== undefined) { - changes.push(new IndexedRowRange({ - bufferRange: Range.fromObject([[currentRangeStart, 0], [bufferRow - 1, 0]]), + if (ranges !== undefined) { + ranges.push(new IndexedRowRange({ + bufferRange: [[currentRangeStart, 0], [bufferRow - 1, 0]], startOffset, endOffset: bufferOffset, })); @@ -136,14 +137,14 @@ function buildHunks(diff) { } if (status !== lastStatus && lastStatus !== null) { - finishCurrentChange(); + finishCurrentRange(); } lastStatus = status; bufferOffset += bufferLine.length; bufferRow++; } - finishCurrentChange(); + finishCurrentRange(); let noNewline = nullIndexedRowRange; if (noNewlines.length === 1) { @@ -158,9 +159,11 @@ function buildHunks(diff) { oldRowCount: hunkData.oldLineCount, newRowCount: hunkData.newLineCount, sectionHeading: hunkData.heading, - bufferStartPosition: null, // fromBufferPosition([bufferStartRow, 0]), - bufferStartOffset, - bufferEndRow: bufferRow, + rowRange: new IndexedRowRange({ + bufferRange: [[bufferStartRow, 0], [bufferRow, 0]], + startOffset: bufferStartOffset, + endOffset: bufferOffset, + }), additions, deletions, noNewline, diff --git a/test/models/patch/builder.test.js b/test/models/patch/builder.test.js index 0c8628b71c..80fe4d494d 100644 --- a/test/models/patch/builder.test.js +++ b/test/models/patch/builder.test.js @@ -5,7 +5,7 @@ describe('buildFilePatch', function() { function assertHunkChanges(changes, expectedStrings, expectedRanges) { const actualStrings = changes.map(change => change.toStringIn(buffer, '*')); - const actualRanges = changes.map(change => change.position.serialize()); + const actualRanges = changes.map(change => change.bufferRange.serialize()); assert.deepEqual( {strings: actualStrings, ranges: actualRanges}, @@ -13,9 +13,8 @@ describe('buildFilePatch', function() { ); } - function assertHunk(hunk, {startPosition, startOffset, header, deletions, additions, noNewline}) { - assert.deepEqual(hunk.getBufferStartPosition().serialize(), startPosition); - assert.strictEqual(hunk.getBufferStartOffset(), startOffset); + function assertHunk(hunk, {startRow, header, deletions, additions, noNewline}) { + assert.deepEqual(hunk.getStartRange().serialize(), [[startRow, 0], [startRow, 0]]); assert.strictEqual(hunk.getHeader(), header); assertHunkChanges(hunk.getDeletions(), deletions.strings, deletions.ranges); @@ -104,9 +103,8 @@ describe('buildFilePatch', function() { assert.lengthOf(p.getHunks(), 3); assertHunk(p.getHunks()[0], { - startPosition: [[0, 0], [0, 0]], - startOffset: 0, - header: '@@ -0,7 +0,6 @@\n', + startRow: 0, + header: '@@ -0,7 +0,6 @@', deletions: { strings: ['*line-1\n*line-2\n*line-3\n'], ranges: [[[1, 0], [3, 0]]], @@ -118,9 +116,8 @@ describe('buildFilePatch', function() { }); assertHunk(p.getHunks()[1], { - startPosition: [[9, 0], [9, 0]], - startOffset: 63, - header: '@@ -10,3 +11,3 @@\n', + startRow: 9, + header: '@@ -10,3 +11,3 @@', deletions: { strings: ['*line-9\n'], ranges: [[[9, 0], [9, 0]]], @@ -132,9 +129,8 @@ describe('buildFilePatch', function() { }); assertHunk(p.getHunks()[2], { - startPosition: [[13, 0], [13, 0]], - startOffset: 94, - header: '@@ -20,4 +21,4 @@\n', + startRow: 13, + header: '@@ -20,4 +21,4 @@', deletions: { strings: ['*line-14\n*line-15\n'], ranges: [[[14, 0], [15, 0]]], @@ -246,9 +242,8 @@ describe('buildFilePatch', function() { assert.lengthOf(p.getHunks(), 1); assertHunk(p.getHunks()[0], { - startPosition: [[0, 0], [0, 0]], - startOffset: 0, - header: '@@ -0,1 +0,1 @@\n', + startRow: 0, + header: '@@ -0,1 +0,1 @@', additions: {strings: ['*line-0\n'], ranges: [[[0, 0], [0, 0]]]}, deletions: {strings: ['*line-1\n'], ranges: [[[1, 0], [1, 0]]]}, noNewline: {string: '*No newline at end of file\n', range: [[2, 0], [2, 0]]}, @@ -320,9 +315,8 @@ describe('buildFilePatch', function() { assert.strictEqual(p.getBufferText(), buffer); assert.lengthOf(p.getHunks(), 1); assertHunk(p.getHunks()[0], { - startPosition: [[0, 0], [0, 0]], - startOffset: 0, - header: '@@ -0,0 +0,2 @@\n', + startRow: 0, + header: '@@ -0,0 +0,2 @@', deletions: {strings: [], ranges: []}, additions: { strings: ['*line-0\n*line-1\n'], @@ -379,9 +373,8 @@ describe('buildFilePatch', function() { assert.strictEqual(p.getBufferText(), buffer); assert.lengthOf(p.getHunks(), 1); assertHunk(p.getHunks()[0], { - startPosition: [[0, 0], [0, 0]], - startOffset: 0, - header: '@@ -0,2 +0,0 @@\n', + startRow: 0, + header: '@@ -0,2 +0,0 @@', deletions: { strings: ['*line-0\n*line-1\n'], ranges: [[[0, 0], [1, 0]]], @@ -437,9 +430,8 @@ describe('buildFilePatch', function() { assert.strictEqual(p.getBufferText(), buffer); assert.lengthOf(p.getHunks(), 1); assertHunk(p.getHunks()[0], { - startPosition: [[0, 0], [0, 0]], - startOffset: 0, - header: '@@ -0,0 +0,2 @@\n', + startRow: 0, + header: '@@ -0,0 +0,2 @@', deletions: {strings: [], ranges: []}, additions: { strings: ['*line-0\n*line-1\n'], From 7109147945f944c666e2a603f308102dc7c1dbc2 Mon Sep 17 00:00:00 2001 From: Dustin Pearson Date: Fri, 3 Aug 2018 00:37:45 -0700 Subject: [PATCH 0079/4252] =?UTF-8?q?=F0=9F=90=9B=20Fix=20default=20dir=20?= =?UTF-8?q?for=20git=20repo=20init?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/controllers/root-controller.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/controllers/root-controller.js b/lib/controllers/root-controller.js index b2c5d2a0cf..383dab104a 100644 --- a/lib/controllers/root-controller.js +++ b/lib/controllers/root-controller.js @@ -410,6 +410,10 @@ export default class RootController extends React.Component { return null; } + if (!initDialogPath) { + initDialogPath = this.props.repository.getWorkingDirectoryPath(); + } + return new Promise(resolve => { this.setState({initDialogActive: true, initDialogPath, initDialogResolve: resolve}); }); From f53cf773e33ccf5282f103c280daf09e446479ce Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 3 Aug 2018 12:52:44 -0400 Subject: [PATCH 0080/4252] Refactor assertHunkChanges and assertHunk into test helpers --- test/helpers.js | 53 +++++++++++++ test/models/patch/builder.test.js | 125 +++++++++++------------------- 2 files changed, 98 insertions(+), 80 deletions(-) diff --git a/test/helpers.js b/test/helpers.js index afff422aa5..5f49a73076 100644 --- a/test/helpers.js +++ b/test/helpers.js @@ -123,6 +123,8 @@ export function buildRepositoryWithPipeline(workingDirPath, options) { return buildRepository(workingDirPath, {pipelineManager}); } +// Custom assertions + export function assertDeepPropertyVals(actual, expected) { function extractObjectSubset(actualValue, expectedValue) { if (actualValue !== Object(actualValue)) { return actualValue; } @@ -150,6 +152,57 @@ export function assertEqualSortedArraysByKey(arr1, arr2, key) { assert.deepEqual(arr1.sort(sortFn), arr2.sort(sortFn)); } +// Helpers for test/models/patch classes + +class PatchBufferAssertions { + constructor(patch) { + this.patch = patch; + } + + hunkChanges(changes, expectedStrings, expectedRanges) { + const actualStrings = changes.map(change => change.toStringIn(this.patch.getBufferText(), '*')); + const actualRanges = changes.map(change => change.bufferRange.serialize()); + + assert.deepEqual( + {strings: actualStrings, ranges: actualRanges}, + {strings: expectedStrings, ranges: expectedRanges}, + ); + } + + hunk(hunkIndex, {startRow, header, deletions, additions, noNewline}) { + const hunk = this.patch.getHunks()[hunkIndex]; + assert.isDefined(hunk); + + assert.deepEqual(hunk.getStartRange().serialize(), [[startRow, 0], [startRow, 0]]); + assert.strictEqual(hunk.getHeader(), header); + + this.hunkChanges(hunk.getDeletions(), deletions.strings, deletions.ranges); + this.hunkChanges(hunk.getAdditions(), additions.strings, additions.ranges); + + const noNewlineChange = hunk.getNoNewline(); + if (noNewlineChange.isPresent()) { + this.hunkChanges([noNewlineChange], [noNewline.string], [noNewline.range]); + } else { + assert.isUndefined(noNewline); + } + } + + hunks(...specs) { + assert.lengthOf(this.patch.getHunks(), specs.length); + for (let i = 0; i < specs.length; i++) { + this.hunk(i, specs[i]); + } + } +} + +export function assertInPatch(patch) { + return new PatchBufferAssertions(patch); +} + +export function assertInFilePatch(filePatch) { + return assertInPatch(filePatch.getPatch()); +} + let activeRenderers = []; export function createRenderer() { let instance; diff --git a/test/models/patch/builder.test.js b/test/models/patch/builder.test.js index 80fe4d494d..a77bbe351b 100644 --- a/test/models/patch/builder.test.js +++ b/test/models/patch/builder.test.js @@ -1,33 +1,7 @@ import {buildFilePatch} from '../../../lib/models/patch'; +import {assertInPatch} from '../../helpers'; describe('buildFilePatch', function() { - let buffer; - - function assertHunkChanges(changes, expectedStrings, expectedRanges) { - const actualStrings = changes.map(change => change.toStringIn(buffer, '*')); - const actualRanges = changes.map(change => change.bufferRange.serialize()); - - assert.deepEqual( - {strings: actualStrings, ranges: actualRanges}, - {strings: expectedStrings, ranges: expectedRanges}, - ); - } - - function assertHunk(hunk, {startRow, header, deletions, additions, noNewline}) { - assert.deepEqual(hunk.getStartRange().serialize(), [[startRow, 0], [startRow, 0]]); - assert.strictEqual(hunk.getHeader(), header); - - assertHunkChanges(hunk.getDeletions(), deletions.strings, deletions.ranges); - assertHunkChanges(hunk.getAdditions(), additions.strings, additions.ranges); - - const noNewlineChange = hunk.getNoNewline(); - if (noNewlineChange.isPresent()) { - assertHunkChanges([noNewlineChange], [noNewline.string], [noNewline.range]); - } else { - assert.isUndefined(noNewline); - } - } - it('returns a null patch for an empty diff list', function() { const p = buildFilePatch([]); assert.isFalse(p.getOldFile().isPresent()); @@ -96,50 +70,49 @@ describe('buildFilePatch', function() { assert.strictEqual(p.getNewMode(), '100755'); assert.strictEqual(p.getPatch().getStatus(), 'modified'); - buffer = + const buffer = 'line-0\nline-1\nline-2\nline-3\nline-4\nline-5\nline-6\nline-7\nline-8\nline-9\nline-10\n' + 'line-11\nline-12\nline-13\nline-14\nline-15\nline-16\nline-17\nline-18\n'; assert.strictEqual(p.getBufferText(), buffer); - assert.lengthOf(p.getHunks(), 3); - assertHunk(p.getHunks()[0], { - startRow: 0, - header: '@@ -0,7 +0,6 @@', - deletions: { - strings: ['*line-1\n*line-2\n*line-3\n'], - ranges: [[[1, 0], [3, 0]]], - }, - additions: { - strings: ['*line-5\n*line-6\n'], - ranges: [[[5, 0], [6, 0]]], - }, - }); - - assertHunk(p.getHunks()[1], { - startRow: 9, - header: '@@ -10,3 +11,3 @@', - deletions: { - strings: ['*line-9\n'], - ranges: [[[9, 0], [9, 0]]], - }, - additions: { - strings: ['*line-12\n'], - ranges: [[[12, 0], [12, 0]]], + assertInPatch(p).hunks( + { + startRow: 0, + header: '@@ -0,7 +0,6 @@', + deletions: { + strings: ['*line-1\n*line-2\n*line-3\n'], + ranges: [[[1, 0], [3, 0]]], + }, + additions: { + strings: ['*line-5\n*line-6\n'], + ranges: [[[5, 0], [6, 0]]], + }, }, - }); - - assertHunk(p.getHunks()[2], { - startRow: 13, - header: '@@ -20,4 +21,4 @@', - deletions: { - strings: ['*line-14\n*line-15\n'], - ranges: [[[14, 0], [15, 0]]], + { + startRow: 9, + header: '@@ -10,3 +11,3 @@', + deletions: { + strings: ['*line-9\n'], + ranges: [[[9, 0], [9, 0]]], + }, + additions: { + strings: ['*line-12\n'], + ranges: [[[12, 0], [12, 0]]], + }, }, - additions: { - strings: ['*line-16\n*line-17\n'], - ranges: [[[16, 0], [17, 0]]], + { + startRow: 13, + header: '@@ -20,4 +21,4 @@', + deletions: { + strings: ['*line-14\n*line-15\n'], + ranges: [[[14, 0], [15, 0]]], + }, + additions: { + strings: ['*line-16\n*line-17\n'], + ranges: [[[16, 0], [17, 0]]], + }, }, - }); + ); }); it("sets the old file's symlink destination", function() { @@ -237,11 +210,9 @@ describe('buildFilePatch', function() { ]}], }]); - buffer = 'line-0\nline-1\nNo newline at end of file\n'; - assert.strictEqual(p.getBufferText(), buffer); + assert.strictEqual(p.getBufferText(), 'line-0\nline-1\nNo newline at end of file\n'); - assert.lengthOf(p.getHunks(), 1); - assertHunk(p.getHunks()[0], { + assertInPatch(p).hunks({ startRow: 0, header: '@@ -0,1 +0,1 @@', additions: {strings: ['*line-0\n'], ranges: [[[0, 0], [0, 0]]]}, @@ -311,10 +282,8 @@ describe('buildFilePatch', function() { assert.strictEqual(p.getNewSymlink(), 'the-destination'); assert.strictEqual(p.getStatus(), 'deleted'); - buffer = 'line-0\nline-1\n'; - assert.strictEqual(p.getBufferText(), buffer); - assert.lengthOf(p.getHunks(), 1); - assertHunk(p.getHunks()[0], { + assert.strictEqual(p.getBufferText(), 'line-0\nline-1\n'); + assertInPatch(p).hunks({ startRow: 0, header: '@@ -0,0 +0,2 @@', deletions: {strings: [], ranges: []}, @@ -369,10 +338,8 @@ describe('buildFilePatch', function() { assert.isNull(p.getNewSymlink()); assert.strictEqual(p.getStatus(), 'added'); - buffer = 'line-0\nline-1\n'; - assert.strictEqual(p.getBufferText(), buffer); - assert.lengthOf(p.getHunks(), 1); - assertHunk(p.getHunks()[0], { + assert.strictEqual(p.getBufferText(), 'line-0\nline-1\n'); + assertInPatch(p).hunks({ startRow: 0, header: '@@ -0,2 +0,0 @@', deletions: { @@ -426,10 +393,8 @@ describe('buildFilePatch', function() { assert.strictEqual(p.getNewSymlink(), 'the-destination'); assert.strictEqual(p.getStatus(), 'deleted'); - buffer = 'line-0\nline-1\n'; - assert.strictEqual(p.getBufferText(), buffer); - assert.lengthOf(p.getHunks(), 1); - assertHunk(p.getHunks()[0], { + assert.strictEqual(p.getBufferText(), 'line-0\nline-1\n'); + assertInPatch(p).hunks({ startRow: 0, header: '@@ -0,0 +0,2 @@', deletions: {strings: [], ranges: []}, From 882925aeb46d0d77bb3d2284994b49352dc68f1a Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 6 Aug 2018 10:12:52 -0400 Subject: [PATCH 0081/4252] Accessor for a hunk's IndexedRowRange --- lib/models/patch/hunk.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/models/patch/hunk.js b/lib/models/patch/hunk.js index 65062534ea..cce3460957 100644 --- a/lib/models/patch/hunk.js +++ b/lib/models/patch/hunk.js @@ -60,6 +60,10 @@ export default class Hunk { return this.noNewline; } + getRowRange() { + return this.rowRange; + } + getStartRange() { return this.rowRange.getStartRange(); } From 48d467e0f7bb51b7c25825afe8e3293d0db994aa Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 6 Aug 2018 10:13:20 -0400 Subject: [PATCH 0082/4252] Default "deletions" and "additions" in assertInPatch().hunk() --- test/helpers.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/helpers.js b/test/helpers.js index 5f49a73076..88b96a2b4e 100644 --- a/test/helpers.js +++ b/test/helpers.js @@ -173,6 +173,9 @@ class PatchBufferAssertions { const hunk = this.patch.getHunks()[hunkIndex]; assert.isDefined(hunk); + deletions = deletions !== undefined ? deletions : {strings: [], ranges: []}; + additions = additions !== undefined ? additions : {strings: [], ranges: []}; + assert.deepEqual(hunk.getStartRange().serialize(), [[startRow, 0], [startRow, 0]]); assert.strictEqual(hunk.getHeader(), header); From fd05d2716d8d7f685d4042c0af588f03cf4ef92f Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 6 Aug 2018 10:13:51 -0400 Subject: [PATCH 0083/4252] Move and start work on FilePatch model --- lib/models/{ => patch}/file-patch.js | 280 ++++++++++++--------------- 1 file changed, 125 insertions(+), 155 deletions(-) rename lib/models/{ => patch}/file-patch.js (52%) diff --git a/lib/models/file-patch.js b/lib/models/patch/file-patch.js similarity index 52% rename from lib/models/file-patch.js rename to lib/models/patch/file-patch.js index 41a6976bf8..b4faebb93e 100644 --- a/lib/models/file-patch.js +++ b/lib/models/patch/file-patch.js @@ -1,92 +1,15 @@ import Hunk from './hunk'; -import {toGitPathSep} from '../helpers'; -import PresentedFilePatch from './presented-file-patch'; - -class File { - static empty() { - return new File({path: null, mode: null, symlink: null}); - } - - constructor({path, mode, symlink}) { - this.path = path; - this.mode = mode; - this.symlink = symlink; - } - - getPath() { - return this.path; - } - - getMode() { - return this.mode; - } - - isSymlink() { - return this.getMode() === '120000'; - } - - isRegularFile() { - return this.getMode() === '100644' || this.getMode() === '100755'; - } - - getSymlink() { - return this.symlink; - } - - clone(opts = {}) { - return new File({ - path: opts.path !== undefined ? opts.path : this.path, - mode: opts.mode !== undefined ? opts.mode : this.mode, - symlink: opts.symlink !== undefined ? opts.symlink : this.symlink, - }); - } -} - -class Patch { - constructor({status, hunks}) { - this.status = status; - this.hunks = hunks; - } - - getStatus() { - return this.status; - } - - getHunks() { - return this.hunks; - } - - getByteSize() { - return this.getHunks().reduce((acc, hunk) => acc + hunk.getByteSize(), 0); - } - - clone(opts = {}) { - return new Patch({ - status: opts.status !== undefined ? opts.status : this.status, - hunks: opts.hunks !== undefined ? opts.hunks : this.hunks, - }); - } -} +import File, {nullFile} from './file'; +import Patch from './patch'; +import {toGitPathSep} from '../../helpers'; export default class FilePatch { - static File = File; - static Patch = Patch; - constructor(oldFile, newFile, patch) { this.oldFile = oldFile; this.newFile = newFile; this.patch = patch; - this.changedLineCount = this.getHunks().reduce((acc, hunk) => { - return acc + hunk.getLines().filter(line => line.isChanged()).length; - }, 0); - } - - clone(opts = {}) { - const oldFile = opts.oldFile !== undefined ? opts.oldFile : this.getOldFile(); - const newFile = opts.newFile !== undefined ? opts.newFile : this.getNewFile(); - const patch = opts.patch !== undefined ? opts.patch : this.patch; - return new FilePatch(oldFile, newFile, patch); + this.changedLineCount = this.getHunks().reduce((acc, hunk) => acc + hunk.changedLineCount(), 0); } getOldFile() { @@ -129,6 +52,38 @@ export default class FilePatch { return this.getPatch().getByteSize(); } + getBufferText() { + return this.getPatch().getBufferText(); + } + + getHunkStartPositions() { + return this.getHunks().map(hunk => hunk.getBufferStartPosition()); + } + + getAddedBufferPositions() { + return this.getHunks().reduce((acc, hunk) => { + acc.push(...hunk.getAddedBufferPositions()); + return acc; + }, []); + } + + getBufferDeletedPositions() { + return this.getHunks().reduce((acc, hunk) => { + acc.push(...hunk.getDeletedBufferPositions()); + return acc; + }, []); + } + + getBufferNoNewlinePosition() { + return this.getHunks().reduce((acc, hunk) => { + const position = hunk.getBufferNoNewlinePosition(); + if (position.isPresent()) { + acc.push(position); + } + return acc; + }, []); + } + didChangeExecutableMode() { const oldMode = this.getOldMode(); const newMode = this.getNewMode(); @@ -166,24 +121,32 @@ export default class FilePatch { return this.getPatch().getHunks(); } + clone(opts) { + return new this.constructor( + opts.oldFile !== undefined ? opts.oldFile : this.oldFile, + opts.newFile !== undefined ? opts.newFile : this.newFile, + opts.patch !== undefined ? opts.patch : this.patch, + ); + } + getStagePatchForHunk(selectedHunk) { - return this.getStagePatchForLines(new Set(selectedHunk.getLines())); + return this.getStagePatchForLines(new Set(selectedHunk.getBufferRows())); } - getStagePatchForLines(selectedLines) { - const wholeFileSelected = this.changedLineCount === [...selectedLines].filter(line => line.isChanged()).length; + getStagePatchForLines(selectedLineSet) { + const wholeFileSelected = this.changedLineCount === selectedLineSet.size; if (wholeFileSelected) { if (this.hasTypechange() && this.getStatus() === 'deleted') { // handle special case when symlink is created where a file was deleted. In order to stage the file deletion, // we must ensure that the created file patch has no new file return this.clone({ - newFile: File.empty(), + newFile: nullFile, }); } else { return this; } } else { - const hunks = this.getStagePatchHunks(selectedLines); + const hunks = this.getStagePatchHunks(selectedLineSet); if (this.getStatus() === 'deleted') { // Set status to modified return this.clone({ @@ -198,56 +161,59 @@ export default class FilePatch { } } - getStagePatchHunks(selectedLines) { + getStagePatchHunks(selectedLineSet) { let delta = 0; const hunks = []; for (const hunk of this.getHunks()) { - const newStartRow = (hunk.getNewStartRow() || 1) + delta; - let newLineNumber = newStartRow; - const lines = []; - let hunkContainsSelectedLines = false; - for (const line of hunk.getLines()) { - if (line.getStatus() === 'nonewline') { - lines.push(line.copy({oldLineNumber: -1, newLineNumber: -1})); - } else if (selectedLines.has(line)) { - hunkContainsSelectedLines = true; - if (line.getStatus() === 'deleted') { - lines.push(line.copy()); - } else { - lines.push(line.copy({newLineNumber: newLineNumber++})); - } - } else if (line.getStatus() === 'deleted') { - lines.push(line.copy({newLineNumber: newLineNumber++, status: 'unchanged'})); - } else if (line.getStatus() === 'unchanged') { - lines.push(line.copy({newLineNumber: newLineNumber++})); + const additions = []; + const deletions = []; + let deletedRowCount = 0; + + for (const change of hunk.getAdditions()) { + additions.push(...change.intersectRowsIn(selectedLineSet, this.getBufferText())); + } + + for (const change of hunk.getDeletions()) { + for (const intersection of change.intersectRowsIn(selectedLineSet, this.getBufferText())) { + deletedRowCount += intersection.bufferRowCount(); + deletions.push(intersection); } } - const newRowCount = newLineNumber - newStartRow; - if (hunkContainsSelectedLines) { - // eslint-disable-next-line max-len - hunks.push(new Hunk(hunk.getOldStartRow(), newStartRow, hunk.getOldRowCount(), newRowCount, hunk.getSectionHeading(), lines)); + + if ( + additions.length > 0 || + deletions.length > 0 + ) { + // Hunk contains at least one selected line + hunks.push(new Hunk({ + oldStartRow: hunk.getOldStartRow(), + newStartRow: hunk.getNewStartRow() - delta, + oldRowCount: hunk.getOldRowCount(), + newRowCount: hunk.getNewRowCount() - deletedRowCount, + sectionHeading: hunk.getSectionHeading(), + rowRange: hunk.getRowRange(), + additions, + deletions, + noNewline: hunk.getNoNewline(), + })); } - delta += newRowCount - hunk.getNewRowCount(); + delta += deletedRowCount; } return hunks; } getUnstagePatch() { - let invertedStatus; - switch (this.getStatus()) { - case 'modified': - invertedStatus = 'modified'; - break; - case 'added': - invertedStatus = 'deleted'; - break; - case 'deleted': - invertedStatus = 'added'; - break; - default: - // throw new Error(`Unknown Status: ${this.getStatus()}`); + const invertedStatus = { + modified: 'modified', + added: 'deleted', + deleted: 'added', + }[this.getStatus()]; + if (!invertedStatus) { + throw new Error(`Unknown Status: ${this.getStatus()}`); } + const invertedHunks = this.getHunks().map(h => h.invert()); + return this.clone({ oldFile: this.getNewFile(), newFile: this.getOldFile(), @@ -259,11 +225,11 @@ export default class FilePatch { } getUnstagePatchForHunk(hunk) { - return this.getUnstagePatchForLines(new Set(hunk.getLines())); + return this.getUnstagePatchForLines(new Set(hunk.getBufferRows())); } - getUnstagePatchForLines(selectedLines) { - if (this.changedLineCount === [...selectedLines].filter(line => line.isChanged()).length) { + getUnstagePatchForLines(selectedRowSet) { + if (this.changedLineCount === selectedRowSet.size) { if (this.hasTypechange() && this.getStatus() === 'added') { // handle special case when a file was created after a symlink was deleted. // In order to unstage the file creation, we must ensure that the unstage patch has no new file, @@ -276,7 +242,7 @@ export default class FilePatch { } } - const hunks = this.getUnstagePatchHunks(selectedLines); + const hunks = this.getUnstagePatchHunks(selectedRowSet); if (this.getStatus() === 'added') { return this.clone({ oldFile: this.getNewFile(), @@ -289,44 +255,48 @@ export default class FilePatch { } } - getUnstagePatchHunks(selectedLines) { + getUnstagePatchHunks(selectedRowSet) { let delta = 0; const hunks = []; for (const hunk of this.getHunks()) { const oldStartRow = (hunk.getOldStartRow() || 1) + delta; - let oldLineNumber = oldStartRow; - const lines = []; - let hunkContainsSelectedLines = false; - for (const line of hunk.getLines()) { - if (line.getStatus() === 'nonewline') { - lines.push(line.copy({oldLineNumber: -1, newLineNumber: -1})); - } else if (selectedLines.has(line)) { - hunkContainsSelectedLines = true; - if (line.getStatus() === 'added') { - lines.push(line.copy()); - } else { - lines.push(line.copy({oldLineNumber: oldLineNumber++})); - } - } else if (line.getStatus() === 'added') { - lines.push(line.copy({oldLineNumber: oldLineNumber++, status: 'unchanged'})); - } else if (line.getStatus() === 'unchanged') { - lines.push(line.copy({oldLineNumber: oldLineNumber++})); + + const additions = []; + const deletions = []; + let addedRowCount = 0; + + for (const change of hunk.getAdditions()) { + for (const intersection of change.intersectRowsIn(selectedRowSet, this.getBufferText())) { + addedRowCount += intersection.bufferRowCount(); + additions.push(intersection); } } - const oldRowCount = oldLineNumber - oldStartRow; - if (hunkContainsSelectedLines) { - // eslint-disable-next-line max-len - hunks.push(new Hunk(oldStartRow, hunk.getNewStartRow(), oldRowCount, hunk.getNewRowCount(), hunk.getSectionHeading(), lines)); + + for (const change of hunk.getBufferDeletedPositions()) { + deletions.push(...change.intersectRowIn(selectedRowSet, this.getBufferText())); } - delta += oldRowCount - hunk.getOldRowCount(); + + if (additions.length > 0 || deletions.length > 0) { + // Hunk contains at least one selected line + hunks.push(new Hunk({ + oldStartRow, + newStartRow: hunk.getNewStartRow(), + oldRowCount: hunk.getOldRowCount() - addedRowCount, + newRowCount: hunk.getNewRowCount(), + sectionHeading: hunk.getSectionHeading(), + bufferStartPosition: hunk.getBufferStartPosition(), + bufferStartOffset: hunk.getBufferStartOffset(), + bufferEndRow: hunk.getBufferEndRow(), + additions, + deletions, + noNewline: hunk.noNewline, + })); + } + delta += addedRowCount; } return hunks; } - present() { - return new PresentedFilePatch(this); - } - toString() { if (this.hasTypechange()) { const left = this.clone({ @@ -346,7 +316,7 @@ export default class FilePatch { const symlinkPath = this.getOldSymlink(); return this.getHeaderString() + `@@ -1 +0,0 @@\n-${symlinkPath}\n\\ No newline at end of file\n`; } else { - return this.getHeaderString() + this.getHunks().map(h => h.toString()).join(''); + return this.getHeaderString() + this.getPatch().toString(); } } From ff60d4e5b56aa530c7312053babaa36445f716c5 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 6 Aug 2018 10:14:05 -0400 Subject: [PATCH 0084/4252] Export buildFilePatch from lib/models/patch --- lib/models/patch/index.js | 1 + 1 file changed, 1 insertion(+) create mode 100644 lib/models/patch/index.js diff --git a/lib/models/patch/index.js b/lib/models/patch/index.js new file mode 100644 index 0000000000..596fcfde50 --- /dev/null +++ b/lib/models/patch/index.js @@ -0,0 +1 @@ +export {default as buildFilePatch} from './builder'; From fa49f658bd0c79f3d18ba239a54d8798dc760650 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 6 Aug 2018 10:14:22 -0400 Subject: [PATCH 0085/4252] Use the new FilePatch builder from Repository --- lib/models/repository-states/present.js | 115 +----------------------- 1 file changed, 3 insertions(+), 112 deletions(-) diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js index 2c00949633..abf67378b5 100644 --- a/lib/models/repository-states/present.js +++ b/lib/models/repository-states/present.js @@ -6,9 +6,7 @@ import State from './state'; import {LargeRepoError} from '../../git-shell-out-strategy'; import {FOCUS} from '../workspace-change-observer'; -import FilePatch from '../file-patch'; -import Hunk from '../hunk'; -import HunkLine from '../hunk-line'; +import {buildFilePatch} from '../patch'; import DiscardHistory from '../discard-history'; import Branch, {nullBranch} from '../branch'; import Author from '../author'; @@ -598,13 +596,8 @@ export default class Present extends State { getFilePatchForPath(filePath, {staged} = {staged: false}) { return this.cache.getOrSet(Keys.filePatch.oneWith(filePath, {staged}), async () => { - const rawDiffs = await this.git().getDiffsForFilePath(filePath, {staged}); - if (rawDiffs.length > 0) { - const filePatch = buildFilePatchFromRawDiffs(rawDiffs); - return filePatch; - } else { - return null; - } + const diffs = await this.git().getDiffsForFilePath(filePath, {staged}); + return buildFilePatch(diffs); }); } @@ -792,108 +785,6 @@ function partition(array, predicate) { return [matches, nonmatches]; } -function buildHunksFromDiff(diff) { - let diffLineNumber = 0; - return diff.hunks.map(hunk => { - let oldLineNumber = hunk.oldStartLine; - let newLineNumber = hunk.newStartLine; - const hunkLines = hunk.lines.map(line => { - const status = HunkLine.statusMap[line[0]]; - const text = line.slice(1); - let hunkLine; - if (status === 'unchanged') { - hunkLine = new HunkLine(text, status, oldLineNumber, newLineNumber, diffLineNumber++); - oldLineNumber++; - newLineNumber++; - } else if (status === 'added') { - hunkLine = new HunkLine(text, status, -1, newLineNumber, diffLineNumber++); - newLineNumber++; - } else if (status === 'deleted') { - hunkLine = new HunkLine(text, status, oldLineNumber, -1, diffLineNumber++); - oldLineNumber++; - } else if (status === 'nonewline') { - hunkLine = new HunkLine(text.substr(1), status, -1, -1, diffLineNumber++); - } else { - throw new Error(`unknown status type: ${status}`); - } - return hunkLine; - }); - return new Hunk( - hunk.oldStartLine, - hunk.newStartLine, - hunk.oldLineCount, - hunk.newLineCount, - hunk.heading, - hunkLines, - ); - }); -} - -function buildFilePatchFromSingleDiff(rawDiff) { - const wasSymlink = rawDiff.oldMode === '120000'; - const isSymlink = rawDiff.newMode === '120000'; - const diff = rawDiff; - const hunks = buildHunksFromDiff(diff); - let oldFile, newFile; - if (wasSymlink && !isSymlink) { - const symlink = diff.hunks[0].lines[0].slice(1); - oldFile = new FilePatch.File({path: diff.oldPath, mode: diff.oldMode, symlink}); - newFile = new FilePatch.File({path: diff.newPath, mode: diff.newMode, symlink: null}); - } else if (!wasSymlink && isSymlink) { - const symlink = diff.hunks[0].lines[0].slice(1); - oldFile = new FilePatch.File({path: diff.oldPath, mode: diff.oldMode, symlink: null}); - newFile = new FilePatch.File({path: diff.newPath, mode: diff.newMode, symlink}); - } else if (wasSymlink && isSymlink) { - const oldSymlink = diff.hunks[0].lines[0].slice(1); - const newSymlink = diff.hunks[0].lines[2].slice(1); - oldFile = new FilePatch.File({path: diff.oldPath, mode: diff.oldMode, symlink: oldSymlink}); - newFile = new FilePatch.File({path: diff.newPath, mode: diff.newMode, symlink: newSymlink}); - } else { - oldFile = new FilePatch.File({path: diff.oldPath, mode: diff.oldMode, symlink: null}); - newFile = new FilePatch.File({path: diff.newPath, mode: diff.newMode, symlink: null}); - } - const patch = new FilePatch.Patch({status: diff.status, hunks}); - return new FilePatch(oldFile, newFile, patch); -} - -function buildFilePatchFromDualDiffs(diff1, diff2) { - let modeChangeDiff, contentChangeDiff; - if (diff1.oldMode === '120000' || diff1.newMode === '120000') { - modeChangeDiff = diff1; - contentChangeDiff = diff2; - } else { - modeChangeDiff = diff2; - contentChangeDiff = diff1; - } - const hunks = buildHunksFromDiff(contentChangeDiff); - const filePath = contentChangeDiff.oldPath || contentChangeDiff.newPath; - const symlink = modeChangeDiff.hunks[0].lines[0].slice(1); - let oldFile, newFile, status; - if (modeChangeDiff.status === 'added') { - oldFile = new FilePatch.File({path: filePath, mode: contentChangeDiff.oldMode, symlink: null}); - newFile = new FilePatch.File({path: filePath, mode: modeChangeDiff.newMode, symlink}); - status = 'deleted'; // contents were deleted and replaced with symlink - } else if (modeChangeDiff.status === 'deleted') { - oldFile = new FilePatch.File({path: filePath, mode: modeChangeDiff.oldMode, symlink}); - newFile = new FilePatch.File({path: filePath, mode: contentChangeDiff.newMode, symlink: null}); - status = 'added'; // contents were added after symlink was deleted - } else { - throw new Error(`Invalid mode change diff status: ${modeChangeDiff.status}`); - } - const patch = new FilePatch.Patch({status, hunks}); - return new FilePatch(oldFile, newFile, patch); -} - -function buildFilePatchFromRawDiffs(rawDiffs) { - if (rawDiffs.length === 1) { - return buildFilePatchFromSingleDiff(rawDiffs[0]); - } else if (rawDiffs.length === 2) { - return buildFilePatchFromDualDiffs(rawDiffs[0], rawDiffs[1]); - } else { - throw new Error(`Unexpected number of diffs: ${rawDiffs.length}`); - } -} - class Cache { constructor() { this.storage = new Map(); From 2d15f7003c470a948b94ce8543cf934de54e6b5d Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 6 Aug 2018 10:23:53 -0400 Subject: [PATCH 0086/4252] Temporarily move broken tests out of the way --- ...h-selection.test.js => file-patch-selection.test.pending.js} | 0 test/models/{file-patch.test.js => file-patch.test.old.js} | 2 ++ ...{file-patch-view.test.js => file-patch-view.test.pending.js} | 0 ...unk-header-view.test.js => hunk-header-view.test.pending.js} | 0 test/views/{hunk-view.test.js => hunk-view.test.pending.js} | 0 5 files changed, 2 insertions(+) rename test/models/{file-patch-selection.test.js => file-patch-selection.test.pending.js} (100%) rename test/models/{file-patch.test.js => file-patch.test.old.js} (99%) rename test/views/{file-patch-view.test.js => file-patch-view.test.pending.js} (100%) rename test/views/{hunk-header-view.test.js => hunk-header-view.test.pending.js} (100%) rename test/views/{hunk-view.test.js => hunk-view.test.pending.js} (100%) diff --git a/test/models/file-patch-selection.test.js b/test/models/file-patch-selection.test.pending.js similarity index 100% rename from test/models/file-patch-selection.test.js rename to test/models/file-patch-selection.test.pending.js diff --git a/test/models/file-patch.test.js b/test/models/file-patch.test.old.js similarity index 99% rename from test/models/file-patch.test.js rename to test/models/file-patch.test.old.js index e78e625a73..9c87041202 100644 --- a/test/models/file-patch.test.js +++ b/test/models/file-patch.test.old.js @@ -16,6 +16,8 @@ function createFilePatch(oldFilePath, newFilePath, status, hunks) { return new FilePatch(oldFile, newFile, patch); } +// oldStartRow, newStartRow, oldRowCount, newRowCount, sectionHeading, lines + describe('FilePatch', function() { describe('getStagePatchForLines()', function() { it('returns a new FilePatch that applies only the specified lines', function() { diff --git a/test/views/file-patch-view.test.js b/test/views/file-patch-view.test.pending.js similarity index 100% rename from test/views/file-patch-view.test.js rename to test/views/file-patch-view.test.pending.js diff --git a/test/views/hunk-header-view.test.js b/test/views/hunk-header-view.test.pending.js similarity index 100% rename from test/views/hunk-header-view.test.js rename to test/views/hunk-header-view.test.pending.js diff --git a/test/views/hunk-view.test.js b/test/views/hunk-view.test.pending.js similarity index 100% rename from test/views/hunk-view.test.js rename to test/views/hunk-view.test.pending.js From b4decf77e3eac678aaa6635e7a340eee4fff9b0e Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 6 Aug 2018 10:24:03 -0400 Subject: [PATCH 0087/4252] Delete an unused import --- lib/models/presented-file-patch.js | 1 - lib/views/file-patch-view.js | 1 - 2 files changed, 2 deletions(-) diff --git a/lib/models/presented-file-patch.js b/lib/models/presented-file-patch.js index 871190dfc2..8fb5c00437 100644 --- a/lib/models/presented-file-patch.js +++ b/lib/models/presented-file-patch.js @@ -1,5 +1,4 @@ import {Point} from 'atom'; -import {fromBufferPosition} from './marker-position'; export default class PresentedFilePatch { constructor(filePatch) { diff --git a/lib/views/file-patch-view.js b/lib/views/file-patch-view.js index dcebafb3e3..1d12eeedcb 100644 --- a/lib/views/file-patch-view.js +++ b/lib/views/file-patch-view.js @@ -11,7 +11,6 @@ import Gutter from '../atom/gutter'; import FilePatchHeaderView from './file-patch-header-view'; import FilePatchMetaView from './file-patch-meta-view'; import HunkHeaderView from './hunk-header-view'; -import {fromBufferPosition} from '../models/marker-position'; const executableText = { 100644: 'non executable', From 6f7511252aeb00fde566f7825fdf406a596646b1 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 6 Aug 2018 10:24:20 -0400 Subject: [PATCH 0088/4252] Start translating FilePatch tests --- test/models/patch/file-patch.test.js | 187 +++++++++++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 test/models/patch/file-patch.test.js diff --git a/test/models/patch/file-patch.test.js b/test/models/patch/file-patch.test.js new file mode 100644 index 0000000000..3c6cb6f1ac --- /dev/null +++ b/test/models/patch/file-patch.test.js @@ -0,0 +1,187 @@ +import {buildFilePatch} from '../../../lib/models/patch'; +import {assertInFilePatch} from '../../helpers'; + +describe('FilePatch', function() { + describe('getStagePatchForLines()', function() { + it('returns a new FilePatch that applies only the selected lines', function() { + const filePatch = buildFilePatch([{ + oldPath: 'a.txt', + oldMode: '100644', + newPath: 'a.txt', + newMode: '100644', + hunks: [ + { + oldStartLine: 1, + oldLineCount: 1, + newStartLine: 1, + newLineCount: 3, + lines: [ + '+line-0', + '+line-1', + ' line-2', + ], + }, + { + oldStartLine: 5, + oldLineCount: 5, + newStartLine: 7, + newLineCount: 4, + lines: [ + ' line-3', + '-line-4', + '-line-5', + '+line-6', + '+line-7', + '+line-8', + '-line-9', + '-line-10', + ], + }, + { + oldStartLine: 20, + oldLineCount: 2, + newStartLine: 19, + newLineCount: 2, + lines: [ + '-line-11', + '+line-12', + ' line-13', + '\\No newline at end of file', + ], + }, + ], + }]); + + assert.strictEqual( + filePatch.getBufferText(), + 'line-0\nline-1\nline-2\nline-3\nline-4\nline-5\nline-6\nline-7\nline-8\nline-9\nline-10\n' + + 'line-11\nline-12\nline-13\nNo newline at end of file\n', + ); + + const stagePatch0 = filePatch.getStagePatchForLines(new Set([4, 5, 6])); + assertInFilePatch(stagePatch0).hunks( + { + startRow: 3, + header: '@@ -5,5 +7,4 @@', + deletions: {strings: ['*line-4\nline-5\n'], ranges: [[[4, 0], [5, 0]]]}, + additions: {strings: ['*line-6\n'], ranges: [[[6, 0], [6, 0]]]}, + }, + ); + + const stagePatch1 = filePatch.getStagePatchForLines(new Set([0, 4, 5, 6, 11])); + assertInFilePatch(stagePatch1).hunks( + { + startRow: 0, + header: '@@ -1,1 +1,2 @@', + additions: {strings: ['*line-0\n'], ranges: [[[0, 0], [0, 0]]]}, + }, + { + startRow: 3, + header: '@@ -5,5 +7,4 @@', + deletions: {strings: ['*line-4\n*line-5\n'], ranges: [[[4, 0], [5, 0]]]}, + additions: {strings: ['*line-6\n'], ranges: [[[6, 0], [6, 0]]]}, + }, + { + startRow: 11, + header: '@@ -20,2 +19,2 @@', + deletions: {strings: ['*line-11\n'], ranges: [[[11, 0], [11, 0]]]}, + noNewline: {string: '*No newline at end of file\n', range: [[14, 0], [14, 0]]}, + }, + ); + }); + + describe('staging lines from deleted files', function() { + it('handles staging part of the file', function() { + const filePatch = buildFilePatch([{ + oldPath: 'a.txt', + oldMode: '100644', + newPath: null, + newMode: '000000', + status: 'deleted', + hunks: [ + { + oldStartLine: 1, + newStartLine: 0, + oldLineCount: 3, + newLineCount: 0, + lines: [ + '-line-1', + '-line-2', + '-line-3', + ], + }, + { + oldStartLine: 19, + newStartLine: 21, + oldLineCount: 2, + newLineCount: 2, + lines: [ + '-line-13', + '+line-12', + ' line-14', + '\\No newline at end of file', + ], + }, + ], + }]); + + assert.strictEqual(filePatch.getBufferText(), + 'line-1\nline-2\nline-3\nline-13\nline-12\nline-14\n' + + 'No newline at end of file\n'); + + const stagePatch = filePatch.getStagePatchForLines(new Set([0, 1])); + assertInFilePatch(stagePatch).hunks( + { + startRow: 0, + header: '@@ -1,1 +3,1 @@', + deletions: {strings: ['*line-1\n*line-2'], ranges: [[[0, 0], [1, 0]]]}, + }, + ); + }); + + it('handles staging all lines, leaving nothing unstaged', function() { + const filePatch = buildFilePatch([{ + oldPath: 'a.txt', + oldMode: '100644', + newPath: null, + newMode: '000000', + status: 'deleted', + hunks: [ + { + oldStartLine: 1, + oldLineCount: 3, + newStartLine: 1, + newLineCount: 0, + lines: [ + '-line-1', + '-line-2', + '-line-3', + ], + }, + ], + }]); + + assert.strictEqual(filePatch.getBufferText(), 'line-1\nline-2\nline-3\n'); + + const stagePatch = filePatch.getStagePatchForLines(new Set(0, 1, 2)); + assertInFilePatch(stagePatch).hunks( + { + startRow: 0, + header: '@@ -1,3 +1,0 @@', + deletions: {strings: ['*line-1\n*line-2\n*line-3\n'], ranges: [[[0, 0], [2, 0]]]}, + }, + ); + }); + }); + }); + + describe('getUnstagePatchForLines()', function() { + it('returns a new FilePatch that applies only the specified lines'); + + describe('unstaging lines from an added file', function() { + it('handles unstaging part of the file'); + + it('handles unstaging all lines, leaving nothing staged'); + }); + }); +}); From a323079a73ce9ee2accf07c37ffd19c74d5c2872 Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Mon, 6 Aug 2018 16:17:35 +0000 Subject: [PATCH 0089/4252] chore(package): update hock to version 1.3.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 89296e4c5d..8b2f3dee7c 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,7 @@ "eslint": "5.0.1", "eslint-config-fbjs-opensource": "1.0.0", "eslint-plugin-jsx-a11y": "^6.1.1", - "hock": "1.3.2", + "hock": "1.3.3", "lodash.isequal": "4.5.0", "mkdirp": "0.5.1", "mocha-appveyor-reporter": "0.4.0", From b1f2ab109e90ec3e3627f277c23fdbe8a8b1352f Mon Sep 17 00:00:00 2001 From: Tilde Ann Thurium Date: Mon, 6 Aug 2018 16:21:08 -0700 Subject: [PATCH 0090/4252] :shirt: --- lib/views/issueish-detail-view.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/views/issueish-detail-view.js b/lib/views/issueish-detail-view.js index 55007ac8e5..8e1bc970a3 100644 --- a/lib/views/issueish-detail-view.js +++ b/lib/views/issueish-detail-view.js @@ -131,14 +131,14 @@ export class BareIssueishDetailView extends React.Component {
{isPr &&
- - {issueish.author.login} wants to merge{' '} - 2 commits and{' '} - 3 changed files into{' '} - {issueish.baseRefName} from{' '} - {issueish.headRefName} - + + {issueish.author.login} wants to merge{' '} + 2 commits and{' '} + 3 changed files into{' '} + {issueish.baseRefName} from{' '} + {issueish.headRefName} +
}
From b5139eb67f0f8c2751cb17ced2bd7d9d3bec0e3b Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 7 Aug 2018 15:32:01 -0400 Subject: [PATCH 0091/4252] :lock: --- package-lock.json | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index ceebc1c79e..0a4fcfacc6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3682,12 +3682,13 @@ "dev": true }, "hock": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/hock/-/hock-1.3.2.tgz", - "integrity": "sha1-btPovkK0ZnmBGNEhUKqA6NbvIhk=", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/hock/-/hock-1.3.3.tgz", + "integrity": "sha512-bEX7KH/KSv2Q5zA+o1EdyeH52+gD2cfpYyAsHMEwjb9txXWttityKVf7cG0y3UVA4D8bxKDzH8LVXCQIr9rClg==", "dev": true, "requires": { - "deep-equal": "0.2.1" + "deep-equal": "0.2.1", + "url-equal": "0.1.2-1" } }, "hoek": { @@ -7845,6 +7846,23 @@ "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", "dev": true }, + "url-equal": { + "version": "0.1.2-1", + "resolved": "https://registry.npmjs.org/url-equal/-/url-equal-0.1.2-1.tgz", + "integrity": "sha1-IjeVIL/gfSa1kIAEkIruEuhNBf8=", + "dev": true, + "requires": { + "deep-equal": "~1.0.1" + }, + "dependencies": { + "deep-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", + "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=", + "dev": true + } + } + }, "use": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/use/-/use-3.1.0.tgz", From 613757c811a6887ff2b849a6d9a87fdda66ccb2c Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 7 Aug 2018 15:54:26 -0400 Subject: [PATCH 0092/4252] Propagate .focus() to an item's .focus() method --- lib/atom/pane-item.js | 6 ++++++ test/atom/pane-item.test.js | 22 ++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/lib/atom/pane-item.js b/lib/atom/pane-item.js index dca80ee487..f564d021d0 100644 --- a/lib/atom/pane-item.js +++ b/lib/atom/pane-item.js @@ -170,6 +170,8 @@ class OpenItem { this.constructor.nextID++; this.domNode = element || document.createElement('div'); + this.domNode.tabIndex = '-1'; + this.domNode.onfocus = this.onFocus.bind(this); this.stubItem = stub; this.match = match; this.itemHolder = new RefHolder(); @@ -211,6 +213,10 @@ class OpenItem { } } + onFocus() { + return this.itemHolder.map(item => item.focus && item.focus()); + } + renderPortal(renderProp) { return ReactDOM.createPortal( renderProp({ diff --git a/test/atom/pane-item.test.js b/test/atom/pane-item.test.js index 47cdac10d1..d85f5b42de 100644 --- a/test/atom/pane-item.test.js +++ b/test/atom/pane-item.test.js @@ -8,6 +8,11 @@ import StubItem from '../../lib/items/stub-item'; class Component extends React.Component { static propTypes = { text: PropTypes.string.isRequired, + didFocus: PropTypes.func, + } + + static defaultProps = { + didFocus: () => {}, } render() { @@ -23,6 +28,10 @@ class Component extends React.Component { getText() { return this.props.text; } + + focus() { + return this.props.didFocus(); + } } describe('PaneItem', function() { @@ -192,6 +201,19 @@ describe('PaneItem', function() { assert.strictEqual(calledWith, 'atom-github://pattern/456'); }); + it('calls focus() on the child item when focus() is called', async function() { + const didFocus = sinon.spy(); + mount( + + {({itemHolder}) => } + , + ); + const item = await workspace.open('atom-github://pattern'); + item.getElement().dispatchEvent(new FocusEvent('focus')); + + assert.isTrue(didFocus.called); + }); + it('removes a child when its pane is destroyed', async function() { const wrapper = mount( From 39dba4feb34190354a3ab41aae0a727c85a028b3 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 7 Aug 2018 15:55:38 -0400 Subject: [PATCH 0093/4252] Restore focus to the GitTabController on item focus --- lib/items/git-tab-item.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/items/git-tab-item.js b/lib/items/git-tab-item.js index 46ec3193d8..75ea960176 100644 --- a/lib/items/git-tab-item.js +++ b/lib/items/git-tab-item.js @@ -75,6 +75,10 @@ export default class GitTabItem extends React.Component { return this.refController.map(c => c.hasFocus(...args)); } + focus() { + return this.refController.map(c => c.restoreFocus()); + } + focusAndSelectStagingItem(...args) { return this.refController.map(c => c.focusAndSelectStagingItem(...args)); } From ef641ddf78994409f3228d6bdf3008df13ef11a2 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 7 Aug 2018 16:12:58 -0400 Subject: [PATCH 0094/4252] Observe tab focus before potentially toggling visibility --- lib/controllers/root-controller.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/controllers/root-controller.js b/lib/controllers/root-controller.js index 383dab104a..3144cbb2fe 100644 --- a/lib/controllers/root-controller.js +++ b/lib/controllers/root-controller.js @@ -771,9 +771,10 @@ class TabTracker { } async toggleFocus() { + const hadFocus = this.hasFocus(); await this.ensureVisible(); - if (this.hasFocus()) { + if (hadFocus) { let workspace = this.getWorkspace(); if (workspace.getCenter) { workspace = workspace.getCenter(); From 47236e5e079203a2bc9e8ea0a92611c32c284068 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 7 Aug 2018 16:13:16 -0400 Subject: [PATCH 0095/4252] Focus flickers back --- test/integration/open.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/open.test.js b/test/integration/open.test.js index f58b2d74f0..4657b30176 100644 --- a/test/integration/open.test.js +++ b/test/integration/open.test.js @@ -81,7 +81,7 @@ describe('opening and closing tabs', function() { assert.isTrue(wrapper.find('.github-Git').exists()); assert.isTrue(atomEnv.workspace.getRightDock().isVisible()); - assert.isFalse(wrapper.find('.github-Git[tabIndex]').getDOMNode().contains(document.activeElement)); + await assert.async.isFalse(wrapper.find('.github-Git[tabIndex]').getDOMNode().contains(document.activeElement)); await commands.dispatch(workspaceElement, 'github:toggle-git-tab-focus'); wrapper.update(); From 7f4db24571761a167d1f0b62cc7ee596de0eb219 Mon Sep 17 00:00:00 2001 From: Tilde Ann Thurium Date: Wed, 8 Aug 2018 15:14:45 -0700 Subject: [PATCH 0096/4252] update graphql schema --- graphql/schema.graphql | 556 ++++++++++++++++++++++++++++++++--------- 1 file changed, 443 insertions(+), 113 deletions(-) diff --git a/graphql/schema.graphql b/graphql/schema.graphql index 720f5e59bb..dfadf986b3 100644 --- a/graphql/schema.graphql +++ b/graphql/schema.graphql @@ -15,7 +15,15 @@ type AcceptTopicSuggestionPayload { """A unique identifier for the client performing the mutation.""" clientMutationId: String - """The accepted topic.""" + """ + The accepted topic. + + **Upcoming Change on 2019-01-01 UTC** + **Description:** Type for `topic` will change from `Topic!` to `Topic`. + **Reason:** In preparation for an upcoming change to the way we report + mutation errors, non-nullable payload fields are becoming nullable. + + """ topic: Topic! } @@ -56,13 +64,37 @@ type AddCommentPayload { """A unique identifier for the client performing the mutation.""" clientMutationId: String - """The edge from the subject's comment connection.""" + """ + The edge from the subject's comment connection. + + **Upcoming Change on 2019-01-01 UTC** + **Description:** Type for `commentEdge` will change from `IssueCommentEdge!` to `IssueCommentEdge`. + **Reason:** In preparation for an upcoming change to the way we report + mutation errors, non-nullable payload fields are becoming nullable. + + """ commentEdge: IssueCommentEdge! - """The subject""" + """ + The subject + + **Upcoming Change on 2019-01-01 UTC** + **Description:** Type for `subject` will change from `Node!` to `Node`. + **Reason:** In preparation for an upcoming change to the way we report + mutation errors, non-nullable payload fields are becoming nullable. + + """ subject: Node! - """The edge from the subject's timeline connection.""" + """ + The edge from the subject's timeline connection. + + **Upcoming Change on 2019-01-01 UTC** + **Description:** Type for `timelineEdge` will change from `IssueTimelineItemEdge!` to `IssueTimelineItemEdge`. + **Reason:** In preparation for an upcoming change to the way we report + mutation errors, non-nullable payload fields are becoming nullable. + + """ timelineEdge: IssueTimelineItemEdge! } @@ -100,13 +132,29 @@ input AddProjectCardInput { """Autogenerated return type of AddProjectCard""" type AddProjectCardPayload { - """The edge from the ProjectColumn's card connection.""" + """ + The edge from the ProjectColumn's card connection. + + **Upcoming Change on 2019-01-01 UTC** + **Description:** Type for `cardEdge` will change from `ProjectCardEdge!` to `ProjectCardEdge`. + **Reason:** In preparation for an upcoming change to the way we report + mutation errors, non-nullable payload fields are becoming nullable. + + """ cardEdge: ProjectCardEdge! """A unique identifier for the client performing the mutation.""" clientMutationId: String - """The ProjectColumn""" + """ + The ProjectColumn + + **Upcoming Change on 2019-01-01 UTC** + **Description:** Type for `projectColumn` will change from `Project!` to `Project`. + **Reason:** In preparation for an upcoming change to the way we report + mutation errors, non-nullable payload fields are becoming nullable. + + """ projectColumn: Project! } @@ -127,10 +175,26 @@ type AddProjectColumnPayload { """A unique identifier for the client performing the mutation.""" clientMutationId: String - """The edge from the project's column connection.""" + """ + The edge from the project's column connection. + + **Upcoming Change on 2019-01-01 UTC** + **Description:** Type for `columnEdge` will change from `ProjectColumnEdge!` to `ProjectColumnEdge`. + **Reason:** In preparation for an upcoming change to the way we report + mutation errors, non-nullable payload fields are becoming nullable. + + """ columnEdge: ProjectColumnEdge! - """The project""" + """ + The project + + **Upcoming Change on 2019-01-01 UTC** + **Description:** Type for `project` will change from `Project!` to `Project`. + **Reason:** In preparation for an upcoming change to the way we report + mutation errors, non-nullable payload fields are becoming nullable. + + """ project: Project! } @@ -163,10 +227,27 @@ type AddPullRequestReviewCommentPayload { """A unique identifier for the client performing the mutation.""" clientMutationId: String - """The newly created comment.""" + """ + The newly created comment. + + **Upcoming Change on 2019-01-01 UTC** + **Description:** Type for `comment` will change from `PullRequestReviewComment!` to `PullRequestReviewComment`. + **Reason:** In preparation for an upcoming change to the way we report + mutation errors, non-nullable payload fields are becoming nullable. + + """ comment: PullRequestReviewComment! - """The edge from the review's comment connection.""" + """ + The edge from the review's comment connection. + + **Upcoming Change on 2019-01-01 UTC** + **Description:** Type for `commentEdge` will change from + `PullRequestReviewCommentEdge!` to `PullRequestReviewCommentEdge`. + **Reason:** In preparation for an upcoming change to the way we report + mutation errors, non-nullable payload fields are becoming nullable. + + """ commentEdge: PullRequestReviewCommentEdge! } @@ -196,10 +277,26 @@ type AddPullRequestReviewPayload { """A unique identifier for the client performing the mutation.""" clientMutationId: String - """The newly created pull request review.""" + """ + The newly created pull request review. + + **Upcoming Change on 2019-01-01 UTC** + **Description:** Type for `pullRequestReview` will change from `PullRequestReview!` to `PullRequestReview`. + **Reason:** In preparation for an upcoming change to the way we report + mutation errors, non-nullable payload fields are becoming nullable. + + """ pullRequestReview: PullRequestReview! - """The edge from the pull request's review connection.""" + """ + The edge from the pull request's review connection. + + **Upcoming Change on 2019-01-01 UTC** + **Description:** Type for `reviewEdge` will change from `PullRequestReviewEdge!` to `PullRequestReviewEdge`. + **Reason:** In preparation for an upcoming change to the way we report + mutation errors, non-nullable payload fields are becoming nullable. + + """ reviewEdge: PullRequestReviewEdge! } @@ -220,10 +317,26 @@ type AddReactionPayload { """A unique identifier for the client performing the mutation.""" clientMutationId: String - """The reaction object.""" + """ + The reaction object. + + **Upcoming Change on 2019-01-01 UTC** + **Description:** Type for `reaction` will change from `Reaction!` to `Reaction`. + **Reason:** In preparation for an upcoming change to the way we report + mutation errors, non-nullable payload fields are becoming nullable. + + """ reaction: Reaction! - """The reactable subject.""" + """ + The reactable subject. + + **Upcoming Change on 2019-01-01 UTC** + **Description:** Type for `subject` will change from `Reactable!` to `Reactable`. + **Reason:** In preparation for an upcoming change to the way we report + mutation errors, non-nullable payload fields are becoming nullable. + + """ subject: Reactable! } @@ -241,10 +354,58 @@ type AddStarPayload { """A unique identifier for the client performing the mutation.""" clientMutationId: String - """The starrable.""" + """ + The starrable. + + **Upcoming Change on 2019-01-01 UTC** + **Description:** Type for `starrable` will change from `Starrable!` to `Starrable`. + **Reason:** In preparation for an upcoming change to the way we report + mutation errors, non-nullable payload fields are becoming nullable. + + """ starrable: Starrable! } +"""A GitHub App.""" +type App implements Node { + """Identifies the date and time when the object was created.""" + createdAt: DateTime! + + """Identifies the primary key from the database.""" + databaseId: Int + + """The description of the app.""" + description: String + id: ID! + + """A URL pointing to the app's logo.""" + logoUrl( + """The size of the resulting image.""" + size: Int + ): URI! + + """The name of the app.""" + name: String! + + """A slug based on the name of the app for use in URLs.""" + slug: String! + + """Identifies the date and time when the object was last updated.""" + updatedAt: DateTime! + + """The URL to the app's homepage.""" + url: URI! +} + +"""An edge in a connection.""" +type AppEdge { + """A cursor for use in pagination.""" + cursor: String! + + """The item at the end of the edge.""" + node: App +} + """An object that can have users assigned to it.""" interface Assignable { """A list of Users assigned to this object.""" @@ -433,9 +594,6 @@ type ClosedEvent implements Node & UniformResourceLocatable { """Object which triggered the creation of this event.""" closer: Closer - """Identifies the commit associated with the 'closed' event.""" - commit: Commit @deprecated(reason: "`ClosedEvent` may be associated with other objects than a commit. Use ClosedEvent.closer instead. Removal on 2018-07-01 UTC.") - """Identifies the date and time when the object was created.""" createdAt: DateTime! id: ID! @@ -1066,7 +1224,15 @@ type CreateProjectPayload { """A unique identifier for the client performing the mutation.""" clientMutationId: String - """The new project.""" + """ + The new project. + + **Upcoming Change on 2019-01-01 UTC** + **Description:** Type for `project` will change from `Project!` to `Project`. + **Reason:** In preparation for an upcoming change to the way we report + mutation errors, non-nullable payload fields are becoming nullable. + + """ project: Project! } @@ -1127,7 +1293,15 @@ type DeclineTopicSuggestionPayload { """A unique identifier for the client performing the mutation.""" clientMutationId: String - """The declined topic.""" + """ + The declined topic. + + **Upcoming Change on 2019-01-01 UTC** + **Description:** Type for `topic` will change from `Topic!` to `Topic`. + **Reason:** In preparation for an upcoming change to the way we report + mutation errors, non-nullable payload fields are becoming nullable. + + """ topic: Topic! } @@ -1166,10 +1340,26 @@ type DeleteProjectCardPayload { """A unique identifier for the client performing the mutation.""" clientMutationId: String - """The column the deleted card was in.""" + """ + The column the deleted card was in. + + **Upcoming Change on 2019-01-01 UTC** + **Description:** Type for `column` will change from `ProjectColumn!` to `ProjectColumn`. + **Reason:** In preparation for an upcoming change to the way we report + mutation errors, non-nullable payload fields are becoming nullable. + + """ column: ProjectColumn! - """The deleted card ID.""" + """ + The deleted card ID. + + **Upcoming Change on 2019-01-01 UTC** + **Description:** Type for `deletedCardId` will change from `ID!` to `ID`. + **Reason:** In preparation for an upcoming change to the way we report + mutation errors, non-nullable payload fields are becoming nullable. + + """ deletedCardId: ID! } @@ -1187,10 +1377,26 @@ type DeleteProjectColumnPayload { """A unique identifier for the client performing the mutation.""" clientMutationId: String - """The deleted column ID.""" + """ + The deleted column ID. + + **Upcoming Change on 2019-01-01 UTC** + **Description:** Type for `deletedColumnId` will change from `ID!` to `ID`. + **Reason:** In preparation for an upcoming change to the way we report + mutation errors, non-nullable payload fields are becoming nullable. + + """ deletedColumnId: ID! - """The project the deleted column was in.""" + """ + The project the deleted column was in. + + **Upcoming Change on 2019-01-01 UTC** + **Description:** Type for `project` will change from `Project!` to `Project`. + **Reason:** In preparation for an upcoming change to the way we report + mutation errors, non-nullable payload fields are becoming nullable. + + """ project: Project! } @@ -1208,7 +1414,15 @@ type DeleteProjectPayload { """A unique identifier for the client performing the mutation.""" clientMutationId: String - """The repository or organization the project was removed from.""" + """ + The repository or organization the project was removed from. + + **Upcoming Change on 2019-01-01 UTC** + **Description:** Type for `owner` will change from `ProjectOwner!` to `ProjectOwner`. + **Reason:** In preparation for an upcoming change to the way we report + mutation errors, non-nullable payload fields are becoming nullable. + + """ owner: ProjectOwner! } @@ -1226,7 +1440,15 @@ type DeletePullRequestReviewPayload { """A unique identifier for the client performing the mutation.""" clientMutationId: String - """The deleted pull request review.""" + """ + The deleted pull request review. + + **Upcoming Change on 2019-01-01 UTC** + **Description:** Type for `pullRequestReview` will change from `PullRequestReview!` to `PullRequestReview`. + **Reason:** In preparation for an upcoming change to the way we report + mutation errors, non-nullable payload fields are becoming nullable. + + """ pullRequestReview: PullRequestReview! } @@ -1327,6 +1549,9 @@ type Deployment implements Node { """Identifies the primary key from the database.""" databaseId: Int + """The deployment description.""" + description: String + """The environment to which this deployment was made.""" environment: String id: ID! @@ -1361,6 +1586,12 @@ type Deployment implements Node { """ before: String ): DeploymentStatusConnection + + """The deployment task.""" + task: String + + """Identifies the date and time when the object was last updated.""" + updatedAt: DateTime! } """The connection type for Deployment.""" @@ -1498,7 +1729,15 @@ type DismissPullRequestReviewPayload { """A unique identifier for the client performing the mutation.""" clientMutationId: String - """The dismissed pull request review.""" + """ + The dismissed pull request review. + + **Upcoming Change on 2019-01-01 UTC** + **Description:** Type for `pullRequestReview` will change from `PullRequestReview!` to `PullRequestReview`. + **Reason:** In preparation for an upcoming change to the way we report + mutation errors, non-nullable payload fields are becoming nullable. + + """ pullRequestReview: PullRequestReview! } @@ -1858,7 +2097,7 @@ type GitActor { """Represents information about the GitHub instance.""" type GitHubMetadata { """Returns a String that's a SHA of `github-services`""" - gitHubServicesSha: String! + gitHubServicesSha: GitObjectID! """IP addresses that users connect to for git operations""" gitIpAddresses: [String!] @@ -2400,7 +2639,7 @@ type IssueComment implements Node & Comment & Deletable & Updatable & UpdatableC """ Returns the pull request associated with the comment, if this comment was made on a pull request. - + """ pullRequest: PullRequest @@ -2969,6 +3208,9 @@ type MarketplaceCategory implements Node { """A listing in the GitHub integration marketplace.""" type MarketplaceListing implements Node { + """The GitHub App this listing represents.""" + app: App + """URL to the listing owner's company site.""" companyUrl: URI @@ -3116,7 +3358,7 @@ type MarketplaceListing implements Node { """ Can the current viewer edit the primary and secondary category of this Marketplace listing. - + """ viewerCanEditCategories: Boolean! @@ -3126,40 +3368,40 @@ type MarketplaceListing implements Node { """ Can the current viewer return this Marketplace listing to draft state so it becomes editable again. - + """ viewerCanRedraft: Boolean! """ Can the current viewer reject this Marketplace listing by returning it to an editable draft state or rejecting it entirely. - + """ viewerCanReject: Boolean! """ Can the current viewer request this listing be reviewed for display in the Marketplace. - + """ viewerCanRequestApproval: Boolean! """ Indicates whether the current user has an active subscription to this Marketplace listing. - + """ viewerHasPurchased: Boolean! """ Indicates if the current user has purchased a subscription to this Marketplace listing for all of the organizations the user owns. - + """ viewerHasPurchasedForAllOrganizations: Boolean! """ Does the current viewer role allow them to administer this Marketplace listing. - + """ viewerIsListingAdmin: Boolean! } @@ -3460,7 +3702,15 @@ input MoveProjectCardInput { """Autogenerated return type of MoveProjectCard""" type MoveProjectCardPayload { - """The new edge of the moved card.""" + """ + The new edge of the moved card. + + **Upcoming Change on 2019-01-01 UTC** + **Description:** Type for `cardEdge` will change from `ProjectCardEdge!` to `ProjectCardEdge`. + **Reason:** In preparation for an upcoming change to the way we report + mutation errors, non-nullable payload fields are becoming nullable. + + """ cardEdge: ProjectCardEdge! """A unique identifier for the client performing the mutation.""" @@ -3486,7 +3736,15 @@ type MoveProjectColumnPayload { """A unique identifier for the client performing the mutation.""" clientMutationId: String - """The new edge of the moved column.""" + """ + The new edge of the moved column. + + **Upcoming Change on 2019-01-01 UTC** + **Description:** Type for `columnEdge` will change from `ProjectColumnEdge!` to `ProjectColumnEdge`. + **Reason:** In preparation for an upcoming change to the way we report + mutation errors, non-nullable payload fields are becoming nullable. + + """ columnEdge: ProjectColumnEdge! } @@ -3610,7 +3868,7 @@ enum OrderDirection { """ An account on GitHub, with one or more owners, that has repositories, members and teams. """ -type Organization implements Node & Actor & ProjectOwner & RepositoryOwner & UniformResourceLocatable { +type Organization implements Node & Actor & RegistryPackageOwner & RegistryPackageSearch & ProjectOwner & RepositoryOwner & UniformResourceLocatable { """A URL pointing to the organization's public avatar.""" avatarUrl( """The size of the resulting square image.""" @@ -3627,6 +3885,9 @@ type Organization implements Node & Actor & ProjectOwner & RepositoryOwner & Uni email: String id: ID! + """Whether the organization has verified its profile email and website.""" + isVerified: Boolean! + """The organization's public profile location.""" location: String @@ -4126,7 +4387,7 @@ type ProjectCard implements Node { project column at a time. The column field will be null if the card is created in a pending state and has yet to be associated with a column. Once cards are associated with a column, they will not become pending in the future. - + """ column: ProjectColumn @@ -4149,9 +4410,6 @@ type ProjectCard implements Node { """The project that contains this card.""" project: Project! - """The column that contains this card.""" - projectColumn: ProjectColumn! @deprecated(reason: "The associated column may be null if the card is in a pending state. Use `ProjectCard.column` instead. Removal on 2018-07-01 UTC.") - """The HTTP path for this card""" resourcePath: URI! @@ -5532,7 +5790,7 @@ type Query { """ Select listings to which user has admin access. If omitted, listings visible to the viewer are returned. - + """ viewerCanAdmin: Boolean @@ -5545,7 +5803,7 @@ type Query { """ Select listings visible to the viewer even if they are not approved. If omitted or false, only approved listings will be returned. - + """ allStates: Boolean @@ -5955,9 +6213,6 @@ type ReferencedEvent implements Node { createdAt: DateTime! id: ID! - """Reference originated in a different repository.""" - isCrossReference: Boolean! @deprecated(reason: "`isCrossReference` will be renamed. Use `ReferencedEvent.isCrossRepository` instead. Removal on 2018-07-01 UTC.") - """Reference originated in a different repository.""" isCrossRepository: Boolean! @@ -5991,6 +6246,16 @@ enum RefOrderField { ALPHABETICAL } +"""Represents an owner of a registry package.""" +interface RegistryPackageOwner { + id: ID! +} + +"""Represents an interface to search packages on an object.""" +interface RegistryPackageSearch { + id: ID! +} + """A release contains the content for a release.""" type Release implements Node & UniformResourceLocatable { """The author of the release""" @@ -6184,7 +6449,15 @@ type RemoveOutsideCollaboratorPayload { """A unique identifier for the client performing the mutation.""" clientMutationId: String - """The user that was removed as an outside collaborator.""" + """ + The user that was removed as an outside collaborator. + + **Upcoming Change on 2019-01-01 UTC** + **Description:** Type for `removedUser` will change from `User!` to `User`. + **Reason:** In preparation for an upcoming change to the way we report + mutation errors, non-nullable payload fields are becoming nullable. + + """ removedUser: User! } @@ -6205,10 +6478,26 @@ type RemoveReactionPayload { """A unique identifier for the client performing the mutation.""" clientMutationId: String - """The reaction object.""" + """ + The reaction object. + + **Upcoming Change on 2019-01-01 UTC** + **Description:** Type for `reaction` will change from `Reaction!` to `Reaction`. + **Reason:** In preparation for an upcoming change to the way we report + mutation errors, non-nullable payload fields are becoming nullable. + + """ reaction: Reaction! - """The reactable subject.""" + """ + The reactable subject. + + **Upcoming Change on 2019-01-01 UTC** + **Description:** Type for `subject` will change from `Reactable!` to `Reactable`. + **Reason:** In preparation for an upcoming change to the way we report + mutation errors, non-nullable payload fields are becoming nullable. + + """ subject: Reactable! } @@ -6226,7 +6515,15 @@ type RemoveStarPayload { """A unique identifier for the client performing the mutation.""" clientMutationId: String - """The starrable.""" + """ + The starrable. + + **Upcoming Change on 2019-01-01 UTC** + **Description:** Type for `starrable` will change from `Starrable!` to `Starrable`. + **Reason:** In preparation for an upcoming change to the way we report + mutation errors, non-nullable payload fields are becoming nullable. + + """ starrable: Starrable! } @@ -6266,7 +6563,7 @@ type ReopenedEvent implements Node { } """A repository contains the content for a project.""" -type Repository implements Node & ProjectOwner & Subscribable & Starrable & UniformResourceLocatable & RepositoryInfo { +type Repository implements Node & ProjectOwner & RegistryPackageOwner & Subscribable & Starrable & UniformResourceLocatable & RepositoryInfo { """A list of users that can be assigned to issues in this repository.""" assignableUsers( """Returns the first _n_ elements from the list.""" @@ -6546,9 +6843,6 @@ type Repository implements Node & ProjectOwner & Subscribable & Starrable & Unif orderBy: LanguageOrder ): LanguageConnection - """The license associated with the repository""" - license: String @deprecated(reason: "Field `license` will be replaced by a more detailed license object. Use `Repository.licenseInfo` instead. Removal on 2018-07-01 UTC.") - """The license associated with the repository""" licenseInfo: License @@ -7058,9 +7352,6 @@ interface RepositoryInfo { """Identifies if the repository is private.""" isPrivate: Boolean! - """The license associated with the repository""" - license: String @deprecated(reason: "Field `license` will be replaced by a more detailed license object. Use `Repository.licenseInfo` instead. Removal on 2018-07-01 UTC.") - """The license associated with the repository""" licenseInfo: License @@ -7355,10 +7646,26 @@ type RequestReviewsPayload { """A unique identifier for the client performing the mutation.""" clientMutationId: String - """The pull request that is getting requests.""" + """ + The pull request that is getting requests. + + **Upcoming Change on 2019-01-01 UTC** + **Description:** Type for `pullRequest` will change from `PullRequest!` to `PullRequest`. + **Reason:** In preparation for an upcoming change to the way we report + mutation errors, non-nullable payload fields are becoming nullable. + + """ pullRequest: PullRequest! - """The edge from the pull request to the requested reviewers.""" + """ + The edge from the pull request to the requested reviewers. + + **Upcoming Change on 2019-01-01 UTC** + **Description:** Type for `requestedReviewersEdge` will change from `UserEdge!` to `UserEdge`. + **Reason:** In preparation for an upcoming change to the way we report + mutation errors, non-nullable payload fields are becoming nullable. + + """ requestedReviewersEdge: UserEdge! } @@ -7455,9 +7762,6 @@ type ReviewRequest implements Node { """The reviewer that is requested.""" requestedReviewer: RequestedReviewer - - """Identifies the author associated with this review request.""" - reviewer: User @deprecated(reason: "Field `reviewer` will be changed in favor of returning a union type. Use `ReviewRequest.requestedReviewer` instead. Removal on 2018-07-01 UTC.") } """The connection type for ReviewRequest.""" @@ -7489,9 +7793,6 @@ type ReviewRequestedEvent implements Node { """Identifies the reviewer whose review was requested.""" requestedReviewer: RequestedReviewer - - """Identifies the user whose review was requested.""" - subject: User @deprecated(reason: "`subject` will be renamed. Use `ReviewRequestedEvent.requestedReviewer` instead. Removal on 2018-07-01 UTC.") } """An edge in a connection.""" @@ -7517,9 +7818,6 @@ type ReviewRequestRemovedEvent implements Node { """Identifies the reviewer whose review request was removed.""" requestedReviewer: RequestedReviewer - - """Identifies the user whose review request was removed.""" - subject: User @deprecated(reason: "`subject` will be renamed. Use `ReviewRequestRemovedEvent.requestedReviewer` instead. Removal on 2018-07-01 UTC.") } """The results of a search.""" @@ -7784,7 +8082,15 @@ type SubmitPullRequestReviewPayload { """A unique identifier for the client performing the mutation.""" clientMutationId: String - """The submitted pull request review.""" + """ + The submitted pull request review. + + **Upcoming Change on 2019-01-01 UTC** + **Description:** Type for `pullRequestReview` will change from `PullRequestReview!` to `PullRequestReview`. + **Reason:** In preparation for an upcoming change to the way we report + mutation errors, non-nullable payload fields are becoming nullable. + + """ pullRequestReview: PullRequestReview! } @@ -8301,7 +8607,7 @@ type Topic implements Node { """ A list of related topics, including aliases of this topic, sorted with the most relevant first. - + """ relatedTopics: [Topic!]! } @@ -8533,7 +8839,15 @@ type UpdateProjectCardPayload { """A unique identifier for the client performing the mutation.""" clientMutationId: String - """The updated ProjectCard.""" + """ + The updated ProjectCard. + + **Upcoming Change on 2019-01-01 UTC** + **Description:** Type for `projectCard` will change from `ProjectCard!` to `ProjectCard`. + **Reason:** In preparation for an upcoming change to the way we report + mutation errors, non-nullable payload fields are becoming nullable. + + """ projectCard: ProjectCard! } @@ -8554,7 +8868,15 @@ type UpdateProjectColumnPayload { """A unique identifier for the client performing the mutation.""" clientMutationId: String - """The updated project column.""" + """ + The updated project column. + + **Upcoming Change on 2019-01-01 UTC** + **Description:** Type for `projectColumn` will change from `ProjectColumn!` to `ProjectColumn`. + **Reason:** In preparation for an upcoming change to the way we report + mutation errors, non-nullable payload fields are becoming nullable. + + """ projectColumn: ProjectColumn! } @@ -8584,7 +8906,15 @@ type UpdateProjectPayload { """A unique identifier for the client performing the mutation.""" clientMutationId: String - """The updated project.""" + """ + The updated project. + + **Upcoming Change on 2019-01-01 UTC** + **Description:** Type for `project` will change from `Project!` to `Project`. + **Reason:** In preparation for an upcoming change to the way we report + mutation errors, non-nullable payload fields are becoming nullable. + + """ project: Project! } @@ -8605,7 +8935,16 @@ type UpdatePullRequestReviewCommentPayload { """A unique identifier for the client performing the mutation.""" clientMutationId: String - """The updated comment.""" + """ + The updated comment. + + **Upcoming Change on 2019-01-01 UTC** + **Description:** Type for `pullRequestReviewComment` will change from + `PullRequestReviewComment!` to `PullRequestReviewComment`. + **Reason:** In preparation for an upcoming change to the way we report + mutation errors, non-nullable payload fields are becoming nullable. + + """ pullRequestReviewComment: PullRequestReviewComment! } @@ -8626,7 +8965,15 @@ type UpdatePullRequestReviewPayload { """A unique identifier for the client performing the mutation.""" clientMutationId: String - """The updated pull request review.""" + """ + The updated pull request review. + + **Upcoming Change on 2019-01-01 UTC** + **Description:** Type for `pullRequestReview` will change from `PullRequestReview!` to `PullRequestReview`. + **Reason:** In preparation for an upcoming change to the way we report + mutation errors, non-nullable payload fields are becoming nullable. + + """ pullRequestReview: PullRequestReview! } @@ -8647,7 +8994,15 @@ type UpdateSubscriptionPayload { """A unique identifier for the client performing the mutation.""" clientMutationId: String - """The input subscribable entity.""" + """ + The input subscribable entity. + + **Upcoming Change on 2019-01-01 UTC** + **Description:** Type for `subscribable` will change from `Subscribable!` to `Subscribable`. + **Reason:** In preparation for an upcoming change to the way we report + mutation errors, non-nullable payload fields are becoming nullable. + + """ subscribable: Subscribable! } @@ -8671,7 +9026,15 @@ type UpdateTopicsPayload { """Names of the provided topics that are not valid.""" invalidTopicNames: [String!] - """The updated repository.""" + """ + The updated repository. + + **Upcoming Change on 2019-01-01 UTC** + **Description:** Type for `repository` will change from `Repository!` to `Repository`. + **Reason:** In preparation for an upcoming change to the way we report + mutation errors, non-nullable payload fields are becoming nullable. + + """ repository: Repository! } @@ -8681,7 +9044,7 @@ scalar URI """ A user is an individual's account on GitHub that owns repositories and can make new content. """ -type User implements Node & Actor & RepositoryOwner & UniformResourceLocatable { +type User implements Node & Actor & RegistryPackageOwner & RegistryPackageSearch & RepositoryOwner & UniformResourceLocatable { """A URL pointing to the user's public avatar.""" avatarUrl( """The size of the resulting square image.""" @@ -8719,39 +9082,6 @@ type User implements Node & Actor & RepositoryOwner & UniformResourceLocatable { """The user's public profile company as HTML.""" companyHTML: HTML! - """A list of repositories that the user recently contributed to.""" - contributedRepositories( - """Returns the first _n_ elements from the list.""" - first: Int - - """ - Returns the elements in the list that come after the specified cursor. - """ - after: String - - """Returns the last _n_ elements from the list.""" - last: Int - - """ - Returns the elements in the list that come before the specified cursor. - """ - before: String - - """If non-null, filters repositories according to privacy""" - privacy: RepositoryPrivacy - - """Ordering options for repositories returned from the connection""" - orderBy: RepositoryOrder - - """Affiliation options for repositories returned from the connection""" - affiliations: [RepositoryAffiliation] - - """ - If non-null, filters repositories according to whether they have been locked - """ - isLocked: Boolean - ): RepositoryConnection! @deprecated(reason: "Arguments for connection `contributedRepositories` are getting redesigned. Use `User.repositoriesContributedTo` instead. Removal on 2018-07-01 UTC.") - """Identifies the date and time when the object was created.""" createdAt: DateTime! From 1a65226a2804a0a899224c012371b8c3be6b68c8 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 9 Aug 2018 08:19:41 -0400 Subject: [PATCH 0097/4252] Allow RefHolders to become empty again --- lib/models/ref-holder.js | 5 ++--- test/models/ref-holder.test.js | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/lib/models/ref-holder.js b/lib/models/ref-holder.js index 61e24956ae..9aebe0c769 100644 --- a/lib/models/ref-holder.js +++ b/lib/models/ref-holder.js @@ -48,7 +48,7 @@ export default class RefHolder { } isEmpty() { - return this.value === undefined; + return this.value === undefined || this.value === null; } get() { @@ -83,10 +83,9 @@ export default class RefHolder { } setter = value => { - if (value === null || value === undefined) { return; } const oldValue = this.value; this.value = value; - if (value !== oldValue) { + if (value !== oldValue && value !== null && value !== undefined) { this.emitter.emit('did-update', value); } } diff --git a/test/models/ref-holder.test.js b/test/models/ref-holder.test.js index 2ff29bb4e7..0b930e2329 100644 --- a/test/models/ref-holder.test.js +++ b/test/models/ref-holder.test.js @@ -97,6 +97,25 @@ describe('RefHolder', function() { assert.isTrue(callback.calledWith(12)); }); + it('does not notify subscribers when it becomes empty', function() { + const h = new RefHolder(); + h.setter(12); + assert.isFalse(h.isEmpty()); + + const callback = sinon.spy(); + sub = h.observe(callback); + + callback.resetHistory(); + h.setter(null); + assert.isTrue(h.isEmpty()); + assert.isFalse(callback.called); + + callback.resetHistory(); + h.setter(undefined); + assert.isTrue(h.isEmpty()); + assert.isFalse(callback.called); + }); + it('resolves a promise when it becomes available', async function() { const thing = Symbol('Thing'); const h = new RefHolder(); From fe5ca79c189d433ad1734b9bd66969137a8f46d5 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 9 Aug 2018 08:19:54 -0400 Subject: [PATCH 0098/4252] Round out RefHolder test coverage --- test/models/ref-holder.test.js | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/test/models/ref-holder.test.js b/test/models/ref-holder.test.js index 0b930e2329..3b535251d8 100644 --- a/test/models/ref-holder.test.js +++ b/test/models/ref-holder.test.js @@ -71,6 +71,21 @@ describe('RefHolder', function() { }); }); + describe('getOr', function() { + it("returns the RefHolder's value if it is non-empty", function() { + const h = new RefHolder(); + h.setter(1234); + + assert.strictEqual(h.getOr(5678), 1234); + }); + + it('returns its argument if the RefHolder is empty', function() { + const h = new RefHolder(); + + assert.strictEqual(h.getOr(5678), 5678); + }); + }); + it('notifies subscribers when it becomes available', function() { const h = new RefHolder(); const callback = sinon.spy(); @@ -97,6 +112,18 @@ describe('RefHolder', function() { assert.isTrue(callback.calledWith(12)); }); + it('does not notify subscribers when it is assigned the same value', function() { + const h = new RefHolder(); + h.setter(12); + + const callback = sinon.spy(); + sub = h.observe(callback); + + callback.resetHistory(); + h.setter(12); + assert.isFalse(callback.called); + }); + it('does not notify subscribers when it becomes empty', function() { const h = new RefHolder(); h.setter(12); From fb728a050ed1c8b8d8c171d477f25fa0ad7ae653 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 9 Aug 2018 08:47:20 -0400 Subject: [PATCH 0099/4252] RefHolders for focus management in CommitView --- lib/views/commit-view.js | 35 ++++++----- test/views/commit-view.test.js | 106 ++++++++++++++++++++++++++++++++- 2 files changed, 120 insertions(+), 21 deletions(-) diff --git a/lib/views/commit-view.js b/lib/views/commit-view.js index 590e38d9bd..59c012f793 100644 --- a/lib/views/commit-view.js +++ b/lib/views/commit-view.js @@ -133,6 +133,7 @@ export default class CommitView extends React.Component { const showAbortMergeButton = this.props.isMerging || null; + /* istanbul ignore next */ const modKey = process.platform === 'darwin' ? 'Cmd' : 'Ctrl'; return ( @@ -550,11 +551,11 @@ export default class CommitView extends React.Component { } hasFocusEditor() { - return this.refEditor.get().contains(document.activeElement); + return this.refEditor.map(editor => editor.contains(document.activeElement)).getOr(false); } rememberFocus(event) { - if (this.refEditor.get().contains(event.target)) { + if (this.refEditor.map(editor => editor.contains(event.target)).getOr(false)) { return CommitView.focus.EDITOR; } @@ -575,41 +576,39 @@ export default class CommitView extends React.Component { setFocus(focus) { let fallback = false; + const focusElement = element => { + element.focus(); + return true; + }; if (focus === CommitView.focus.EDITOR) { - this.refEditor.get().focus(); - return true; + if (this.refEditor.map(focusElement).getOr(false)) { + return true; + } } if (focus === CommitView.focus.ABORT_MERGE_BUTTON) { - if (!this.refAbortMergeButton.isEmpty()) { - this.refAbortMergeButton.get().focus(); + if (this.refAbortMergeButton.map(focusElement).getOr(false)) { return true; - } else { - fallback = true; } + fallback = true; } if (focus === CommitView.focus.COMMIT_BUTTON) { - if (!this.refCommitButton.isEmpty()) { - this.refCommitButton.get().focus(); + if (this.refCommitButton.map(focusElement).getOr(false)) { return true; - } else { - fallback = true; } + fallback = true; } if (focus === CommitView.focus.COAUTHOR_INPUT) { - if (!this.refCoAuthorSelect.isEmpty()) { - this.refCoAuthorSelect.get().focus(); + if (this.refCoAuthorSelect.map(focusElement).getOr(false)) { return true; - } else { - fallback = true; } + fallback = true; } - if (fallback) { - this.refEditor.get().focus(); + if (fallback && this.refEditor.map(focusElement).getOr(false)) { return true; } diff --git a/test/views/commit-view.test.js b/test/views/commit-view.test.js index 8411be4a3c..e9750ed893 100644 --- a/test/views/commit-view.test.js +++ b/test/views/commit-view.test.js @@ -321,13 +321,113 @@ describe('CommitView', function() { assert.isTrue(abortMerge.calledOnce); }); + it('detects when the editor has focus', function() { + const wrapper = mount(app); + + const editorNode = wrapper.find('atom-text-editor').getDOMNode(); + sinon.stub(editorNode, 'contains').returns(true); + assert.isTrue(wrapper.instance().hasFocusEditor()); + + editorNode.contains.returns(false); + assert.isFalse(wrapper.instance().hasFocusEditor()); + + editorNode.contains.returns(true); + wrapper.instance().refEditor.setter(null); + assert.isFalse(wrapper.instance().hasFocusEditor()); + }); + + it('remembers the current focus', function() { + const wrapper = mount(React.cloneElement(app, {isMerging: true})); + wrapper.instance().toggleCoAuthorInput(); + wrapper.update(); + + const foci = [ + ['atom-text-editor', CommitView.focus.EDITOR], + ['.github-CommitView-abortMerge', CommitView.focus.ABORT_MERGE_BUTTON], + ['.github-CommitView-commit', CommitView.focus.COMMIT_BUTTON], + ['.github-CommitView-coAuthorEditor input', CommitView.focus.COAUTHOR_INPUT], + ]; + for (const [selector, focus] of foci) { + const event = {target: wrapper.find(selector).getDOMNode()}; + assert.strictEqual(wrapper.instance().rememberFocus(event), focus); + } + + assert.isNull(wrapper.instance().rememberFocus({target: document.body})); + + const holders = [ + 'refEditor', 'refAbortMergeButton', 'refCommitButton', 'refCoAuthorSelect', + ].map(ivar => wrapper.instance()[ivar]); + for (const holder of holders) { + holder.setter(null); + } + assert.isNull(wrapper.instance().rememberFocus({target: document.body})); + }); + describe('restoring focus', function() { + it('to the editor', function() { + const wrapper = mount(app); + sinon.spy(wrapper.find('atom-text-editor').getDOMNode(), 'focus'); + + assert.isTrue(wrapper.instance().setFocus(CommitView.focus.EDITOR)); + assert.isTrue(wrapper.find('atom-text-editor').getDOMNode().focus.called); + }); + + it('to the abort merge button', function() { + const wrapper = mount(React.cloneElement(app, {isMerging: true})); + sinon.spy(wrapper.find('.github-CommitView-abortMerge').getDOMNode(), 'focus'); + + assert.isTrue(wrapper.instance().setFocus(CommitView.focus.ABORT_MERGE_BUTTON)); + assert.isTrue(wrapper.find('.github-CommitView-abortMerge').getDOMNode().focus.called); + }); + it('to the commit button', function() { const wrapper = mount(app); - sinon.spy(wrapper.instance().refCommitButton.get(), 'focus'); + sinon.spy(wrapper.find('.github-CommitView-commit').getDOMNode(), 'focus'); + + assert.isTrue(wrapper.instance().setFocus(CommitView.focus.COMMIT_BUTTON)); + assert.isTrue(wrapper.find('.github-CommitView-commit').getDOMNode().focus.called); + }); + + it('to the co-author input', function() { + const wrapper = mount(app); + wrapper.instance().toggleCoAuthorInput(); + + sinon.spy(wrapper.update().find('.github-CommitView-coAuthorEditor input').getDOMNode(), 'focus'); + + assert.isTrue(wrapper.instance().setFocus(CommitView.focus.COAUTHOR_INPUT)); + assert.isTrue(wrapper.find('.github-CommitView-coAuthorEditor input').getDOMNode().focus.called); + }); + + it('with an unrecognized symbol', function() { + const wrapper = mount(app); + assert.isFalse(wrapper.instance().setFocus(Symbol('lolno'))); + }); + + it('when the named element is no longer rendered', function() { + const wrapper = mount(app); + sinon.spy(wrapper.find('atom-text-editor').getDOMNode(), 'focus'); + + assert.isTrue(wrapper.instance().setFocus(CommitView.focus.ABORT_MERGE_BUTTON)); + assert.strictEqual(wrapper.find('atom-text-editor').getDOMNode().focus.callCount, 1); + + assert.isTrue(wrapper.instance().setFocus(CommitView.focus.COAUTHOR_INPUT)); + assert.strictEqual(wrapper.find('atom-text-editor').getDOMNode().focus.callCount, 2); + }); + + it('when refs have not been assigned yet', function() { + const wrapper = mount(app); - wrapper.instance().setFocus(CommitView.focus.COMMIT_BUTTON); - assert.isTrue(wrapper.instance().refCommitButton.get().focus.called); + // Simulate an unmounted component by clearing out RefHolders manually. + const holders = [ + 'refEditor', 'refAbortMergeButton', 'refCommitButton', 'refCoAuthorSelect', + ].map(ivar => wrapper.instance()[ivar]); + for (const holder of holders) { + holder.setter(null); + } + + for (const focusKey of Object.keys(CommitView.focus)) { + assert.isFalse(wrapper.instance().setFocus(CommitView.focus[focusKey])); + } }); }); }); From 948bfeca6f07ba88b6431d25f3f1aaf4580a1090 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 9 Aug 2018 08:57:24 -0400 Subject: [PATCH 0100/4252] Use .map().getOr() instead of .get() in AtomTextEditor --- lib/atom/atom-text-editor.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/atom/atom-text-editor.js b/lib/atom/atom-text-editor.js index 73139403e3..565aa599ec 100644 --- a/lib/atom/atom-text-editor.js +++ b/lib/atom/atom-text-editor.js @@ -129,14 +129,14 @@ export default class AtomTextEditor extends React.PureComponent { } contains(element) { - return this.refElement.get().contains(element); + return this.refElement.map(e => e.contains(element)).getOr(false); } focus() { - this.refElement.get().focus(); + this.refElement.map(e => e.focus()); } getModel() { - return this.refElement.get().getModel(); + return this.refElement.map(e => e.getModel()).getOr(null); } } From c0c30f632854a75111c15ca72da8b15745d2f6ed Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 9 Aug 2018 09:19:10 -0400 Subject: [PATCH 0101/4252] Move hasFocus to CommitView --- lib/views/commit-view.js | 14 ++++++++++++-- test/views/commit-view.test.js | 15 +++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/lib/views/commit-view.js b/lib/views/commit-view.js index 59c012f793..e3c5da6a2e 100644 --- a/lib/views/commit-view.js +++ b/lib/views/commit-view.js @@ -12,7 +12,7 @@ import RefHolder from '../models/ref-holder'; import Author from '../models/author'; import ObserveModel from './observe-model'; import {LINE_ENDING_REGEX, autobind} from '../helpers'; -import {AuthorPropType, UserStorePropType} from '../prop-types'; +import {AuthorPropType, UserStorePropType, RefHolderPropType} from '../prop-types'; import {incrementCounter} from '../reporter-proxy'; const TOOLTIP_DELAY = 200; @@ -30,6 +30,8 @@ export default class CommitView extends React.Component { }; static propTypes = { + refRoot: RefHolderPropType, + config: PropTypes.object.isRequired, tooltips: PropTypes.object.isRequired, commandRegistry: PropTypes.object.isRequired, @@ -53,6 +55,10 @@ export default class CommitView extends React.Component { toggleExpandedCommitMessageEditor: PropTypes.func.isRequired, }; + static defaultProps = { + refRoot: new RefHolder(), + } + constructor(props, context) { super(props, context); autobind( @@ -137,7 +143,7 @@ export default class CommitView extends React.Component { const modKey = process.platform === 'darwin' ? 'Cmd' : 'Ctrl'; return ( -
+
@@ -550,6 +556,10 @@ export default class CommitView extends React.Component { } } + hasFocus() { + return this.props.refRoot.map(element => element.contains(document.activeElement)).getOr(false); + } + hasFocusEditor() { return this.refEditor.map(editor => editor.contains(document.activeElement)).getOr(false); } diff --git a/test/views/commit-view.test.js b/test/views/commit-view.test.js index e9750ed893..aa698d7d36 100644 --- a/test/views/commit-view.test.js +++ b/test/views/commit-view.test.js @@ -321,6 +321,21 @@ describe('CommitView', function() { assert.isTrue(abortMerge.calledOnce); }); + it('detects when the component has focus', function() { + const wrapper = mount(app); + const rootElement = wrapper.find('.github-CommitView').getDOMNode(); + + sinon.stub(rootElement, 'contains').returns(true); + assert.isTrue(wrapper.instance().hasFocus()); + + rootElement.contains.returns(false); + assert.isFalse(wrapper.instance().hasFocus()); + + rootElement.contains.returns(true); + wrapper.prop('refRoot').setter(null); + assert.isFalse(wrapper.instance().hasFocus()); + }); + it('detects when the editor has focus', function() { const wrapper = mount(app); From 41023ac3078db321ec8feb6a4b54f9f3a3b054a7 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 9 Aug 2018 09:25:06 -0400 Subject: [PATCH 0102/4252] RefHolders in CommitController --- lib/controllers/commit-controller.js | 18 +++++------ test/controllers/commit-controller.test.js | 36 +++++++++++++++++++++- 2 files changed, 42 insertions(+), 12 deletions(-) diff --git a/lib/controllers/commit-controller.js b/lib/controllers/commit-controller.js index 87f89d7471..cda915f2fe 100644 --- a/lib/controllers/commit-controller.js +++ b/lib/controllers/commit-controller.js @@ -1,12 +1,12 @@ import path from 'path'; import React from 'react'; -import ReactDom from 'react-dom'; import PropTypes from 'prop-types'; import {CompositeDisposable} from 'event-kit'; import fs from 'fs-extra'; import CommitView from '../views/commit-view'; +import RefHolder from '../models/ref-holder'; import {AuthorPropType, UserStorePropType} from '../prop-types'; import {autobind} from '../helpers'; @@ -44,7 +44,7 @@ export default class CommitController extends React.Component { autobind(this, 'commit', 'handleMessageChange', 'toggleExpandedCommitMessageEditor', 'grammarAdded'); this.subscriptions = new CompositeDisposable(); - this.refCommitView = null; + this.refCommitView = new RefHolder(); } // eslint-disable-next-line camelcase @@ -78,17 +78,13 @@ export default class CommitController extends React.Component { } } - componentDidMount() { - this.domNode = ReactDom.findDOMNode(this); - } - render() { const message = this.getCommitMessage(); const operationStates = this.props.repository.getOperationStates(); return ( { this.refCommitView = c; }} + ref={this.refCommitView.setter} tooltips={this.props.tooltips} config={this.props.config} stagedChangesExist={this.props.stagedChangesExist} @@ -241,19 +237,19 @@ export default class CommitController extends React.Component { } rememberFocus(event) { - return this.refCommitView.rememberFocus(event); + return this.refCommitView.map(view => view.rememberFocus(event)).getOr(null); } setFocus(focus) { - return this.refCommitView.setFocus(focus); + return this.refCommitView.map(view => view.setFocus(focus)).getOr(false); } hasFocus() { - return this.domNode && this.domNode.contains(document.activeElement); + return this.refCommitView.map(view => view.hasFocus()).getOr(false); } hasFocusEditor() { - return this.refCommitView.hasFocusEditor(); + return this.refCommitView.map(view => view.hasFocusEditor()).getOr(false); } } diff --git a/test/controllers/commit-controller.test.js b/test/controllers/commit-controller.test.js index 858a8fe0fd..e2e97cf57c 100644 --- a/test/controllers/commit-controller.test.js +++ b/test/controllers/commit-controller.test.js @@ -1,7 +1,7 @@ import path from 'path'; import fs from 'fs-extra'; import React from 'react'; -import {shallow} from 'enzyme'; +import {shallow, mount} from 'enzyme'; import Commit from '../../lib/models/commit'; import {nullBranch} from '../../lib/models/branch'; @@ -354,5 +354,39 @@ describe('CommitController', function() { await assert.async.lengthOf(workspace.getTextEditors(), 0); }); }); + + it('delegates focus management to its view', function() { + const wrapper = mount(app); + const viewHolder = wrapper.instance().refCommitView; + assert.isFalse(viewHolder.isEmpty()); + const view = viewHolder.get(); + + sinon.spy(view, 'rememberFocus'); + sinon.spy(view, 'setFocus'); + sinon.spy(view, 'hasFocus'); + sinon.spy(view, 'hasFocusEditor'); + + wrapper.instance().rememberFocus({target: wrapper.find('atom-text-editor').getDOMNode()}); + assert.isTrue(view.rememberFocus.called); + + wrapper.instance().setFocus(CommitController.focus.EDITOR); + assert.isTrue(view.setFocus.called); + + wrapper.instance().hasFocus(); + assert.isTrue(view.hasFocus.called); + + wrapper.instance().hasFocusEditor(); + assert.isTrue(view.hasFocusEditor.called); + }); + + it('no-ops focus management methods when the view ref is unassigned', function() { + const wrapper = shallow(app); + assert.isTrue(wrapper.instance().refCommitView.isEmpty()); + + assert.isNull(wrapper.instance().rememberFocus({})); + assert.isFalse(wrapper.instance().setFocus(CommitController.focus.EDITOR)); + assert.isFalse(wrapper.instance().hasFocus()); + assert.isFalse(wrapper.instance().hasFocusEditor()); + }); }); }); From 7269cde973233baa30d85cff7037830bd65a9162 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 9 Aug 2018 09:25:49 -0400 Subject: [PATCH 0103/4252] CommitView should own refRoot --- lib/views/commit-view.js | 13 ++++--------- test/views/commit-view.test.js | 2 +- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/lib/views/commit-view.js b/lib/views/commit-view.js index e3c5da6a2e..d6e9e3c28b 100644 --- a/lib/views/commit-view.js +++ b/lib/views/commit-view.js @@ -12,7 +12,7 @@ import RefHolder from '../models/ref-holder'; import Author from '../models/author'; import ObserveModel from './observe-model'; import {LINE_ENDING_REGEX, autobind} from '../helpers'; -import {AuthorPropType, UserStorePropType, RefHolderPropType} from '../prop-types'; +import {AuthorPropType, UserStorePropType} from '../prop-types'; import {incrementCounter} from '../reporter-proxy'; const TOOLTIP_DELAY = 200; @@ -30,8 +30,6 @@ export default class CommitView extends React.Component { }; static propTypes = { - refRoot: RefHolderPropType, - config: PropTypes.object.isRequired, tooltips: PropTypes.object.isRequired, commandRegistry: PropTypes.object.isRequired, @@ -55,10 +53,6 @@ export default class CommitView extends React.Component { toggleExpandedCommitMessageEditor: PropTypes.func.isRequired, }; - static defaultProps = { - refRoot: new RefHolder(), - } - constructor(props, context) { super(props, context); autobind( @@ -78,6 +72,7 @@ export default class CommitView extends React.Component { this.timeoutHandle = null; this.subscriptions = new CompositeDisposable(); + this.refRoot = new RefHolder(); this.refExpandButton = new RefHolder(); this.refCommitButton = new RefHolder(); this.refHardWrapButton = new RefHolder(); @@ -143,7 +138,7 @@ export default class CommitView extends React.Component { const modKey = process.platform === 'darwin' ? 'Cmd' : 'Ctrl'; return ( -
+
@@ -557,7 +552,7 @@ export default class CommitView extends React.Component { } hasFocus() { - return this.props.refRoot.map(element => element.contains(document.activeElement)).getOr(false); + return this.refRoot.map(element => element.contains(document.activeElement)).getOr(false); } hasFocusEditor() { diff --git a/test/views/commit-view.test.js b/test/views/commit-view.test.js index aa698d7d36..fbac9e15ff 100644 --- a/test/views/commit-view.test.js +++ b/test/views/commit-view.test.js @@ -332,7 +332,7 @@ describe('CommitView', function() { assert.isFalse(wrapper.instance().hasFocus()); rootElement.contains.returns(true); - wrapper.prop('refRoot').setter(null); + wrapper.instance().refRoot.setter(null); assert.isFalse(wrapper.instance().hasFocus()); }); From 056b1f33f2e9cf2b2714ee66a57dababb1675215 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 9 Aug 2018 09:39:20 -0400 Subject: [PATCH 0104/4252] RefHolders in StagingView --- lib/views/staging-view.js | 11 ++++---- test/views/staging-view.test.js | 45 +++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/lib/views/staging-view.js b/lib/views/staging-view.js index 0409f73dad..7f92cfb9ac 100644 --- a/lib/views/staging-view.js +++ b/lib/views/staging-view.js @@ -12,6 +12,7 @@ import ObserveModel from './observe-model'; import MergeConflictListItemView from './merge-conflict-list-item-view'; import CompositeListSelection from '../models/composite-list-selection'; import ResolutionProgress from '../models/conflicts/resolution-progress'; +import RefHolder from '../models/ref-holder'; import FilePatchController from '../controllers/file-patch-controller'; import Commands, {Command} from '../atom/commands'; import {autobind} from '../helpers'; @@ -114,7 +115,7 @@ export default class StagingView extends React.Component { this.mouseSelectionInProgress = false; this.listElementsByItem = new WeakMap(); - this.refRoot = null; + this.refRoot = new RefHolder(); } static getDerivedStateFromProps(nextProps, prevState) { @@ -185,7 +186,7 @@ export default class StagingView extends React.Component { return (
{ this.refRoot = c; }} + ref={this.refRoot.setter} className={`github-StagingView ${this.state.selection.getActiveListKey()}-changes-focused`} tabIndex="-1"> {this.renderCommands()} @@ -805,12 +806,12 @@ export default class StagingView extends React.Component { } rememberFocus(event) { - return this.refRoot.contains(event.target) ? StagingView.focus.STAGING : null; + return this.refRoot.map(root => root.contains(event.target)).getOr(false) ? StagingView.focus.STAGING : null; } setFocus(focus) { if (focus === StagingView.focus.STAGING) { - this.refRoot.focus(); + this.refRoot.map(root => root.focus()); return true; } @@ -818,6 +819,6 @@ export default class StagingView extends React.Component { } hasFocus() { - return this.refRoot.contains(document.activeElement); + return this.refRoot.map(root => root.contains(document.activeElement)).getOr(false); } } diff --git a/test/views/staging-view.test.js b/test/views/staging-view.test.js index b266b7e785..b268a44093 100644 --- a/test/views/staging-view.test.js +++ b/test/views/staging-view.test.js @@ -740,4 +740,49 @@ describe('StagingView', function() { }); }); } + + it('remembers the current focus', function() { + const wrapper = mount(app); + const rootElement = wrapper.find('.github-StagingView').getDOMNode(); + + assert.strictEqual(wrapper.instance().rememberFocus({target: rootElement}), StagingView.focus.STAGING); + assert.isNull(wrapper.instance().rememberFocus({target: document.body})); + + wrapper.instance().refRoot.setter(null); + assert.isNull(wrapper.instance().rememberFocus({target: rootElement})); + }); + + it('sets a new focus', function() { + const wrapper = mount(app); + const rootElement = wrapper.find('.github-StagingView').getDOMNode(); + + sinon.stub(rootElement, 'focus'); + + assert.isFalse(wrapper.instance().setFocus(Symbol('nope'))); + assert.isFalse(rootElement.focus.called); + + assert.isTrue(wrapper.instance().setFocus(StagingView.focus.STAGING)); + assert.isTrue(rootElement.focus.called); + + wrapper.instance().refRoot.setter(null); + rootElement.focus.resetHistory(); + assert.isTrue(wrapper.instance().setFocus(StagingView.focus.STAGING)); + assert.isFalse(rootElement.focus.called); + }); + + it('detects when the component does have focus', function() { + const wrapper = mount(app); + const rootElement = wrapper.find('.github-StagingView').getDOMNode(); + sinon.stub(rootElement, 'contains'); + + rootElement.contains.returns(true); + assert.isTrue(wrapper.instance().hasFocus()); + + rootElement.contains.returns(false); + assert.isFalse(wrapper.instance().hasFocus()); + + rootElement.contains.returns(true); + wrapper.instance().refRoot.setter(null); + assert.isFalse(wrapper.instance().hasFocus()); + }); }); From ec3675e0ba2f9cdaeb80f04201f679f1cd41d0b2 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 9 Aug 2018 11:03:39 -0400 Subject: [PATCH 0105/4252] RefHolders in GitTabView --- lib/views/git-tab-view.js | 92 ++++++----- test/fixtures/props/git-tab-props.js | 59 +++++++ test/views/git-tab-view.test.js | 226 +++++++++++++++++++++++++++ 3 files changed, 330 insertions(+), 47 deletions(-) create mode 100644 test/views/git-tab-view.test.js diff --git a/lib/views/git-tab-view.js b/lib/views/git-tab-view.js index bfa35ec1f1..d748e3bf71 100644 --- a/lib/views/git-tab-view.js +++ b/lib/views/git-tab-view.js @@ -7,8 +7,9 @@ import StagingView from './staging-view'; import GitLogo from './git-logo'; import CommitController from '../controllers/commit-controller'; import RecentCommitsController from '../controllers/recent-commits-controller'; +import RefHolder from '../models/ref-holder'; import {isValidWorkdir, autobind} from '../helpers'; -import {AuthorPropType, UserStorePropType} from '../prop-types'; +import {AuthorPropType, UserStorePropType, RefHolderPropType} from '../prop-types'; export default class GitTabView extends React.Component { static focus = { @@ -18,6 +19,9 @@ export default class GitTabView extends React.Component { }; static propTypes = { + refRoot: RefHolderPropType, + refStagingView: RefHolderPropType, + repository: PropTypes.object.isRequired, isLoading: PropTypes.bool.isRequired, @@ -65,25 +69,25 @@ export default class GitTabView extends React.Component { this.subscriptions = new CompositeDisposable(); - this.refRoot = null; - this.refStagingView = null; - this.refCommitViewComponent = null; + this.refCommitController = new RefHolder(); } componentDidMount() { - this.subscriptions.add( - this.props.commandRegistry.add(this.refRoot, { - 'tool-panel:unfocus': this.blur, - 'core:focus-next': this.advanceFocus, - 'core:focus-previous': this.retreatFocus, - }), - ); + this.props.refRoot.map(root => { + return this.subscriptions.add( + this.props.commandRegistry.add(root, { + 'tool-panel:unfocus': this.blur, + 'core:focus-next': this.advanceFocus, + 'core:focus-previous': this.retreatFocus, + }), + ); + }); } render() { if (this.props.repository.isTooLarge()) { return ( -
{ this.refRoot = c; }}> +
@@ -99,7 +103,7 @@ export default class GitTabView extends React.Component { } else if (this.props.repository.hasDirectory() && !isValidWorkdir(this.props.repository.getWorkingDirectoryPath())) { return ( -
{ this.refRoot = c; }}> +
@@ -122,7 +126,7 @@ export default class GitTabView extends React.Component { : Initialize a new project directory with a Git repository; return ( -
{ this.refRoot = c; }}> +
@@ -141,9 +145,9 @@ export default class GitTabView extends React.Component {
{ this.refRoot = c; }}> + ref={this.props.refRoot.setter}> { this.refStagingView = c; }} + ref={this.props.refStagingView.setter} commandRegistry={this.props.commandRegistry} notificationManager={this.props.notificationManager} workspace={this.props.workspace} @@ -166,7 +170,7 @@ export default class GitTabView extends React.Component { isMerging={this.props.isMerging} /> { this.refCommitController = c; }} + ref={this.refCommitController.setter} tooltips={this.props.tooltips} config={this.props.config} stagedChangesExist={this.props.stagedChanges.length > 0} @@ -189,7 +193,6 @@ export default class GitTabView extends React.Component { updateSelectedCoAuthors={this.props.updateSelectedCoAuthors} /> { this.refRecentCommitController = c; }} commits={this.props.recentCommits} isLoading={this.props.isLoading} undoLastCommit={this.props.undoLastCommit} @@ -219,75 +222,70 @@ export default class GitTabView extends React.Component { rememberFocus(event) { let currentFocus = null; - if (this.refStagingView) { - currentFocus = this.refStagingView.rememberFocus(event); - } + currentFocus = this.props.refStagingView.map(view => view.rememberFocus(event)).getOr(null); - if (!currentFocus && this.refCommitController) { - currentFocus = this.refCommitController.rememberFocus(event); + if (!currentFocus) { + currentFocus = this.refCommitController.map(controller => controller.rememberFocus(event)).getOr(null); } return currentFocus; } setFocus(focus) { - if (this.refStagingView) { - if (this.refStagingView.setFocus(focus)) { - return true; - } + if (this.props.refStagingView.map(view => view.setFocus(focus)).getOr(false)) { + return true; } - if (this.refCommitController) { - if (this.refCommitController.setFocus(focus)) { - return true; - } + if (this.refCommitController.map(controller => controller.setFocus(focus)).getOr(false)) { + return true; } return false; } blur() { - this.props.workspace.getActivePane().activate(); + this.props.workspace.getCenter().activate(); } async advanceFocus(evt) { // The commit controller manages its own focus - if (this.refCommitController.hasFocus()) { return; } - if (await this.refStagingView.activateNextList()) { + if (this.refCommitController.map(c => c.hasFocus()).getOr(false)) { + return; + } + + if (await this.props.refStagingView.map(view => view.activateNextList()).getOr(false)) { evt.stopPropagation(); } else { - if (this.refCommitController.setFocus(GitTabView.focus.EDITOR)) { + if (this.refCommitController.map(c => c.setFocus(GitTabView.focus.EDITOR)).getOr(false)) { evt.stopPropagation(); } } } async retreatFocus(evt) { - const stagingView = this.refStagingView; - const commitController = this.refCommitController; - - if (commitController.hasFocus()) { + if (this.refCommitController.map(c => c.hasFocus()).getOr(false)) { // if the commit editor is focused, focus the last staging view list - if (commitController.hasFocusEditor() && await stagingView.activateLastList()) { + if (this.refCommitController.map(c => c.hasFocusEditor()).getOr(false) && + await this.props.refStagingView.map(view => view.activateLastList()).getOr(null) + ) { this.setFocus(GitTabView.focus.STAGING); evt.stopPropagation(); } - } else { - await stagingView.activatePreviousList(); + } else if (await this.props.refStagingView.map(c => c.activatePreviousList()).getOr(null)) { evt.stopPropagation(); } } async focusAndSelectStagingItem(filePath, stagingStatus) { - await this.refStagingView.quietlySelectItem(filePath, stagingStatus); + await this.quietlySelectItem(filePath, stagingStatus); this.setFocus(GitTabView.focus.STAGING); } - hasFocus() { - return this.refRoot.contains(document.activeElement); + quietlySelectItem(filePath, stagingStatus) { + return this.props.refStagingView.map(view => view.quietlySelectItem(filePath, stagingStatus)).getOr(false); } - quietlySelectItem(filePath, stagingStatus) { - return this.refStagingView.quietlySelectItem(filePath, stagingStatus); + hasFocus() { + return this.props.refRoot.map(root => root.contains(document.activeElement)).getOr(false); } } diff --git a/test/fixtures/props/git-tab-props.js b/test/fixtures/props/git-tab-props.js index daa570bcd5..84cd537a2c 100644 --- a/test/fixtures/props/git-tab-props.js +++ b/test/fixtures/props/git-tab-props.js @@ -1,6 +1,8 @@ import ResolutionProgress from '../../../lib/models/conflicts/resolution-progress'; import {InMemoryStrategy} from '../../../lib/shared/keytar-strategy'; import GithubLoginModel from '../../../lib/models/github-login-model'; +import RefHolder from '../../../lib/models/ref-holder'; +import UserStore from '../../../lib/models/user-store'; function noop() {} @@ -51,3 +53,60 @@ export async function gitTabControllerProps(atomEnv, repository, overrides = {}) return gitTabContainerProps(atomEnv, repository, repoProps); } + +export async function gitTabViewProps(atomEnv, repository, overrides = {}) { + const props = { + refRoot: new RefHolder(), + refStagingView: new RefHolder(), + + repository, + isLoading: false, + + lastCommit: await repository.getLastCommit(), + currentBranch: await repository.getCurrentBranch(), + recentCommits: await repository.getRecentCommits({max: 10}), + isMerging: await repository.isMerging(), + isRebasing: await repository.isRebasing(), + hasUndoHistory: await repository.hasDiscardHistory(), + unstagedChanges: await repository.getUnstagedChanges(), + stagedChanges: await repository.getStagedChanges(), + mergeConflicts: await repository.getMergeConflicts(), + workingDirectoryPath: repository.getWorkingDirectoryPath(), + + selectedCoAuthors: [], + updateSelectedCoAuthors: () => {}, + resolutionProgress: new ResolutionProgress(), + + workspace: atomEnv.workspace, + commandRegistry: atomEnv.commands, + grammars: atomEnv.grammars, + notificationManager: atomEnv.notifications, + config: atomEnv.config, + project: atomEnv.project, + tooltips: atomEnv.tooltips, + + initializeRepo: () => {}, + abortMerge: () => {}, + commit: () => {}, + undoLastCommit: () => {}, + prepareToCommit: () => {}, + resolveAsOurs: () => {}, + resolveAsTheirs: () => {}, + undoLastDiscard: () => {}, + attemptStageAllOperation: () => {}, + attemptFileStageOperation: () => {}, + discardWorkDirChangesForPaths: () => {}, + openFiles: () => {}, + + ...overrides, + }; + + props.mergeMessage = props.isMerging ? await repository.getMergeMessage() : null; + props.userStore = new UserStore({ + repository: props.repository, + login: new GithubLoginModel(InMemoryStrategy), + config: props.config, + }); + + return props; +} diff --git a/test/views/git-tab-view.test.js b/test/views/git-tab-view.test.js new file mode 100644 index 0000000000..ea16ebd5a9 --- /dev/null +++ b/test/views/git-tab-view.test.js @@ -0,0 +1,226 @@ +import React from 'react'; +import {shallow, mount} from 'enzyme'; + +import {cloneRepository, buildRepository} from '../helpers'; +import GitTabView from '../../lib/views/git-tab-view'; +import {gitTabViewProps} from '../fixtures/props/git-tab-props'; + +describe('GitTabView', function() { + let atomEnv, repository; + + beforeEach(async function() { + atomEnv = global.buildAtomEnvironment(); + repository = await buildRepository(await cloneRepository()); + }); + + afterEach(function() { + atomEnv.destroy(); + }); + + async function buildApp(overrides = {}) { + return ; + } + + it('remembers the current focus', async function() { + const wrapper = mount(await buildApp()); + + assert.strictEqual( + wrapper.instance().rememberFocus({target: wrapper.find('div.github-StagingView').getDOMNode()}), + GitTabView.focus.STAGING, + ); + + assert.strictEqual( + wrapper.instance().rememberFocus({target: wrapper.find('atom-text-editor').getDOMNode()}), + GitTabView.focus.EDITOR, + ); + + assert.isNull(wrapper.instance().rememberFocus({target: document.body})); + }); + + it('sets a new focus', async function() { + const wrapper = mount(await buildApp()); + const stagingElement = wrapper.find('div.github-StagingView').getDOMNode(); + const editorElement = wrapper.find('atom-text-editor').getDOMNode(); + + sinon.spy(stagingElement, 'focus'); + assert.isTrue(wrapper.instance().setFocus(GitTabView.focus.STAGING)); + assert.isTrue(stagingElement.focus.called); + + sinon.spy(editorElement, 'focus'); + assert.isTrue(wrapper.instance().setFocus(GitTabView.focus.EDITOR)); + assert.isTrue(editorElement.focus.called); + + assert.isFalse(wrapper.instance().setFocus(Symbol('nah'))); + }); + + it('blurs by focusing the workspace center', async function() { + const editor = await atomEnv.workspace.open(__filename); + atomEnv.workspace.getLeftDock().activate(); + assert.notStrictEqual(atomEnv.workspace.getActivePaneItem(), editor); + + const wrapper = shallow(await buildApp()); + wrapper.instance().blur(); + + assert.strictEqual(atomEnv.workspace.getActivePaneItem(), editor); + }); + + it('no-ops focus management methods when refs are unavailable', async function() { + const wrapper = shallow(await buildApp()); + assert.isNull(wrapper.instance().rememberFocus({})); + assert.isFalse(wrapper.instance().setFocus(GitTabView.focus.EDITOR)); + }); + + describe('advanceFocus', function() { + let wrapper, event, commitController, stagingView; + + beforeEach(async function() { + wrapper = mount(await buildApp()); + + commitController = wrapper.instance().refCommitController.get(); + stagingView = wrapper.prop('refStagingView').get(); + + event = {stopPropagation: sinon.spy()}; + }); + + it('does nothing if the commit controller has focus', async function() { + sinon.stub(commitController, 'hasFocus').returns(true); + sinon.spy(stagingView, 'activateNextList'); + + await wrapper.instance().advanceFocus(event); + + assert.isFalse(event.stopPropagation.called); + assert.isFalse(stagingView.activateNextList.called); + }); + + it('activates the next staging view list and stops', async function() { + sinon.stub(stagingView, 'activateNextList').resolves(true); + sinon.spy(commitController, 'setFocus'); + + await wrapper.instance().advanceFocus(event); + + assert.isTrue(stagingView.activateNextList.called); + assert.isTrue(event.stopPropagation.called); + assert.isFalse(commitController.setFocus.called); + }); + + it('moves focus to the commit message editor from the end of the staging view', async function() { + sinon.stub(stagingView, 'activateNextList').resolves(false); + sinon.stub(commitController, 'setFocus').returns(true); + + await wrapper.instance().advanceFocus(event); + + assert.isTrue(commitController.setFocus.calledWith(GitTabView.focus.EDITOR)); + assert.isTrue(event.stopPropagation.called); + }); + + it('does nothing if refs are unavailable', async function() { + wrapper.instance().refCommitController.setter(null); + + await wrapper.instance().advanceFocus(event); + + assert.isFalse(event.stopPropagation.called); + }); + }); + + describe('retreatFocus', function() { + let wrapper, event, commitController, stagingView; + + beforeEach(async function() { + wrapper = mount(await buildApp()); + + commitController = wrapper.instance().refCommitController.get(); + stagingView = wrapper.prop('refStagingView').get(); + + event = {stopPropagation: sinon.spy()}; + }); + + it('focuses the last staging list if the commit editor has focus', async function() { + sinon.stub(commitController, 'hasFocus').returns(true); + sinon.stub(commitController, 'hasFocusEditor').returns(true); + sinon.stub(stagingView, 'activateLastList').resolves(true); + + await wrapper.instance().retreatFocus(event); + + assert.isTrue(stagingView.activateLastList.called); + assert.isTrue(event.stopPropagation.called); + }); + + it('does nothing if the commit controller has focus but not in its editor', async function() { + sinon.stub(commitController, 'hasFocus').returns(true); + sinon.stub(commitController, 'hasFocusEditor').returns(false); + sinon.spy(stagingView, 'activateLastList'); + sinon.spy(stagingView, 'activatePreviousList'); + + await wrapper.instance().retreatFocus(event); + + assert.isFalse(stagingView.activateLastList.called); + assert.isFalse(stagingView.activatePreviousList.called); + assert.isFalse(event.stopPropagation.called); + }); + + it('activates the previous staging list and stops', async function() { + sinon.stub(commitController, 'hasFocus').returns(false); + sinon.stub(stagingView, 'activatePreviousList').resolves(true); + + await wrapper.instance().retreatFocus(event); + + assert.isTrue(stagingView.activatePreviousList.called); + assert.isTrue(event.stopPropagation.called); + }); + + it('does nothing if refs are unavailable', async function() { + wrapper.instance().refCommitController.setter(null); + wrapper.prop('refStagingView').setter(null); + + await wrapper.instance().retreatFocus(event); + + assert.isFalse(event.stopPropagation.called); + }); + }); + + it('selects a staging item', async function() { + const wrapper = mount(await buildApp({ + unstagedChanges: [{filePath: 'aaa.txt', status: 'modified'}], + })); + + const stagingView = wrapper.prop('refStagingView').get(); + sinon.spy(stagingView, 'quietlySelectItem'); + sinon.spy(stagingView, 'setFocus'); + + await wrapper.instance().quietlySelectItem('aaa.txt', 'unstaged'); + + assert.isTrue(stagingView.quietlySelectItem.calledWith('aaa.txt', 'unstaged')); + assert.isFalse(stagingView.setFocus.calledWith(GitTabView.focus.STAGING)); + }); + + it('selects a staging item and focuses itself', async function() { + const wrapper = mount(await buildApp({ + unstagedChanges: [{filePath: 'aaa.txt', status: 'modified'}], + })); + + const stagingView = wrapper.prop('refStagingView').get(); + sinon.spy(stagingView, 'quietlySelectItem'); + sinon.spy(stagingView, 'setFocus'); + + await wrapper.instance().focusAndSelectStagingItem('aaa.txt', 'unstaged'); + + assert.isTrue(stagingView.quietlySelectItem.calledWith('aaa.txt', 'unstaged')); + assert.isTrue(stagingView.setFocus.calledWith(GitTabView.focus.STAGING)); + }); + + it('detects when it has focus', async function() { + const wrapper = mount(await buildApp()); + const rootElement = wrapper.prop('refRoot').get(); + sinon.stub(rootElement, 'contains'); + + rootElement.contains.returns(true); + assert.isTrue(wrapper.instance().hasFocus()); + + rootElement.contains.returns(false); + assert.isFalse(wrapper.instance().hasFocus()); + + rootElement.contains.returns(true); + wrapper.prop('refRoot').setter(null); + assert.isFalse(wrapper.instance().hasFocus()); + }); +}); From 5db897f2ae43ec5a49b633857993663d51201267 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 9 Aug 2018 11:31:56 -0400 Subject: [PATCH 0106/4252] RefHolders in GitTabController --- lib/controllers/git-tab-controller.js | 31 ++++++++------- test/controllers/git-tab-controller.test.js | 43 ++++++++++----------- 2 files changed, 38 insertions(+), 36 deletions(-) diff --git a/lib/controllers/git-tab-controller.js b/lib/controllers/git-tab-controller.js index 96bb9f708a..d226aff6cd 100644 --- a/lib/controllers/git-tab-controller.js +++ b/lib/controllers/git-tab-controller.js @@ -6,6 +6,7 @@ import PropTypes from 'prop-types'; import Author from '../models/author'; import GitTabView from '../views/git-tab-view'; import UserStore from '../models/user-store'; +import RefHolder from '../models/ref-holder'; import { CommitPropType, BranchPropType, FilePatchItemPropType, MergeConflictItemPropType, RefHolderPropType, } from '../prop-types'; @@ -64,7 +65,9 @@ export default class GitTabController extends React.Component { this.stagingOperationInProgress = false; this.lastFocus = GitTabView.focus.STAGING; - this.refView = null; + this.refView = new RefHolder(); + this.refRoot = new RefHolder(); + this.refStagingView = new RefHolder(); this.state = { selectedCoAuthors: [], @@ -80,7 +83,9 @@ export default class GitTabController extends React.Component { render() { return ( { this.refView = c; }} + ref={this.refView.setter} + refRoot={this.refRoot} + refStagingView={this.refStagingView} isLoading={this.props.fetchInProgress} repository={this.props.repository} @@ -135,7 +140,7 @@ export default class GitTabController extends React.Component { componentDidMount() { this.refreshResolutionProgress(false, false); - this.refView.refRoot.addEventListener('focusin', this.rememberLastFocus); + this.refRoot.map(root => root.addEventListener('focusin', this.rememberLastFocus)); if (this.props.controllerRef) { this.props.controllerRef.setter(this); @@ -149,7 +154,7 @@ export default class GitTabController extends React.Component { } componentWillUnmount() { - this.refView.refRoot.removeEventListener('focusin', this.rememberLastFocus); + this.refRoot.map(root => root.removeEventListener('focusin', this.rememberLastFocus)); } /* @@ -204,7 +209,9 @@ export default class GitTabController extends React.Component { this.stagingOperationInProgress = true; - const fileListUpdatePromise = this.refView.refStagingView.getNextListUpdatePromise(); + const fileListUpdatePromise = this.refStagingView.map(view => { + return view.getNextListUpdatePromise(); + }).getOr(Promise.resolve()); let stageOperationPromise; if (stageStatus === 'staged') { stageOperationPromise = this.unstageFiles(filePaths); @@ -324,19 +331,15 @@ export default class GitTabController extends React.Component { } rememberLastFocus(event) { - if (!this.refView) { - return; - } - - this.lastFocus = this.refView.rememberFocus(event) || GitTabView.focus.STAGING; + this.lastFocus = this.refView.map(view => view.rememberFocus(event)).getOr(false) || GitTabView.focus.STAGING; } restoreFocus() { - this.refView.setFocus(this.lastFocus); + this.refView.map(view => view.setFocus(this.lastFocus)); } hasFocus() { - return this.refView.refRoot.contains(document.activeElement); + return this.refRoot.map(root => root.contains(document.activeElement)).getOr(false); } wasActivated(isStillActive) { @@ -346,10 +349,10 @@ export default class GitTabController extends React.Component { } focusAndSelectStagingItem(filePath, stagingStatus) { - return this.refView.focusAndSelectStagingItem(filePath, stagingStatus); + return this.refView.map(view => view.focusAndSelectStagingItem(filePath, stagingStatus)).getOr(null); } quietlySelectItem(filePath, stagingStatus) { - return this.refView.quietlySelectItem(filePath, stagingStatus); + return this.refView.map(view => view.quietlySelectItem(filePath, stagingStatus)).getOr(null); } } diff --git a/test/controllers/git-tab-controller.test.js b/test/controllers/git-tab-controller.test.js index 2294cd32c1..0a9c3d3019 100644 --- a/test/controllers/git-tab-controller.test.js +++ b/test/controllers/git-tab-controller.test.js @@ -192,8 +192,7 @@ describe('GitTabController', function() { await assert.async.lengthOf(wrapper.update().find('GitTabView').prop('unstagedChanges'), 3); const controller = wrapper.instance(); - const gitTab = controller.refView; - const stagingView = gitTab.refStagingView; + const stagingView = controller.refStagingView.get(); sinon.spy(stagingView, 'setFocus'); @@ -222,14 +221,15 @@ describe('GitTabController', function() { }); describe('keyboard navigation commands', function() { - let wrapper, gitTab, stagingView, commitView, commitController, focusElement; + let wrapper, rootElement, gitTab, stagingView, commitView, commitController, focusElement; const focuses = GitTabController.focus; const extractReferences = () => { - gitTab = wrapper.instance().refView; - stagingView = gitTab.refStagingView; - commitController = gitTab.refCommitController; - commitView = commitController.refCommitView; + rootElement = wrapper.instance().refRoot.get(); + gitTab = wrapper.instance().refView.get(); + stagingView = wrapper.instance().refStagingView.get(); + commitController = gitTab.refCommitController.get(); + commitView = commitController.refCommitView.get(); focusElement = stagingView.element; const commitViewElements = []; @@ -242,7 +242,7 @@ describe('GitTabController', function() { focusElement = element; }); }; - stubFocus(stagingView.refRoot); + stubFocus(stagingView.refRoot.get()); for (const e of commitViewElements) { stubFocus(e); } @@ -300,18 +300,18 @@ describe('GitTabController', function() { it('advances focus through StagingView groups and CommitView, but does not cycle', async function() { assertSelected(['unstaged-1.txt']); - commandRegistry.dispatch(gitTab.refRoot, 'core:focus-next'); + commandRegistry.dispatch(rootElement, 'core:focus-next'); assertSelected(['conflict-1.txt']); - commandRegistry.dispatch(gitTab.refRoot, 'core:focus-next'); + commandRegistry.dispatch(rootElement, 'core:focus-next'); assertSelected(['staged-1.txt']); - commandRegistry.dispatch(gitTab.refRoot, 'core:focus-next'); + commandRegistry.dispatch(rootElement, 'core:focus-next'); assertSelected(['staged-1.txt']); await assert.async.strictEqual(focusElement, wrapper.find('atom-text-editor').getDOMNode()); // This should be a no-op. (Actually, it'll insert a tab in the CommitView editor.) - commandRegistry.dispatch(gitTab.refRoot, 'core:focus-next'); + commandRegistry.dispatch(rootElement, 'core:focus-next'); assertSelected(['staged-1.txt']); assert.strictEqual(focusElement, wrapper.find('atom-text-editor').getDOMNode()); }); @@ -320,18 +320,18 @@ describe('GitTabController', function() { gitTab.setFocus(focuses.EDITOR); sinon.stub(commitView, 'hasFocusEditor').returns(true); - commandRegistry.dispatch(gitTab.refRoot, 'core:focus-previous'); - await assert.async.strictEqual(focusElement, stagingView.refRoot); + commandRegistry.dispatch(rootElement, 'core:focus-previous'); + await assert.async.strictEqual(focusElement, stagingView.refRoot.get()); assertSelected(['staged-1.txt']); - commandRegistry.dispatch(gitTab.refRoot, 'core:focus-previous'); + commandRegistry.dispatch(rootElement, 'core:focus-previous'); await assertAsyncSelected(['conflict-1.txt']); - commandRegistry.dispatch(gitTab.refRoot, 'core:focus-previous'); + commandRegistry.dispatch(rootElement, 'core:focus-previous'); await assertAsyncSelected(['unstaged-1.txt']); // This should be a no-op. - commandRegistry.dispatch(gitTab.refRoot, 'core:focus-previous'); + commandRegistry.dispatch(rootElement, 'core:focus-previous'); await assertAsyncSelected(['unstaged-1.txt']); }); }); @@ -391,8 +391,7 @@ describe('GitTabController', function() { await assert.async.lengthOf(wrapper.update().find('GitTabView').prop('unstagedChanges'), 2); - const gitTab = wrapper.instance().refView; - const stagingView = gitTab.refStagingView; + const stagingView = wrapper.instance().refStagingView.get(); const commitView = wrapper.find('CommitView'); assert.lengthOf(stagingView.props.unstagedChanges, 2); @@ -433,7 +432,7 @@ describe('GitTabController', function() { const wrapper = mount(await buildApp(repository, props)); assert.lengthOf(wrapper.find('GitTabView').prop('mergeConflicts'), 5); - const stagingView = wrapper.instance().refView.refStagingView; + const stagingView = wrapper.instance().refStagingView.get(); assert.equal(stagingView.props.mergeConflicts.length, 5); assert.equal(stagingView.props.stagedChanges.length, 0); @@ -482,7 +481,7 @@ describe('GitTabController', function() { const wrapper = mount(await buildApp(repository)); - const stagingView = wrapper.instance().refView.refStagingView; + const stagingView = wrapper.instance().refStagingView.get(); assert.lengthOf(stagingView.props.unstagedChanges, 2); // ensure staging the same file twice does not cause issues @@ -509,7 +508,7 @@ describe('GitTabController', function() { const wrapper = mount(await buildApp(repository)); - const stagingView = wrapper.instance().refView.refStagingView; + const stagingView = wrapper.instance().refStagingView.get(); assert.include(stagingView.props.unstagedChanges.map(c => c.filePath), 'new-file.txt'); const [addedFilePatch] = stagingView.props.unstagedChanges; From 41d3a0420a68c6072762c7d7376bbe07c1264390 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 9 Aug 2018 11:52:54 -0400 Subject: [PATCH 0107/4252] GitTabController test for restoring focus --- test/controllers/git-tab-controller.test.js | 32 +++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/test/controllers/git-tab-controller.test.js b/test/controllers/git-tab-controller.test.js index 0a9c3d3019..41d9f225dc 100644 --- a/test/controllers/git-tab-controller.test.js +++ b/test/controllers/git-tab-controller.test.js @@ -206,6 +206,38 @@ describe('GitTabController', function() { }); describe('focus management', function() { + it('remembers the last focus reported by the view', async function() { + const repository = await buildRepository(await cloneRepository()); + const wrapper = mount(await buildApp(repository)); + const view = wrapper.instance().refView.get(); + const editorElement = wrapper.find('atom-text-editor').getDOMNode(); + const commitElement = wrapper.find('.github-CommitView-commit').getDOMNode(); + + wrapper.instance().rememberLastFocus({target: editorElement}); + + sinon.spy(view, 'setFocus'); + wrapper.instance().restoreFocus(); + assert.isTrue(view.setFocus.calledWith(GitTabController.focus.EDITOR)); + + wrapper.instance().rememberLastFocus({target: commitElement}); + + view.setFocus.resetHistory(); + wrapper.instance().restoreFocus(); + assert.isTrue(view.setFocus.calledWith(GitTabController.focus.COMMIT_BUTTON)); + + wrapper.instance().rememberLastFocus({target: document.body}); + + view.setFocus.resetHistory(); + wrapper.instance().restoreFocus(); + assert.isTrue(view.setFocus.calledWith(GitTabController.focus.STAGING)); + + wrapper.instance().refView.setter(null); + + view.setFocus.resetHistory(); + wrapper.instance().restoreFocus(); + assert.isFalse(view.setFocus.called); + }); + it('does nothing on an absent repository', async function() { const repository = Repository.absent(); From c05f2a333cfdeb892cd00759af20bb4dcb87e20b Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 9 Aug 2018 11:53:10 -0400 Subject: [PATCH 0108/4252] Use null for consistency --- lib/controllers/git-tab-controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/controllers/git-tab-controller.js b/lib/controllers/git-tab-controller.js index d226aff6cd..8744a518ba 100644 --- a/lib/controllers/git-tab-controller.js +++ b/lib/controllers/git-tab-controller.js @@ -331,7 +331,7 @@ export default class GitTabController extends React.Component { } rememberLastFocus(event) { - this.lastFocus = this.refView.map(view => view.rememberFocus(event)).getOr(false) || GitTabView.focus.STAGING; + this.lastFocus = this.refView.map(view => view.rememberFocus(event)).getOr(null) || GitTabView.focus.STAGING; } restoreFocus() { From a3a7763e4e998d5cacaf1ddeb93f3e407c88cdf2 Mon Sep 17 00:00:00 2001 From: Tilde Ann Thurium Date: Thu, 9 Aug 2018 09:30:21 -0700 Subject: [PATCH 0109/4252] add commitCount to pull request metadata Co-Authored-By: Ash Wilson --- .../issueishDetailContainerQuery.graphql.js | 52 +++-- .../issueishDetailViewRefetchQuery.graphql.js | 184 ++++++++++-------- .../issueishDetailView_issueish.graphql.js | 64 +++--- lib/views/issueish-detail-view.js | 10 +- test/fixtures/props/issueish-pane-props.js | 4 + test/views/issueish-detail-view.test.js | 4 + 6 files changed, 187 insertions(+), 131 deletions(-) diff --git a/lib/containers/__generated__/issueishDetailContainerQuery.graphql.js b/lib/containers/__generated__/issueishDetailContainerQuery.graphql.js index ed618f16d9..567232f3e8 100644 --- a/lib/containers/__generated__/issueishDetailContainerQuery.graphql.js +++ b/lib/containers/__generated__/issueishDetailContainerQuery.graphql.js @@ -1,6 +1,6 @@ /** * @flow - * @relayHash b7439356a73154992a0173f7672eb97d + * @relayHash 545b98e21577ba8d1fdd921d06f4fc52 */ /* eslint-disable */ @@ -114,6 +114,9 @@ fragment issueishDetailView_issueish_3D8CP9 on IssueOrPullRequest { ...issueTimelineController_issue_3D8CP9 } ... on PullRequest { + countedCommits: commits { + totalCount + } ...prStatusesView_pullRequest state number @@ -854,9 +857,18 @@ v27 = { ] }, v28 = [ + { + "kind": "ScalarField", + "alias": null, + "name": "totalCount", + "args": null, + "storageKey": null + } +], +v29 = [ v10 ], -v29 = { +v30 = { "kind": "LinkedField", "alias": null, "name": "author", @@ -872,16 +884,16 @@ v29 = { { "kind": "InlineFragment", "type": "Bot", - "selections": v28 + "selections": v29 }, { "kind": "InlineFragment", "type": "User", - "selections": v28 + "selections": v29 } ] }, -v30 = { +v31 = { "kind": "LinkedField", "alias": null, "name": "reactionGroups", @@ -905,15 +917,7 @@ v30 = { "args": null, "concreteType": "ReactingUserConnection", "plural": false, - "selections": [ - { - "kind": "ScalarField", - "alias": null, - "name": "totalCount", - "args": null, - "storageKey": null - } - ] + "selections": v28 } ] }; @@ -922,7 +926,7 @@ return { "operationKind": "query", "name": "issueishDetailContainerQuery", "id": null, - "text": "query issueishDetailContainerQuery(\n $repoOwner: String!\n $repoName: String!\n $issueishNumber: Int!\n $timelineCount: Int!\n $timelineCursor: String\n) {\n repository(owner: $repoOwner, name: $repoName) {\n ...issueishDetailController_repository_n0A9R\n id\n }\n}\n\nfragment issueishDetailController_repository_n0A9R on Repository {\n ...issueishDetailView_repository\n name\n owner {\n __typename\n login\n id\n }\n issueish: issueOrPullRequest(number: $issueishNumber) {\n __typename\n ... on Issue {\n title\n number\n ...issueishDetailView_issueish_3D8CP9\n }\n ... on PullRequest {\n title\n number\n headRefName\n headRepository {\n name\n owner {\n __typename\n login\n id\n }\n url\n sshUrl\n id\n }\n ...issueishDetailView_issueish_3D8CP9\n }\n ... on Node {\n id\n }\n }\n}\n\nfragment issueishDetailView_repository on Repository {\n id\n name\n owner {\n __typename\n login\n id\n }\n}\n\nfragment issueishDetailView_issueish_3D8CP9 on IssueOrPullRequest {\n __typename\n ... on Node {\n id\n }\n ... on Issue {\n state\n number\n title\n bodyHTML\n author {\n __typename\n login\n avatarUrl\n ... on User {\n url\n }\n ... on Bot {\n url\n }\n ... on Node {\n id\n }\n }\n ...issueTimelineController_issue_3D8CP9\n }\n ... on PullRequest {\n ...prStatusesView_pullRequest\n state\n number\n title\n bodyHTML\n baseRefName\n headRefName\n author {\n __typename\n login\n avatarUrl\n ... on User {\n url\n }\n ... on Bot {\n url\n }\n ... on Node {\n id\n }\n }\n ...prTimelineController_pullRequest_3D8CP9\n }\n ... on UniformResourceLocatable {\n url\n }\n ... on Reactable {\n reactionGroups {\n content\n users {\n totalCount\n }\n }\n }\n}\n\nfragment issueTimelineController_issue_3D8CP9 on Issue {\n url\n timeline(first: $timelineCount, after: $timelineCursor) {\n pageInfo {\n endCursor\n hasNextPage\n }\n edges {\n cursor\n node {\n __typename\n ...commitsView_nodes\n ...issueCommentView_item\n ...crossReferencedEventsView_nodes\n ... on Node {\n id\n }\n }\n }\n }\n}\n\nfragment prStatusesView_pullRequest on PullRequest {\n id\n commits(last: 1) {\n edges {\n node {\n commit {\n status {\n state\n contexts {\n id\n state\n ...prStatusContextView_context\n }\n id\n }\n id\n }\n id\n }\n }\n }\n}\n\nfragment prTimelineController_pullRequest_3D8CP9 on PullRequest {\n url\n ...headRefForcePushedEventView_issueish\n timeline(first: $timelineCount, after: $timelineCursor) {\n pageInfo {\n endCursor\n hasNextPage\n }\n edges {\n cursor\n node {\n __typename\n ...commitsView_nodes\n ...issueCommentView_item\n ...mergedEventView_item\n ...headRefForcePushedEventView_item\n ...commitCommentThreadView_item\n ...crossReferencedEventsView_nodes\n ... on Node {\n id\n }\n }\n }\n }\n}\n\nfragment headRefForcePushedEventView_issueish on PullRequest {\n headRefName\n headRepositoryOwner {\n __typename\n login\n id\n }\n repository {\n owner {\n __typename\n login\n id\n }\n id\n }\n}\n\nfragment commitsView_nodes on Commit {\n id\n author {\n name\n user {\n login\n id\n }\n }\n ...commitView_item\n}\n\nfragment issueCommentView_item on IssueComment {\n author {\n __typename\n avatarUrl\n login\n ... on Node {\n id\n }\n }\n bodyHTML\n createdAt\n url\n}\n\nfragment mergedEventView_item on MergedEvent {\n actor {\n __typename\n avatarUrl\n login\n ... on Node {\n id\n }\n }\n commit {\n oid\n id\n }\n mergeRefName\n createdAt\n}\n\nfragment headRefForcePushedEventView_item on HeadRefForcePushedEvent {\n actor {\n __typename\n avatarUrl\n login\n ... on Node {\n id\n }\n }\n beforeCommit {\n oid\n id\n }\n afterCommit {\n oid\n id\n }\n createdAt\n}\n\nfragment commitCommentThreadView_item on CommitCommentThread {\n commit {\n oid\n id\n }\n comments(first: 100) {\n edges {\n node {\n id\n ...commitCommentView_item\n }\n }\n }\n}\n\nfragment crossReferencedEventsView_nodes on CrossReferencedEvent {\n id\n referencedAt\n isCrossRepository\n actor {\n __typename\n login\n avatarUrl\n ... on Node {\n id\n }\n }\n source {\n __typename\n ... on RepositoryNode {\n repository {\n name\n owner {\n __typename\n login\n id\n }\n id\n }\n }\n ... on Node {\n id\n }\n }\n ...crossReferencedEventView_item\n}\n\nfragment crossReferencedEventView_item on CrossReferencedEvent {\n id\n isCrossRepository\n source {\n __typename\n ... on Issue {\n number\n title\n url\n issueState: state\n }\n ... on PullRequest {\n number\n title\n url\n prState: state\n }\n ... on RepositoryNode {\n repository {\n name\n isPrivate\n owner {\n __typename\n login\n id\n }\n id\n }\n }\n ... on Node {\n id\n }\n }\n}\n\nfragment commitCommentView_item on CommitComment {\n author {\n __typename\n login\n avatarUrl\n ... on Node {\n id\n }\n }\n commit {\n oid\n id\n }\n bodyHTML\n createdAt\n path\n position\n}\n\nfragment commitView_item on Commit {\n author {\n name\n avatarUrl\n user {\n login\n id\n }\n }\n committer {\n name\n avatarUrl\n user {\n login\n id\n }\n }\n authoredByCommitter\n oid\n message\n messageHeadlineHTML\n commitUrl\n}\n\nfragment prStatusContextView_context on StatusContext {\n context\n description\n state\n targetUrl\n}\n", + "text": "query issueishDetailContainerQuery(\n $repoOwner: String!\n $repoName: String!\n $issueishNumber: Int!\n $timelineCount: Int!\n $timelineCursor: String\n) {\n repository(owner: $repoOwner, name: $repoName) {\n ...issueishDetailController_repository_n0A9R\n id\n }\n}\n\nfragment issueishDetailController_repository_n0A9R on Repository {\n ...issueishDetailView_repository\n name\n owner {\n __typename\n login\n id\n }\n issueish: issueOrPullRequest(number: $issueishNumber) {\n __typename\n ... on Issue {\n title\n number\n ...issueishDetailView_issueish_3D8CP9\n }\n ... on PullRequest {\n title\n number\n headRefName\n headRepository {\n name\n owner {\n __typename\n login\n id\n }\n url\n sshUrl\n id\n }\n ...issueishDetailView_issueish_3D8CP9\n }\n ... on Node {\n id\n }\n }\n}\n\nfragment issueishDetailView_repository on Repository {\n id\n name\n owner {\n __typename\n login\n id\n }\n}\n\nfragment issueishDetailView_issueish_3D8CP9 on IssueOrPullRequest {\n __typename\n ... on Node {\n id\n }\n ... on Issue {\n state\n number\n title\n bodyHTML\n author {\n __typename\n login\n avatarUrl\n ... on User {\n url\n }\n ... on Bot {\n url\n }\n ... on Node {\n id\n }\n }\n ...issueTimelineController_issue_3D8CP9\n }\n ... on PullRequest {\n countedCommits: commits {\n totalCount\n }\n ...prStatusesView_pullRequest\n state\n number\n title\n bodyHTML\n baseRefName\n headRefName\n author {\n __typename\n login\n avatarUrl\n ... on User {\n url\n }\n ... on Bot {\n url\n }\n ... on Node {\n id\n }\n }\n ...prTimelineController_pullRequest_3D8CP9\n }\n ... on UniformResourceLocatable {\n url\n }\n ... on Reactable {\n reactionGroups {\n content\n users {\n totalCount\n }\n }\n }\n}\n\nfragment issueTimelineController_issue_3D8CP9 on Issue {\n url\n timeline(first: $timelineCount, after: $timelineCursor) {\n pageInfo {\n endCursor\n hasNextPage\n }\n edges {\n cursor\n node {\n __typename\n ...commitsView_nodes\n ...issueCommentView_item\n ...crossReferencedEventsView_nodes\n ... on Node {\n id\n }\n }\n }\n }\n}\n\nfragment prStatusesView_pullRequest on PullRequest {\n id\n commits(last: 1) {\n edges {\n node {\n commit {\n status {\n state\n contexts {\n id\n state\n ...prStatusContextView_context\n }\n id\n }\n id\n }\n id\n }\n }\n }\n}\n\nfragment prTimelineController_pullRequest_3D8CP9 on PullRequest {\n url\n ...headRefForcePushedEventView_issueish\n timeline(first: $timelineCount, after: $timelineCursor) {\n pageInfo {\n endCursor\n hasNextPage\n }\n edges {\n cursor\n node {\n __typename\n ...commitsView_nodes\n ...issueCommentView_item\n ...mergedEventView_item\n ...headRefForcePushedEventView_item\n ...commitCommentThreadView_item\n ...crossReferencedEventsView_nodes\n ... on Node {\n id\n }\n }\n }\n }\n}\n\nfragment headRefForcePushedEventView_issueish on PullRequest {\n headRefName\n headRepositoryOwner {\n __typename\n login\n id\n }\n repository {\n owner {\n __typename\n login\n id\n }\n id\n }\n}\n\nfragment commitsView_nodes on Commit {\n id\n author {\n name\n user {\n login\n id\n }\n }\n ...commitView_item\n}\n\nfragment issueCommentView_item on IssueComment {\n author {\n __typename\n avatarUrl\n login\n ... on Node {\n id\n }\n }\n bodyHTML\n createdAt\n url\n}\n\nfragment mergedEventView_item on MergedEvent {\n actor {\n __typename\n avatarUrl\n login\n ... on Node {\n id\n }\n }\n commit {\n oid\n id\n }\n mergeRefName\n createdAt\n}\n\nfragment headRefForcePushedEventView_item on HeadRefForcePushedEvent {\n actor {\n __typename\n avatarUrl\n login\n ... on Node {\n id\n }\n }\n beforeCommit {\n oid\n id\n }\n afterCommit {\n oid\n id\n }\n createdAt\n}\n\nfragment commitCommentThreadView_item on CommitCommentThread {\n commit {\n oid\n id\n }\n comments(first: 100) {\n edges {\n node {\n id\n ...commitCommentView_item\n }\n }\n }\n}\n\nfragment crossReferencedEventsView_nodes on CrossReferencedEvent {\n id\n referencedAt\n isCrossRepository\n actor {\n __typename\n login\n avatarUrl\n ... on Node {\n id\n }\n }\n source {\n __typename\n ... on RepositoryNode {\n repository {\n name\n owner {\n __typename\n login\n id\n }\n id\n }\n }\n ... on Node {\n id\n }\n }\n ...crossReferencedEventView_item\n}\n\nfragment crossReferencedEventView_item on CrossReferencedEvent {\n id\n isCrossRepository\n source {\n __typename\n ... on Issue {\n number\n title\n url\n issueState: state\n }\n ... on PullRequest {\n number\n title\n url\n prState: state\n }\n ... on RepositoryNode {\n repository {\n name\n isPrivate\n owner {\n __typename\n login\n id\n }\n id\n }\n }\n ... on Node {\n id\n }\n }\n}\n\nfragment commitCommentView_item on CommitComment {\n author {\n __typename\n login\n avatarUrl\n ... on Node {\n id\n }\n }\n commit {\n oid\n id\n }\n bodyHTML\n createdAt\n path\n position\n}\n\nfragment commitView_item on Commit {\n author {\n name\n avatarUrl\n user {\n login\n id\n }\n }\n committer {\n name\n avatarUrl\n user {\n login\n id\n }\n }\n authoredByCommitter\n oid\n message\n messageHeadlineHTML\n commitUrl\n}\n\nfragment prStatusContextView_context on StatusContext {\n context\n description\n state\n targetUrl\n}\n", "metadata": {}, "fragment": { "kind": "Fragment", @@ -1206,6 +1210,16 @@ return { "key": "prTimelineContainer_timeline", "filters": null }, + { + "kind": "LinkedField", + "alias": "countedCommits", + "name": "commits", + "storageKey": null, + "args": null, + "concreteType": "PullRequestCommitConnection", + "plural": false, + "selections": v28 + }, { "kind": "LinkedField", "alias": null, @@ -1315,7 +1329,7 @@ return { "args": null, "storageKey": null }, - v29, + v30, v10, { "kind": "LinkedField", @@ -1340,7 +1354,7 @@ return { v2 ] }, - v30 + v31 ] }, { @@ -1351,7 +1365,7 @@ return { v9, v8, v16, - v29, + v30, v10, { "kind": "LinkedField", @@ -1402,7 +1416,7 @@ return { "key": "IssueTimelineController_timeline", "filters": null }, - v30 + v31 ] } ] diff --git a/lib/views/__generated__/issueishDetailViewRefetchQuery.graphql.js b/lib/views/__generated__/issueishDetailViewRefetchQuery.graphql.js index a623ce0f77..23ee5cf12b 100644 --- a/lib/views/__generated__/issueishDetailViewRefetchQuery.graphql.js +++ b/lib/views/__generated__/issueishDetailViewRefetchQuery.graphql.js @@ -1,6 +1,6 @@ /** * @flow - * @relayHash 6b0733755f9c894ca3a872a48956d78f + * @relayHash 6958a5b4d857593786dd3f0d2d6e2c4e */ /* eslint-disable */ @@ -84,6 +84,9 @@ fragment issueishDetailView_issueish_3D8CP9 on IssueOrPullRequest { ...issueTimelineController_issue_3D8CP9 } ... on PullRequest { + countedCommits: commits { + totalCount + } ...prStatusesView_pullRequest state number @@ -511,45 +514,54 @@ v10 = { "args": null, "storageKey": null }, -v11 = { +v11 = [ + { + "kind": "ScalarField", + "alias": null, + "name": "totalCount", + "args": null, + "storageKey": null + } +], +v12 = { "kind": "ScalarField", "alias": null, "name": "state", "args": null, "storageKey": null }, -v12 = { +v13 = { "kind": "ScalarField", "alias": null, "name": "number", "args": null, "storageKey": null }, -v13 = { +v14 = { "kind": "ScalarField", "alias": null, "name": "title", "args": null, "storageKey": null }, -v14 = { +v15 = { "kind": "ScalarField", "alias": null, "name": "bodyHTML", "args": null, "storageKey": null }, -v15 = { +v16 = { "kind": "ScalarField", "alias": null, "name": "avatarUrl", "args": null, "storageKey": null }, -v16 = [ +v17 = [ v10 ], -v17 = { +v18 = { "kind": "LinkedField", "alias": null, "name": "author", @@ -560,21 +572,21 @@ v17 = { "selections": [ v4, v7, - v15, + v16, v5, { "kind": "InlineFragment", "type": "Bot", - "selections": v16 + "selections": v17 }, { "kind": "InlineFragment", "type": "User", - "selections": v16 + "selections": v17 } ] }, -v18 = [ +v19 = [ { "kind": "Variable", "name": "after", @@ -588,7 +600,7 @@ v18 = [ "type": "Int" } ], -v19 = { +v20 = { "kind": "LinkedField", "alias": null, "name": "pageInfo", @@ -613,20 +625,20 @@ v19 = { } ] }, -v20 = { +v21 = { "kind": "ScalarField", "alias": null, "name": "cursor", "args": null, "storageKey": null }, -v21 = [ +v22 = [ v4, v7, - v15, + v16, v5 ], -v22 = { +v23 = { "kind": "InlineFragment", "type": "CrossReferencedEvent", "selections": [ @@ -652,7 +664,7 @@ v22 = { "args": null, "concreteType": null, "plural": false, - "selections": v21 + "selections": v22 }, { "kind": "LinkedField", @@ -690,8 +702,8 @@ v22 = { "kind": "InlineFragment", "type": "PullRequest", "selections": [ - v12, v13, + v14, v10, { "kind": "ScalarField", @@ -706,8 +718,8 @@ v22 = { "kind": "InlineFragment", "type": "Issue", "selections": [ - v12, v13, + v14, v10, { "kind": "ScalarField", @@ -722,18 +734,18 @@ v22 = { } ] }, -v23 = { +v24 = { "kind": "ScalarField", "alias": null, "name": "oid", "args": null, "storageKey": null }, -v24 = [ - v23, +v25 = [ + v24, v5 ], -v25 = { +v26 = { "kind": "LinkedField", "alias": null, "name": "commit", @@ -741,22 +753,22 @@ v25 = { "args": null, "concreteType": "Commit", "plural": false, - "selections": v24 + "selections": v25 }, -v26 = { +v27 = { "kind": "ScalarField", "alias": null, "name": "createdAt", "args": null, "storageKey": null }, -v27 = [ +v28 = [ v4, - v15, + v16, v7, v5 ], -v28 = { +v29 = { "kind": "LinkedField", "alias": null, "name": "actor", @@ -764,9 +776,9 @@ v28 = { "args": null, "concreteType": null, "plural": false, - "selections": v27 + "selections": v28 }, -v29 = { +v30 = { "kind": "InlineFragment", "type": "IssueComment", "selections": [ @@ -778,14 +790,14 @@ v29 = { "args": null, "concreteType": null, "plural": false, - "selections": v27 + "selections": v28 }, - v14, - v26, + v15, + v27, v10 ] }, -v30 = { +v31 = { "kind": "LinkedField", "alias": null, "name": "user", @@ -798,7 +810,7 @@ v30 = { v5 ] }, -v31 = { +v32 = { "kind": "InlineFragment", "type": "Commit", "selections": [ @@ -812,8 +824,8 @@ v31 = { "plural": false, "selections": [ v6, - v30, - v15 + v31, + v16 ] }, { @@ -826,8 +838,8 @@ v31 = { "plural": false, "selections": [ v6, - v15, - v30 + v16, + v31 ] }, { @@ -837,7 +849,7 @@ v31 = { "args": null, "storageKey": null }, - v23, + v24, { "kind": "ScalarField", "alias": null, @@ -866,7 +878,7 @@ return { "operationKind": "query", "name": "issueishDetailViewRefetchQuery", "id": null, - "text": "query issueishDetailViewRefetchQuery(\n $repoId: ID!\n $issueishId: ID!\n $timelineCount: Int!\n $timelineCursor: String\n) {\n repository: node(id: $repoId) {\n __typename\n ...issueishDetailView_repository_3D8CP9\n id\n }\n issueish: node(id: $issueishId) {\n __typename\n ...issueishDetailView_issueish_3D8CP9\n id\n }\n}\n\nfragment issueishDetailView_repository_3D8CP9 on Repository {\n id\n name\n owner {\n __typename\n login\n id\n }\n}\n\nfragment issueishDetailView_issueish_3D8CP9 on IssueOrPullRequest {\n __typename\n ... on Node {\n id\n }\n ... on Issue {\n state\n number\n title\n bodyHTML\n author {\n __typename\n login\n avatarUrl\n ... on User {\n url\n }\n ... on Bot {\n url\n }\n ... on Node {\n id\n }\n }\n ...issueTimelineController_issue_3D8CP9\n }\n ... on PullRequest {\n ...prStatusesView_pullRequest\n state\n number\n title\n bodyHTML\n baseRefName\n headRefName\n author {\n __typename\n login\n avatarUrl\n ... on User {\n url\n }\n ... on Bot {\n url\n }\n ... on Node {\n id\n }\n }\n ...prTimelineController_pullRequest_3D8CP9\n }\n ... on UniformResourceLocatable {\n url\n }\n ... on Reactable {\n reactionGroups {\n content\n users {\n totalCount\n }\n }\n }\n}\n\nfragment issueTimelineController_issue_3D8CP9 on Issue {\n url\n timeline(first: $timelineCount, after: $timelineCursor) {\n pageInfo {\n endCursor\n hasNextPage\n }\n edges {\n cursor\n node {\n __typename\n ...commitsView_nodes\n ...issueCommentView_item\n ...crossReferencedEventsView_nodes\n ... on Node {\n id\n }\n }\n }\n }\n}\n\nfragment prStatusesView_pullRequest on PullRequest {\n id\n commits(last: 1) {\n edges {\n node {\n commit {\n status {\n state\n contexts {\n id\n state\n ...prStatusContextView_context\n }\n id\n }\n id\n }\n id\n }\n }\n }\n}\n\nfragment prTimelineController_pullRequest_3D8CP9 on PullRequest {\n url\n ...headRefForcePushedEventView_issueish\n timeline(first: $timelineCount, after: $timelineCursor) {\n pageInfo {\n endCursor\n hasNextPage\n }\n edges {\n cursor\n node {\n __typename\n ...commitsView_nodes\n ...issueCommentView_item\n ...mergedEventView_item\n ...headRefForcePushedEventView_item\n ...commitCommentThreadView_item\n ...crossReferencedEventsView_nodes\n ... on Node {\n id\n }\n }\n }\n }\n}\n\nfragment headRefForcePushedEventView_issueish on PullRequest {\n headRefName\n headRepositoryOwner {\n __typename\n login\n id\n }\n repository {\n owner {\n __typename\n login\n id\n }\n id\n }\n}\n\nfragment commitsView_nodes on Commit {\n id\n author {\n name\n user {\n login\n id\n }\n }\n ...commitView_item\n}\n\nfragment issueCommentView_item on IssueComment {\n author {\n __typename\n avatarUrl\n login\n ... on Node {\n id\n }\n }\n bodyHTML\n createdAt\n url\n}\n\nfragment mergedEventView_item on MergedEvent {\n actor {\n __typename\n avatarUrl\n login\n ... on Node {\n id\n }\n }\n commit {\n oid\n id\n }\n mergeRefName\n createdAt\n}\n\nfragment headRefForcePushedEventView_item on HeadRefForcePushedEvent {\n actor {\n __typename\n avatarUrl\n login\n ... on Node {\n id\n }\n }\n beforeCommit {\n oid\n id\n }\n afterCommit {\n oid\n id\n }\n createdAt\n}\n\nfragment commitCommentThreadView_item on CommitCommentThread {\n commit {\n oid\n id\n }\n comments(first: 100) {\n edges {\n node {\n id\n ...commitCommentView_item\n }\n }\n }\n}\n\nfragment crossReferencedEventsView_nodes on CrossReferencedEvent {\n id\n referencedAt\n isCrossRepository\n actor {\n __typename\n login\n avatarUrl\n ... on Node {\n id\n }\n }\n source {\n __typename\n ... on RepositoryNode {\n repository {\n name\n owner {\n __typename\n login\n id\n }\n id\n }\n }\n ... on Node {\n id\n }\n }\n ...crossReferencedEventView_item\n}\n\nfragment crossReferencedEventView_item on CrossReferencedEvent {\n id\n isCrossRepository\n source {\n __typename\n ... on Issue {\n number\n title\n url\n issueState: state\n }\n ... on PullRequest {\n number\n title\n url\n prState: state\n }\n ... on RepositoryNode {\n repository {\n name\n isPrivate\n owner {\n __typename\n login\n id\n }\n id\n }\n }\n ... on Node {\n id\n }\n }\n}\n\nfragment commitCommentView_item on CommitComment {\n author {\n __typename\n login\n avatarUrl\n ... on Node {\n id\n }\n }\n commit {\n oid\n id\n }\n bodyHTML\n createdAt\n path\n position\n}\n\nfragment commitView_item on Commit {\n author {\n name\n avatarUrl\n user {\n login\n id\n }\n }\n committer {\n name\n avatarUrl\n user {\n login\n id\n }\n }\n authoredByCommitter\n oid\n message\n messageHeadlineHTML\n commitUrl\n}\n\nfragment prStatusContextView_context on StatusContext {\n context\n description\n state\n targetUrl\n}\n", + "text": "query issueishDetailViewRefetchQuery(\n $repoId: ID!\n $issueishId: ID!\n $timelineCount: Int!\n $timelineCursor: String\n) {\n repository: node(id: $repoId) {\n __typename\n ...issueishDetailView_repository_3D8CP9\n id\n }\n issueish: node(id: $issueishId) {\n __typename\n ...issueishDetailView_issueish_3D8CP9\n id\n }\n}\n\nfragment issueishDetailView_repository_3D8CP9 on Repository {\n id\n name\n owner {\n __typename\n login\n id\n }\n}\n\nfragment issueishDetailView_issueish_3D8CP9 on IssueOrPullRequest {\n __typename\n ... on Node {\n id\n }\n ... on Issue {\n state\n number\n title\n bodyHTML\n author {\n __typename\n login\n avatarUrl\n ... on User {\n url\n }\n ... on Bot {\n url\n }\n ... on Node {\n id\n }\n }\n ...issueTimelineController_issue_3D8CP9\n }\n ... on PullRequest {\n countedCommits: commits {\n totalCount\n }\n ...prStatusesView_pullRequest\n state\n number\n title\n bodyHTML\n baseRefName\n headRefName\n author {\n __typename\n login\n avatarUrl\n ... on User {\n url\n }\n ... on Bot {\n url\n }\n ... on Node {\n id\n }\n }\n ...prTimelineController_pullRequest_3D8CP9\n }\n ... on UniformResourceLocatable {\n url\n }\n ... on Reactable {\n reactionGroups {\n content\n users {\n totalCount\n }\n }\n }\n}\n\nfragment issueTimelineController_issue_3D8CP9 on Issue {\n url\n timeline(first: $timelineCount, after: $timelineCursor) {\n pageInfo {\n endCursor\n hasNextPage\n }\n edges {\n cursor\n node {\n __typename\n ...commitsView_nodes\n ...issueCommentView_item\n ...crossReferencedEventsView_nodes\n ... on Node {\n id\n }\n }\n }\n }\n}\n\nfragment prStatusesView_pullRequest on PullRequest {\n id\n commits(last: 1) {\n edges {\n node {\n commit {\n status {\n state\n contexts {\n id\n state\n ...prStatusContextView_context\n }\n id\n }\n id\n }\n id\n }\n }\n }\n}\n\nfragment prTimelineController_pullRequest_3D8CP9 on PullRequest {\n url\n ...headRefForcePushedEventView_issueish\n timeline(first: $timelineCount, after: $timelineCursor) {\n pageInfo {\n endCursor\n hasNextPage\n }\n edges {\n cursor\n node {\n __typename\n ...commitsView_nodes\n ...issueCommentView_item\n ...mergedEventView_item\n ...headRefForcePushedEventView_item\n ...commitCommentThreadView_item\n ...crossReferencedEventsView_nodes\n ... on Node {\n id\n }\n }\n }\n }\n}\n\nfragment headRefForcePushedEventView_issueish on PullRequest {\n headRefName\n headRepositoryOwner {\n __typename\n login\n id\n }\n repository {\n owner {\n __typename\n login\n id\n }\n id\n }\n}\n\nfragment commitsView_nodes on Commit {\n id\n author {\n name\n user {\n login\n id\n }\n }\n ...commitView_item\n}\n\nfragment issueCommentView_item on IssueComment {\n author {\n __typename\n avatarUrl\n login\n ... on Node {\n id\n }\n }\n bodyHTML\n createdAt\n url\n}\n\nfragment mergedEventView_item on MergedEvent {\n actor {\n __typename\n avatarUrl\n login\n ... on Node {\n id\n }\n }\n commit {\n oid\n id\n }\n mergeRefName\n createdAt\n}\n\nfragment headRefForcePushedEventView_item on HeadRefForcePushedEvent {\n actor {\n __typename\n avatarUrl\n login\n ... on Node {\n id\n }\n }\n beforeCommit {\n oid\n id\n }\n afterCommit {\n oid\n id\n }\n createdAt\n}\n\nfragment commitCommentThreadView_item on CommitCommentThread {\n commit {\n oid\n id\n }\n comments(first: 100) {\n edges {\n node {\n id\n ...commitCommentView_item\n }\n }\n }\n}\n\nfragment crossReferencedEventsView_nodes on CrossReferencedEvent {\n id\n referencedAt\n isCrossRepository\n actor {\n __typename\n login\n avatarUrl\n ... on Node {\n id\n }\n }\n source {\n __typename\n ... on RepositoryNode {\n repository {\n name\n owner {\n __typename\n login\n id\n }\n id\n }\n }\n ... on Node {\n id\n }\n }\n ...crossReferencedEventView_item\n}\n\nfragment crossReferencedEventView_item on CrossReferencedEvent {\n id\n isCrossRepository\n source {\n __typename\n ... on Issue {\n number\n title\n url\n issueState: state\n }\n ... on PullRequest {\n number\n title\n url\n prState: state\n }\n ... on RepositoryNode {\n repository {\n name\n isPrivate\n owner {\n __typename\n login\n id\n }\n id\n }\n }\n ... on Node {\n id\n }\n }\n}\n\nfragment commitCommentView_item on CommitComment {\n author {\n __typename\n login\n avatarUrl\n ... on Node {\n id\n }\n }\n commit {\n oid\n id\n }\n bodyHTML\n createdAt\n path\n position\n}\n\nfragment commitView_item on Commit {\n author {\n name\n avatarUrl\n user {\n login\n id\n }\n }\n committer {\n name\n avatarUrl\n user {\n login\n id\n }\n }\n authoredByCommitter\n oid\n message\n messageHeadlineHTML\n commitUrl\n}\n\nfragment prStatusContextView_context on StatusContext {\n context\n description\n state\n targetUrl\n}\n", "metadata": {}, "fragment": { "kind": "Fragment", @@ -971,15 +983,7 @@ return { "args": null, "concreteType": "ReactingUserConnection", "plural": false, - "selections": [ - { - "kind": "ScalarField", - "alias": null, - "name": "totalCount", - "args": null, - "storageKey": null - } - ] + "selections": v11 } ] }, @@ -994,10 +998,16 @@ return { "args": null, "storageKey": null }, - v11, - v12, - v13, - v14, + { + "kind": "LinkedField", + "alias": "countedCommits", + "name": "commits", + "storageKey": null, + "args": null, + "concreteType": "PullRequestCommitConnection", + "plural": false, + "selections": v11 + }, { "kind": "LinkedField", "alias": null, @@ -1050,7 +1060,7 @@ return { "concreteType": "Status", "plural": false, "selections": [ - v11, + v12, { "kind": "LinkedField", "alias": null, @@ -1061,7 +1071,7 @@ return { "plural": true, "selections": [ v5, - v11, + v12, { "kind": "ScalarField", "alias": null, @@ -1098,6 +1108,10 @@ return { } ] }, + v12, + v13, + v14, + v15, { "kind": "ScalarField", "alias": null, @@ -1105,7 +1119,7 @@ return { "args": null, "storageKey": null }, - v17, + v18, { "kind": "LinkedField", "alias": null, @@ -1134,11 +1148,11 @@ return { "alias": null, "name": "timeline", "storageKey": null, - "args": v18, + "args": v19, "concreteType": "PullRequestTimelineConnection", "plural": false, "selections": [ - v19, + v20, { "kind": "LinkedField", "alias": null, @@ -1148,7 +1162,7 @@ return { "concreteType": "PullRequestTimelineItemEdge", "plural": true, "selections": [ - v20, + v21, { "kind": "LinkedField", "alias": null, @@ -1160,12 +1174,12 @@ return { "selections": [ v4, v5, - v22, + v23, { "kind": "InlineFragment", "type": "CommitCommentThread", "selections": [ - v25, + v26, { "kind": "LinkedField", "alias": null, @@ -1209,11 +1223,11 @@ return { "args": null, "concreteType": null, "plural": false, - "selections": v21 + "selections": v22 }, - v25, - v14, v26, + v15, + v27, { "kind": "ScalarField", "alias": null, @@ -1240,7 +1254,7 @@ return { "kind": "InlineFragment", "type": "HeadRefForcePushedEvent", "selections": [ - v28, + v29, { "kind": "LinkedField", "alias": null, @@ -1249,7 +1263,7 @@ return { "args": null, "concreteType": "Commit", "plural": false, - "selections": v24 + "selections": v25 }, { "kind": "LinkedField", @@ -1259,17 +1273,17 @@ return { "args": null, "concreteType": "Commit", "plural": false, - "selections": v24 + "selections": v25 }, - v26 + v27 ] }, { "kind": "InlineFragment", "type": "MergedEvent", "selections": [ - v28, - v25, + v29, + v26, { "kind": "ScalarField", "alias": null, @@ -1277,11 +1291,11 @@ return { "args": null, "storageKey": null }, - v26 + v27 ] }, - v29, - v31 + v30, + v32 ] } ] @@ -1292,7 +1306,7 @@ return { "kind": "LinkedHandle", "alias": null, "name": "timeline", - "args": v18, + "args": v19, "handle": "connection", "key": "prTimelineContainer_timeline", "filters": null @@ -1303,21 +1317,21 @@ return { "kind": "InlineFragment", "type": "Issue", "selections": [ - v11, v12, v13, v14, - v17, + v15, + v18, { "kind": "LinkedField", "alias": null, "name": "timeline", "storageKey": null, - "args": v18, + "args": v19, "concreteType": "IssueTimelineConnection", "plural": false, "selections": [ - v19, + v20, { "kind": "LinkedField", "alias": null, @@ -1327,7 +1341,7 @@ return { "concreteType": "IssueTimelineItemEdge", "plural": true, "selections": [ - v20, + v21, { "kind": "LinkedField", "alias": null, @@ -1339,9 +1353,9 @@ return { "selections": [ v4, v5, - v22, - v29, - v31 + v23, + v30, + v32 ] } ] @@ -1352,7 +1366,7 @@ return { "kind": "LinkedHandle", "alias": null, "name": "timeline", - "args": v18, + "args": v19, "handle": "connection", "key": "IssueTimelineController_timeline", "filters": null diff --git a/lib/views/__generated__/issueishDetailView_issueish.graphql.js b/lib/views/__generated__/issueishDetailView_issueish.graphql.js index a28361de4a..22a7f60c5b 100644 --- a/lib/views/__generated__/issueishDetailView_issueish.graphql.js +++ b/lib/views/__generated__/issueishDetailView_issueish.graphql.js @@ -35,6 +35,9 @@ export type issueishDetailView_issueish = {| +avatarUrl: any, +url?: any, |}, + +countedCommits?: {| + +totalCount: number + |}, +baseRefName?: string, +headRefName?: string, +$fragmentRefs: issueTimelineController_issue$ref & prStatusesView_pullRequest$ref & prTimelineController_pullRequest$ref, @@ -51,38 +54,47 @@ var v0 = { "args": null, "storageKey": null }, -v1 = { +v1 = [ + { + "kind": "ScalarField", + "alias": null, + "name": "totalCount", + "args": null, + "storageKey": null + } +], +v2 = { "kind": "ScalarField", "alias": null, "name": "state", "args": null, "storageKey": null }, -v2 = { +v3 = { "kind": "ScalarField", "alias": null, "name": "number", "args": null, "storageKey": null }, -v3 = { +v4 = { "kind": "ScalarField", "alias": null, "name": "title", "args": null, "storageKey": null }, -v4 = { +v5 = { "kind": "ScalarField", "alias": null, "name": "bodyHTML", "args": null, "storageKey": null }, -v5 = [ +v6 = [ v0 ], -v6 = { +v7 = { "kind": "LinkedField", "alias": null, "name": "author", @@ -108,16 +120,16 @@ v6 = { { "kind": "InlineFragment", "type": "Bot", - "selections": v5 + "selections": v6 }, { "kind": "InlineFragment", "type": "User", - "selections": v5 + "selections": v6 } ] }, -v7 = [ +v8 = [ { "kind": "Variable", "name": "timelineCount", @@ -190,15 +202,7 @@ return { "args": null, "concreteType": "ReactingUserConnection", "plural": false, - "selections": [ - { - "kind": "ScalarField", - "alias": null, - "name": "totalCount", - "args": null, - "storageKey": null - } - ] + "selections": v1 } ] }, @@ -206,15 +210,25 @@ return { "kind": "InlineFragment", "type": "PullRequest", "selections": [ + { + "kind": "LinkedField", + "alias": "countedCommits", + "name": "commits", + "storageKey": null, + "args": null, + "concreteType": "PullRequestCommitConnection", + "plural": false, + "selections": v1 + }, { "kind": "FragmentSpread", "name": "prStatusesView_pullRequest", "args": null }, - v1, v2, v3, v4, + v5, { "kind": "ScalarField", "alias": null, @@ -229,11 +243,11 @@ return { "args": null, "storageKey": null }, - v6, + v7, { "kind": "FragmentSpread", "name": "prTimelineController_pullRequest", - "args": v7 + "args": v8 } ] }, @@ -241,15 +255,15 @@ return { "kind": "InlineFragment", "type": "Issue", "selections": [ - v1, v2, v3, v4, - v6, + v5, + v7, { "kind": "FragmentSpread", "name": "issueTimelineController_issue", - "args": v7 + "args": v8 } ] } @@ -257,5 +271,5 @@ return { }; })(); // prettier-ignore -(node/*: any*/).hash = '66ea55633a5c3dad06211d4a83154181'; +(node/*: any*/).hash = '396343db62d5fc803ae1913d7122eb6e'; module.exports = node; diff --git a/lib/views/issueish-detail-view.js b/lib/views/issueish-detail-view.js index 8e1bc970a3..10ccb1e10e 100644 --- a/lib/views/issueish-detail-view.js +++ b/lib/views/issueish-detail-view.js @@ -53,6 +53,9 @@ export class BareIssueishDetailView extends React.Component { __typename: PropTypes.string.isRequired, id: PropTypes.string.isRequired, title: PropTypes.string, + countedCommits: PropTypes.shape({ + totalCount: PropTypes.number.isRequired, + }).isRequired, url: PropTypes.string.isRequired, bodyHTML: PropTypes.string, number: PropTypes.number, @@ -134,8 +137,8 @@ export class BareIssueishDetailView extends React.Component { {issueish.author.login} wants to merge{' '} - 2 commits and{' '} - 3 changed files into{' '} + {issueish.countedCommits.totalCount} commits and{' '} + 3 changed files into{' '} {issueish.baseRefName} from{' '} {issueish.headRefName} @@ -297,6 +300,9 @@ export default createRefetchContainer(BareIssueishDetailView, { } ... on PullRequest { + countedCommits: commits { + totalCount + } ...prStatusesView_pullRequest state number title bodyHTML baseRefName headRefName author { diff --git a/test/fixtures/props/issueish-pane-props.js b/test/fixtures/props/issueish-pane-props.js index 8758b82637..e496d1caf3 100644 --- a/test/fixtures/props/issueish-pane-props.js +++ b/test/fixtures/props/issueish-pane-props.js @@ -86,6 +86,7 @@ export function issueishDetailViewProps(opts, overrides = {}) { issueishHeadRepoOwner: 'head-owner', issueishHeadRepoName: 'head-name', issueishReactions: [], + issueishCommitCount: 0, relayRefetch: () => {}, ...opts, @@ -123,6 +124,9 @@ export function issueishDetailViewProps(opts, overrides = {}) { bodyHTML: o.issueishBodyHTML, number: o.issueishNumber, state: o.issueishState, + countedCommits: { + totalCount: o.issueishCommitCount, + }, headRefName: o.issueishHeadRef, headRepository: { name: o.issueishHeadRepoName, diff --git a/test/views/issueish-detail-view.test.js b/test/views/issueish-detail-view.test.js index 77a0c67dd7..b42943c37a 100644 --- a/test/views/issueish-detail-view.test.js +++ b/test/views/issueish-detail-view.test.js @@ -11,6 +11,7 @@ describe('IssueishDetailView', function() { } it('renders pull request information', function() { + const commitCount = 11; const wrapper = shallow(buildApp({ repositoryName: 'repo', ownerLogin: 'user0', @@ -22,6 +23,7 @@ describe('IssueishDetailView', function() { issueishAuthorAvatarURL: 'https://avatars3.githubusercontent.com/u/1', issueishNumber: 100, issueishState: 'MERGED', + issueishCommitCount: commitCount, issueishReactions: [{content: 'THUMBS_UP', count: 10}, {content: 'THUMBS_DOWN', count: 5}, {content: 'LAUGH', count: 0}], })); @@ -55,6 +57,8 @@ describe('IssueishDetailView', function() { assert.isNull(wrapper.find('Relay(IssueishTimelineView)').prop('issue')); assert.isNotNull(wrapper.find('Relay(IssueishTimelineView)').prop('pullRequest')); assert.isNotNull(wrapper.find('Relay(BarePrStatusesView)[displayType="full"]').prop('pullRequest')); + + assert.strictEqual(wrapper.find('.github-IssueishDetailView-commitCount').text(), `${commitCount} commits`); }); it('renders issue information', function() { From 5a181530bd68e4cc962ebf21e3655d3c59f2d8a2 Mon Sep 17 00:00:00 2001 From: Tilde Ann Thurium Date: Thu, 9 Aug 2018 09:41:45 -0700 Subject: [PATCH 0110/4252] add changed file count to pull request metadata Co-Authored-By: Ash Wilson --- .../issueishDetailContainerQuery.graphql.js | 214 +++++++++--------- .../issueishDetailViewRefetchQuery.graphql.js | 58 +++-- .../issueishDetailView_issueish.graphql.js | 34 +-- lib/views/issueish-detail-view.js | 4 +- test/fixtures/props/issueish-pane-props.js | 2 + test/views/issueish-detail-view.test.js | 3 + 6 files changed, 173 insertions(+), 142 deletions(-) diff --git a/lib/containers/__generated__/issueishDetailContainerQuery.graphql.js b/lib/containers/__generated__/issueishDetailContainerQuery.graphql.js index 567232f3e8..4b661fd5bb 100644 --- a/lib/containers/__generated__/issueishDetailContainerQuery.graphql.js +++ b/lib/containers/__generated__/issueishDetailContainerQuery.graphql.js @@ -1,6 +1,6 @@ /** * @flow - * @relayHash 545b98e21577ba8d1fdd921d06f4fc52 + * @relayHash 3ff8f08909b2d8ece9d3fe0bf2950805 */ /* eslint-disable */ @@ -114,6 +114,7 @@ fragment issueishDetailView_issueish_3D8CP9 on IssueOrPullRequest { ...issueTimelineController_issue_3D8CP9 } ... on PullRequest { + changedFiles countedCommits: commits { totalCount } @@ -926,7 +927,7 @@ return { "operationKind": "query", "name": "issueishDetailContainerQuery", "id": null, - "text": "query issueishDetailContainerQuery(\n $repoOwner: String!\n $repoName: String!\n $issueishNumber: Int!\n $timelineCount: Int!\n $timelineCursor: String\n) {\n repository(owner: $repoOwner, name: $repoName) {\n ...issueishDetailController_repository_n0A9R\n id\n }\n}\n\nfragment issueishDetailController_repository_n0A9R on Repository {\n ...issueishDetailView_repository\n name\n owner {\n __typename\n login\n id\n }\n issueish: issueOrPullRequest(number: $issueishNumber) {\n __typename\n ... on Issue {\n title\n number\n ...issueishDetailView_issueish_3D8CP9\n }\n ... on PullRequest {\n title\n number\n headRefName\n headRepository {\n name\n owner {\n __typename\n login\n id\n }\n url\n sshUrl\n id\n }\n ...issueishDetailView_issueish_3D8CP9\n }\n ... on Node {\n id\n }\n }\n}\n\nfragment issueishDetailView_repository on Repository {\n id\n name\n owner {\n __typename\n login\n id\n }\n}\n\nfragment issueishDetailView_issueish_3D8CP9 on IssueOrPullRequest {\n __typename\n ... on Node {\n id\n }\n ... on Issue {\n state\n number\n title\n bodyHTML\n author {\n __typename\n login\n avatarUrl\n ... on User {\n url\n }\n ... on Bot {\n url\n }\n ... on Node {\n id\n }\n }\n ...issueTimelineController_issue_3D8CP9\n }\n ... on PullRequest {\n countedCommits: commits {\n totalCount\n }\n ...prStatusesView_pullRequest\n state\n number\n title\n bodyHTML\n baseRefName\n headRefName\n author {\n __typename\n login\n avatarUrl\n ... on User {\n url\n }\n ... on Bot {\n url\n }\n ... on Node {\n id\n }\n }\n ...prTimelineController_pullRequest_3D8CP9\n }\n ... on UniformResourceLocatable {\n url\n }\n ... on Reactable {\n reactionGroups {\n content\n users {\n totalCount\n }\n }\n }\n}\n\nfragment issueTimelineController_issue_3D8CP9 on Issue {\n url\n timeline(first: $timelineCount, after: $timelineCursor) {\n pageInfo {\n endCursor\n hasNextPage\n }\n edges {\n cursor\n node {\n __typename\n ...commitsView_nodes\n ...issueCommentView_item\n ...crossReferencedEventsView_nodes\n ... on Node {\n id\n }\n }\n }\n }\n}\n\nfragment prStatusesView_pullRequest on PullRequest {\n id\n commits(last: 1) {\n edges {\n node {\n commit {\n status {\n state\n contexts {\n id\n state\n ...prStatusContextView_context\n }\n id\n }\n id\n }\n id\n }\n }\n }\n}\n\nfragment prTimelineController_pullRequest_3D8CP9 on PullRequest {\n url\n ...headRefForcePushedEventView_issueish\n timeline(first: $timelineCount, after: $timelineCursor) {\n pageInfo {\n endCursor\n hasNextPage\n }\n edges {\n cursor\n node {\n __typename\n ...commitsView_nodes\n ...issueCommentView_item\n ...mergedEventView_item\n ...headRefForcePushedEventView_item\n ...commitCommentThreadView_item\n ...crossReferencedEventsView_nodes\n ... on Node {\n id\n }\n }\n }\n }\n}\n\nfragment headRefForcePushedEventView_issueish on PullRequest {\n headRefName\n headRepositoryOwner {\n __typename\n login\n id\n }\n repository {\n owner {\n __typename\n login\n id\n }\n id\n }\n}\n\nfragment commitsView_nodes on Commit {\n id\n author {\n name\n user {\n login\n id\n }\n }\n ...commitView_item\n}\n\nfragment issueCommentView_item on IssueComment {\n author {\n __typename\n avatarUrl\n login\n ... on Node {\n id\n }\n }\n bodyHTML\n createdAt\n url\n}\n\nfragment mergedEventView_item on MergedEvent {\n actor {\n __typename\n avatarUrl\n login\n ... on Node {\n id\n }\n }\n commit {\n oid\n id\n }\n mergeRefName\n createdAt\n}\n\nfragment headRefForcePushedEventView_item on HeadRefForcePushedEvent {\n actor {\n __typename\n avatarUrl\n login\n ... on Node {\n id\n }\n }\n beforeCommit {\n oid\n id\n }\n afterCommit {\n oid\n id\n }\n createdAt\n}\n\nfragment commitCommentThreadView_item on CommitCommentThread {\n commit {\n oid\n id\n }\n comments(first: 100) {\n edges {\n node {\n id\n ...commitCommentView_item\n }\n }\n }\n}\n\nfragment crossReferencedEventsView_nodes on CrossReferencedEvent {\n id\n referencedAt\n isCrossRepository\n actor {\n __typename\n login\n avatarUrl\n ... on Node {\n id\n }\n }\n source {\n __typename\n ... on RepositoryNode {\n repository {\n name\n owner {\n __typename\n login\n id\n }\n id\n }\n }\n ... on Node {\n id\n }\n }\n ...crossReferencedEventView_item\n}\n\nfragment crossReferencedEventView_item on CrossReferencedEvent {\n id\n isCrossRepository\n source {\n __typename\n ... on Issue {\n number\n title\n url\n issueState: state\n }\n ... on PullRequest {\n number\n title\n url\n prState: state\n }\n ... on RepositoryNode {\n repository {\n name\n isPrivate\n owner {\n __typename\n login\n id\n }\n id\n }\n }\n ... on Node {\n id\n }\n }\n}\n\nfragment commitCommentView_item on CommitComment {\n author {\n __typename\n login\n avatarUrl\n ... on Node {\n id\n }\n }\n commit {\n oid\n id\n }\n bodyHTML\n createdAt\n path\n position\n}\n\nfragment commitView_item on Commit {\n author {\n name\n avatarUrl\n user {\n login\n id\n }\n }\n committer {\n name\n avatarUrl\n user {\n login\n id\n }\n }\n authoredByCommitter\n oid\n message\n messageHeadlineHTML\n commitUrl\n}\n\nfragment prStatusContextView_context on StatusContext {\n context\n description\n state\n targetUrl\n}\n", + "text": "query issueishDetailContainerQuery(\n $repoOwner: String!\n $repoName: String!\n $issueishNumber: Int!\n $timelineCount: Int!\n $timelineCursor: String\n) {\n repository(owner: $repoOwner, name: $repoName) {\n ...issueishDetailController_repository_n0A9R\n id\n }\n}\n\nfragment issueishDetailController_repository_n0A9R on Repository {\n ...issueishDetailView_repository\n name\n owner {\n __typename\n login\n id\n }\n issueish: issueOrPullRequest(number: $issueishNumber) {\n __typename\n ... on Issue {\n title\n number\n ...issueishDetailView_issueish_3D8CP9\n }\n ... on PullRequest {\n title\n number\n headRefName\n headRepository {\n name\n owner {\n __typename\n login\n id\n }\n url\n sshUrl\n id\n }\n ...issueishDetailView_issueish_3D8CP9\n }\n ... on Node {\n id\n }\n }\n}\n\nfragment issueishDetailView_repository on Repository {\n id\n name\n owner {\n __typename\n login\n id\n }\n}\n\nfragment issueishDetailView_issueish_3D8CP9 on IssueOrPullRequest {\n __typename\n ... on Node {\n id\n }\n ... on Issue {\n state\n number\n title\n bodyHTML\n author {\n __typename\n login\n avatarUrl\n ... on User {\n url\n }\n ... on Bot {\n url\n }\n ... on Node {\n id\n }\n }\n ...issueTimelineController_issue_3D8CP9\n }\n ... on PullRequest {\n changedFiles\n countedCommits: commits {\n totalCount\n }\n ...prStatusesView_pullRequest\n state\n number\n title\n bodyHTML\n baseRefName\n headRefName\n author {\n __typename\n login\n avatarUrl\n ... on User {\n url\n }\n ... on Bot {\n url\n }\n ... on Node {\n id\n }\n }\n ...prTimelineController_pullRequest_3D8CP9\n }\n ... on UniformResourceLocatable {\n url\n }\n ... on Reactable {\n reactionGroups {\n content\n users {\n totalCount\n }\n }\n }\n}\n\nfragment issueTimelineController_issue_3D8CP9 on Issue {\n url\n timeline(first: $timelineCount, after: $timelineCursor) {\n pageInfo {\n endCursor\n hasNextPage\n }\n edges {\n cursor\n node {\n __typename\n ...commitsView_nodes\n ...issueCommentView_item\n ...crossReferencedEventsView_nodes\n ... on Node {\n id\n }\n }\n }\n }\n}\n\nfragment prStatusesView_pullRequest on PullRequest {\n id\n commits(last: 1) {\n edges {\n node {\n commit {\n status {\n state\n contexts {\n id\n state\n ...prStatusContextView_context\n }\n id\n }\n id\n }\n id\n }\n }\n }\n}\n\nfragment prTimelineController_pullRequest_3D8CP9 on PullRequest {\n url\n ...headRefForcePushedEventView_issueish\n timeline(first: $timelineCount, after: $timelineCursor) {\n pageInfo {\n endCursor\n hasNextPage\n }\n edges {\n cursor\n node {\n __typename\n ...commitsView_nodes\n ...issueCommentView_item\n ...mergedEventView_item\n ...headRefForcePushedEventView_item\n ...commitCommentThreadView_item\n ...crossReferencedEventsView_nodes\n ... on Node {\n id\n }\n }\n }\n }\n}\n\nfragment headRefForcePushedEventView_issueish on PullRequest {\n headRefName\n headRepositoryOwner {\n __typename\n login\n id\n }\n repository {\n owner {\n __typename\n login\n id\n }\n id\n }\n}\n\nfragment commitsView_nodes on Commit {\n id\n author {\n name\n user {\n login\n id\n }\n }\n ...commitView_item\n}\n\nfragment issueCommentView_item on IssueComment {\n author {\n __typename\n avatarUrl\n login\n ... on Node {\n id\n }\n }\n bodyHTML\n createdAt\n url\n}\n\nfragment mergedEventView_item on MergedEvent {\n actor {\n __typename\n avatarUrl\n login\n ... on Node {\n id\n }\n }\n commit {\n oid\n id\n }\n mergeRefName\n createdAt\n}\n\nfragment headRefForcePushedEventView_item on HeadRefForcePushedEvent {\n actor {\n __typename\n avatarUrl\n login\n ... on Node {\n id\n }\n }\n beforeCommit {\n oid\n id\n }\n afterCommit {\n oid\n id\n }\n createdAt\n}\n\nfragment commitCommentThreadView_item on CommitCommentThread {\n commit {\n oid\n id\n }\n comments(first: 100) {\n edges {\n node {\n id\n ...commitCommentView_item\n }\n }\n }\n}\n\nfragment crossReferencedEventsView_nodes on CrossReferencedEvent {\n id\n referencedAt\n isCrossRepository\n actor {\n __typename\n login\n avatarUrl\n ... on Node {\n id\n }\n }\n source {\n __typename\n ... on RepositoryNode {\n repository {\n name\n owner {\n __typename\n login\n id\n }\n id\n }\n }\n ... on Node {\n id\n }\n }\n ...crossReferencedEventView_item\n}\n\nfragment crossReferencedEventView_item on CrossReferencedEvent {\n id\n isCrossRepository\n source {\n __typename\n ... on Issue {\n number\n title\n url\n issueState: state\n }\n ... on PullRequest {\n number\n title\n url\n prState: state\n }\n ... on RepositoryNode {\n repository {\n name\n isPrivate\n owner {\n __typename\n login\n id\n }\n id\n }\n }\n ... on Node {\n id\n }\n }\n}\n\nfragment commitCommentView_item on CommitComment {\n author {\n __typename\n login\n avatarUrl\n ... on Node {\n id\n }\n }\n commit {\n oid\n id\n }\n bodyHTML\n createdAt\n path\n position\n}\n\nfragment commitView_item on Commit {\n author {\n name\n avatarUrl\n user {\n login\n id\n }\n }\n committer {\n name\n avatarUrl\n user {\n login\n id\n }\n }\n authoredByCommitter\n oid\n message\n messageHeadlineHTML\n commitUrl\n}\n\nfragment prStatusContextView_context on StatusContext {\n context\n description\n state\n targetUrl\n}\n", "metadata": {}, "fragment": { "kind": "Fragment", @@ -1011,7 +1012,106 @@ return { "kind": "InlineFragment", "type": "PullRequest", "selections": [ - v8, + { + "kind": "LinkedField", + "alias": null, + "name": "commits", + "storageKey": "commits(last:1)", + "args": [ + { + "kind": "Literal", + "name": "last", + "value": 1, + "type": "Int" + } + ], + "concreteType": "PullRequestCommitConnection", + "plural": false, + "selections": [ + { + "kind": "LinkedField", + "alias": null, + "name": "edges", + "storageKey": null, + "args": null, + "concreteType": "PullRequestCommitEdge", + "plural": true, + "selections": [ + { + "kind": "LinkedField", + "alias": null, + "name": "node", + "storageKey": null, + "args": null, + "concreteType": "PullRequestCommit", + "plural": false, + "selections": [ + { + "kind": "LinkedField", + "alias": null, + "name": "commit", + "storageKey": null, + "args": null, + "concreteType": "Commit", + "plural": false, + "selections": [ + { + "kind": "LinkedField", + "alias": null, + "name": "status", + "storageKey": null, + "args": null, + "concreteType": "Status", + "plural": false, + "selections": [ + v8, + { + "kind": "LinkedField", + "alias": null, + "name": "contexts", + "storageKey": null, + "args": null, + "concreteType": "StatusContext", + "plural": true, + "selections": [ + v2, + v8, + { + "kind": "ScalarField", + "alias": null, + "name": "context", + "args": null, + "storageKey": null + }, + { + "kind": "ScalarField", + "alias": null, + "name": "description", + "args": null, + "storageKey": null + }, + { + "kind": "ScalarField", + "alias": null, + "name": "targetUrl", + "args": null, + "storageKey": null + } + ] + }, + v2 + ] + }, + v2 + ] + }, + v2 + ] + } + ] + } + ] + }, v9, { "kind": "ScalarField", @@ -1210,6 +1310,13 @@ return { "key": "prTimelineContainer_timeline", "filters": null }, + { + "kind": "ScalarField", + "alias": null, + "name": "changedFiles", + "args": null, + "storageKey": null + }, { "kind": "LinkedField", "alias": "countedCommits", @@ -1220,107 +1327,8 @@ return { "plural": false, "selections": v28 }, - { - "kind": "LinkedField", - "alias": null, - "name": "commits", - "storageKey": "commits(last:1)", - "args": [ - { - "kind": "Literal", - "name": "last", - "value": 1, - "type": "Int" - } - ], - "concreteType": "PullRequestCommitConnection", - "plural": false, - "selections": [ - { - "kind": "LinkedField", - "alias": null, - "name": "edges", - "storageKey": null, - "args": null, - "concreteType": "PullRequestCommitEdge", - "plural": true, - "selections": [ - { - "kind": "LinkedField", - "alias": null, - "name": "node", - "storageKey": null, - "args": null, - "concreteType": "PullRequestCommit", - "plural": false, - "selections": [ - { - "kind": "LinkedField", - "alias": null, - "name": "commit", - "storageKey": null, - "args": null, - "concreteType": "Commit", - "plural": false, - "selections": [ - { - "kind": "LinkedField", - "alias": null, - "name": "status", - "storageKey": null, - "args": null, - "concreteType": "Status", - "plural": false, - "selections": [ - v8, - { - "kind": "LinkedField", - "alias": null, - "name": "contexts", - "storageKey": null, - "args": null, - "concreteType": "StatusContext", - "plural": true, - "selections": [ - v2, - v8, - { - "kind": "ScalarField", - "alias": null, - "name": "context", - "args": null, - "storageKey": null - }, - { - "kind": "ScalarField", - "alias": null, - "name": "description", - "args": null, - "storageKey": null - }, - { - "kind": "ScalarField", - "alias": null, - "name": "targetUrl", - "args": null, - "storageKey": null - } - ] - }, - v2 - ] - }, - v2 - ] - }, - v2 - ] - } - ] - } - ] - }, v16, + v8, v21, { "kind": "ScalarField", diff --git a/lib/views/__generated__/issueishDetailViewRefetchQuery.graphql.js b/lib/views/__generated__/issueishDetailViewRefetchQuery.graphql.js index 23ee5cf12b..f8b0576993 100644 --- a/lib/views/__generated__/issueishDetailViewRefetchQuery.graphql.js +++ b/lib/views/__generated__/issueishDetailViewRefetchQuery.graphql.js @@ -1,6 +1,6 @@ /** * @flow - * @relayHash 6958a5b4d857593786dd3f0d2d6e2c4e + * @relayHash 827fdb512f164dfe6bbffbdf0bcb5df8 */ /* eslint-disable */ @@ -84,6 +84,7 @@ fragment issueishDetailView_issueish_3D8CP9 on IssueOrPullRequest { ...issueTimelineController_issue_3D8CP9 } ... on PullRequest { + changedFiles countedCommits: commits { totalCount } @@ -526,28 +527,28 @@ v11 = [ v12 = { "kind": "ScalarField", "alias": null, - "name": "state", + "name": "bodyHTML", "args": null, "storageKey": null }, v13 = { "kind": "ScalarField", "alias": null, - "name": "number", + "name": "state", "args": null, "storageKey": null }, v14 = { "kind": "ScalarField", "alias": null, - "name": "title", + "name": "number", "args": null, "storageKey": null }, v15 = { "kind": "ScalarField", "alias": null, - "name": "bodyHTML", + "name": "title", "args": null, "storageKey": null }, @@ -702,8 +703,8 @@ v23 = { "kind": "InlineFragment", "type": "PullRequest", "selections": [ - v13, v14, + v15, v10, { "kind": "ScalarField", @@ -718,8 +719,8 @@ v23 = { "kind": "InlineFragment", "type": "Issue", "selections": [ - v13, v14, + v15, v10, { "kind": "ScalarField", @@ -792,7 +793,7 @@ v30 = { "plural": false, "selections": v28 }, - v15, + v12, v27, v10 ] @@ -878,7 +879,7 @@ return { "operationKind": "query", "name": "issueishDetailViewRefetchQuery", "id": null, - "text": "query issueishDetailViewRefetchQuery(\n $repoId: ID!\n $issueishId: ID!\n $timelineCount: Int!\n $timelineCursor: String\n) {\n repository: node(id: $repoId) {\n __typename\n ...issueishDetailView_repository_3D8CP9\n id\n }\n issueish: node(id: $issueishId) {\n __typename\n ...issueishDetailView_issueish_3D8CP9\n id\n }\n}\n\nfragment issueishDetailView_repository_3D8CP9 on Repository {\n id\n name\n owner {\n __typename\n login\n id\n }\n}\n\nfragment issueishDetailView_issueish_3D8CP9 on IssueOrPullRequest {\n __typename\n ... on Node {\n id\n }\n ... on Issue {\n state\n number\n title\n bodyHTML\n author {\n __typename\n login\n avatarUrl\n ... on User {\n url\n }\n ... on Bot {\n url\n }\n ... on Node {\n id\n }\n }\n ...issueTimelineController_issue_3D8CP9\n }\n ... on PullRequest {\n countedCommits: commits {\n totalCount\n }\n ...prStatusesView_pullRequest\n state\n number\n title\n bodyHTML\n baseRefName\n headRefName\n author {\n __typename\n login\n avatarUrl\n ... on User {\n url\n }\n ... on Bot {\n url\n }\n ... on Node {\n id\n }\n }\n ...prTimelineController_pullRequest_3D8CP9\n }\n ... on UniformResourceLocatable {\n url\n }\n ... on Reactable {\n reactionGroups {\n content\n users {\n totalCount\n }\n }\n }\n}\n\nfragment issueTimelineController_issue_3D8CP9 on Issue {\n url\n timeline(first: $timelineCount, after: $timelineCursor) {\n pageInfo {\n endCursor\n hasNextPage\n }\n edges {\n cursor\n node {\n __typename\n ...commitsView_nodes\n ...issueCommentView_item\n ...crossReferencedEventsView_nodes\n ... on Node {\n id\n }\n }\n }\n }\n}\n\nfragment prStatusesView_pullRequest on PullRequest {\n id\n commits(last: 1) {\n edges {\n node {\n commit {\n status {\n state\n contexts {\n id\n state\n ...prStatusContextView_context\n }\n id\n }\n id\n }\n id\n }\n }\n }\n}\n\nfragment prTimelineController_pullRequest_3D8CP9 on PullRequest {\n url\n ...headRefForcePushedEventView_issueish\n timeline(first: $timelineCount, after: $timelineCursor) {\n pageInfo {\n endCursor\n hasNextPage\n }\n edges {\n cursor\n node {\n __typename\n ...commitsView_nodes\n ...issueCommentView_item\n ...mergedEventView_item\n ...headRefForcePushedEventView_item\n ...commitCommentThreadView_item\n ...crossReferencedEventsView_nodes\n ... on Node {\n id\n }\n }\n }\n }\n}\n\nfragment headRefForcePushedEventView_issueish on PullRequest {\n headRefName\n headRepositoryOwner {\n __typename\n login\n id\n }\n repository {\n owner {\n __typename\n login\n id\n }\n id\n }\n}\n\nfragment commitsView_nodes on Commit {\n id\n author {\n name\n user {\n login\n id\n }\n }\n ...commitView_item\n}\n\nfragment issueCommentView_item on IssueComment {\n author {\n __typename\n avatarUrl\n login\n ... on Node {\n id\n }\n }\n bodyHTML\n createdAt\n url\n}\n\nfragment mergedEventView_item on MergedEvent {\n actor {\n __typename\n avatarUrl\n login\n ... on Node {\n id\n }\n }\n commit {\n oid\n id\n }\n mergeRefName\n createdAt\n}\n\nfragment headRefForcePushedEventView_item on HeadRefForcePushedEvent {\n actor {\n __typename\n avatarUrl\n login\n ... on Node {\n id\n }\n }\n beforeCommit {\n oid\n id\n }\n afterCommit {\n oid\n id\n }\n createdAt\n}\n\nfragment commitCommentThreadView_item on CommitCommentThread {\n commit {\n oid\n id\n }\n comments(first: 100) {\n edges {\n node {\n id\n ...commitCommentView_item\n }\n }\n }\n}\n\nfragment crossReferencedEventsView_nodes on CrossReferencedEvent {\n id\n referencedAt\n isCrossRepository\n actor {\n __typename\n login\n avatarUrl\n ... on Node {\n id\n }\n }\n source {\n __typename\n ... on RepositoryNode {\n repository {\n name\n owner {\n __typename\n login\n id\n }\n id\n }\n }\n ... on Node {\n id\n }\n }\n ...crossReferencedEventView_item\n}\n\nfragment crossReferencedEventView_item on CrossReferencedEvent {\n id\n isCrossRepository\n source {\n __typename\n ... on Issue {\n number\n title\n url\n issueState: state\n }\n ... on PullRequest {\n number\n title\n url\n prState: state\n }\n ... on RepositoryNode {\n repository {\n name\n isPrivate\n owner {\n __typename\n login\n id\n }\n id\n }\n }\n ... on Node {\n id\n }\n }\n}\n\nfragment commitCommentView_item on CommitComment {\n author {\n __typename\n login\n avatarUrl\n ... on Node {\n id\n }\n }\n commit {\n oid\n id\n }\n bodyHTML\n createdAt\n path\n position\n}\n\nfragment commitView_item on Commit {\n author {\n name\n avatarUrl\n user {\n login\n id\n }\n }\n committer {\n name\n avatarUrl\n user {\n login\n id\n }\n }\n authoredByCommitter\n oid\n message\n messageHeadlineHTML\n commitUrl\n}\n\nfragment prStatusContextView_context on StatusContext {\n context\n description\n state\n targetUrl\n}\n", + "text": "query issueishDetailViewRefetchQuery(\n $repoId: ID!\n $issueishId: ID!\n $timelineCount: Int!\n $timelineCursor: String\n) {\n repository: node(id: $repoId) {\n __typename\n ...issueishDetailView_repository_3D8CP9\n id\n }\n issueish: node(id: $issueishId) {\n __typename\n ...issueishDetailView_issueish_3D8CP9\n id\n }\n}\n\nfragment issueishDetailView_repository_3D8CP9 on Repository {\n id\n name\n owner {\n __typename\n login\n id\n }\n}\n\nfragment issueishDetailView_issueish_3D8CP9 on IssueOrPullRequest {\n __typename\n ... on Node {\n id\n }\n ... on Issue {\n state\n number\n title\n bodyHTML\n author {\n __typename\n login\n avatarUrl\n ... on User {\n url\n }\n ... on Bot {\n url\n }\n ... on Node {\n id\n }\n }\n ...issueTimelineController_issue_3D8CP9\n }\n ... on PullRequest {\n changedFiles\n countedCommits: commits {\n totalCount\n }\n ...prStatusesView_pullRequest\n state\n number\n title\n bodyHTML\n baseRefName\n headRefName\n author {\n __typename\n login\n avatarUrl\n ... on User {\n url\n }\n ... on Bot {\n url\n }\n ... on Node {\n id\n }\n }\n ...prTimelineController_pullRequest_3D8CP9\n }\n ... on UniformResourceLocatable {\n url\n }\n ... on Reactable {\n reactionGroups {\n content\n users {\n totalCount\n }\n }\n }\n}\n\nfragment issueTimelineController_issue_3D8CP9 on Issue {\n url\n timeline(first: $timelineCount, after: $timelineCursor) {\n pageInfo {\n endCursor\n hasNextPage\n }\n edges {\n cursor\n node {\n __typename\n ...commitsView_nodes\n ...issueCommentView_item\n ...crossReferencedEventsView_nodes\n ... on Node {\n id\n }\n }\n }\n }\n}\n\nfragment prStatusesView_pullRequest on PullRequest {\n id\n commits(last: 1) {\n edges {\n node {\n commit {\n status {\n state\n contexts {\n id\n state\n ...prStatusContextView_context\n }\n id\n }\n id\n }\n id\n }\n }\n }\n}\n\nfragment prTimelineController_pullRequest_3D8CP9 on PullRequest {\n url\n ...headRefForcePushedEventView_issueish\n timeline(first: $timelineCount, after: $timelineCursor) {\n pageInfo {\n endCursor\n hasNextPage\n }\n edges {\n cursor\n node {\n __typename\n ...commitsView_nodes\n ...issueCommentView_item\n ...mergedEventView_item\n ...headRefForcePushedEventView_item\n ...commitCommentThreadView_item\n ...crossReferencedEventsView_nodes\n ... on Node {\n id\n }\n }\n }\n }\n}\n\nfragment headRefForcePushedEventView_issueish on PullRequest {\n headRefName\n headRepositoryOwner {\n __typename\n login\n id\n }\n repository {\n owner {\n __typename\n login\n id\n }\n id\n }\n}\n\nfragment commitsView_nodes on Commit {\n id\n author {\n name\n user {\n login\n id\n }\n }\n ...commitView_item\n}\n\nfragment issueCommentView_item on IssueComment {\n author {\n __typename\n avatarUrl\n login\n ... on Node {\n id\n }\n }\n bodyHTML\n createdAt\n url\n}\n\nfragment mergedEventView_item on MergedEvent {\n actor {\n __typename\n avatarUrl\n login\n ... on Node {\n id\n }\n }\n commit {\n oid\n id\n }\n mergeRefName\n createdAt\n}\n\nfragment headRefForcePushedEventView_item on HeadRefForcePushedEvent {\n actor {\n __typename\n avatarUrl\n login\n ... on Node {\n id\n }\n }\n beforeCommit {\n oid\n id\n }\n afterCommit {\n oid\n id\n }\n createdAt\n}\n\nfragment commitCommentThreadView_item on CommitCommentThread {\n commit {\n oid\n id\n }\n comments(first: 100) {\n edges {\n node {\n id\n ...commitCommentView_item\n }\n }\n }\n}\n\nfragment crossReferencedEventsView_nodes on CrossReferencedEvent {\n id\n referencedAt\n isCrossRepository\n actor {\n __typename\n login\n avatarUrl\n ... on Node {\n id\n }\n }\n source {\n __typename\n ... on RepositoryNode {\n repository {\n name\n owner {\n __typename\n login\n id\n }\n id\n }\n }\n ... on Node {\n id\n }\n }\n ...crossReferencedEventView_item\n}\n\nfragment crossReferencedEventView_item on CrossReferencedEvent {\n id\n isCrossRepository\n source {\n __typename\n ... on Issue {\n number\n title\n url\n issueState: state\n }\n ... on PullRequest {\n number\n title\n url\n prState: state\n }\n ... on RepositoryNode {\n repository {\n name\n isPrivate\n owner {\n __typename\n login\n id\n }\n id\n }\n }\n ... on Node {\n id\n }\n }\n}\n\nfragment commitCommentView_item on CommitComment {\n author {\n __typename\n login\n avatarUrl\n ... on Node {\n id\n }\n }\n commit {\n oid\n id\n }\n bodyHTML\n createdAt\n path\n position\n}\n\nfragment commitView_item on Commit {\n author {\n name\n avatarUrl\n user {\n login\n id\n }\n }\n committer {\n name\n avatarUrl\n user {\n login\n id\n }\n }\n authoredByCommitter\n oid\n message\n messageHeadlineHTML\n commitUrl\n}\n\nfragment prStatusContextView_context on StatusContext {\n context\n description\n state\n targetUrl\n}\n", "metadata": {}, "fragment": { "kind": "Fragment", @@ -991,23 +992,14 @@ return { "kind": "InlineFragment", "type": "PullRequest", "selections": [ + v12, { "kind": "ScalarField", "alias": null, - "name": "baseRefName", + "name": "changedFiles", "args": null, "storageKey": null }, - { - "kind": "LinkedField", - "alias": "countedCommits", - "name": "commits", - "storageKey": null, - "args": null, - "concreteType": "PullRequestCommitConnection", - "plural": false, - "selections": v11 - }, { "kind": "LinkedField", "alias": null, @@ -1060,7 +1052,7 @@ return { "concreteType": "Status", "plural": false, "selections": [ - v12, + v13, { "kind": "LinkedField", "alias": null, @@ -1071,7 +1063,7 @@ return { "plural": true, "selections": [ v5, - v12, + v13, { "kind": "ScalarField", "alias": null, @@ -1108,10 +1100,26 @@ return { } ] }, - v12, v13, v14, v15, + { + "kind": "LinkedField", + "alias": "countedCommits", + "name": "commits", + "storageKey": null, + "args": null, + "concreteType": "PullRequestCommitConnection", + "plural": false, + "selections": v11 + }, + { + "kind": "ScalarField", + "alias": null, + "name": "baseRefName", + "args": null, + "storageKey": null + }, { "kind": "ScalarField", "alias": null, @@ -1226,7 +1234,7 @@ return { "selections": v22 }, v26, - v15, + v12, v27, { "kind": "ScalarField", @@ -1317,10 +1325,10 @@ return { "kind": "InlineFragment", "type": "Issue", "selections": [ - v12, v13, v14, v15, + v12, v18, { "kind": "LinkedField", diff --git a/lib/views/__generated__/issueishDetailView_issueish.graphql.js b/lib/views/__generated__/issueishDetailView_issueish.graphql.js index 22a7f60c5b..94eb7ae273 100644 --- a/lib/views/__generated__/issueishDetailView_issueish.graphql.js +++ b/lib/views/__generated__/issueishDetailView_issueish.graphql.js @@ -35,6 +35,7 @@ export type issueishDetailView_issueish = {| +avatarUrl: any, +url?: any, |}, + +changedFiles?: number, +countedCommits?: {| +totalCount: number |}, @@ -66,21 +67,21 @@ v1 = [ v2 = { "kind": "ScalarField", "alias": null, - "name": "state", + "name": "title", "args": null, "storageKey": null }, v3 = { "kind": "ScalarField", "alias": null, - "name": "number", + "name": "state", "args": null, "storageKey": null }, v4 = { "kind": "ScalarField", "alias": null, - "name": "title", + "name": "number", "args": null, "storageKey": null }, @@ -210,24 +211,31 @@ return { "kind": "InlineFragment", "type": "PullRequest", "selections": [ + v2, { - "kind": "LinkedField", - "alias": "countedCommits", - "name": "commits", - "storageKey": null, + "kind": "ScalarField", + "alias": null, + "name": "changedFiles", "args": null, - "concreteType": "PullRequestCommitConnection", - "plural": false, - "selections": v1 + "storageKey": null }, { "kind": "FragmentSpread", "name": "prStatusesView_pullRequest", "args": null }, - v2, v3, v4, + { + "kind": "LinkedField", + "alias": "countedCommits", + "name": "commits", + "storageKey": null, + "args": null, + "concreteType": "PullRequestCommitConnection", + "plural": false, + "selections": v1 + }, v5, { "kind": "ScalarField", @@ -255,9 +263,9 @@ return { "kind": "InlineFragment", "type": "Issue", "selections": [ - v2, v3, v4, + v2, v5, v7, { @@ -271,5 +279,5 @@ return { }; })(); // prettier-ignore -(node/*: any*/).hash = '396343db62d5fc803ae1913d7122eb6e'; +(node/*: any*/).hash = '1abbbad89e413b8d11c163c63987a99a'; module.exports = node; diff --git a/lib/views/issueish-detail-view.js b/lib/views/issueish-detail-view.js index 10ccb1e10e..dc86395dc7 100644 --- a/lib/views/issueish-detail-view.js +++ b/lib/views/issueish-detail-view.js @@ -56,6 +56,7 @@ export class BareIssueishDetailView extends React.Component { countedCommits: PropTypes.shape({ totalCount: PropTypes.number.isRequired, }).isRequired, + changedFiles: PropTypes.number.isRequired, url: PropTypes.string.isRequired, bodyHTML: PropTypes.string, number: PropTypes.number, @@ -138,7 +139,7 @@ export class BareIssueishDetailView extends React.Component { {issueish.author.login} wants to merge{' '} {issueish.countedCommits.totalCount} commits and{' '} - 3 changed files into{' '} + {issueish.changedFiles} changed files into{' '} {issueish.baseRefName} from{' '} {issueish.headRefName} @@ -300,6 +301,7 @@ export default createRefetchContainer(BareIssueishDetailView, { } ... on PullRequest { + changedFiles countedCommits: commits { totalCount } diff --git a/test/fixtures/props/issueish-pane-props.js b/test/fixtures/props/issueish-pane-props.js index e496d1caf3..710c4ce172 100644 --- a/test/fixtures/props/issueish-pane-props.js +++ b/test/fixtures/props/issueish-pane-props.js @@ -87,6 +87,7 @@ export function issueishDetailViewProps(opts, overrides = {}) { issueishHeadRepoName: 'head-name', issueishReactions: [], issueishCommitCount: 0, + issueishChangedFileCount: 0, relayRefetch: () => {}, ...opts, @@ -127,6 +128,7 @@ export function issueishDetailViewProps(opts, overrides = {}) { countedCommits: { totalCount: o.issueishCommitCount, }, + changedFiles: o.issueishChangedFileCount, headRefName: o.issueishHeadRef, headRepository: { name: o.issueishHeadRepoName, diff --git a/test/views/issueish-detail-view.test.js b/test/views/issueish-detail-view.test.js index b42943c37a..09994f2d7a 100644 --- a/test/views/issueish-detail-view.test.js +++ b/test/views/issueish-detail-view.test.js @@ -12,6 +12,7 @@ describe('IssueishDetailView', function() { it('renders pull request information', function() { const commitCount = 11; + const fileCount = 22; const wrapper = shallow(buildApp({ repositoryName: 'repo', ownerLogin: 'user0', @@ -24,6 +25,7 @@ describe('IssueishDetailView', function() { issueishNumber: 100, issueishState: 'MERGED', issueishCommitCount: commitCount, + issueishChangedFileCount: fileCount, issueishReactions: [{content: 'THUMBS_UP', count: 10}, {content: 'THUMBS_DOWN', count: 5}, {content: 'LAUGH', count: 0}], })); @@ -59,6 +61,7 @@ describe('IssueishDetailView', function() { assert.isNotNull(wrapper.find('Relay(BarePrStatusesView)[displayType="full"]').prop('pullRequest')); assert.strictEqual(wrapper.find('.github-IssueishDetailView-commitCount').text(), `${commitCount} commits`); + assert.strictEqual(wrapper.find('.github-IssueishDetailView-fileCount').text(), `${fileCount} changed files`); }); it('renders issue information', function() { From ad6db4dd5c0ac5c2fd8f77c6569ec824ca695f92 Mon Sep 17 00:00:00 2001 From: Tilde Ann Thurium Date: Thu, 9 Aug 2018 09:45:26 -0700 Subject: [PATCH 0111/4252] :shirt: Co-Authored-By: Ash Wilson --- lib/views/issueish-detail-view.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/views/issueish-detail-view.js b/lib/views/issueish-detail-view.js index dc86395dc7..73f51b3ee7 100644 --- a/lib/views/issueish-detail-view.js +++ b/lib/views/issueish-detail-view.js @@ -138,8 +138,10 @@ export class BareIssueishDetailView extends React.Component { {issueish.author.login} wants to merge{' '} - {issueish.countedCommits.totalCount} commits and{' '} - {issueish.changedFiles} changed files into{' '} + {issueish.countedCommits.totalCount} commits and{' '} + {issueish.changedFiles} changed files into{' '} {issueish.baseRefName} from{' '} {issueish.headRefName} @@ -163,8 +165,6 @@ export class BareIssueishDetailView extends React.Component {
- - {isPr &&
} From 8e403cbd33203fb2f500ae7897aee412ec0a8250 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 9 Aug 2018 13:11:38 -0400 Subject: [PATCH 0112/4252] Support calling .destroy() on unmounted item components --- lib/helpers.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/helpers.js b/lib/helpers.js index 5e7c020f11..cb630481d7 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -416,6 +416,10 @@ export function createItem(node, componentHolder = null, uri = null, extra = {}) getRealItemPromise: () => componentHolder.getPromise(), + destroy: () => componentHolder.map(component => { + return component.destroy && component.destroy(); + }), + ...extra, }; From 99eccda034176936bf6069de9582650fc471a10b Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 9 Aug 2018 13:28:56 -0400 Subject: [PATCH 0113/4252] Sometimes componentHolder itself is null :eyes: --- lib/helpers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/helpers.js b/lib/helpers.js index cb630481d7..794e7c1896 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -416,7 +416,7 @@ export function createItem(node, componentHolder = null, uri = null, extra = {}) getRealItemPromise: () => componentHolder.getPromise(), - destroy: () => componentHolder.map(component => { + destroy: () => componentHolder && componentHolder.map(component => { return component.destroy && component.destroy(); }), From ab1a11236ce500983692310c78471df7e85e2d25 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 9 Aug 2018 14:25:54 -0400 Subject: [PATCH 0114/4252] Cover those last two touched GitTabController methods --- test/controllers/git-tab-controller.test.js | 31 +++++++++++++++++++-- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/test/controllers/git-tab-controller.test.js b/test/controllers/git-tab-controller.test.js index 41d9f225dc..890b39dfaf 100644 --- a/test/controllers/git-tab-controller.test.js +++ b/test/controllers/git-tab-controller.test.js @@ -196,11 +196,19 @@ describe('GitTabController', function() { sinon.spy(stagingView, 'setFocus'); + await controller.quietlySelectItem('unstaged-3.txt', 'unstaged'); + + const selections0 = Array.from(stagingView.state.selection.getSelectedItems()); + assert.lengthOf(selections0, 1); + assert.equal(selections0[0].filePath, 'unstaged-3.txt'); + + assert.isFalse(stagingView.setFocus.called); + await controller.focusAndSelectStagingItem('unstaged-2.txt', 'unstaged'); - const selections = Array.from(stagingView.state.selection.getSelectedItems()); - assert.lengthOf(selections, 1); - assert.equal(selections[0].filePath, 'unstaged-2.txt'); + const selections1 = Array.from(stagingView.state.selection.getSelectedItems()); + assert.lengthOf(selections1, 1); + assert.equal(selections1[0].filePath, 'unstaged-2.txt'); assert.equal(stagingView.setFocus.callCount, 1); }); @@ -238,6 +246,23 @@ describe('GitTabController', function() { assert.isFalse(view.setFocus.called); }); + it('detects focus', async function() { + const repository = await buildRepository(await cloneRepository()); + const wrapper = mount(await buildApp(repository)); + const rootElement = wrapper.instance().refRoot.get(); + sinon.stub(rootElement, 'contains'); + + rootElement.contains.returns(true); + assert.isTrue(wrapper.instance().hasFocus()); + + rootElement.contains.returns(false); + assert.isFalse(wrapper.instance().hasFocus()); + + rootElement.contains.returns(true); + wrapper.instance().refRoot.setter(null); + assert.isFalse(wrapper.instance().hasFocus()); + }); + it('does nothing on an absent repository', async function() { const repository = Repository.absent(); From 0f3d13d55008d3ada0e8d4e38c4fc5458a7f973c Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 9 Aug 2018 14:57:58 -0400 Subject: [PATCH 0115/4252] The DOM node can be detached before the animation frame is triggered --- lib/views/staging-view.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/views/staging-view.js b/lib/views/staging-view.js index 7f92cfb9ac..c0d567d445 100644 --- a/lib/views/staging-view.js +++ b/lib/views/staging-view.js @@ -739,6 +739,9 @@ export default class StagingView extends React.Component { const newEvent = new MouseEvent(event.type, event); requestAnimationFrame(() => { + if (!event.target.parentNode) { + return; + } event.target.parentNode.dispatchEvent(newEvent); }); } From c760c9f30c124f23c7670c7b6d57dc3ec715b237 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 9 Aug 2018 15:24:06 -0400 Subject: [PATCH 0116/4252] Don't detect executable mode changes on added or deleted files --- lib/models/file-patch.js | 6 ++++++ test/models/file-patch.test.js | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/lib/models/file-patch.js b/lib/models/file-patch.js index 0e0a91cf0f..6641974de7 100644 --- a/lib/models/file-patch.js +++ b/lib/models/file-patch.js @@ -131,6 +131,12 @@ export default class FilePatch { didChangeExecutableMode() { const oldMode = this.getOldMode(); const newMode = this.getNewMode(); + + if (!oldMode || !newMode) { + // Addition or deletion + return false; + } + return oldMode === '100755' && newMode !== '100755' || oldMode !== '100755' && newMode === '100755'; } diff --git a/test/models/file-patch.test.js b/test/models/file-patch.test.js index e0e7aab8c2..09998c40c6 100644 --- a/test/models/file-patch.test.js +++ b/test/models/file-patch.test.js @@ -17,6 +17,38 @@ function createFilePatch(oldFilePath, newFilePath, status, hunks) { } describe('FilePatch', function() { + it('detects executable mode changes', function() { + const of0 = new FilePatch.File({path: 'a.txt', mode: '100644'}); + const nf0 = new FilePatch.File({path: 'a.txt', mode: '100755'}); + const p0 = new FilePatch.Patch({status: 'modified', hunks: []}); + const fp0 = new FilePatch(of0, nf0, p0); + assert.isTrue(fp0.didChangeExecutableMode()); + + const of1 = new FilePatch.File({path: 'a.txt', mode: '100755'}); + const nf1 = new FilePatch.File({path: 'a.txt', mode: '100644'}); + const p1 = new FilePatch.Patch({status: 'modified', hunks: []}); + const fp1 = new FilePatch(of1, nf1, p1); + assert.isTrue(fp1.didChangeExecutableMode()); + + const of2 = new FilePatch.File({path: 'a.txt', mode: '100755'}); + const nf2 = new FilePatch.File({path: 'a.txt', mode: '100755'}); + const p2 = new FilePatch.Patch({status: 'modified', hunks: []}); + const fp2 = new FilePatch(of2, nf2, p2); + assert.isFalse(fp2.didChangeExecutableMode()); + + const of3 = FilePatch.File.empty(); + const nf3 = new FilePatch.File({path: 'a.txt', mode: '100755'}); + const p3 = new FilePatch.Patch({status: 'modified', hunks: []}); + const fp3 = new FilePatch(of3, nf3, p3); + assert.isFalse(fp3.didChangeExecutableMode()); + + const of4 = FilePatch.File.empty(); + const nf4 = new FilePatch.File({path: 'a.txt', mode: '100755'}); + const p4 = new FilePatch.Patch({status: 'modified', hunks: []}); + const fp4 = new FilePatch(of4, nf4, p4); + assert.isFalse(fp4.didChangeExecutableMode()); + }); + describe('getStagePatchForLines()', function() { it('returns a new FilePatch that applies only the specified lines', function() { const filePatch = createFilePatch('a.txt', 'a.txt', 'modified', [ From 2cb337b9bfe7051ba40a31726d162e981f978226 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 9 Aug 2018 15:34:22 -0400 Subject: [PATCH 0117/4252] Don't attempt to stage or unstage with no selection --- lib/views/file-patch-view.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/views/file-patch-view.js b/lib/views/file-patch-view.js index e8067025dc..efd9e7edf9 100644 --- a/lib/views/file-patch-view.js +++ b/lib/views/file-patch-view.js @@ -668,7 +668,12 @@ export default class FilePatchView extends React.Component { } didConfirm() { - return this.didClickStageButtonForHunk([...this.state.selection.getSelectedHunks()][0]); + const hunk = [...this.state.selection.getSelectedHunks()][0]; + if (!hunk) { + return; + } + + this.didClickStageButtonForHunk(hunk); } didMoveRight() { From 6ed02ab06cea6abd9377b0d7f19be1a0c0c58742 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 9 Aug 2018 16:03:57 -0400 Subject: [PATCH 0118/4252] Use deleteRef() to "reset" the initial commit --- lib/git-shell-out-strategy.js | 4 ++++ test/git-strategies.test.js | 25 +++++++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/lib/git-shell-out-strategy.js b/lib/git-shell-out-strategy.js index 011e07f83d..c8d5ae1a2b 100644 --- a/lib/git-shell-out-strategy.js +++ b/lib/git-shell-out-strategy.js @@ -812,6 +812,10 @@ export default class GitShellOutStrategy { return this.exec(['reset', `--${type}`, revision]); } + deleteRef(ref) { + return this.exec(['update-ref', '-d', ref]); + } + /** * Branches */ diff --git a/test/git-strategies.test.js b/test/git-strategies.test.js index b5c6c53c71..686138469d 100644 --- a/test/git-strategies.test.js +++ b/test/git-strategies.test.js @@ -714,6 +714,31 @@ import * as reporterProxy from '../lib/reporter-proxy'; }); }); + describe('deleteRef()', function() { + it('soft-resets an initial commit', async function() { + const workingDirPath = await cloneRepository('three-files'); + const git = createTestStrategy(workingDirPath); + + // Ensure that three-files still has only a single commit + assert.lengthOf(await git.getCommits({max: 10}), 1); + + // Put something into the index to ensure it doesn't get lost + fs.appendFileSync(path.join(workingDirPath, 'a.txt'), 'zzz\n', 'utf8'); + await git.exec(['add', '.']); + + await git.deleteRef('HEAD'); + + const after = await git.getCommit('HEAD'); + assert.isTrue(after.unbornRef); + + const stagedChanges = await git.getDiffsForFilePath('a.txt', {staged: true}); + assert.lengthOf(stagedChanges, 1); + const stagedChange = stagedChanges[0]; + assert.strictEqual(stagedChange.newPath, 'a.txt'); + assert.deepEqual(stagedChange.hunks[0].lines, ['+foo', '+zzz']); + }); + }); + describe('getBranches()', function() { const sha = '66d11860af6d28eb38349ef83de475597cb0e8b4'; From abd40e10fe8577f619ee3391d2f739dad12392b9 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 9 Aug 2018 16:21:30 -0400 Subject: [PATCH 0119/4252] Fall back to update-ref -d on the initial commit --- lib/models/repository-states/present.js | 13 +++++- test/models/repository.test.js | 57 +++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js index c7b42da7c6..a6034a898b 100644 --- a/lib/models/repository-states/present.js +++ b/lib/models/repository-states/present.js @@ -341,7 +341,18 @@ export default class Present extends State { ...Keys.filePatch.eachWithOpts({staged: true}), Keys.headDescription, ], - () => this.git().reset('soft', 'HEAD~'), + async () => { + try { + await this.git().reset('soft', 'HEAD~'); + } catch (e) { + if (/unknown revision/.test(e.stdErr)) { + // Initial commit + await this.git().deleteRef('HEAD'); + } else { + throw e; + } + } + }, ); } diff --git a/test/models/repository.test.js b/test/models/repository.test.js index 0e7c9274d4..3003496211 100644 --- a/test/models/repository.test.js +++ b/test/models/repository.test.js @@ -575,6 +575,63 @@ describe('Repository', function() { }); }); + describe('undoLastCommit()', function() { + it('performs a soft reset', async function() { + const workingDirPath = await cloneRepository('multiple-commits'); + const repo = new Repository(workingDirPath); + await repo.getLoadPromise(); + + fs.appendFileSync(path.join(workingDirPath, 'file.txt'), 'qqq\n', 'utf8'); + await repo.git.exec(['add', '.']); + await repo.git.commit('add stuff'); + + const parentCommit = await repo.git.getCommit('HEAD~'); + + await repo.undoLastCommit(); + + const commitAfterReset = await repo.git.getCommit('HEAD'); + assert.strictEqual(commitAfterReset.sha, parentCommit.sha); + + const fp = await repo.getFilePatchForPath('file.txt', {staged: true}); + assert.strictEqual( + fp.toString(), + dedent` + diff --git a/file.txt b/file.txt + --- a/file.txt + +++ b/file.txt + @@ -1,1 +1,2 @@ + three + +qqq\n + `, + ); + }); + + it('deletes the HEAD ref when only a single commit is present', async function() { + const workingDirPath = await cloneRepository('three-files'); + const repo = new Repository(workingDirPath); + await repo.getLoadPromise(); + + fs.appendFileSync(path.join(workingDirPath, 'b.txt'), 'qqq\n', 'utf8'); + await repo.git.exec(['add', '.']); + + await repo.undoLastCommit(); + + const fp = await repo.getFilePatchForPath('b.txt', {staged: true}); + assert.strictEqual( + fp.toString(), + dedent` + diff --git a/b.txt b/b.txt + new file mode 100644 + --- /dev/null + +++ b/b.txt + @@ -0,0 +1,2 @@ + +bar + +qqq\n + `, + ); + }); + }); + describe('fetch(branchName, {remoteName})', function() { it('brings commits from the remote and updates remote branch, and does not update branch', async function() { const {localRepoPath} = await setUpLocalAndRemoteRepositories({remoteAhead: true}); From 9d1b9bb1bbcfe6cf94f98c412fbffa930cbf8868 Mon Sep 17 00:00:00 2001 From: Tilde Ann Thurium Date: Thu, 9 Aug 2018 14:06:18 -0700 Subject: [PATCH 0120/4252] fix issueish-detail-container test --- test/fixtures/factories/pull-request-result.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/fixtures/factories/pull-request-result.js b/test/fixtures/factories/pull-request-result.js index 5d89b387b0..abe137acf9 100644 --- a/test/fixtures/factories/pull-request-result.js +++ b/test/fixtures/factories/pull-request-result.js @@ -130,6 +130,9 @@ export function createPullRequestDetailResult(attrs = {}) { headRepositoryName: 'headrepo', headRepositoryLogin: 'headlogin', baseRepositoryLogin: 'baseLogin', + changedFileCount: 0, + commitCount: 0, + baseRefName: 'baseRefName', ...attrs, }; @@ -143,8 +146,13 @@ export function createPullRequestDetailResult(attrs = {}) { id: o.id, title: o.title, number: o.number, + countedCommits: { + totalCount: o.commitCount + }, + changedFiles: o.changedFileCount, state: o.state, bodyHTML: '

body

', + baseRefName: o.baseRefName, author: { __typename: 'User', id: idGen.generate('user'), From 82ffd493c22e7c4ff982728c1743ab78341ea8e5 Mon Sep 17 00:00:00 2001 From: Tilde Ann Thurium Date: Thu, 9 Aug 2018 14:44:51 -0700 Subject: [PATCH 0121/4252] label owners and authors for cross repository pull requests --- .../issueishDetailContainerQuery.graphql.js | 143 +++++++------- .../issueishDetailViewRefetchQuery.graphql.js | 179 +++++++++--------- .../issueishDetailView_issueish.graphql.js | 26 ++- lib/views/issueish-detail-view.js | 7 +- 4 files changed, 186 insertions(+), 169 deletions(-) diff --git a/lib/containers/__generated__/issueishDetailContainerQuery.graphql.js b/lib/containers/__generated__/issueishDetailContainerQuery.graphql.js index ecd96e7f8c..72cf4818ef 100644 --- a/lib/containers/__generated__/issueishDetailContainerQuery.graphql.js +++ b/lib/containers/__generated__/issueishDetailContainerQuery.graphql.js @@ -1,6 +1,6 @@ /** * @flow - * @relayHash 3ff8f08909b2d8ece9d3fe0bf2950805 + * @relayHash 912d92f3084e3cdb76ba148142374ae5 */ /* eslint-disable */ @@ -118,6 +118,7 @@ fragment issueishDetailView_issueish_3D8CP9 on IssueOrPullRequest { ...issueTimelineController_issue_3D8CP9 } ... on PullRequest { + isCrossRepository changedFiles countedCommits: commits { totalCount @@ -600,26 +601,33 @@ v13 = { "storageKey": null }, v14 = { + "kind": "ScalarField", + "alias": null, + "name": "isCrossRepository", + "args": null, + "storageKey": null +}, +v15 = { "kind": "ScalarField", "alias": null, "name": "avatarUrl", "args": null, "storageKey": null }, -v15 = [ +v16 = [ v4, v5, - v14, + v15, v2 ], -v16 = { +v17 = { "kind": "ScalarField", "alias": null, "name": "number", "args": null, "storageKey": null }, -v17 = { +v18 = { "kind": "InlineFragment", "type": "CrossReferencedEvent", "selections": [ @@ -630,13 +638,7 @@ v17 = { "args": null, "storageKey": null }, - { - "kind": "ScalarField", - "alias": null, - "name": "isCrossRepository", - "args": null, - "storageKey": null - }, + v14, { "kind": "LinkedField", "alias": null, @@ -645,7 +647,7 @@ v17 = { "args": null, "concreteType": null, "plural": false, - "selections": v15 + "selections": v16 }, { "kind": "LinkedField", @@ -683,7 +685,7 @@ v17 = { "kind": "InlineFragment", "type": "PullRequest", "selections": [ - v16, + v17, v9, v10, { @@ -699,7 +701,7 @@ v17 = { "kind": "InlineFragment", "type": "Issue", "selections": [ - v16, + v17, v9, v10, { @@ -715,18 +717,18 @@ v17 = { } ] }, -v18 = { +v19 = { "kind": "ScalarField", "alias": null, "name": "oid", "args": null, "storageKey": null }, -v19 = [ - v18, +v20 = [ + v19, v2 ], -v20 = { +v21 = { "kind": "LinkedField", "alias": null, "name": "commit", @@ -734,29 +736,29 @@ v20 = { "args": null, "concreteType": "Commit", "plural": false, - "selections": v19 + "selections": v20 }, -v21 = { +v22 = { "kind": "ScalarField", "alias": null, "name": "bodyHTML", "args": null, "storageKey": null }, -v22 = { +v23 = { "kind": "ScalarField", "alias": null, "name": "createdAt", "args": null, "storageKey": null }, -v23 = [ +v24 = [ v4, - v14, + v15, v5, v2 ], -v24 = { +v25 = { "kind": "LinkedField", "alias": null, "name": "actor", @@ -764,9 +766,9 @@ v24 = { "args": null, "concreteType": null, "plural": false, - "selections": v23 + "selections": v24 }, -v25 = { +v26 = { "kind": "InlineFragment", "type": "IssueComment", "selections": [ @@ -778,14 +780,14 @@ v25 = { "args": null, "concreteType": null, "plural": false, - "selections": v23 + "selections": v24 }, - v21, v22, + v23, v10 ] }, -v26 = { +v27 = { "kind": "LinkedField", "alias": null, "name": "user", @@ -798,7 +800,7 @@ v26 = { v2 ] }, -v27 = { +v28 = { "kind": "InlineFragment", "type": "Commit", "selections": [ @@ -812,8 +814,8 @@ v27 = { "plural": false, "selections": [ v3, - v26, - v14 + v27, + v15 ] }, { @@ -826,8 +828,8 @@ v27 = { "plural": false, "selections": [ v3, - v14, - v26 + v15, + v27 ] }, { @@ -837,7 +839,7 @@ v27 = { "args": null, "storageKey": null }, - v18, + v19, { "kind": "ScalarField", "alias": null, @@ -861,7 +863,7 @@ v27 = { } ] }, -v28 = [ +v29 = [ { "kind": "ScalarField", "alias": null, @@ -870,10 +872,10 @@ v28 = [ "storageKey": null } ], -v29 = [ +v30 = [ v10 ], -v30 = { +v31 = { "kind": "LinkedField", "alias": null, "name": "author", @@ -884,21 +886,21 @@ v30 = { "selections": [ v4, v5, - v14, + v15, v2, { "kind": "InlineFragment", "type": "Bot", - "selections": v29 + "selections": v30 }, { "kind": "InlineFragment", "type": "User", - "selections": v29 + "selections": v30 } ] }, -v31 = { +v32 = { "kind": "LinkedField", "alias": null, "name": "reactionGroups", @@ -922,7 +924,7 @@ v31 = { "args": null, "concreteType": "ReactingUserConnection", "plural": false, - "selections": v28 + "selections": v29 } ] }; @@ -931,7 +933,7 @@ return { "operationKind": "query", "name": "issueishDetailContainerQuery", "id": null, - "text": "query issueishDetailContainerQuery(\n $repoOwner: String!\n $repoName: String!\n $issueishNumber: Int!\n $timelineCount: Int!\n $timelineCursor: String\n) {\n repository(owner: $repoOwner, name: $repoName) {\n ...issueishDetailController_repository_n0A9R\n id\n }\n}\n\nfragment issueishDetailController_repository_n0A9R on Repository {\n ...issueishDetailView_repository\n name\n owner {\n __typename\n login\n id\n }\n issueish: issueOrPullRequest(number: $issueishNumber) {\n __typename\n ... on Issue {\n title\n number\n ...issueishDetailView_issueish_3D8CP9\n }\n ... on PullRequest {\n title\n number\n headRefName\n headRepository {\n name\n owner {\n __typename\n login\n id\n }\n url\n sshUrl\n id\n }\n ...issueishDetailView_issueish_3D8CP9\n }\n ... on Node {\n id\n }\n }\n}\n\nfragment issueishDetailView_repository on Repository {\n id\n name\n owner {\n __typename\n login\n id\n }\n}\n\nfragment issueishDetailView_issueish_3D8CP9 on IssueOrPullRequest {\n __typename\n ... on Node {\n id\n }\n ... on Issue {\n state\n number\n title\n bodyHTML\n author {\n __typename\n login\n avatarUrl\n ... on User {\n url\n }\n ... on Bot {\n url\n }\n ... on Node {\n id\n }\n }\n ...issueTimelineController_issue_3D8CP9\n }\n ... on PullRequest {\n changedFiles\n countedCommits: commits {\n totalCount\n }\n ...prStatusesView_pullRequest\n state\n number\n title\n bodyHTML\n baseRefName\n headRefName\n author {\n __typename\n login\n avatarUrl\n ... on User {\n url\n }\n ... on Bot {\n url\n }\n ... on Node {\n id\n }\n }\n ...prTimelineController_pullRequest_3D8CP9\n }\n ... on UniformResourceLocatable {\n url\n }\n ... on Reactable {\n reactionGroups {\n content\n users {\n totalCount\n }\n }\n }\n}\n\nfragment issueTimelineController_issue_3D8CP9 on Issue {\n url\n timeline(first: $timelineCount, after: $timelineCursor) {\n pageInfo {\n endCursor\n hasNextPage\n }\n edges {\n cursor\n node {\n __typename\n ...commitsView_nodes\n ...issueCommentView_item\n ...crossReferencedEventsView_nodes\n ... on Node {\n id\n }\n }\n }\n }\n}\n\nfragment prStatusesView_pullRequest on PullRequest {\n id\n commits(last: 1) {\n edges {\n node {\n commit {\n status {\n state\n contexts {\n id\n state\n ...prStatusContextView_context\n }\n id\n }\n id\n }\n id\n }\n }\n }\n}\n\nfragment prTimelineController_pullRequest_3D8CP9 on PullRequest {\n url\n ...headRefForcePushedEventView_issueish\n timeline(first: $timelineCount, after: $timelineCursor) {\n pageInfo {\n endCursor\n hasNextPage\n }\n edges {\n cursor\n node {\n __typename\n ...commitsView_nodes\n ...issueCommentView_item\n ...mergedEventView_item\n ...headRefForcePushedEventView_item\n ...commitCommentThreadView_item\n ...crossReferencedEventsView_nodes\n ... on Node {\n id\n }\n }\n }\n }\n}\n\nfragment headRefForcePushedEventView_issueish on PullRequest {\n headRefName\n headRepositoryOwner {\n __typename\n login\n id\n }\n repository {\n owner {\n __typename\n login\n id\n }\n id\n }\n}\n\nfragment commitsView_nodes on Commit {\n id\n author {\n name\n user {\n login\n id\n }\n }\n ...commitView_item\n}\n\nfragment issueCommentView_item on IssueComment {\n author {\n __typename\n avatarUrl\n login\n ... on Node {\n id\n }\n }\n bodyHTML\n createdAt\n url\n}\n\nfragment mergedEventView_item on MergedEvent {\n actor {\n __typename\n avatarUrl\n login\n ... on Node {\n id\n }\n }\n commit {\n oid\n id\n }\n mergeRefName\n createdAt\n}\n\nfragment headRefForcePushedEventView_item on HeadRefForcePushedEvent {\n actor {\n __typename\n avatarUrl\n login\n ... on Node {\n id\n }\n }\n beforeCommit {\n oid\n id\n }\n afterCommit {\n oid\n id\n }\n createdAt\n}\n\nfragment commitCommentThreadView_item on CommitCommentThread {\n commit {\n oid\n id\n }\n comments(first: 100) {\n edges {\n node {\n id\n ...commitCommentView_item\n }\n }\n }\n}\n\nfragment crossReferencedEventsView_nodes on CrossReferencedEvent {\n id\n referencedAt\n isCrossRepository\n actor {\n __typename\n login\n avatarUrl\n ... on Node {\n id\n }\n }\n source {\n __typename\n ... on RepositoryNode {\n repository {\n name\n owner {\n __typename\n login\n id\n }\n id\n }\n }\n ... on Node {\n id\n }\n }\n ...crossReferencedEventView_item\n}\n\nfragment crossReferencedEventView_item on CrossReferencedEvent {\n id\n isCrossRepository\n source {\n __typename\n ... on Issue {\n number\n title\n url\n issueState: state\n }\n ... on PullRequest {\n number\n title\n url\n prState: state\n }\n ... on RepositoryNode {\n repository {\n name\n isPrivate\n owner {\n __typename\n login\n id\n }\n id\n }\n }\n ... on Node {\n id\n }\n }\n}\n\nfragment commitCommentView_item on CommitComment {\n author {\n __typename\n login\n avatarUrl\n ... on Node {\n id\n }\n }\n commit {\n oid\n id\n }\n bodyHTML\n createdAt\n path\n position\n}\n\nfragment commitView_item on Commit {\n author {\n name\n avatarUrl\n user {\n login\n id\n }\n }\n committer {\n name\n avatarUrl\n user {\n login\n id\n }\n }\n authoredByCommitter\n oid\n message\n messageHeadlineHTML\n commitUrl\n}\n\nfragment prStatusContextView_context on StatusContext {\n context\n description\n state\n targetUrl\n}\n", + "text": "query issueishDetailContainerQuery(\n $repoOwner: String!\n $repoName: String!\n $issueishNumber: Int!\n $timelineCount: Int!\n $timelineCursor: String\n) {\n repository(owner: $repoOwner, name: $repoName) {\n ...issueishDetailController_repository_n0A9R\n id\n }\n}\n\nfragment issueishDetailController_repository_n0A9R on Repository {\n ...issueishDetailView_repository\n name\n owner {\n __typename\n login\n id\n }\n issueish: issueOrPullRequest(number: $issueishNumber) {\n __typename\n ... on Issue {\n title\n number\n ...issueishDetailView_issueish_3D8CP9\n }\n ... on PullRequest {\n title\n number\n headRefName\n headRepository {\n name\n owner {\n __typename\n login\n id\n }\n url\n sshUrl\n id\n }\n ...issueishDetailView_issueish_3D8CP9\n }\n ... on Node {\n id\n }\n }\n}\n\nfragment issueishDetailView_repository on Repository {\n id\n name\n owner {\n __typename\n login\n id\n }\n}\n\nfragment issueishDetailView_issueish_3D8CP9 on IssueOrPullRequest {\n __typename\n ... on Node {\n id\n }\n ... on Issue {\n state\n number\n title\n bodyHTML\n author {\n __typename\n login\n avatarUrl\n ... on User {\n url\n }\n ... on Bot {\n url\n }\n ... on Node {\n id\n }\n }\n ...issueTimelineController_issue_3D8CP9\n }\n ... on PullRequest {\n isCrossRepository\n changedFiles\n countedCommits: commits {\n totalCount\n }\n ...prStatusesView_pullRequest\n state\n number\n title\n bodyHTML\n baseRefName\n headRefName\n author {\n __typename\n login\n avatarUrl\n ... on User {\n url\n }\n ... on Bot {\n url\n }\n ... on Node {\n id\n }\n }\n ...prTimelineController_pullRequest_3D8CP9\n }\n ... on UniformResourceLocatable {\n url\n }\n ... on Reactable {\n reactionGroups {\n content\n users {\n totalCount\n }\n }\n }\n}\n\nfragment issueTimelineController_issue_3D8CP9 on Issue {\n url\n timeline(first: $timelineCount, after: $timelineCursor) {\n pageInfo {\n endCursor\n hasNextPage\n }\n edges {\n cursor\n node {\n __typename\n ...commitsView_nodes\n ...issueCommentView_item\n ...crossReferencedEventsView_nodes\n ... on Node {\n id\n }\n }\n }\n }\n}\n\nfragment prStatusesView_pullRequest on PullRequest {\n id\n commits(last: 1) {\n edges {\n node {\n commit {\n status {\n state\n contexts {\n id\n state\n ...prStatusContextView_context\n }\n id\n }\n id\n }\n id\n }\n }\n }\n}\n\nfragment prTimelineController_pullRequest_3D8CP9 on PullRequest {\n url\n ...headRefForcePushedEventView_issueish\n timeline(first: $timelineCount, after: $timelineCursor) {\n pageInfo {\n endCursor\n hasNextPage\n }\n edges {\n cursor\n node {\n __typename\n ...commitsView_nodes\n ...issueCommentView_item\n ...mergedEventView_item\n ...headRefForcePushedEventView_item\n ...commitCommentThreadView_item\n ...crossReferencedEventsView_nodes\n ... on Node {\n id\n }\n }\n }\n }\n}\n\nfragment headRefForcePushedEventView_issueish on PullRequest {\n headRefName\n headRepositoryOwner {\n __typename\n login\n id\n }\n repository {\n owner {\n __typename\n login\n id\n }\n id\n }\n}\n\nfragment commitsView_nodes on Commit {\n id\n author {\n name\n user {\n login\n id\n }\n }\n ...commitView_item\n}\n\nfragment issueCommentView_item on IssueComment {\n author {\n __typename\n avatarUrl\n login\n ... on Node {\n id\n }\n }\n bodyHTML\n createdAt\n url\n}\n\nfragment mergedEventView_item on MergedEvent {\n actor {\n __typename\n avatarUrl\n login\n ... on Node {\n id\n }\n }\n commit {\n oid\n id\n }\n mergeRefName\n createdAt\n}\n\nfragment headRefForcePushedEventView_item on HeadRefForcePushedEvent {\n actor {\n __typename\n avatarUrl\n login\n ... on Node {\n id\n }\n }\n beforeCommit {\n oid\n id\n }\n afterCommit {\n oid\n id\n }\n createdAt\n}\n\nfragment commitCommentThreadView_item on CommitCommentThread {\n commit {\n oid\n id\n }\n comments(first: 100) {\n edges {\n node {\n id\n ...commitCommentView_item\n }\n }\n }\n}\n\nfragment crossReferencedEventsView_nodes on CrossReferencedEvent {\n id\n referencedAt\n isCrossRepository\n actor {\n __typename\n login\n avatarUrl\n ... on Node {\n id\n }\n }\n source {\n __typename\n ... on RepositoryNode {\n repository {\n name\n owner {\n __typename\n login\n id\n }\n id\n }\n }\n ... on Node {\n id\n }\n }\n ...crossReferencedEventView_item\n}\n\nfragment crossReferencedEventView_item on CrossReferencedEvent {\n id\n isCrossRepository\n source {\n __typename\n ... on Issue {\n number\n title\n url\n issueState: state\n }\n ... on PullRequest {\n number\n title\n url\n prState: state\n }\n ... on RepositoryNode {\n repository {\n name\n isPrivate\n owner {\n __typename\n login\n id\n }\n id\n }\n }\n ... on Node {\n id\n }\n }\n}\n\nfragment commitCommentView_item on CommitComment {\n author {\n __typename\n login\n avatarUrl\n ... on Node {\n id\n }\n }\n commit {\n oid\n id\n }\n bodyHTML\n createdAt\n path\n position\n}\n\nfragment commitView_item on Commit {\n author {\n name\n avatarUrl\n user {\n login\n id\n }\n }\n committer {\n name\n avatarUrl\n user {\n login\n id\n }\n }\n authoredByCommitter\n oid\n message\n messageHeadlineHTML\n commitUrl\n}\n\nfragment prStatusContextView_context on StatusContext {\n context\n description\n state\n targetUrl\n}\n", "metadata": {}, "fragment": { "kind": "Fragment", @@ -1177,12 +1179,12 @@ return { "selections": [ v4, v2, - v17, + v18, { "kind": "InlineFragment", "type": "CommitCommentThread", "selections": [ - v20, + v21, { "kind": "LinkedField", "alias": null, @@ -1226,11 +1228,11 @@ return { "args": null, "concreteType": null, "plural": false, - "selections": v15 + "selections": v16 }, - v20, v21, v22, + v23, { "kind": "ScalarField", "alias": null, @@ -1257,7 +1259,7 @@ return { "kind": "InlineFragment", "type": "HeadRefForcePushedEvent", "selections": [ - v24, + v25, { "kind": "LinkedField", "alias": null, @@ -1266,7 +1268,7 @@ return { "args": null, "concreteType": "Commit", "plural": false, - "selections": v19 + "selections": v20 }, { "kind": "LinkedField", @@ -1276,17 +1278,17 @@ return { "args": null, "concreteType": "Commit", "plural": false, - "selections": v19 + "selections": v20 }, - v22 + v23 ] }, { "kind": "InlineFragment", "type": "MergedEvent", "selections": [ - v24, - v20, + v25, + v21, { "kind": "ScalarField", "alias": null, @@ -1294,11 +1296,11 @@ return { "args": null, "storageKey": null }, - v22 + v23 ] }, - v25, - v27 + v26, + v28 ] } ] @@ -1314,6 +1316,7 @@ return { "key": "prTimelineContainer_timeline", "filters": null }, + v14, { "kind": "ScalarField", "alias": null, @@ -1329,11 +1332,11 @@ return { "args": null, "concreteType": "PullRequestCommitConnection", "plural": false, - "selections": v28 + "selections": v29 }, - v16, + v17, v8, - v21, + v22, { "kind": "ScalarField", "alias": null, @@ -1341,7 +1344,7 @@ return { "args": null, "storageKey": null }, - v30, + v31, v10, { "kind": "LinkedField", @@ -1366,18 +1369,18 @@ return { v2 ] }, - v31 + v32 ] }, { "kind": "InlineFragment", "type": "Issue", "selections": [ - v21, + v22, v9, v8, - v16, - v30, + v17, + v31, v10, { "kind": "LinkedField", @@ -1410,9 +1413,9 @@ return { "selections": [ v4, v2, - v17, - v25, - v27 + v18, + v26, + v28 ] } ] @@ -1428,7 +1431,7 @@ return { "key": "IssueTimelineController_timeline", "filters": null }, - v31 + v32 ] } ] diff --git a/lib/views/__generated__/issueishDetailViewRefetchQuery.graphql.js b/lib/views/__generated__/issueishDetailViewRefetchQuery.graphql.js index 8b0edbf536..cfb36f3823 100644 --- a/lib/views/__generated__/issueishDetailViewRefetchQuery.graphql.js +++ b/lib/views/__generated__/issueishDetailViewRefetchQuery.graphql.js @@ -1,6 +1,6 @@ /** * @flow - * @relayHash 827fdb512f164dfe6bbffbdf0bcb5df8 + * @relayHash 2cc661981c27bf81ed0ac897879ae286 */ /* eslint-disable */ @@ -88,6 +88,7 @@ fragment issueishDetailView_issueish_3D8CP9 on IssueOrPullRequest { ...issueTimelineController_issue_3D8CP9 } ... on PullRequest { + isCrossRepository changedFiles countedCommits: commits { totalCount @@ -538,35 +539,42 @@ v12 = { v13 = { "kind": "ScalarField", "alias": null, - "name": "state", + "name": "isCrossRepository", "args": null, "storageKey": null }, v14 = { "kind": "ScalarField", "alias": null, - "name": "number", + "name": "state", "args": null, "storageKey": null }, v15 = { "kind": "ScalarField", "alias": null, - "name": "title", + "name": "number", "args": null, "storageKey": null }, v16 = { + "kind": "ScalarField", + "alias": null, + "name": "title", + "args": null, + "storageKey": null +}, +v17 = { "kind": "ScalarField", "alias": null, "name": "avatarUrl", "args": null, "storageKey": null }, -v17 = [ +v18 = [ v10 ], -v18 = { +v19 = { "kind": "LinkedField", "alias": null, "name": "author", @@ -577,21 +585,21 @@ v18 = { "selections": [ v4, v7, - v16, + v17, v5, { "kind": "InlineFragment", "type": "Bot", - "selections": v17 + "selections": v18 }, { "kind": "InlineFragment", "type": "User", - "selections": v17 + "selections": v18 } ] }, -v19 = [ +v20 = [ { "kind": "Variable", "name": "after", @@ -605,7 +613,7 @@ v19 = [ "type": "Int" } ], -v20 = { +v21 = { "kind": "LinkedField", "alias": null, "name": "pageInfo", @@ -630,20 +638,20 @@ v20 = { } ] }, -v21 = { +v22 = { "kind": "ScalarField", "alias": null, "name": "cursor", "args": null, "storageKey": null }, -v22 = [ +v23 = [ v4, v7, - v16, + v17, v5 ], -v23 = { +v24 = { "kind": "InlineFragment", "type": "CrossReferencedEvent", "selections": [ @@ -654,13 +662,7 @@ v23 = { "args": null, "storageKey": null }, - { - "kind": "ScalarField", - "alias": null, - "name": "isCrossRepository", - "args": null, - "storageKey": null - }, + v13, { "kind": "LinkedField", "alias": null, @@ -669,7 +671,7 @@ v23 = { "args": null, "concreteType": null, "plural": false, - "selections": v22 + "selections": v23 }, { "kind": "LinkedField", @@ -707,8 +709,8 @@ v23 = { "kind": "InlineFragment", "type": "PullRequest", "selections": [ - v14, v15, + v16, v10, { "kind": "ScalarField", @@ -723,8 +725,8 @@ v23 = { "kind": "InlineFragment", "type": "Issue", "selections": [ - v14, v15, + v16, v10, { "kind": "ScalarField", @@ -739,18 +741,18 @@ v23 = { } ] }, -v24 = { +v25 = { "kind": "ScalarField", "alias": null, "name": "oid", "args": null, "storageKey": null }, -v25 = [ - v24, +v26 = [ + v25, v5 ], -v26 = { +v27 = { "kind": "LinkedField", "alias": null, "name": "commit", @@ -758,22 +760,22 @@ v26 = { "args": null, "concreteType": "Commit", "plural": false, - "selections": v25 + "selections": v26 }, -v27 = { +v28 = { "kind": "ScalarField", "alias": null, "name": "createdAt", "args": null, "storageKey": null }, -v28 = [ +v29 = [ v4, - v16, + v17, v7, v5 ], -v29 = { +v30 = { "kind": "LinkedField", "alias": null, "name": "actor", @@ -781,9 +783,9 @@ v29 = { "args": null, "concreteType": null, "plural": false, - "selections": v28 + "selections": v29 }, -v30 = { +v31 = { "kind": "InlineFragment", "type": "IssueComment", "selections": [ @@ -795,14 +797,14 @@ v30 = { "args": null, "concreteType": null, "plural": false, - "selections": v28 + "selections": v29 }, v12, - v27, + v28, v10 ] }, -v31 = { +v32 = { "kind": "LinkedField", "alias": null, "name": "user", @@ -815,7 +817,7 @@ v31 = { v5 ] }, -v32 = { +v33 = { "kind": "InlineFragment", "type": "Commit", "selections": [ @@ -829,8 +831,8 @@ v32 = { "plural": false, "selections": [ v6, - v31, - v16 + v32, + v17 ] }, { @@ -843,8 +845,8 @@ v32 = { "plural": false, "selections": [ v6, - v16, - v31 + v17, + v32 ] }, { @@ -854,7 +856,7 @@ v32 = { "args": null, "storageKey": null }, - v24, + v25, { "kind": "ScalarField", "alias": null, @@ -883,7 +885,7 @@ return { "operationKind": "query", "name": "issueishDetailViewRefetchQuery", "id": null, - "text": "query issueishDetailViewRefetchQuery(\n $repoId: ID!\n $issueishId: ID!\n $timelineCount: Int!\n $timelineCursor: String\n) {\n repository: node(id: $repoId) {\n __typename\n ...issueishDetailView_repository_3D8CP9\n id\n }\n issueish: node(id: $issueishId) {\n __typename\n ...issueishDetailView_issueish_3D8CP9\n id\n }\n}\n\nfragment issueishDetailView_repository_3D8CP9 on Repository {\n id\n name\n owner {\n __typename\n login\n id\n }\n}\n\nfragment issueishDetailView_issueish_3D8CP9 on IssueOrPullRequest {\n __typename\n ... on Node {\n id\n }\n ... on Issue {\n state\n number\n title\n bodyHTML\n author {\n __typename\n login\n avatarUrl\n ... on User {\n url\n }\n ... on Bot {\n url\n }\n ... on Node {\n id\n }\n }\n ...issueTimelineController_issue_3D8CP9\n }\n ... on PullRequest {\n changedFiles\n countedCommits: commits {\n totalCount\n }\n ...prStatusesView_pullRequest\n state\n number\n title\n bodyHTML\n baseRefName\n headRefName\n author {\n __typename\n login\n avatarUrl\n ... on User {\n url\n }\n ... on Bot {\n url\n }\n ... on Node {\n id\n }\n }\n ...prTimelineController_pullRequest_3D8CP9\n }\n ... on UniformResourceLocatable {\n url\n }\n ... on Reactable {\n reactionGroups {\n content\n users {\n totalCount\n }\n }\n }\n}\n\nfragment issueTimelineController_issue_3D8CP9 on Issue {\n url\n timeline(first: $timelineCount, after: $timelineCursor) {\n pageInfo {\n endCursor\n hasNextPage\n }\n edges {\n cursor\n node {\n __typename\n ...commitsView_nodes\n ...issueCommentView_item\n ...crossReferencedEventsView_nodes\n ... on Node {\n id\n }\n }\n }\n }\n}\n\nfragment prStatusesView_pullRequest on PullRequest {\n id\n commits(last: 1) {\n edges {\n node {\n commit {\n status {\n state\n contexts {\n id\n state\n ...prStatusContextView_context\n }\n id\n }\n id\n }\n id\n }\n }\n }\n}\n\nfragment prTimelineController_pullRequest_3D8CP9 on PullRequest {\n url\n ...headRefForcePushedEventView_issueish\n timeline(first: $timelineCount, after: $timelineCursor) {\n pageInfo {\n endCursor\n hasNextPage\n }\n edges {\n cursor\n node {\n __typename\n ...commitsView_nodes\n ...issueCommentView_item\n ...mergedEventView_item\n ...headRefForcePushedEventView_item\n ...commitCommentThreadView_item\n ...crossReferencedEventsView_nodes\n ... on Node {\n id\n }\n }\n }\n }\n}\n\nfragment headRefForcePushedEventView_issueish on PullRequest {\n headRefName\n headRepositoryOwner {\n __typename\n login\n id\n }\n repository {\n owner {\n __typename\n login\n id\n }\n id\n }\n}\n\nfragment commitsView_nodes on Commit {\n id\n author {\n name\n user {\n login\n id\n }\n }\n ...commitView_item\n}\n\nfragment issueCommentView_item on IssueComment {\n author {\n __typename\n avatarUrl\n login\n ... on Node {\n id\n }\n }\n bodyHTML\n createdAt\n url\n}\n\nfragment mergedEventView_item on MergedEvent {\n actor {\n __typename\n avatarUrl\n login\n ... on Node {\n id\n }\n }\n commit {\n oid\n id\n }\n mergeRefName\n createdAt\n}\n\nfragment headRefForcePushedEventView_item on HeadRefForcePushedEvent {\n actor {\n __typename\n avatarUrl\n login\n ... on Node {\n id\n }\n }\n beforeCommit {\n oid\n id\n }\n afterCommit {\n oid\n id\n }\n createdAt\n}\n\nfragment commitCommentThreadView_item on CommitCommentThread {\n commit {\n oid\n id\n }\n comments(first: 100) {\n edges {\n node {\n id\n ...commitCommentView_item\n }\n }\n }\n}\n\nfragment crossReferencedEventsView_nodes on CrossReferencedEvent {\n id\n referencedAt\n isCrossRepository\n actor {\n __typename\n login\n avatarUrl\n ... on Node {\n id\n }\n }\n source {\n __typename\n ... on RepositoryNode {\n repository {\n name\n owner {\n __typename\n login\n id\n }\n id\n }\n }\n ... on Node {\n id\n }\n }\n ...crossReferencedEventView_item\n}\n\nfragment crossReferencedEventView_item on CrossReferencedEvent {\n id\n isCrossRepository\n source {\n __typename\n ... on Issue {\n number\n title\n url\n issueState: state\n }\n ... on PullRequest {\n number\n title\n url\n prState: state\n }\n ... on RepositoryNode {\n repository {\n name\n isPrivate\n owner {\n __typename\n login\n id\n }\n id\n }\n }\n ... on Node {\n id\n }\n }\n}\n\nfragment commitCommentView_item on CommitComment {\n author {\n __typename\n login\n avatarUrl\n ... on Node {\n id\n }\n }\n commit {\n oid\n id\n }\n bodyHTML\n createdAt\n path\n position\n}\n\nfragment commitView_item on Commit {\n author {\n name\n avatarUrl\n user {\n login\n id\n }\n }\n committer {\n name\n avatarUrl\n user {\n login\n id\n }\n }\n authoredByCommitter\n oid\n message\n messageHeadlineHTML\n commitUrl\n}\n\nfragment prStatusContextView_context on StatusContext {\n context\n description\n state\n targetUrl\n}\n", + "text": "query issueishDetailViewRefetchQuery(\n $repoId: ID!\n $issueishId: ID!\n $timelineCount: Int!\n $timelineCursor: String\n) {\n repository: node(id: $repoId) {\n __typename\n ...issueishDetailView_repository_3D8CP9\n id\n }\n issueish: node(id: $issueishId) {\n __typename\n ...issueishDetailView_issueish_3D8CP9\n id\n }\n}\n\nfragment issueishDetailView_repository_3D8CP9 on Repository {\n id\n name\n owner {\n __typename\n login\n id\n }\n}\n\nfragment issueishDetailView_issueish_3D8CP9 on IssueOrPullRequest {\n __typename\n ... on Node {\n id\n }\n ... on Issue {\n state\n number\n title\n bodyHTML\n author {\n __typename\n login\n avatarUrl\n ... on User {\n url\n }\n ... on Bot {\n url\n }\n ... on Node {\n id\n }\n }\n ...issueTimelineController_issue_3D8CP9\n }\n ... on PullRequest {\n isCrossRepository\n changedFiles\n countedCommits: commits {\n totalCount\n }\n ...prStatusesView_pullRequest\n state\n number\n title\n bodyHTML\n baseRefName\n headRefName\n author {\n __typename\n login\n avatarUrl\n ... on User {\n url\n }\n ... on Bot {\n url\n }\n ... on Node {\n id\n }\n }\n ...prTimelineController_pullRequest_3D8CP9\n }\n ... on UniformResourceLocatable {\n url\n }\n ... on Reactable {\n reactionGroups {\n content\n users {\n totalCount\n }\n }\n }\n}\n\nfragment issueTimelineController_issue_3D8CP9 on Issue {\n url\n timeline(first: $timelineCount, after: $timelineCursor) {\n pageInfo {\n endCursor\n hasNextPage\n }\n edges {\n cursor\n node {\n __typename\n ...commitsView_nodes\n ...issueCommentView_item\n ...crossReferencedEventsView_nodes\n ... on Node {\n id\n }\n }\n }\n }\n}\n\nfragment prStatusesView_pullRequest on PullRequest {\n id\n commits(last: 1) {\n edges {\n node {\n commit {\n status {\n state\n contexts {\n id\n state\n ...prStatusContextView_context\n }\n id\n }\n id\n }\n id\n }\n }\n }\n}\n\nfragment prTimelineController_pullRequest_3D8CP9 on PullRequest {\n url\n ...headRefForcePushedEventView_issueish\n timeline(first: $timelineCount, after: $timelineCursor) {\n pageInfo {\n endCursor\n hasNextPage\n }\n edges {\n cursor\n node {\n __typename\n ...commitsView_nodes\n ...issueCommentView_item\n ...mergedEventView_item\n ...headRefForcePushedEventView_item\n ...commitCommentThreadView_item\n ...crossReferencedEventsView_nodes\n ... on Node {\n id\n }\n }\n }\n }\n}\n\nfragment headRefForcePushedEventView_issueish on PullRequest {\n headRefName\n headRepositoryOwner {\n __typename\n login\n id\n }\n repository {\n owner {\n __typename\n login\n id\n }\n id\n }\n}\n\nfragment commitsView_nodes on Commit {\n id\n author {\n name\n user {\n login\n id\n }\n }\n ...commitView_item\n}\n\nfragment issueCommentView_item on IssueComment {\n author {\n __typename\n avatarUrl\n login\n ... on Node {\n id\n }\n }\n bodyHTML\n createdAt\n url\n}\n\nfragment mergedEventView_item on MergedEvent {\n actor {\n __typename\n avatarUrl\n login\n ... on Node {\n id\n }\n }\n commit {\n oid\n id\n }\n mergeRefName\n createdAt\n}\n\nfragment headRefForcePushedEventView_item on HeadRefForcePushedEvent {\n actor {\n __typename\n avatarUrl\n login\n ... on Node {\n id\n }\n }\n beforeCommit {\n oid\n id\n }\n afterCommit {\n oid\n id\n }\n createdAt\n}\n\nfragment commitCommentThreadView_item on CommitCommentThread {\n commit {\n oid\n id\n }\n comments(first: 100) {\n edges {\n node {\n id\n ...commitCommentView_item\n }\n }\n }\n}\n\nfragment crossReferencedEventsView_nodes on CrossReferencedEvent {\n id\n referencedAt\n isCrossRepository\n actor {\n __typename\n login\n avatarUrl\n ... on Node {\n id\n }\n }\n source {\n __typename\n ... on RepositoryNode {\n repository {\n name\n owner {\n __typename\n login\n id\n }\n id\n }\n }\n ... on Node {\n id\n }\n }\n ...crossReferencedEventView_item\n}\n\nfragment crossReferencedEventView_item on CrossReferencedEvent {\n id\n isCrossRepository\n source {\n __typename\n ... on Issue {\n number\n title\n url\n issueState: state\n }\n ... on PullRequest {\n number\n title\n url\n prState: state\n }\n ... on RepositoryNode {\n repository {\n name\n isPrivate\n owner {\n __typename\n login\n id\n }\n id\n }\n }\n ... on Node {\n id\n }\n }\n}\n\nfragment commitCommentView_item on CommitComment {\n author {\n __typename\n login\n avatarUrl\n ... on Node {\n id\n }\n }\n commit {\n oid\n id\n }\n bodyHTML\n createdAt\n path\n position\n}\n\nfragment commitView_item on Commit {\n author {\n name\n avatarUrl\n user {\n login\n id\n }\n }\n committer {\n name\n avatarUrl\n user {\n login\n id\n }\n }\n authoredByCommitter\n oid\n message\n messageHeadlineHTML\n commitUrl\n}\n\nfragment prStatusContextView_context on StatusContext {\n context\n description\n state\n targetUrl\n}\n", "metadata": {}, "fragment": { "kind": "Fragment", @@ -997,12 +999,16 @@ return { "type": "PullRequest", "selections": [ v12, + v13, { - "kind": "ScalarField", - "alias": null, - "name": "changedFiles", + "kind": "LinkedField", + "alias": "countedCommits", + "name": "commits", + "storageKey": null, "args": null, - "storageKey": null + "concreteType": "PullRequestCommitConnection", + "plural": false, + "selections": v11 }, { "kind": "LinkedField", @@ -1056,7 +1062,7 @@ return { "concreteType": "Status", "plural": false, "selections": [ - v13, + v14, { "kind": "LinkedField", "alias": null, @@ -1067,7 +1073,7 @@ return { "plural": true, "selections": [ v5, - v13, + v14, { "kind": "ScalarField", "alias": null, @@ -1104,18 +1110,15 @@ return { } ] }, - v13, v14, v15, + v16, { - "kind": "LinkedField", - "alias": "countedCommits", - "name": "commits", - "storageKey": null, + "kind": "ScalarField", + "alias": null, + "name": "changedFiles", "args": null, - "concreteType": "PullRequestCommitConnection", - "plural": false, - "selections": v11 + "storageKey": null }, { "kind": "ScalarField", @@ -1131,7 +1134,7 @@ return { "args": null, "storageKey": null }, - v18, + v19, { "kind": "LinkedField", "alias": null, @@ -1160,11 +1163,11 @@ return { "alias": null, "name": "timeline", "storageKey": null, - "args": v19, + "args": v20, "concreteType": "PullRequestTimelineConnection", "plural": false, "selections": [ - v20, + v21, { "kind": "LinkedField", "alias": null, @@ -1174,7 +1177,7 @@ return { "concreteType": "PullRequestTimelineItemEdge", "plural": true, "selections": [ - v21, + v22, { "kind": "LinkedField", "alias": null, @@ -1186,12 +1189,12 @@ return { "selections": [ v4, v5, - v23, + v24, { "kind": "InlineFragment", "type": "CommitCommentThread", "selections": [ - v26, + v27, { "kind": "LinkedField", "alias": null, @@ -1235,11 +1238,11 @@ return { "args": null, "concreteType": null, "plural": false, - "selections": v22 + "selections": v23 }, - v26, - v12, v27, + v12, + v28, { "kind": "ScalarField", "alias": null, @@ -1266,7 +1269,7 @@ return { "kind": "InlineFragment", "type": "HeadRefForcePushedEvent", "selections": [ - v29, + v30, { "kind": "LinkedField", "alias": null, @@ -1275,7 +1278,7 @@ return { "args": null, "concreteType": "Commit", "plural": false, - "selections": v25 + "selections": v26 }, { "kind": "LinkedField", @@ -1285,17 +1288,17 @@ return { "args": null, "concreteType": "Commit", "plural": false, - "selections": v25 + "selections": v26 }, - v27 + v28 ] }, { "kind": "InlineFragment", "type": "MergedEvent", "selections": [ - v29, - v26, + v30, + v27, { "kind": "ScalarField", "alias": null, @@ -1303,11 +1306,11 @@ return { "args": null, "storageKey": null }, - v27 + v28 ] }, - v30, - v32 + v31, + v33 ] } ] @@ -1318,7 +1321,7 @@ return { "kind": "LinkedHandle", "alias": null, "name": "timeline", - "args": v19, + "args": v20, "handle": "connection", "key": "prTimelineContainer_timeline", "filters": null @@ -1329,21 +1332,21 @@ return { "kind": "InlineFragment", "type": "Issue", "selections": [ - v13, v14, v15, + v16, v12, - v18, + v19, { "kind": "LinkedField", "alias": null, "name": "timeline", "storageKey": null, - "args": v19, + "args": v20, "concreteType": "IssueTimelineConnection", "plural": false, "selections": [ - v20, + v21, { "kind": "LinkedField", "alias": null, @@ -1353,7 +1356,7 @@ return { "concreteType": "IssueTimelineItemEdge", "plural": true, "selections": [ - v21, + v22, { "kind": "LinkedField", "alias": null, @@ -1365,9 +1368,9 @@ return { "selections": [ v4, v5, - v23, - v30, - v32 + v24, + v31, + v33 ] } ] @@ -1378,7 +1381,7 @@ return { "kind": "LinkedHandle", "alias": null, "name": "timeline", - "args": v19, + "args": v20, "handle": "connection", "key": "IssueTimelineController_timeline", "filters": null diff --git a/lib/views/__generated__/issueishDetailView_issueish.graphql.js b/lib/views/__generated__/issueishDetailView_issueish.graphql.js index 94eb7ae273..3327b66ab2 100644 --- a/lib/views/__generated__/issueishDetailView_issueish.graphql.js +++ b/lib/views/__generated__/issueishDetailView_issueish.graphql.js @@ -35,6 +35,7 @@ export type issueishDetailView_issueish = {| +avatarUrl: any, +url?: any, |}, + +isCrossRepository?: boolean, +changedFiles?: number, +countedCommits?: {| +totalCount: number @@ -215,17 +216,10 @@ return { { "kind": "ScalarField", "alias": null, - "name": "changedFiles", + "name": "isCrossRepository", "args": null, "storageKey": null }, - { - "kind": "FragmentSpread", - "name": "prStatusesView_pullRequest", - "args": null - }, - v3, - v4, { "kind": "LinkedField", "alias": "countedCommits", @@ -236,6 +230,20 @@ return { "plural": false, "selections": v1 }, + { + "kind": "FragmentSpread", + "name": "prStatusesView_pullRequest", + "args": null + }, + v3, + v4, + { + "kind": "ScalarField", + "alias": null, + "name": "changedFiles", + "args": null, + "storageKey": null + }, v5, { "kind": "ScalarField", @@ -279,5 +287,5 @@ return { }; })(); // prettier-ignore -(node/*: any*/).hash = '1abbbad89e413b8d11c163c63987a99a'; +(node/*: any*/).hash = '07832335ad9d97ffef797e5598426ad2'; module.exports = node; diff --git a/lib/views/issueish-detail-view.js b/lib/views/issueish-detail-view.js index 193f9a9eeb..fafe87cbb0 100644 --- a/lib/views/issueish-detail-view.js +++ b/lib/views/issueish-detail-view.js @@ -56,6 +56,7 @@ export class BareIssueishDetailView extends React.Component { countedCommits: PropTypes.shape({ totalCount: PropTypes.number.isRequired, }).isRequired, + isCrossRepository: PropTypes.bool, changedFiles: PropTypes.number.isRequired, url: PropTypes.string.isRequired, bodyHTML: PropTypes.string, @@ -143,8 +144,9 @@ export class BareIssueishDetailView extends React.Component { href={issueish.url + '/commits'}>{issueish.countedCommits.totalCount} commits and{' '} {issueish.changedFiles} changed files into{' '} - {issueish.baseRefName} from{' '} - {issueish.headRefName} + {issueish.isCrossRepository ? `${repo.owner.login}/${issueish.baseRefName}` : issueish.baseRefName} from{' '} + {issueish.isCrossRepository ? + `${issueish.author.login}/${issueish.headRefName}` : issueish.headRefName}
}
@@ -302,6 +304,7 @@ export default createRefetchContainer(BareIssueishDetailView, { } ... on PullRequest { + isCrossRepository changedFiles countedCommits: commits { totalCount From eafaa9b2e01e7adb134b18f29bc5f394360cc9f4 Mon Sep 17 00:00:00 2001 From: Tilde Ann Thurium Date: Thu, 9 Aug 2018 15:05:31 -0700 Subject: [PATCH 0122/4252] add tests for displaying baseRefName and headRefName --- lib/views/issueish-detail-view.js | 4 ++-- test/fixtures/props/issueish-pane-props.js | 4 ++++ test/views/issueish-detail-view.test.js | 7 +++++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/lib/views/issueish-detail-view.js b/lib/views/issueish-detail-view.js index fafe87cbb0..fb2660b5c7 100644 --- a/lib/views/issueish-detail-view.js +++ b/lib/views/issueish-detail-view.js @@ -144,8 +144,8 @@ export class BareIssueishDetailView extends React.Component { href={issueish.url + '/commits'}>{issueish.countedCommits.totalCount} commits and{' '} {issueish.changedFiles} changed files into{' '} - {issueish.isCrossRepository ? `${repo.owner.login}/${issueish.baseRefName}` : issueish.baseRefName} from{' '} - {issueish.isCrossRepository ? + {issueish.isCrossRepository ? `${repo.owner.login}/${issueish.baseRefName}` : issueish.baseRefName} from{' '} + {issueish.isCrossRepository ? `${issueish.author.login}/${issueish.headRefName}` : issueish.headRefName}
} diff --git a/test/fixtures/props/issueish-pane-props.js b/test/fixtures/props/issueish-pane-props.js index 710c4ce172..3ff94e5269 100644 --- a/test/fixtures/props/issueish-pane-props.js +++ b/test/fixtures/props/issueish-pane-props.js @@ -78,6 +78,7 @@ export function issueishDetailViewProps(opts, overrides = {}) { issueishKind: 'PullRequest', issueishTitle: 'title', issueishBodyHTML: '

body

', + issueishBaseRef: 'master', issueishAuthorLogin: 'author', issueishAuthorAvatarURL: 'https://avatars3.githubusercontent.com/u/000?v=4', issueishNumber: 1, @@ -88,6 +89,7 @@ export function issueishDetailViewProps(opts, overrides = {}) { issueishReactions: [], issueishCommitCount: 0, issueishChangedFileCount: 0, + issueishCrossRepository: false, relayRefetch: () => {}, ...opts, @@ -128,7 +130,9 @@ export function issueishDetailViewProps(opts, overrides = {}) { countedCommits: { totalCount: o.issueishCommitCount, }, + isCrossRepository: o.isCrossRepository, changedFiles: o.issueishChangedFileCount, + baseRefName: o.issueishBaseRef, headRefName: o.issueishHeadRef, headRepository: { name: o.issueishHeadRepoName, diff --git a/test/views/issueish-detail-view.test.js b/test/views/issueish-detail-view.test.js index 09994f2d7a..6588d55475 100644 --- a/test/views/issueish-detail-view.test.js +++ b/test/views/issueish-detail-view.test.js @@ -13,12 +13,16 @@ describe('IssueishDetailView', function() { it('renders pull request information', function() { const commitCount = 11; const fileCount = 22; + const baseRefName = 'master'; + const headRefName = 'tt/heck-yes'; const wrapper = shallow(buildApp({ repositoryName: 'repo', ownerLogin: 'user0', issueishKind: 'PullRequest', issueishTitle: 'PR title', + issueishBaseRef: baseRefName, + issueishHeadRef: headRefName, issueishBodyHTML: 'stuff', issueishAuthorLogin: 'author0', issueishAuthorAvatarURL: 'https://avatars3.githubusercontent.com/u/1', @@ -62,6 +66,9 @@ describe('IssueishDetailView', function() { assert.strictEqual(wrapper.find('.github-IssueishDetailView-commitCount').text(), `${commitCount} commits`); assert.strictEqual(wrapper.find('.github-IssueishDetailView-fileCount').text(), `${fileCount} changed files`); + + assert.strictEqual(wrapper.find('.github-IssueishDetailView-baseRefName').text(), baseRefName); + assert.strictEqual(wrapper.find('.github-IssueishDetailView-headRefName').text(), headRefName); }); it('renders issue information', function() { From 955655c0dbc1b9c642abdbecf8a8f179e536f5b0 Mon Sep 17 00:00:00 2001 From: Tilde Ann Thurium Date: Thu, 9 Aug 2018 15:15:47 -0700 Subject: [PATCH 0123/4252] add test for rendering cross repository PR --- test/fixtures/props/issueish-pane-props.js | 2 +- test/views/issueish-detail-view.test.js | 28 ++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/test/fixtures/props/issueish-pane-props.js b/test/fixtures/props/issueish-pane-props.js index 3ff94e5269..d408d259e3 100644 --- a/test/fixtures/props/issueish-pane-props.js +++ b/test/fixtures/props/issueish-pane-props.js @@ -130,7 +130,7 @@ export function issueishDetailViewProps(opts, overrides = {}) { countedCommits: { totalCount: o.issueishCommitCount, }, - isCrossRepository: o.isCrossRepository, + isCrossRepository: o.issueishCrossRepository, changedFiles: o.issueishChangedFileCount, baseRefName: o.issueishBaseRef, headRefName: o.issueishHeadRef, diff --git a/test/views/issueish-detail-view.test.js b/test/views/issueish-detail-view.test.js index 6588d55475..1bd4b1c05a 100644 --- a/test/views/issueish-detail-view.test.js +++ b/test/views/issueish-detail-view.test.js @@ -70,6 +70,34 @@ describe('IssueishDetailView', function() { assert.strictEqual(wrapper.find('.github-IssueishDetailView-baseRefName').text(), baseRefName); assert.strictEqual(wrapper.find('.github-IssueishDetailView-headRefName').text(), headRefName); }); + it.only('renders pull request information for cross repository PR', function() { + const commitCount = 11; + const fileCount = 22; + const baseRefName = 'master'; + const headRefName = 'tt-heck-yes'; + const ownerLogin = 'user0'; + const authorLogin = 'author0'; + const wrapper = shallow(buildApp({ + repositoryName: 'repo', + ownerLogin, + issueishKind: 'PullRequest', + issueishTitle: 'PR title', + issueishBaseRef: baseRefName, + issueishHeadRef: headRefName, + issueishBodyHTML: 'stuff', + issueishAuthorLogin: authorLogin, + issueishAuthorAvatarURL: 'https://avatars3.githubusercontent.com/u/1', + issueishNumber: 100, + issueishState: 'MERGED', + issueishCommitCount: commitCount, + issueishChangedFileCount: fileCount, + issueishReactions: [{content: 'THUMBS_UP', count: 10}, {content: 'THUMBS_DOWN', count: 5}, {content: 'LAUGH', count: 0}], + issueishCrossRepository: true, + })); + + assert.strictEqual(wrapper.find('.github-IssueishDetailView-baseRefName').text(), `${ownerLogin}/${baseRefName}`); + assert.strictEqual(wrapper.find('.github-IssueishDetailView-headRefName').text(), `${authorLogin}/${headRefName}`); + }); it('renders issue information', function() { const wrapper = shallow(buildApp({ From b19f4f5d8dfc7204ff578b9593532b4bb0903e4e Mon Sep 17 00:00:00 2001 From: Tilde Ann Thurium Date: Thu, 9 Aug 2018 15:17:56 -0700 Subject: [PATCH 0124/4252] :fire: unused test fixture data --- test/views/issueish-detail-view.test.js | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/test/views/issueish-detail-view.test.js b/test/views/issueish-detail-view.test.js index 1bd4b1c05a..1fe5748208 100644 --- a/test/views/issueish-detail-view.test.js +++ b/test/views/issueish-detail-view.test.js @@ -70,28 +70,17 @@ describe('IssueishDetailView', function() { assert.strictEqual(wrapper.find('.github-IssueishDetailView-baseRefName').text(), baseRefName); assert.strictEqual(wrapper.find('.github-IssueishDetailView-headRefName').text(), headRefName); }); - it.only('renders pull request information for cross repository PR', function() { - const commitCount = 11; - const fileCount = 22; + + it('renders pull request information for cross repository PR', function() { const baseRefName = 'master'; const headRefName = 'tt-heck-yes'; const ownerLogin = 'user0'; const authorLogin = 'author0'; const wrapper = shallow(buildApp({ - repositoryName: 'repo', ownerLogin, - issueishKind: 'PullRequest', - issueishTitle: 'PR title', issueishBaseRef: baseRefName, issueishHeadRef: headRefName, - issueishBodyHTML: 'stuff', issueishAuthorLogin: authorLogin, - issueishAuthorAvatarURL: 'https://avatars3.githubusercontent.com/u/1', - issueishNumber: 100, - issueishState: 'MERGED', - issueishCommitCount: commitCount, - issueishChangedFileCount: fileCount, - issueishReactions: [{content: 'THUMBS_UP', count: 10}, {content: 'THUMBS_DOWN', count: 5}, {content: 'LAUGH', count: 0}], issueishCrossRepository: true, })); From 49a4dca334b94d1cdcbec00c05a3e8797a49584a Mon Sep 17 00:00:00 2001 From: Tilde Ann Thurium Date: Thu, 9 Aug 2018 15:19:48 -0700 Subject: [PATCH 0125/4252] :shirt: --- lib/views/issueish-detail-view.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/views/issueish-detail-view.js b/lib/views/issueish-detail-view.js index fb2660b5c7..8e81f6ab31 100644 --- a/lib/views/issueish-detail-view.js +++ b/lib/views/issueish-detail-view.js @@ -144,7 +144,8 @@ export class BareIssueishDetailView extends React.Component { href={issueish.url + '/commits'}>{issueish.countedCommits.totalCount} commits and{' '} {issueish.changedFiles} changed files into{' '} - {issueish.isCrossRepository ? `${repo.owner.login}/${issueish.baseRefName}` : issueish.baseRefName} from{' '} + {issueish.isCrossRepository ? + `${repo.owner.login}/${issueish.baseRefName}` : issueish.baseRefName} from{' '} {issueish.isCrossRepository ? `${issueish.author.login}/${issueish.headRefName}` : issueish.headRefName} From b3879be0e097f82859b4256f72506e5d050ad6e6 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 10 Aug 2018 16:37:06 -0400 Subject: [PATCH 0126/4252] Return gap ranges from IndexedRowRange::intersectRowRange() --- lib/models/indexed-row-range.js | 55 +++++++++++++++++---------- test/models/indexed-row-range.test.js | 53 ++++++++++++++++++++------ 2 files changed, 75 insertions(+), 33 deletions(-) diff --git a/lib/models/indexed-row-range.js b/lib/models/indexed-row-range.js index 6098792466..0911012e85 100644 --- a/lib/models/indexed-row-range.js +++ b/lib/models/indexed-row-range.js @@ -27,44 +27,57 @@ export default class IndexedRowRange { return buffer.slice(this.startOffset, this.endOffset).replace(/(^|\n)(?!$)/g, '$&' + prefix); } - intersectRowsIn(rowSet, buffer) { - // Identify Ranges within our bufferRange that intersect the rows in rowSet. + // Identify {IndexedRowRanges} within our bufferRange that intersect the rows in rowSet. If {includeGaps} is true, + // also return an {IndexedRowRange} for each gap between intersecting ranges. + intersectRowsIn(rowSet, buffer, includeGaps) { const intersections = []; - let nextStartRow = null; - let nextStartOffset = null; + let withinIntersection = false; let currentRow = this.bufferRange.start.row; let currentOffset = this.startOffset; + let nextStartRow = currentRow; + let nextStartOffset = currentOffset; - while (currentRow <= this.bufferRange.end.row) { - if (rowSet.has(currentRow) && nextStartRow === null) { - // Start of intersecting row range + const finishRowRange = isGap => { + if (isGap && !includeGaps) { nextStartRow = currentRow; nextStartOffset = currentOffset; - } else if (!rowSet.has(currentRow) && nextStartRow !== null) { - // One row past the end of intersecting row range - intersections.push(new IndexedRowRange({ + return; + } + + if (nextStartOffset === currentOffset) { + return; + } + + intersections.push({ + intersection: new IndexedRowRange({ bufferRange: Range.fromObject([[nextStartRow, 0], [currentRow - 1, 0]]), startOffset: nextStartOffset, endOffset: currentOffset, - })); + }), + gap: isGap, + }); - nextStartRow = null; - nextStartOffset = null; + nextStartRow = currentRow; + nextStartOffset = currentOffset; + }; + + while (currentRow <= this.bufferRange.end.row) { + if (rowSet.has(currentRow) && !withinIntersection) { + // One row past the end of a gap. Start of intersecting row range. + finishRowRange(true); + withinIntersection = true; + } else if (!rowSet.has(currentRow) && withinIntersection) { + // One row past the end of intersecting row range. Start of the next gap. + finishRowRange(false); + withinIntersection = false; } currentOffset = buffer.indexOf('\n', currentOffset) + 1; currentRow++; } - if (nextStartRow !== null) { - intersections.push(new IndexedRowRange({ - bufferRange: Range.fromObject([[nextStartRow, 0], this.bufferRange.end]), - startOffset: nextStartOffset, - endOffset: currentOffset, - })); - } - + finishRowRange(!withinIntersection); return intersections; } diff --git a/test/models/indexed-row-range.test.js b/test/models/indexed-row-range.test.js index 8eae81e685..2b96c3501b 100644 --- a/test/models/indexed-row-range.test.js +++ b/test/models/indexed-row-range.test.js @@ -45,14 +45,22 @@ describe('IndexedRowRange', function() { const buffer = '0000\n1111\n2222\n3333\n4444\n5555\n6666\n7777\n8888\n9999\n'; // 0000.1111.2222.3333.4444.5555.6666.7777.8888.9999. - it('returns an empty array with no intersection rows', function() { + function assertIntersections(actual, expected) { + const serialized = actual.map(({intersection, gap}) => ({intersection: intersection.serialize(), gap})); + assert.deepEqual(serialized, expected); + } + + it('returns an array containing all gaps with no intersection rows', function() { const range = new IndexedRowRange({ bufferRange: [[1, 0], [3, 0]], startOffset: 5, endOffset: 20, }); - assert.deepEqual(range.intersectRowsIn(new Set([0, 5, 6]), buffer), []); + assertIntersections(range.intersectRowsIn(new Set([0, 5, 6]), buffer, false), []); + assertIntersections(range.intersectRowsIn(new Set([0, 5, 6]), buffer, true), [ + {intersection: {bufferRange: [[1, 0], [3, 0]], startOffset: 5, endOffset: 20}, gap: true}, + ]); }); it('detects an intersection at the beginning of the range', function() { @@ -63,8 +71,12 @@ describe('IndexedRowRange', function() { }); const rowSet = new Set([0, 1, 2, 3]); - assert.deepEqual(range.intersectRowsIn(rowSet, buffer).map(i => i.serialize()), [ - {bufferRange: [[2, 0], [3, 0]], startOffset: 10, endOffset: 20}, + assertIntersections(range.intersectRowsIn(rowSet, buffer, false), [ + {intersection: {bufferRange: [[2, 0], [3, 0]], startOffset: 10, endOffset: 20}, gap: false}, + ]); + assertIntersections(range.intersectRowsIn(rowSet, buffer, true), [ + {intersection: {bufferRange: [[2, 0], [3, 0]], startOffset: 10, endOffset: 20}, gap: false}, + {intersection: {bufferRange: [[4, 0], [6, 0]], startOffset: 20, endOffset: 35}, gap: true}, ]); }); @@ -76,8 +88,13 @@ describe('IndexedRowRange', function() { }); const rowSet = new Set([0, 3, 4, 8, 9]); - assert.deepEqual(range.intersectRowsIn(rowSet, buffer).map(i => i.serialize()), [ - {bufferRange: [[3, 0], [4, 0]], startOffset: 15, endOffset: 25}, + assertIntersections(range.intersectRowsIn(rowSet, buffer, false), [ + {intersection: {bufferRange: [[3, 0], [4, 0]], startOffset: 15, endOffset: 25}, gap: false}, + ]); + assertIntersections(range.intersectRowsIn(rowSet, buffer, true), [ + {intersection: {bufferRange: [[2, 0], [2, 0]], startOffset: 10, endOffset: 15}, gap: true}, + {intersection: {bufferRange: [[3, 0], [4, 0]], startOffset: 15, endOffset: 25}, gap: false}, + {intersection: {bufferRange: [[5, 0], [6, 0]], startOffset: 25, endOffset: 35}, gap: true}, ]); }); @@ -89,8 +106,12 @@ describe('IndexedRowRange', function() { }); const rowSet = new Set([4, 5, 6, 7, 10, 11]); - assert.deepEqual(range.intersectRowsIn(rowSet, buffer).map(i => i.serialize()), [ - {bufferRange: [[4, 0], [6, 0]], startOffset: 20, endOffset: 35}, + assertIntersections(range.intersectRowsIn(rowSet, buffer, false), [ + {intersection: {bufferRange: [[4, 0], [6, 0]], startOffset: 20, endOffset: 35}, gap: false}, + ]); + assertIntersections(range.intersectRowsIn(rowSet, buffer, true), [ + {intersection: {bufferRange: [[2, 0], [3, 0]], startOffset: 10, endOffset: 20}, gap: true}, + {intersection: {bufferRange: [[4, 0], [6, 0]], startOffset: 20, endOffset: 35}, gap: false}, ]); }); @@ -102,14 +123,22 @@ describe('IndexedRowRange', function() { }); const rowSet = new Set([0, 3, 4, 6, 7, 10]); - assert.deepEqual(range.intersectRowsIn(rowSet, buffer).map(i => i.serialize()), [ - {bufferRange: [[3, 0], [4, 0]], startOffset: 15, endOffset: 25}, - {bufferRange: [[6, 0], [7, 0]], startOffset: 30, endOffset: 40}, + assertIntersections(range.intersectRowsIn(rowSet, buffer, false), [ + {intersection: {bufferRange: [[3, 0], [4, 0]], startOffset: 15, endOffset: 25}, gap: false}, + {intersection: {bufferRange: [[6, 0], [7, 0]], startOffset: 30, endOffset: 40}, gap: false}, + ]); + assertIntersections(range.intersectRowsIn(rowSet, buffer, true), [ + {intersection: {bufferRange: [[2, 0], [2, 0]], startOffset: 10, endOffset: 15}, gap: true}, + {intersection: {bufferRange: [[3, 0], [4, 0]], startOffset: 15, endOffset: 25}, gap: false}, + {intersection: {bufferRange: [[5, 0], [5, 0]], startOffset: 25, endOffset: 30}, gap: true}, + {intersection: {bufferRange: [[6, 0], [7, 0]], startOffset: 30, endOffset: 40}, gap: false}, + {intersection: {bufferRange: [[8, 0], [8, 0]], startOffset: 40, endOffset: 45}, gap: true}, ]); }); it('returns an empty array for the null range', function() { - assert.deepEqual(nullIndexedRowRange.intersectRowsIn(new Set([1, 2, 3]), buffer), []); + assertIntersections(nullIndexedRowRange.intersectRowsIn(new Set([1, 2, 3]), buffer, true), []); + assertIntersections(nullIndexedRowRange.intersectRowsIn(new Set([1, 2, 3]), buffer, false), []); }); }); From 4badd00860e1542ebc906c3dfef0dda506f9cfe0 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 13 Aug 2018 10:21:53 -0400 Subject: [PATCH 0127/4252] Remove unused methods --- lib/controllers/git-tab-controller.js | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/lib/controllers/git-tab-controller.js b/lib/controllers/git-tab-controller.js index 96bb9f708a..3459d08d14 100644 --- a/lib/controllers/git-tab-controller.js +++ b/lib/controllers/git-tab-controller.js @@ -56,7 +56,7 @@ export default class GitTabController extends React.Component { super(props, context); autobind( this, - 'unstageFilePatch', 'attemptStageAllOperation', 'attemptFileStageOperation', 'unstageFiles', 'prepareToCommit', + 'attemptStageAllOperation', 'attemptFileStageOperation', 'unstageFiles', 'prepareToCommit', 'commit', 'updateSelectedCoAuthors', 'undoLastCommit', 'abortMerge', 'resolveAsOurs', 'resolveAsTheirs', 'checkout', 'rememberLastFocus', 'quietlySelectItem', ); @@ -115,8 +115,6 @@ export default class GitTabController extends React.Component { discardWorkDirChangesForPaths={this.props.discardWorkDirChangesForPaths} undoLastDiscard={this.props.undoLastDiscard} - stageFilePatch={this.stageFilePatch} - unstageFilePatch={this.unstageFilePatch} attemptFileStageOperation={this.attemptFileStageOperation} attemptStageAllOperation={this.attemptStageAllOperation} prepareToCommit={this.prepareToCommit} @@ -186,10 +184,6 @@ export default class GitTabController extends React.Component { } } - unstageFilePatch(filePatch) { - return this.props.repository.applyPatchToIndex(filePatch.getUnstagePatch()); - } - attemptStageAllOperation(stageStatus) { return this.attemptFileStageOperation(['.'], stageStatus); } From a5a5244a2edaa80f70e83b2501c469452f2b6cd3 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 13 Aug 2018 10:43:12 -0400 Subject: [PATCH 0128/4252] Use my fork of atom-mocha-test-runner --- package-lock.json | 559 +++++++++------------------------------------- package.json | 2 +- 2 files changed, 101 insertions(+), 460 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0a4fcfacc6..02501012a0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -200,6 +200,100 @@ "samsam": "1.3.0" } }, + "@smashwilson/atom-mocha-test-runner": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@smashwilson/atom-mocha-test-runner/-/atom-mocha-test-runner-1.2.1.tgz", + "integrity": "sha512-6t223PNmz1jYbyYcIeFB3s+wDY+hYNV+UFBDV2LokWBHqJ4VQwMYJCCXTFx+zHod3b5H4talxx4U908gotamJw==", + "dev": true, + "requires": { + "etch": "^0.8.0", + "grim": "^2.0.1", + "less": "^3.7.1", + "mocha": "^5.2.0", + "tmp": "0.0.31" + }, + "dependencies": { + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "commander": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", + "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", + "dev": true + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", + "dev": true + }, + "less": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/less/-/less-3.8.1.tgz", + "integrity": "sha512-8HFGuWmL3FhQR0aH89escFNBQH/nEiYPP2ltDFdQw2chE28Yx2E3lhAIq9Y2saYwLSwa699s4dBVEfCY8Drf7Q==", + "dev": true, + "requires": { + "clone": "^2.1.2", + "errno": "^0.1.1", + "graceful-fs": "^4.1.2", + "image-size": "~0.5.0", + "mime": "^1.4.1", + "mkdirp": "^0.5.0", + "promise": "^7.1.1", + "request": "^2.83.0", + "source-map": "~0.6.0" + } + }, + "mocha": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz", + "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==", + "dev": true, + "requires": { + "browser-stdout": "1.3.1", + "commander": "2.15.1", + "debug": "3.1.0", + "diff": "3.5.0", + "escape-string-regexp": "1.0.5", + "glob": "7.1.2", + "growl": "1.10.5", + "he": "1.1.1", + "minimatch": "3.0.4", + "mkdirp": "0.5.1", + "supports-color": "5.4.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "optional": true + }, + "supports-color": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, "@smashwilson/enzyme-adapter-react-16": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@smashwilson/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.0.2.tgz", @@ -485,55 +579,6 @@ "babel-core": "6.x" } }, - "atom-mocha-test-runner": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/atom-mocha-test-runner/-/atom-mocha-test-runner-1.2.0.tgz", - "integrity": "sha1-qPZQm40pqAn8tv9H8FiEthLNxqk=", - "dev": true, - "requires": { - "etch": "^0.8.0", - "grim": "^2.0.1", - "less": "^2.7.1", - "mocha": "^3.0.0", - "tmp": "0.0.31" - }, - "dependencies": { - "debug": { - "version": "2.6.8", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz", - "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=", - "requires": { - "ms": "2.0.0" - } - }, - "glob": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.1.tgz", - "integrity": "sha1-gFIR3wT6rxxjo2ADBs31reULLsg=", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.2", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "has-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", - "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" - }, - "supports-color": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.1.2.tgz", - "integrity": "sha1-cqJiiU2dQIuVbKBf83su2KbiotU=", - "requires": { - "has-flag": "^1.0.0" - } - } - } - }, "aws-sign2": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", @@ -1724,12 +1769,6 @@ "integrity": "sha1-ewl1dPjj6tYG+0Zk5krf3aKYGpM=", "dev": true }, - "browser-stdout": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.0.tgz", - "integrity": "sha1-81HTKWnTL6XXpVZxVCY9korjvR8=", - "dev": true - }, "bser": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/bser/-/bser-2.0.0.tgz", @@ -1991,6 +2030,12 @@ } } }, + "clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", + "dev": true + }, "co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -2038,15 +2083,6 @@ "delayed-stream": "~1.0.0" } }, - "commander": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", - "integrity": "sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q=", - "dev": true, - "requires": { - "graceful-readlink": ">= 1.0.0" - } - }, "commondir": { "version": "1.0.1", "resolved": false, @@ -3506,12 +3542,6 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" }, - "graceful-readlink": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", - "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=", - "dev": true - }, "graphql": { "version": "0.13.2", "resolved": "https://registry.npmjs.org/graphql/-/graphql-0.13.2.tgz", @@ -3540,12 +3570,6 @@ "event-kit": "^2.0.0" } }, - "growl": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/growl/-/growl-1.9.2.tgz", - "integrity": "sha1-Dqd0NxXbjY3ixe3hd14bRayFwC8=", - "dev": true - }, "handlebars": { "version": "4.0.11", "resolved": false, @@ -4372,16 +4396,6 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" }, - "json-stable-stringify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", - "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", - "dev": true, - "optional": true, - "requires": { - "jsonify": "~0.0.0" - } - }, "json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", @@ -4393,12 +4407,6 @@ "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" }, - "json3": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz", - "integrity": "sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE=", - "dev": true - }, "json5": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", @@ -4412,13 +4420,6 @@ "graceful-fs": "^4.1.6" } }, - "jsonify": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", - "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", - "dev": true, - "optional": true - }, "jsprim": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", @@ -4477,232 +4478,6 @@ "integrity": "sha1-GwuP+ayceIklBYK3C3ExXZ2m2aM=", "dev": true }, - "less": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/less/-/less-2.7.3.tgz", - "integrity": "sha1-zBJg9RyQCp7A2R+2mYE54CUHtjs=", - "dev": true, - "requires": { - "errno": "^0.1.1", - "graceful-fs": "^4.1.2", - "image-size": "~0.5.0", - "mime": "^1.2.11", - "mkdirp": "^0.5.0", - "promise": "^7.1.1", - "request": "2.81.0", - "source-map": "^0.5.3" - }, - "dependencies": { - "ajv": { - "version": "4.11.8", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", - "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=", - "dev": true, - "optional": true, - "requires": { - "co": "^4.6.0", - "json-stable-stringify": "^1.0.1" - } - }, - "assert-plus": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", - "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=", - "dev": true, - "optional": true - }, - "aws-sign2": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", - "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=", - "dev": true, - "optional": true - }, - "boom": { - "version": "2.10.1", - "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", - "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", - "dev": true, - "requires": { - "hoek": "2.x.x" - } - }, - "combined-stream": { - "version": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", - "integrity": "sha512-JgSRe4l4UzPwpJuxfcPWEK1SCrL4dxNjp1uqrQLMop3QZUVo+hDU8w9BJKA4JPbulTWI+UzrI2UA3tK12yQ6bg==", - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "cryptiles": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", - "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=", - "dev": true, - "optional": true, - "requires": { - "boom": "2.x.x" - } - }, - "form-data": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", - "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=", - "dev": true, - "optional": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.5", - "mime-types": "^2.1.12" - }, - "dependencies": { - "combined-stream": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", - "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", - "dev": true, - "optional": true, - "requires": { - "delayed-stream": "~1.0.0" - } - } - } - }, - "har-schema": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-1.0.5.tgz", - "integrity": "sha1-0mMTX0MwfALGAq/I/pWXDAFRNp4=", - "dev": true, - "optional": true - }, - "har-validator": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-4.2.1.tgz", - "integrity": "sha1-M0gdDxu/9gDdID11gSpqX7oALio=", - "dev": true, - "optional": true, - "requires": { - "ajv": "^4.9.1", - "har-schema": "^1.0.5" - } - }, - "hawk": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", - "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=", - "dev": true, - "optional": true, - "requires": { - "boom": "2.x.x", - "cryptiles": "2.x.x", - "hoek": "2.x.x", - "sntp": "1.x.x" - } - }, - "hoek": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", - "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=", - "dev": true - }, - "http-signature": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", - "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=", - "dev": true, - "optional": true, - "requires": { - "assert-plus": "^0.2.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - } - }, - "performance-now": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz", - "integrity": "sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU=", - "dev": true, - "optional": true - }, - "qs": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz", - "integrity": "sha1-E+JtKK1rD/qpExLNO/cI7TUecjM=", - "dev": true, - "optional": true - }, - "request": { - "version": "2.81.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.81.0.tgz", - "integrity": "sha1-xpKJRqDgbF+Nb4qTM0af/aRimKA=", - "dev": true, - "optional": true, - "requires": { - "aws-sign2": "~0.6.0", - "aws4": "^1.2.1", - "caseless": "~0.12.0", - "combined-stream": "~1.0.5", - "extend": "~3.0.0", - "forever-agent": "~0.6.1", - "form-data": "~2.1.1", - "har-validator": "~4.2.1", - "hawk": "~3.1.3", - "http-signature": "~1.1.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.7", - "oauth-sign": "~0.8.1", - "performance-now": "^0.2.0", - "qs": "~6.4.0", - "safe-buffer": "^5.0.1", - "stringstream": "~0.0.4", - "tough-cookie": "~2.3.0", - "tunnel-agent": "^0.6.0", - "uuid": "^3.0.0" - }, - "dependencies": { - "combined-stream": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", - "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", - "dev": true, - "optional": true, - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "tough-cookie": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", - "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==", - "dev": true, - "optional": true, - "requires": { - "punycode": "^1.4.1" - } - } - } - }, - "sntp": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", - "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=", - "dev": true, - "optional": true, - "requires": { - "hoek": "2.x.x" - } - }, - "tough-cookie": { - "version": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.2.tgz", - "integrity": "sha512-42UXjmzk88F7URyg9wDV/dlQ7hXtl/SDV6xIMVdDq82cnDGQDyg8mI8xGBPOwpEfbhvrja6cJ8H1wr0xxykBKA==", - "requires": { - "punycode": "^1.4.1" - } - } - } - }, "levn": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", @@ -4740,51 +4515,6 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.5.tgz", "integrity": "sha1-maktZcAnLevoyWtgV7yPv6O+1RE=" }, - "lodash._baseassign": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz", - "integrity": "sha1-jDigmVAPIVrQnlnxci/QxSv+Ck4=", - "dev": true, - "requires": { - "lodash._basecopy": "^3.0.0", - "lodash.keys": "^3.0.0" - } - }, - "lodash._basecopy": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz", - "integrity": "sha1-jaDmqHbPNEwK2KVIghEd08XHyjY=", - "dev": true - }, - "lodash._basecreate": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash._basecreate/-/lodash._basecreate-3.0.3.tgz", - "integrity": "sha1-G8ZhYU2qf8MRt9A78WgGoCE8+CE=", - "dev": true - }, - "lodash._getnative": { - "version": "3.9.1", - "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", - "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=", - "dev": true - }, - "lodash._isiterateecall": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz", - "integrity": "sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=", - "dev": true - }, - "lodash.create": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/lodash.create/-/lodash.create-3.1.1.tgz", - "integrity": "sha1-1/KEnw29p+BGgruM1yqwIkYd6+c=", - "dev": true, - "requires": { - "lodash._baseassign": "^3.0.0", - "lodash._basecreate": "^3.0.0", - "lodash._isiterateecall": "^3.0.0" - } - }, "lodash.flattendeep": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", @@ -4797,35 +4527,12 @@ "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", "dev": true }, - "lodash.isarguments": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", - "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=", - "dev": true - }, - "lodash.isarray": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", - "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=", - "dev": true - }, "lodash.isequal": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=", "dev": true }, - "lodash.keys": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", - "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=", - "dev": true, - "requires": { - "lodash._getnative": "^3.0.0", - "lodash.isarguments": "^3.0.0", - "lodash.isarray": "^3.0.0" - } - }, "lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -5074,72 +4781,6 @@ } } }, - "mocha": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-3.5.3.tgz", - "integrity": "sha1-HgSA/jbS2lhY0etqzDhBiybqog0=", - "dev": true, - "requires": { - "browser-stdout": "1.3.0", - "commander": "2.9.0", - "debug": "2.6.8", - "diff": "3.2.0", - "escape-string-regexp": "1.0.5", - "glob": "7.1.1", - "growl": "1.9.2", - "he": "1.1.1", - "json3": "3.3.2", - "lodash.create": "3.1.1", - "mkdirp": "0.5.1", - "supports-color": "3.1.2" - }, - "dependencies": { - "debug": { - "version": "2.6.8", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz", - "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "diff": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.2.0.tgz", - "integrity": "sha1-yc45Okt8vQsFinJck98pkCeGj/k=", - "dev": true - }, - "glob": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.1.tgz", - "integrity": "sha1-gFIR3wT6rxxjo2ADBs31reULLsg=", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.2", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "has-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", - "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", - "dev": true - }, - "supports-color": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.1.2.tgz", - "integrity": "sha1-cqJiiU2dQIuVbKBf83su2KbiotU=", - "dev": true, - "requires": { - "has-flag": "^1.0.0" - } - } - } - }, "mocha-appveyor-reporter": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/mocha-appveyor-reporter/-/mocha-appveyor-reporter-0.4.0.tgz", diff --git a/package.json b/package.json index 8b2f3dee7c..74831bf785 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,7 @@ }, "devDependencies": { "@smashwilson/enzyme-adapter-react-16": "1.0.2", - "atom-mocha-test-runner": "1.2.0", + "@smashwilson/atom-mocha-test-runner": "1.2.1", "babel-plugin-istanbul": "4.1.6", "chai": "4.1.2", "chai-as-promised": "7.1.1", From 98bab1112c334ee21dad2d73d0fd229bfa06979b Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 13 Aug 2018 10:47:40 -0400 Subject: [PATCH 0129/4252] Change the import --- test/runner.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/runner.js b/test/runner.js index 230552f84a..44ca0073fb 100644 --- a/test/runner.js +++ b/test/runner.js @@ -1,4 +1,4 @@ -import {createRunner} from 'atom-mocha-test-runner'; +import {createRunner} from '@smashwilson/atom-mocha-test-runner'; import chai from 'chai'; import chaiAsPromised from 'chai-as-promised'; import path from 'path'; From ba551f6d323234fb7acecb09258a8ec41363790e Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 13 Aug 2018 11:04:52 -0400 Subject: [PATCH 0130/4252] Prepare 0.19.0-0 release --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 02501012a0..b32cf6ddba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "github", - "version": "0.18.0-1", + "version": "0.19.0-0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 74831bf785..95584a57b4 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "github", "main": "./lib/index", - "version": "0.18.0-1", + "version": "0.19.0-0", "description": "GitHub integration", "repository": "https://github.com/atom/github", "license": "MIT", From 006671f9d5c17e4e3c67a3cbe153f11e149c341f Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 13 Aug 2018 14:32:09 -0400 Subject: [PATCH 0131/4252] Proxy items need to be more resilient to unexpected properties --- lib/helpers.js | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/lib/helpers.js b/lib/helpers.js index 794e7c1896..a52fcd304f 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -4,6 +4,7 @@ import os from 'os'; import temp from 'temp'; import FilePatchController from './controllers/file-patch-controller'; +import RefHolder from './models/ref-holder'; export const LINE_ENDING_REGEX = /\r?\n/; export const CO_AUTHOR_REGEX = /^co-authored-by. (.+?) <(.+?)>$/i; @@ -409,16 +410,14 @@ export function extractCoAuthorsAndRawCommitMessage(commitMessage) { } export function createItem(node, componentHolder = null, uri = null, extra = {}) { + const holder = componentHolder || new RefHolder(); + const override = { getElement: () => node, - getRealItem: () => componentHolder.getOr(null), - - getRealItemPromise: () => componentHolder.getPromise(), + getRealItem: () => holder.getOr(null), - destroy: () => componentHolder && componentHolder.map(component => { - return component.destroy && component.destroy(); - }), + getRealItemPromise: () => holder.getPromise(), ...extra, }; @@ -434,16 +433,18 @@ export function createItem(node, componentHolder = null, uri = null, extra = {}) return target[name]; } - return componentHolder.get()[name]; + return holder.map(component => component[name]).getOr(undefined); }, set(target, name, value) { - componentHolder.get()[name] = value; - return true; + return holder.map(component => { + component[name] = value; + return true; + }).getOr(true); }, has(target, name) { - return Reflect.has(componentHolder.get(), name) || Reflect.has(target, name); + return holder.map(component => Reflect.has(component, name)).getOr(false) || Reflect.has(target, name); }, }); } else { From e1b40143485aceb73c9468d37bcf3d358873fef3 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 13 Aug 2018 15:23:20 -0400 Subject: [PATCH 0132/4252] The item proxy should not unwrap a RefHolder property --- lib/helpers.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/helpers.js b/lib/helpers.js index a52fcd304f..5243788a3a 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -433,7 +433,9 @@ export function createItem(node, componentHolder = null, uri = null, extra = {}) return target[name]; } - return holder.map(component => component[name]).getOr(undefined); + // The {value: ...} wrapper prevents .map() from flattening a returned RefHolder. + // If component[name] is a RefHolder, we want to return that RefHolder as-is. + return holder.map(component => ({value: component[name]})).getOr({value: undefined}); }, set(target, name, value) { From 7c802fb4f3b2d96ccc43fcd883e9383e74df7d19 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 13 Aug 2018 15:33:57 -0400 Subject: [PATCH 0133/4252] Er, actually unwrap that --- lib/helpers.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/helpers.js b/lib/helpers.js index 5243788a3a..62397bc79c 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -435,7 +435,8 @@ export function createItem(node, componentHolder = null, uri = null, extra = {}) // The {value: ...} wrapper prevents .map() from flattening a returned RefHolder. // If component[name] is a RefHolder, we want to return that RefHolder as-is. - return holder.map(component => ({value: component[name]})).getOr({value: undefined}); + const {value} = holder.map(component => ({value: component[name]})).getOr({value: undefined}); + return value; }, set(target, name, value) { From 62ee1c0827548352880823c2e4065f760b67f073 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 13 Aug 2018 15:46:39 -0400 Subject: [PATCH 0134/4252] Prepare 0.20.0-0 release --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index b32cf6ddba..5d29ecca92 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "github", - "version": "0.19.0-0", + "version": "0.20.0-0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 95584a57b4..b99bc0e9b3 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "github", "main": "./lib/index", - "version": "0.19.0-0", + "version": "0.20.0-0", "description": "GitHub integration", "repository": "https://github.com/atom/github", "license": "MIT", From 3507a230e1ad46a797d36559a405550669dc834a Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 13 Aug 2018 15:48:19 -0400 Subject: [PATCH 0135/4252] 0.19.0-1 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5d29ecca92..4c303d8b24 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "github", - "version": "0.20.0-0", + "version": "0.19.0-1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index b99bc0e9b3..5db58b997e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "github", "main": "./lib/index", - "version": "0.20.0-0", + "version": "0.19.0-1", "description": "GitHub integration", "repository": "https://github.com/atom/github", "license": "MIT", From 9785a8a72f99bb2f07cee64cb3df14a648ff0883 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 14 Aug 2018 08:07:49 -0400 Subject: [PATCH 0136/4252] Prepare 0.19.0 release --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4c303d8b24..ac37321df2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "github", - "version": "0.19.0-1", + "version": "0.19.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 5db58b997e..031f5e99a9 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "github", "main": "./lib/index", - "version": "0.19.0-1", + "version": "0.19.0", "description": "GitHub integration", "repository": "https://github.com/atom/github", "license": "MIT", From 824ae186732c791aa8c6c2e443ea2cdb8dc43e04 Mon Sep 17 00:00:00 2001 From: Dan Wallis Date: Tue, 14 Aug 2018 13:43:58 +0100 Subject: [PATCH 0137/4252] Always keep hunk-view header visible --- styles/hunk-view.less | 2 ++ 1 file changed, 2 insertions(+) diff --git a/styles/hunk-view.less b/styles/hunk-view.less index b2ecf4b55c..85d5707eb3 100644 --- a/styles/hunk-view.less +++ b/styles/hunk-view.less @@ -14,6 +14,8 @@ font-size: .9em; background-color: @panel-heading-background-color; border-bottom: 1px solid @panel-heading-border-color; + position: sticky; + top: 0; } &-title { From d339ca0aa1d7e05aca9cb86790b3e1dccf73b9fb Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 14 Aug 2018 14:46:57 -0400 Subject: [PATCH 0138/4252] :ghost: populate missing links --- docs/how-we-work.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/how-we-work.md b/docs/how-we-work.md index 095404b7ba..5d6808a349 100644 --- a/docs/how-we-work.md +++ b/docs/how-we-work.md @@ -70,8 +70,8 @@ To introduce brand-new functionality into the package, follow this guide. ##### Process -1. On a feature branch, write a proposal as a markdown document beneath [`docs/rfcs`]() in this repository. Copy the [template]() to begin. Open a pull request. The RFC document should include: - * A description of the feature, writted as though it already exists; +1. On a feature branch, write a proposal as a markdown document beneath [`docs/rfcs`](/docs/rfcs) in this repository. Copy the [template](/docs/rfcs/000-template.md) to begin. Open a pull request. The RFC document should include: + * A description of the feature, written as though it already exists; * An analysis of the risks and drawbacks; * A specification of when the feature will be considered "done"; * Unresolved questions or possible follow-on work; From c73cf1e725266bf26a498f333889021f55ab11c1 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 14 Aug 2018 14:47:11 -0400 Subject: [PATCH 0139/4252] Mention the sprint board as a short-term planning tool. --- docs/how-we-work.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/how-we-work.md b/docs/how-we-work.md index 5d6808a349..683316a465 100644 --- a/docs/how-we-work.md +++ b/docs/how-we-work.md @@ -6,6 +6,10 @@ This is an attempt to make explicit the way that the core team plans, designs, a Process should serve the developers who use it and not the other way around. This is a live document! As our needs change and as we find that something here isn't bringing us the value we want, we should send pull requests to change it. +## Planning + +Our short-term planning is done in a series of [Project boards on this repository](https://github.com/atom/github/projects). Each project board is associated with a three-week period of time and a target version of the package. Our goal is to release a minor version of the package to atom/atom corresponding to the "Merged" column of its project board - in other words, it is less important to us to have an accurate Planned column before the sprint begins than it is to have an accurate Merged column after it's complete. + ## Kinds of change One size does not fit all, and accordingly, we do not prescribe the same amount of rigor for every pull request. These options lay out a spectrum of approaches to be followed for changes of increasing complexity and scope. Not everything will fall neatly into one of these categories; we trust each other's judgement in choosing which is appropriate for any given effort. When in doubt, ask and we can decide together. From f6556a2a6e9ddfe2a72d753c24bb12aa713bd8c8 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 14 Aug 2018 14:48:14 -0400 Subject: [PATCH 0140/4252] Mention adding PRs to the sprint planning board approximately everywhere --- docs/how-we-work.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/how-we-work.md b/docs/how-we-work.md index 683316a465..345f12ddd3 100644 --- a/docs/how-we-work.md +++ b/docs/how-we-work.md @@ -26,7 +26,7 @@ This includes work like typos in comments or documentation, localized work, or r ##### Process -1. Isolate work on a feature branch in the `atom/github` repository and open a pull request. Title-only pull requests are fine. If it's _really_ minor, like a one-line diff, committing directly to `master` is also perfectly acceptable. +1. Isolate work on a feature branch in the `atom/github` repository and open a pull request. Remember to add the pull request to the current sprint board. Title-only pull requests are fine. If it's _really_ minor, like a one-line diff, committing directly to `master` is also perfectly acceptable. 2. Ensure that our CI remains green across platforms. 3. Merge your own pull request; no code review necessary. @@ -38,8 +38,8 @@ Addressing unhandled exceptions, lock-ups, or correcting other unintended behavi 1. Open an issue on `atom/github` describing the bug if there isn't one already. 2. Identify the root cause of the bug and leave a description of it as an issue comment. If necessary, modify the issue body and title to clarify the bug as you go. -3. When you're ready to begin writing the fix, assign the issue to yourself and move it to the "in progress" column on the [short-term roadmap project](https://github.com/atom/github/projects/8). :rainbow: _This signals to the team and to the community that it's actively being addressed, and keeps us from colliding._ -4. Work on a feature branch in the `atom/github` repository and open a pull request. +3. When you're ready to begin writing the fix, assign the issue to yourself and move it to the "in progress" column on the current active sprint project. :rainbow: _This signals to the team and to the community that it's actively being addressed, and keeps us from colliding._ +4. Work on a feature branch in the `atom/github` repository and open a pull request. Remember to add the pull request to the current sprint project. 5. Write a failing test case that demonstrates the bug (or a rationale for why it isn't worth it -- but bias toward writing one). 6. Iteratively make whatever changes are necessary to make the test suite pass on that branch. 7. Merge your own pull request and close the issue. @@ -63,8 +63,8 @@ Major, cross-cutting refactoring efforts fit within this category. Our goals wit 2. Capture the context of the change in an issue, which can then be prioritized accordingly within our normal channels. * Should we stop or delay existing work in favor of a refactoring? * Should we leave it as-is until we complete other work that's more impactful? -3. When you're ready to begin refactoring, assign the issue to yourself and move it to "in progress" column on the [short-term roadmap project](https://github.com/atom/github/projects/8). -4. Work in a feature branch in the `atom/github` repository and open a pull request to track your progress. +3. When you're ready to begin refactoring, assign the issue to yourself and move it to "in progress" column on the current sprint project. +4. Work in a feature branch in the `atom/github` repository and open a pull request to track your progress. Remember to add the pull request to the current sprint project board. 5. Iteratively change code and tests until the change is complete and CI builds are green. 6. Merge your own pull request and close the issue. @@ -83,7 +83,7 @@ To introduce brand-new functionality into the package, follow this guide. * The acceptance criteria for the RFC itself, as chosen by your current understanding of its scope and impact. Some options you may use here include _(a)_ you're satisfied with its state; _(b)_ the pull request has collected a predetermined number of :+1: votes from core team members; or _(c)_ unanimous :+1: votes from the full core team. 2. @-mention @simurai on the open pull request for design input. Begin hashing out mock-ups, look and feel, specific user interaction details, and decide on a high-level direction for the feature. 3. The RFC's author is responsible for recognizing when its acceptance criteria have been met and merging its pull request. :rainbow: _Our intent here is to give the feature's advocate the ability to cut [bikeshedding](https://en.wiktionary.org/wiki/bikeshedding) short and accept responsibility for guiding it forward._ -4. Work on the RFC's implementation is performed in one or more pull requests. +4. Work on the RFC's implementation is performed in one or more pull requests. Remember to add each pull request to the current sprint project. * Consider gating your work behind a feature flag or a configuration option. * Write tests for your new work. * Optionally [request reviewers](#how-we-review) if you want feedback. Ping @simurai for ongoing UI/UX considerations if appropriate. From f24277a0898648c5f083d8b7446599cba8b01d1b Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 14 Aug 2018 15:22:27 -0400 Subject: [PATCH 0141/4252] Update publishing instructions to include a QA pass and backporting --- docs/how-we-work.md | 42 +++++++++++++++++++++++++----------------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/docs/how-we-work.md b/docs/how-we-work.md index 345f12ddd3..54b763f54a 100644 --- a/docs/how-we-work.md +++ b/docs/how-we-work.md @@ -118,20 +118,28 @@ When finalizing your review: The github package ships as a bundled part of Atom, which affects the way that our progress is delivered to users. After using `apm` to publish a new version, we also need to add a commit to [Atom's `package.json` file](https://github.com/atom/atom/blob/master/package.json#L114) to make our work available. -When the team is preparing to ship a new version of Atom, run `apm publish minor` and update `package.json` on Atom's master branch to reference the new version. This will ship our work to Atom's [beta channel](https://atom.io/beta) and allow a smaller subset of our users to discover regressions before we release it to the full Atom user population. - -When you update Atom's `package.json`, make sure you wait for Atom to build before you merge your changes. In particular, we've had issues with snapshot tests. You can either do `apm publish pre` on the branch with the fix, then modify `package.json` in your local atom and try a `script/build`. Or you can open a pull requests and let the CI tests run for you. - -When you've merged substantial new functionality, consider running `apm publish minor` and updating `package.json` on Atom's master branch outside of the Atom release cycle, to give the rest of the Atom team time to dogfood the change internally and weigh in with opinions. - -After shipping a minor version release for either of the above situations, create and push a release branch from that version's tag: - -```sh -$ apm publish minor -version 0.11.0 -$ git branch 0.11-releases && git push -u origin 0.11-releases -``` - -When you merge a fix for a bug, cherry-pick the merge commit onto to the most recent release branch, then run `apm publish patch` and update `package.json` on the most recent beta release branch on the `atom/atom` repository. This will ensure bug fixes are delivered to users on Atom's stable channel as part of the next release. - -When you merge a fix for a **security problem**, a **data loss bug**, or fix a **crash** or a **lock-up** that affect a large portion of the user population, cherry-pick the merge commit onto the most recent beta _and_ stable release branches of atom/github that contain the bug, then run `apm publish patch` on both and update `package.json` on the affected release branches on the `atom/atom` repository. Consider advocating for a hotfix release of Atom to deliver these fixes to the user population as soon as possible. +At the end of each development sprint: + +1. _In your atom/github repository:_ run `apm publish preminor` to create the first prerelease version or `apm publish prerelease` to increment an existing prerelease version. Note the generated version number and ensure that it's correct. If the currently deployed version is `v0.19.2`, the first prerelease should be `v0.20.0-0`; if the existing prerelease is `v0.20.0-0`, the next prerelease should be `v0.20.0-1`. +2. _In your atom/atom repository:_ create a new branch and edit `package.json` in its root directory. Change the version of the `"github"` entry beneath `packageDependencies` to match the prerelease you just published. +3. _In your atom/atom repository:_ Run `script/build --install`. This will update Atom's `package-lock.json` files and produce a local development build of Atom with your prerelease version of atom/github bundled. + * :boom: _If the build fails,_ correct any bugs and begin again at (1) with a new prerelease version. +4. Run `apm uninstall github` and `apm uninstall --dev github` to ensure that you don't have any [locally installed atom/github versions](/CONTRIBUTING.md#living-on-the-edge) that would override the bundled one. +5. _In your atom/atom repository:_ Push your branch to atom/atom and open a pull request to start running CI. +6. Create a [QA issue](https://github.com/atom/github/issues?utf8=%E2%9C%93&q=is%3Aissue+label%3Aquality) in the atom/github repository. Its title should be "_prerelease version_ QA Review" and it should have the "quality" label applied. Populate the issue body with a checklist containing the pull requests that were included in this release; these should be the ones in the "Merged" column of the project board. Omit pull requests that don't have verification steps (like renames, refactoring, or dependency upgrades, for example). Add a final entry for a clean CI check on the atom/atom pull request. +7. Use your `atom-dev` build to verify each and check it off the list. + * :boom: _If verification fails,_ note the failure in an issue comment. Close the issue. Correct the failure with more work in the current sprint board, then begin again at (1). + * :white_check_mark: _Otherwise,_ comment in and close the issue, then continue. +8. _In your atom/github repository:_ run `apm publish minor` to publish the next minor version. +9. _In your atom/github repository:_ create a release branch for this minor version with `git checkout -b 0.${MINOR}-releases`. Push it to atom/github. +9. _In your atom/atom repository:_ update the version of the `"github"` entry beneath `packageDependencies` in `package.json` to match the published minor version. Run `script/build` to update `package-lock.json` files. Commit and push these changes. +10. When the CI build for your atom/atom pull request is successful, merge it. + +Now cherry-pick any suitably minor or low-risk bugfix PRs from this release to the previous one: + +1. _In your atom/github repository:_ run `git checkout 0.${LASTMINOR}-releases`. For example, if the current release is v0.19.0, the target release branch should be `0.18-releases`. +2. _In your atom/github repository:_ identify the merge SHA of each pull request eligible for backporting. One way to do this is to run `git log --oneline --first-parent master ^HEAD` and identify commits by the "Merge pull request #..." commit messages. +3. _In your atom/github repository:_ cherry-pick each merge commit onto the release branch with `git cherry-pick -m 1 ${SHA}`. Resolve any merge conflicts that arise. +4. Follow the instructions above to publish a new patch version of the package. (Use `apm publish prepatch` / `apm publish prerelease` to generate the correct version numbers.) + +For _really_ urgent fixes, like security problems, data loss bugs, or frequently occurring crashes or lock-ups, consider repeating the cherry-pick instructions for the minor version sequence published on Atom stable, and advocating for an Atom hotfix to deliver it as soon as possible. From 95f50b53bd97aeeaa102f8e9364e2277a0fbe854 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 15 Aug 2018 07:54:15 -0400 Subject: [PATCH 0142/4252] WIP --- lib/models/patch/builder.js | 2 +- lib/models/patch/file-patch.js | 113 ++++++----------------- lib/models/patch/hunk.js | 4 + lib/models/patch/patch.js | 116 ++++++++++++++++++++++++ notes.txt | 66 ++++++++++++++ stage.patch | 20 +++++ test/helpers.js | 4 +- test/models/indexed-row-range.test.js | 8 +- test/models/patch/file-patch.test.js | 123 +++++++++++++++++++++++--- unstage.patch | 15 ++++ 10 files changed, 368 insertions(+), 103 deletions(-) create mode 100644 notes.txt create mode 100644 stage.patch create mode 100644 unstage.patch diff --git a/lib/models/patch/builder.js b/lib/models/patch/builder.js index eec19f6609..b611f711f6 100644 --- a/lib/models/patch/builder.js +++ b/lib/models/patch/builder.js @@ -160,7 +160,7 @@ function buildHunks(diff) { newRowCount: hunkData.newLineCount, sectionHeading: hunkData.heading, rowRange: new IndexedRowRange({ - bufferRange: [[bufferStartRow, 0], [bufferRow, 0]], + bufferRange: [[bufferStartRow, 0], [bufferRow - 1, 0]], startOffset: bufferStartOffset, endOffset: bufferOffset, }), diff --git a/lib/models/patch/file-patch.js b/lib/models/patch/file-patch.js index b4faebb93e..d765ca82c7 100644 --- a/lib/models/patch/file-patch.js +++ b/lib/models/patch/file-patch.js @@ -8,8 +8,6 @@ export default class FilePatch { this.oldFile = oldFile; this.newFile = newFile; this.patch = patch; - - this.changedLineCount = this.getHunks().reduce((acc, hunk) => acc + hunk.changedLineCount(), 0); } getOldFile() { @@ -134,94 +132,27 @@ export default class FilePatch { } getStagePatchForLines(selectedLineSet) { - const wholeFileSelected = this.changedLineCount === selectedLineSet.size; + const wholeFileSelected = this.patch.getChangedLineCount() === selectedLineSet.size; if (wholeFileSelected) { if (this.hasTypechange() && this.getStatus() === 'deleted') { // handle special case when symlink is created where a file was deleted. In order to stage the file deletion, // we must ensure that the created file patch has no new file - return this.clone({ - newFile: nullFile, - }); + return this.clone({newFile: nullFile}); } else { return this; } } else { - const hunks = this.getStagePatchHunks(selectedLineSet); + const patch = this.patch.getStagePatchForLines(selectedLineSet); if (this.getStatus() === 'deleted') { - // Set status to modified + // Populate newFile return this.clone({ newFile: this.getOldFile(), - patch: this.getPatch().clone({hunks, status: 'modified'}), + patch, }); } else { - return this.clone({ - patch: this.getPatch().clone({hunks}), - }); - } - } - } - - getStagePatchHunks(selectedLineSet) { - let delta = 0; - const hunks = []; - for (const hunk of this.getHunks()) { - const additions = []; - const deletions = []; - let deletedRowCount = 0; - - for (const change of hunk.getAdditions()) { - additions.push(...change.intersectRowsIn(selectedLineSet, this.getBufferText())); - } - - for (const change of hunk.getDeletions()) { - for (const intersection of change.intersectRowsIn(selectedLineSet, this.getBufferText())) { - deletedRowCount += intersection.bufferRowCount(); - deletions.push(intersection); - } - } - - if ( - additions.length > 0 || - deletions.length > 0 - ) { - // Hunk contains at least one selected line - hunks.push(new Hunk({ - oldStartRow: hunk.getOldStartRow(), - newStartRow: hunk.getNewStartRow() - delta, - oldRowCount: hunk.getOldRowCount(), - newRowCount: hunk.getNewRowCount() - deletedRowCount, - sectionHeading: hunk.getSectionHeading(), - rowRange: hunk.getRowRange(), - additions, - deletions, - noNewline: hunk.getNoNewline(), - })); + return this.clone({patch}); } - delta += deletedRowCount; } - return hunks; - } - - getUnstagePatch() { - const invertedStatus = { - modified: 'modified', - added: 'deleted', - deleted: 'added', - }[this.getStatus()]; - if (!invertedStatus) { - throw new Error(`Unknown Status: ${this.getStatus()}`); - } - - const invertedHunks = this.getHunks().map(h => h.invert()); - - return this.clone({ - oldFile: this.getNewFile(), - newFile: this.getOldFile(), - patch: this.getPatch().clone({ - status: invertedStatus, - hunks: invertedHunks, - }), - }); } getUnstagePatchForHunk(hunk) { @@ -259,40 +190,46 @@ export default class FilePatch { let delta = 0; const hunks = []; for (const hunk of this.getHunks()) { - const oldStartRow = (hunk.getOldStartRow() || 1) + delta; - const additions = []; const deletions = []; + let notAddedRowCount = 0; let addedRowCount = 0; + let notDeletedRowCount = 0; + let deletedRowCount = 0; for (const change of hunk.getAdditions()) { + notDeletedRowCount += change.bufferRowCount(); for (const intersection of change.intersectRowsIn(selectedRowSet, this.getBufferText())) { - addedRowCount += intersection.bufferRowCount(); - additions.push(intersection); + deletedRowCount += intersection.bufferRowCount(); + notDeletedRowCount -= intersection.bufferRowCount(); + deletions.push(intersection); } } - for (const change of hunk.getBufferDeletedPositions()) { - deletions.push(...change.intersectRowIn(selectedRowSet, this.getBufferText())); + for (const change of hunk.getDeletions()) { + notAddedRowCount = change.bufferRowCount(); + for (const intersection of change.intersectRowsIn(selectedRowSet, this.getBufferText())) { + addedRowCount += intersection.bufferRowCount(); + notAddedRowCount -= intersection.bufferRowCount(); + additions.push(intersection); + } } if (additions.length > 0 || deletions.length > 0) { // Hunk contains at least one selected line hunks.push(new Hunk({ - oldStartRow, + oldStartRow: hunk.getOldStartRow() + delta, newStartRow: hunk.getNewStartRow(), - oldRowCount: hunk.getOldRowCount() - addedRowCount, + oldRowCount: hunk.bufferRowCount() - addedRowCount, newRowCount: hunk.getNewRowCount(), sectionHeading: hunk.getSectionHeading(), - bufferStartPosition: hunk.getBufferStartPosition(), - bufferStartOffset: hunk.getBufferStartOffset(), - bufferEndRow: hunk.getBufferEndRow(), + rowRange: hunk.getRowRange(), additions, deletions, - noNewline: hunk.noNewline, + noNewline: hunk.getNoNewline(), })); } - delta += addedRowCount; + delta += notDeletedRowCount - notAddedRowCount; } return hunks; } diff --git a/lib/models/patch/hunk.js b/lib/models/patch/hunk.js index cce3460957..f4510ef180 100644 --- a/lib/models/patch/hunk.js +++ b/lib/models/patch/hunk.js @@ -72,6 +72,10 @@ export default class Hunk { return this.rowRange.getBufferRows(); } + bufferRowCount() { + return this.rowRange.bufferRowCount(); + } + changedLineCount() { return [ this.additions, diff --git a/lib/models/patch/patch.js b/lib/models/patch/patch.js index 07ac604109..aebdd83a94 100644 --- a/lib/models/patch/patch.js +++ b/lib/models/patch/patch.js @@ -1,8 +1,12 @@ +import Hunk from './hunk'; + export default class Patch { constructor({status, hunks, bufferText}) { this.status = status; this.hunks = hunks; this.bufferText = bufferText; + + this.changedLineCount = this.getHunks().reduce((acc, hunk) => acc + hunk.changedLineCount(), 0); } getStatus() { @@ -21,6 +25,10 @@ export default class Patch { return Buffer.byteLength(this.bufferText, 'utf8'); } + getChangedLineCount() { + return this.changedLineCount; + } + clone(opts = {}) { return new this.constructor({ status: opts.status !== undefined ? opts.status : this.getStatus(), @@ -29,6 +37,114 @@ export default class Patch { }); } + getStagePatchForLines(lineSet) { + const hunks = []; + let delta = 0; + + for (const hunk of this.getHunks()) { + const additions = []; + const deletions = []; + let notAddedRowCount = 0; + let deletedRowCount = 0; + let notDeletedRowCount = 0; + + for (const change of hunk.getAdditions()) { + notAddedRowCount += change.bufferRowCount(); + for (const intersection of change.intersectRowsIn(lineSet, this.getBufferText())) { + notAddedRowCount -= intersection.bufferRowCount(); + additions.push(intersection); + } + } + + for (const change of hunk.getDeletions()) { + notDeletedRowCount += change.bufferRowCount(); + for (const intersection of change.intersectRowsIn(lineSet, this.getBufferText())) { + deletedRowCount += intersection.bufferRowCount(); + notDeletedRowCount -= intersection.bufferRowCount(); + deletions.push(intersection); + } + } + + if (additions.length > 0 || deletions.length > 0) { + // Hunk contains at least one selected line + hunks.push(new Hunk({ + oldStartRow: hunk.getOldStartRow(), + newStartRow: hunk.getNewStartRow() + delta, + oldRowCount: hunk.getOldRowCount(), + newRowCount: hunk.bufferRowCount() - deletedRowCount, + sectionHeading: hunk.getSectionHeading(), + rowRange: hunk.getRowRange(), + additions, + deletions, + noNewline: hunk.getNoNewline(), + })); + } + + delta += notDeletedRowCount - notAddedRowCount; + } + + if (this.getStatus() === 'deleted') { + // Set status to modified + return this.clone({hunks, status: 'modified'}); + } else { + return this.clone({hunks}); + } + } + + getUnstagePatchForLines(lineSet) { + let delta = 0; + const hunks = []; + let bufferText = this.getBufferText(); + let bufferOffset = 0; + + for (const hunk of this.getHunks()) { + const additions = []; + const deletions = []; + let notAddedRowCount = 0; + let addedRowCount = 0; + let notDeletedRowCount = 0; + + for (const change of hunk.getAdditions()) { + notDeletedRowCount += change.bufferRowCount(); + for (const intersection of change.intersectRowsIn(lineSet, bufferText)) { + notDeletedRowCount -= intersection.bufferRowCount(); + deletions.push(intersection); + } + } + + for (const change of hunk.getDeletions()) { + notAddedRowCount = change.bufferRowCount(); + for (const intersection of change.intersectRowsIn(lineSet, bufferText)) { + addedRowCount += intersection.bufferRowCount(); + notAddedRowCount -= intersection.bufferRowCount(); + additions.push(intersection); + } + } + + if (additions.length > 0 || deletions.length > 0) { + // Hunk contains at least one selected line + hunks.push(new Hunk({ + oldStartRow: hunk.getOldStartRow() + delta, + newStartRow: hunk.getNewStartRow(), + oldRowCount: hunk.bufferRowCount() - addedRowCount, + newRowCount: hunk.getNewRowCount(), + sectionHeading: hunk.getSectionHeading(), + rowRange: hunk.getRowRange(), + additions, + deletions, + noNewline: hunk.getNoNewline(), + })); + } + delta += notAddedRowCount - notDeletedRowCount; + } + + if (this.getStatus() === 'added') { + return this.clone({hunks, bufferText, status: 'modified'}); + } else { + return this.clone({hunks, bufferText}); + } + } + toString() { return this.getHunks().reduce((str, hunk) => { str += hunk.toStringIn(this.getBufferText()); diff --git a/notes.txt b/notes.txt new file mode 100644 index 0000000000..c323737af0 --- /dev/null +++ b/notes.txt @@ -0,0 +1,66 @@ +file in index: +----------------- +00: aaa +01: line-0 +02: line-1 +03: line-2 +04: bbb +05: ccc +06: ddd +07: line-3 +08: line-6 +09: line-7 +10: line-8 +11: eee +12: fff +13: ggg +14: hhh +15: iii +16: jjj +17: kkk +18: lll +19: mmm +20: nnn +21: line-12 +22: line-13 +----------------- + +file on HEAD: +----------------- +00: aaa +01: line-2 +02: bbb +03: ccc +04: ddd +05: line-3 +06: line-4 +07: line-5 +08: line-9 +09: line-10 +10: eee +11: fff +12: ggg +13: hhh +14: iii +15: jjj +16: kkk +17: lll +18: mmm +19: nnn +20: line-11 +21: line-13 +----------------- + +unstage patch +----------------- +@@ -7,4 +7,4 @@ + line-3 ++line-4 ++line-5 +-line-6 +-line-7 + line-8 +@@ -21,2 +21,3 @@ ++line-11 + line-12 + line-13 diff --git a/stage.patch b/stage.patch new file mode 100644 index 0000000000..d820eaef0d --- /dev/null +++ b/stage.patch @@ -0,0 +1,20 @@ +dif --git a/a.txt b/a.txt +--- a/a.txt ++++ b/a.txt +@@ -2,1 +2,3 @@ ++line-0 ++line-1 + line-2 +@@ -6,5 +8,4 @@ + line-3 +-line-4 - +-line-5 - ++line-6 - ++line-7 - ++line-8 +-line-9 +-line-10 +@@ -20,2 +21,2 @@ +-line-11 - ++line-12 - + line-13 - diff --git a/test/helpers.js b/test/helpers.js index 88b96a2b4e..00a47c7e7c 100644 --- a/test/helpers.js +++ b/test/helpers.js @@ -169,14 +169,14 @@ class PatchBufferAssertions { ); } - hunk(hunkIndex, {startRow, header, deletions, additions, noNewline}) { + hunk(hunkIndex, {startRow, endRow, header, deletions, additions, noNewline}) { const hunk = this.patch.getHunks()[hunkIndex]; assert.isDefined(hunk); deletions = deletions !== undefined ? deletions : {strings: [], ranges: []}; additions = additions !== undefined ? additions : {strings: [], ranges: []}; - assert.deepEqual(hunk.getStartRange().serialize(), [[startRow, 0], [startRow, 0]]); + assert.deepEqual(hunk.getRowRange().serialize().bufferRange, [[startRow, 0], [endRow, 0]]); assert.strictEqual(hunk.getHeader(), header); this.hunkChanges(hunk.getDeletions(), deletions.strings, deletions.ranges); diff --git a/test/models/indexed-row-range.test.js b/test/models/indexed-row-range.test.js index 2b96c3501b..01e970059a 100644 --- a/test/models/indexed-row-range.test.js +++ b/test/models/indexed-row-range.test.js @@ -1,6 +1,6 @@ import IndexedRowRange, {nullIndexedRowRange} from '../../lib/models/indexed-row-range'; -describe('IndexedRowRange', function() { +describe.only('IndexedRowRange', function() { it('computes its row count', function() { const range = new IndexedRowRange({ bufferRange: [[0, 0], [1, 0]], @@ -142,6 +142,12 @@ describe('IndexedRowRange', function() { }); }); + describe('offsetBy()', function() { + it('returns the receiver as-is when there is no change'); + + it('modifies the buffer range and the buffer offset'); + }); + it('returns appropriate values from nullIndexedRowRange methods', function() { assert.deepEqual(nullIndexedRowRange.intersectRowsIn(new Set([0, 1, 2]), ''), []); assert.strictEqual(nullIndexedRowRange.toStringIn('', '+'), ''); diff --git a/test/models/patch/file-patch.test.js b/test/models/patch/file-patch.test.js index 3c6cb6f1ac..594beb7732 100644 --- a/test/models/patch/file-patch.test.js +++ b/test/models/patch/file-patch.test.js @@ -62,8 +62,9 @@ describe('FilePatch', function() { assertInFilePatch(stagePatch0).hunks( { startRow: 3, - header: '@@ -5,5 +7,4 @@', - deletions: {strings: ['*line-4\nline-5\n'], ranges: [[[4, 0], [5, 0]]]}, + endRow: 10, + header: '@@ -5,5 +5,6 @@', + deletions: {strings: ['*line-4\n*line-5\n'], ranges: [[[4, 0], [5, 0]]]}, additions: {strings: ['*line-6\n'], ranges: [[[6, 0], [6, 0]]]}, }, ); @@ -72,18 +73,21 @@ describe('FilePatch', function() { assertInFilePatch(stagePatch1).hunks( { startRow: 0, - header: '@@ -1,1 +1,2 @@', + endRow: 2, + header: '@@ -1,1 +1,3 @@', additions: {strings: ['*line-0\n'], ranges: [[[0, 0], [0, 0]]]}, }, { startRow: 3, - header: '@@ -5,5 +7,4 @@', + endRow: 10, + header: '@@ -5,5 +6,6 @@', deletions: {strings: ['*line-4\n*line-5\n'], ranges: [[[4, 0], [5, 0]]]}, additions: {strings: ['*line-6\n'], ranges: [[[6, 0], [6, 0]]]}, }, { startRow: 11, - header: '@@ -20,2 +19,2 @@', + endRow: 14, + header: '@@ -20,2 +18,3 @@', deletions: {strings: ['*line-11\n'], ranges: [[[11, 0], [11, 0]]]}, noNewline: {string: '*No newline at end of file\n', range: [[14, 0], [14, 0]]}, }, @@ -96,7 +100,7 @@ describe('FilePatch', function() { oldPath: 'a.txt', oldMode: '100644', newPath: null, - newMode: '000000', + newMode: null, status: 'deleted', hunks: [ { @@ -133,8 +137,9 @@ describe('FilePatch', function() { assertInFilePatch(stagePatch).hunks( { startRow: 0, - header: '@@ -1,1 +3,1 @@', - deletions: {strings: ['*line-1\n*line-2'], ranges: [[[0, 0], [1, 0]]]}, + endRow: 2, + header: '@@ -1,3 +0,1 @@', + deletions: {strings: ['*line-1\n*line-2\n'], ranges: [[[0, 0], [1, 0]]]}, }, ); }); @@ -144,7 +149,7 @@ describe('FilePatch', function() { oldPath: 'a.txt', oldMode: '100644', newPath: null, - newMode: '000000', + newMode: null, status: 'deleted', hunks: [ { @@ -163,10 +168,11 @@ describe('FilePatch', function() { assert.strictEqual(filePatch.getBufferText(), 'line-1\nline-2\nline-3\n'); - const stagePatch = filePatch.getStagePatchForLines(new Set(0, 1, 2)); + const stagePatch = filePatch.getStagePatchForLines(new Set([0, 1, 2])); assertInFilePatch(stagePatch).hunks( { startRow: 0, + endRow: 2, header: '@@ -1,3 +1,0 @@', deletions: {strings: ['*line-1\n*line-2\n*line-3\n'], ranges: [[[0, 0], [2, 0]]]}, }, @@ -176,7 +182,82 @@ describe('FilePatch', function() { }); describe('getUnstagePatchForLines()', function() { - it('returns a new FilePatch that applies only the specified lines'); + it('returns a new FilePatch that unstages only the specified lines', function() { + const filePatch = buildFilePatch([{ + oldPath: 'a.txt', + oldMode: '100644', + newPath: 'a.txt', + newMode: '100644', + status: 'modified', + hunks: [ + { + oldStartLine: 1, + oldLineCount: 1, + newStartLine: 1, + newLineCount: 3, + lines: [ + '+line-0', + '+line-1', + ' line-2', + ], + }, + { + oldStartLine: 5, + oldLineCount: 5, + newStartLine: 7, + newLineCount: 4, + lines: [ + ' line-3', + '-line-4', + '-line-5', + '+line-6', + '+line-7', + '+line-8', + '-line-9', + '-line-10', + ], + }, + { + oldStartLine: 20, + oldLineCount: 2, + newStartLine: 21, + newLineCount: 2, + lines: [ + '-line-11', + '+line-12', + ' line-13', + '\\No newline at end of file', + ], + }, + ], + }]); + + assert.strictEqual( + filePatch.getBufferText(), + 'line-0\nline-1\nline-2\nline-3\nline-4\nline-5\nline-6\nline-7\nline-8\nline-9\nline-10\n' + + 'line-11\nline-12\nline-13\nNo newline at end of file\n', + ); + + const unstagedPatch0 = filePatch.getUnstagePatchForLines(new Set([4, 5, 6, 7, 11, 12, 13])); + console.log(unstagedPatch0.toString()); + assertInFilePatch(unstagedPatch0).hunks( + { + startRow: 3, + endRow: 10, + header: '@@ -7,4 +7,4 @@', + additions: {strings: ['*line-4\n*line-5\n'], ranges: [[[4, 0], [5, 0]]]}, + deletions: {strings: ['*line-6\n*line-7\n'], ranges: [[[5, 0], [6, 0]]]}, + }, + { + startRow: 11, + endRow: 14, + header: '@@ -19,2 +21,2 @@', + additions: {strings: ['*line-11\n'], ranges: [[[11, 0], [11, 0]]]}, + deletions: {strings: ['*line-12\n'], ranges: [[[12, 0], [12, 0]]]}, + noNewline: {string: '*No newline at end of file\n', range: [[14, 0], [14, 0]]}, + }, + ); + }); describe('unstaging lines from an added file', function() { it('handles unstaging part of the file'); @@ -184,4 +265,24 @@ describe('FilePatch', function() { it('handles unstaging all lines, leaving nothing staged'); }); }); + + it('handles newly added files'); + + describe('toString()', function() { + it('converts the patch to the standard textual format'); + + it('correctly formats new files with no newline at the end'); + + describe('typechange file patches', function() { + it('handles typechange patches for a symlink replaced with a file'); + + it('handles typechange patches for a file replaced with a symlink'); + }); + }); + + describe('getHeaderString()', function() { + it('formats paths with git path separators'); + }); + + it('getByteSize() returns the size in bytes'); }); diff --git a/unstage.patch b/unstage.patch new file mode 100644 index 0000000000..d66cfb3978 --- /dev/null +++ b/unstage.patch @@ -0,0 +1,15 @@ +dif --git a/a.txt b/a.txt +--- a/a.txt ++++ b/a.txt +@@ -8,4 +8,4 @@ + line-3 ++line-4 ++line-5 +-line-6 +-line-7 + line-8 +@@ -22,2 +22,2 @@ ++line-11 +-line-12 + line-13 +\ No newline at end of file From 379fa2713e69f2cf0d7b6851fed3849000e53f69 Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Wed, 15 Aug 2018 12:04:06 +0000 Subject: [PATCH 0143/4252] chore(package): update enzyme to version 3.4.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 031f5e99a9..a0f1028595 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,7 @@ "coveralls": "^3.0.1", "dedent-js": "1.0.1", "electron-devtools-installer": "2.2.4", - "enzyme": "3.3.0", + "enzyme": "3.4.1", "eslint": "5.0.1", "eslint-config-fbjs-opensource": "1.0.0", "eslint-plugin-jsx-a11y": "^6.1.1", From c8fe7f6dffc070396ce837050c629fea116cbb49 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 15 Aug 2018 08:36:55 -0400 Subject: [PATCH 0144/4252] Change model --- lib/models/patch/change.js | 54 +++++++++++ test/models/patch/change.test.js | 150 +++++++++++++++++++++++++++++++ 2 files changed, 204 insertions(+) create mode 100644 lib/models/patch/change.js create mode 100644 test/models/patch/change.test.js diff --git a/lib/models/patch/change.js b/lib/models/patch/change.js new file mode 100644 index 0000000000..2e103faff2 --- /dev/null +++ b/lib/models/patch/change.js @@ -0,0 +1,54 @@ +class Change { + constructor(range) { + this.range = range; + } + + getRange() { + return this.range; + } + + isAddition() { + return false; + } + + isDeletion() { + return false; + } + + isNoNewline() { + return false; + } + + when(callbacks) { + const callback = callbacks[this.constructor.name.toLowerCase()] || callbacks.default || (() => undefined); + return callback(); + } + + toStringIn(buffer) { + return this.range.toStringIn(buffer, this.constructor.origin); + } +} + +export class Addition extends Change { + static origin = '+'; + + isAddition() { + return true; + } +} + +export class Deletion extends Change { + static origin = '-'; + + isDeletion() { + return true; + } +} + +export class NoNewline extends Change { + static origin = '\\ '; + + isNoNewline() { + return true; + } +} diff --git a/test/models/patch/change.test.js b/test/models/patch/change.test.js new file mode 100644 index 0000000000..ddf4ce35f3 --- /dev/null +++ b/test/models/patch/change.test.js @@ -0,0 +1,150 @@ +import {Addition, Deletion, NoNewline} from '../../../lib/models/patch/change'; +import IndexedRowRange from '../../../lib/models/indexed-row-range'; + +describe('Changes', function() { + let buffer, range; + + beforeEach(function() { + buffer = '0000\n1111\n2222\n3333\n4444\n5555\n'; + range = new IndexedRowRange({bufferRange: [[1, 0], [3, 0]], startOffset: 5, endOffset: 20}); + }); + + describe('Addition', function() { + let addition; + + beforeEach(function() { + addition = new Addition(range); + }); + + it('has a range accessor', function() { + assert.strictEqual(addition.getRange(), range); + }); + + it('can be recognized by the isAddition predicate', function() { + assert.isTrue(addition.isAddition()); + assert.isFalse(addition.isDeletion()); + assert.isFalse(addition.isNoNewline()); + }); + + it('executes the "addition" branch of a when() call', function() { + const result = addition.when({ + addition: () => 'correct', + deletion: () => 'wrong: deletion', + nonewline: () => 'wrong: nonewline', + default: () => 'wrong: default', + }); + assert.strictEqual(result, 'correct'); + }); + + it('executes the "default" branch of a when() call when no "addition" is provided', function() { + const result = addition.when({ + deletion: () => 'wrong: deletion', + nonewline: () => 'wrong: nonewline', + default: () => 'correct', + }); + assert.strictEqual(result, 'correct'); + }); + + it('returns undefined from when() if neither "addition" nor "default" are provided', function() { + const result = addition.when({ + deletion: () => 'wrong: deletion', + nonewline: () => 'wrong: nonewline', + }); + assert.isUndefined(result); + }); + + it('uses "+" as a prefix for toStringIn()', function() { + assert.strictEqual(addition.toStringIn(buffer), '+1111\n+2222\n+3333\n'); + }); + }); + + describe('Deletion', function() { + let deletion; + + beforeEach(function() { + deletion = new Deletion(range); + }); + + it('can be recognized by the isDeletion predicate', function() { + assert.isFalse(deletion.isAddition()); + assert.isTrue(deletion.isDeletion()); + assert.isFalse(deletion.isNoNewline()); + }); + + it('executes the "deletion" branch of a when() call', function() { + const result = deletion.when({ + addition: () => 'wrong: addition', + deletion: () => 'correct', + nonewline: () => 'wrong: nonewline', + default: () => 'wrong: default', + }); + assert.strictEqual(result, 'correct'); + }); + + it('executes the "default" branch of a when() call when no "deletion" is provided', function() { + const result = deletion.when({ + addition: () => 'wrong: addition', + nonewline: () => 'wrong: nonewline', + default: () => 'correct', + }); + assert.strictEqual(result, 'correct'); + }); + + it('returns undefined from when() if neither "deletion" nor "default" are provided', function() { + const result = deletion.when({ + addition: () => 'wrong: addition', + nonewline: () => 'wrong: nonewline', + }); + assert.isUndefined(result); + }); + + it('uses "-" as a prefix for toStringIn()', function() { + assert.strictEqual(deletion.toStringIn(buffer), '-1111\n-2222\n-3333\n'); + }); + }); + + describe('NoNewline', function() { + let noNewline; + + beforeEach(function() { + noNewline = new NoNewline(range); + }); + + it('can be recognized by the isNoNewline predicate', function() { + assert.isFalse(noNewline.isAddition()); + assert.isFalse(noNewline.isDeletion()); + assert.isTrue(noNewline.isNoNewline()); + }); + + it('executes the "nonewline" branch of a when() call', function() { + const result = noNewline.when({ + addition: () => 'wrong: addition', + deletion: () => 'wrong: deletion', + nonewline: () => 'correct', + default: () => 'wrong: default', + }); + assert.strictEqual(result, 'correct'); + }); + + it('executes the "default" branch of a when() call when no "nonewline" is provided', function() { + const result = noNewline.when({ + addition: () => 'wrong: addition', + deletion: () => 'wrong: deletion', + default: () => 'correct', + }); + assert.strictEqual(result, 'correct'); + }); + + it('returns undefined from when() if neither "nonewline" nor "default" are provided', function() { + const result = noNewline.when({ + addition: () => 'wrong: addition', + deletion: () => 'wrong: deletion', + }); + assert.isUndefined(result); + }); + + it('uses "\\ " as a prefix for toStringIn()', function() { + assert.strictEqual(noNewline.toStringIn(buffer), '\\ 1111\n\\ 2222\n\\ 3333\n'); + }); + }); +}); From 7040d1f7c117809c52df16f81aea17830de6c564 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 15 Aug 2018 08:37:01 -0400 Subject: [PATCH 0145/4252] :fire: dot only --- test/models/indexed-row-range.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/models/indexed-row-range.test.js b/test/models/indexed-row-range.test.js index 01e970059a..1aa7a0331c 100644 --- a/test/models/indexed-row-range.test.js +++ b/test/models/indexed-row-range.test.js @@ -1,6 +1,6 @@ import IndexedRowRange, {nullIndexedRowRange} from '../../lib/models/indexed-row-range'; -describe.only('IndexedRowRange', function() { +describe('IndexedRowRange', function() { it('computes its row count', function() { const range = new IndexedRowRange({ bufferRange: [[0, 0], [1, 0]], From d203e585e2fdb2650f9f908f98b1c7b2c5d7f237 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 15 Aug 2018 09:15:05 -0400 Subject: [PATCH 0146/4252] Teach Changes to invert themselves --- lib/models/patch/change.js | 12 ++++++++++++ test/models/patch/change.test.js | 16 ++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/lib/models/patch/change.js b/lib/models/patch/change.js index 2e103faff2..5f75f9e0a0 100644 --- a/lib/models/patch/change.js +++ b/lib/models/patch/change.js @@ -35,6 +35,10 @@ export class Addition extends Change { isAddition() { return true; } + + invert() { + return new Deletion(this.getRange()); + } } export class Deletion extends Change { @@ -43,6 +47,10 @@ export class Deletion extends Change { isDeletion() { return true; } + + invert() { + return new Addition(this.getRange()); + } } export class NoNewline extends Change { @@ -51,4 +59,8 @@ export class NoNewline extends Change { isNoNewline() { return true; } + + invert() { + return this; + } } diff --git a/test/models/patch/change.test.js b/test/models/patch/change.test.js index ddf4ce35f3..dfab23f61c 100644 --- a/test/models/patch/change.test.js +++ b/test/models/patch/change.test.js @@ -56,6 +56,12 @@ describe('Changes', function() { it('uses "+" as a prefix for toStringIn()', function() { assert.strictEqual(addition.toStringIn(buffer), '+1111\n+2222\n+3333\n'); }); + + it('inverts to a deletion', function() { + const inverted = addition.invert(); + assert.isTrue(inverted.isDeletion()); + assert.strictEqual(inverted.getRange(), addition.getRange()); + }); }); describe('Deletion', function() { @@ -101,6 +107,12 @@ describe('Changes', function() { it('uses "-" as a prefix for toStringIn()', function() { assert.strictEqual(deletion.toStringIn(buffer), '-1111\n-2222\n-3333\n'); }); + + it('inverts to an addition', function() { + const inverted = deletion.invert(); + assert.isTrue(inverted.isAddition()); + assert.strictEqual(inverted.getRange(), deletion.getRange()); + }); }); describe('NoNewline', function() { @@ -146,5 +158,9 @@ describe('Changes', function() { it('uses "\\ " as a prefix for toStringIn()', function() { assert.strictEqual(noNewline.toStringIn(buffer), '\\ 1111\n\\ 2222\n\\ 3333\n'); }); + + it('inverts as itself', function() { + assert.strictEqual(noNewline.invert(), noNewline); + }); }); }); From 905e5607d9182ead50e3a958c84529bb7c8b3f8f Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 15 Aug 2018 09:15:24 -0400 Subject: [PATCH 0147/4252] Use Changes within the Hunk model --- lib/models/patch/hunk.js | 99 +++++++++++++--------------------- test/models/patch/hunk.test.js | 99 +++++++++++++++------------------- 2 files changed, 81 insertions(+), 117 deletions(-) diff --git a/lib/models/patch/hunk.js b/lib/models/patch/hunk.js index f4510ef180..97f89a330d 100644 --- a/lib/models/patch/hunk.js +++ b/lib/models/patch/hunk.js @@ -8,9 +8,7 @@ export default class Hunk { newRowCount, sectionHeading, rowRange, - additions, - deletions, - noNewline, + changes, }) { this.oldStartRow = oldStartRow; this.newStartRow = newStartRow; @@ -19,9 +17,7 @@ export default class Hunk { this.sectionHeading = sectionHeading; this.rowRange = rowRange; - this.additions = additions; - this.deletions = deletions; - this.noNewline = noNewline; + this.changes = changes; } getOldStartRow() { @@ -48,16 +44,25 @@ export default class Hunk { return this.sectionHeading; } + getChanges() { + return this.changes; + } + getAdditions() { - return this.additions; + return this.changes.filter(change => change.isAddition()).map(change => change.getRange()); } getDeletions() { - return this.deletions; + return this.changes.filter(change => change.isDeletion()).map(change => change.getRange()); } getNoNewline() { - return this.noNewline; + const lastChange = this.changes[this.changes.length - 1]; + if (lastChange && lastChange.isNoNewline()) { + return lastChange.getRange(); + } else { + return nullIndexedRowRange; + } } getRowRange() { @@ -77,13 +82,10 @@ export default class Hunk { } changedLineCount() { - return [ - this.additions, - this.deletions, - [this.noNewline], - ].reduce((count, ranges) => { - return ranges.reduce((subCount, range) => subCount + range.bufferRowCount(), count); - }, 0); + return this.changes.reduce((count, change) => change.when({ + nonewline: () => count, + default: () => count + change.getRange().bufferRowCount(), + }), 0); } invert() { @@ -94,65 +96,38 @@ export default class Hunk { newRowCount: this.getOldRowCount(), sectionHeading: this.getSectionHeading(), rowRange: this.rowRange, - additions: this.getDeletions(), - deletions: this.getAdditions(), - noNewline: this.getNoNewline(), + changes: this.getChanges().map(change => change.invert()), }); } toStringIn(bufferText) { let str = this.getHeader() + '\n'; - let additionIndex = 0; - let deletionIndex = 0; - - const endRange = new IndexedRowRange({ - bufferRange: [[0, 0], [0, 0]], - startOffset: this.rowRange.endOffset, - endOffset: this.rowRange.endOffset, - }); - - const nextRange = () => { - const nextAddition = this.additions[additionIndex] || nullIndexedRowRange; - const nextDeletion = this.deletions[deletionIndex] || nullIndexedRowRange; - - const minRange = [this.noNewline, nextAddition, nextDeletion, endRange].reduce((least, range) => { - return range.startOffset < least.startOffset ? range : least; - }); - - const unchanged = minRange.startOffset === currentOffset - ? nullIndexedRowRange - : new IndexedRowRange({ + let currentOffset = this.rowRange.startOffset; + for (const change of this.getChanges()) { + const range = change.getRange(); + if (range.startOffset !== currentOffset) { + const unchanged = new IndexedRowRange({ bufferRange: [[0, 0], [0, 0]], startOffset: currentOffset, - endOffset: minRange.startOffset, + endOffset: range.startOffset, }); - - if (minRange === nextAddition) { - additionIndex++; - return {origin: '+', range: minRange, unchanged}; - } else if (minRange === nextDeletion) { - deletionIndex++; - return {origin: '-', range: minRange, unchanged}; - } else if (minRange === endRange) { - return {origin: ' ', range: minRange, unchanged}; - } else { - return {origin: '\\', range: this.noNewline, unchanged}; + str += unchanged.toStringIn(bufferText, ' '); } - }; - let currentOffset = this.rowRange.startOffset; - while (currentOffset < bufferText.length) { - const {origin, range, unchanged} = nextRange(); - str += unchanged.toStringIn(bufferText, ' '); - - if (range === endRange) { - break; - } - - str += range.toStringIn(bufferText, origin); + str += change.toStringIn(bufferText); currentOffset = range.endOffset; } + + if (currentOffset !== this.rowRange.endOffset) { + const unchanged = new IndexedRowRange({ + bufferRange: [[0, 0], [0, 0]], + startOffset: currentOffset, + endOffset: this.rowRange.endOffset, + }); + str += unchanged.toStringIn(bufferText, ' '); + } + return str; } } diff --git a/test/models/patch/hunk.test.js b/test/models/patch/hunk.test.js index 530ab39c58..28ac982057 100644 --- a/test/models/patch/hunk.test.js +++ b/test/models/patch/hunk.test.js @@ -1,5 +1,6 @@ import Hunk from '../../../lib/models/patch/hunk'; -import IndexedRowRange, {nullIndexedRowRange} from '../../../lib/models/indexed-row-range'; +import IndexedRowRange from '../../../lib/models/indexed-row-range'; +import {Addition, Deletion, NoNewline} from '../../../lib/models/patch/change'; describe('Hunk', function() { const attrs = { @@ -13,14 +14,11 @@ describe('Hunk', function() { startOffset: 5, endOffset: 100, }), - additions: [ - new IndexedRowRange({bufferRange: [[1, 0], [2, 0]], startOffset: 6, endOffset: 7}), + changes: [ + new Addition(new IndexedRowRange({bufferRange: [[1, 0], [2, 0]], startOffset: 6, endOffset: 7})), + new Deletion(new IndexedRowRange({bufferRange: [[3, 0], [4, 0]], startOffset: 8, endOffset: 9})), + new Deletion(new IndexedRowRange({bufferRange: [[5, 0], [6, 0]], startOffset: 10, endOffset: 11})), ], - deletions: [ - new IndexedRowRange({bufferRange: [[3, 0], [4, 0]], startOffset: 8, endOffset: 9}), - new IndexedRowRange({bufferRange: [[5, 0], [6, 0]], startOffset: 10, endOffset: 11}), - ], - noNewline: nullIndexedRowRange, }; it('has some basic accessors', function() { @@ -35,14 +33,11 @@ describe('Hunk', function() { startOffset: 0, endOffset: 100, }), - additions: [ - new IndexedRowRange({bufferRange: [[1, 0], [2, 0]], startOffset: 6, endOffset: 7}), - ], - deletions: [ - new IndexedRowRange({bufferRange: [[3, 0], [4, 0]], startOffset: 8, endOffset: 9}), - new IndexedRowRange({bufferRange: [[5, 0], [6, 0]], startOffset: 10, endOffset: 11}), + changes: [ + new Addition(new IndexedRowRange({bufferRange: [[1, 0], [2, 0]], startOffset: 6, endOffset: 7})), + new Deletion(new IndexedRowRange({bufferRange: [[3, 0], [4, 0]], startOffset: 8, endOffset: 9})), + new Deletion(new IndexedRowRange({bufferRange: [[5, 0], [6, 0]], startOffset: 10, endOffset: 11})), ], - noNewline: nullIndexedRowRange, }); assert.strictEqual(h.getOldStartRow(), 0); @@ -50,6 +45,13 @@ describe('Hunk', function() { assert.strictEqual(h.getOldRowCount(), 2); assert.strictEqual(h.getNewRowCount(), 3); assert.strictEqual(h.getSectionHeading(), 'sectionHeading'); + assert.deepEqual(h.getRowRange().serialize(), { + bufferRange: [[0, 0], [10, 0]], + startOffset: 0, + endOffset: 100, + }); + assert.strictEqual(h.bufferRowCount(), 11); + assert.lengthOf(h.getChanges(), 3); assert.lengthOf(h.getAdditions(), 1); assert.lengthOf(h.getDeletions(), 2); assert.isFalse(h.getNoNewline().isPresent()); @@ -95,22 +97,18 @@ describe('Hunk', function() { it('computes the total number of changed lines', function() { const h0 = new Hunk({ ...attrs, - additions: [ - new IndexedRowRange({bufferRange: [[2, 0], [4, 0]], startOffset: 0, endOffset: 0}), - new IndexedRowRange({bufferRange: [[6, 0], [6, 0]], startOffset: 0, endOffset: 0}), + changes: [ + new Addition(new IndexedRowRange({bufferRange: [[2, 0], [4, 0]], startOffset: 0, endOffset: 0})), + new Addition(new IndexedRowRange({bufferRange: [[6, 0], [6, 0]], startOffset: 0, endOffset: 0})), + new Deletion(new IndexedRowRange({bufferRange: [[7, 0], [10, 0]], startOffset: 0, endOffset: 0})), + new NoNewline(new IndexedRowRange({bufferRange: [[12, 0], [12, 0]], startOffset: 0, endOffset: 0})), ], - deletions: [ - new IndexedRowRange({bufferRange: [[7, 0], [10, 0]], startOffset: 0, endOffset: 0}), - ], - noNewline: new IndexedRowRange({bufferRange: [[12, 0], [12, 0]], startOffset: 0, endOffset: 0}), }); - assert.strictEqual(h0.changedLineCount(), 9); + assert.strictEqual(h0.changedLineCount(), 8); const h1 = new Hunk({ ...attrs, - additions: [], - deletions: [], - noNewline: nullIndexedRowRange, + changes: [], }); assert.strictEqual(h1.changedLineCount(), 0); }); @@ -123,14 +121,12 @@ describe('Hunk', function() { oldRowCount: 2, newRowCount: 3, sectionHeading: 'the-heading', - additions: [ - new IndexedRowRange({bufferRange: [[2, 0], [4, 0]], startOffset: 0, endOffset: 0}), - new IndexedRowRange({bufferRange: [[6, 0], [6, 0]], startOffset: 0, endOffset: 0}), - ], - deletions: [ - new IndexedRowRange({bufferRange: [[7, 0], [10, 0]], startOffset: 0, endOffset: 0}), + changes: [ + new Addition(new IndexedRowRange({bufferRange: [[2, 0], [4, 0]], startOffset: 0, endOffset: 0})), + new Addition(new IndexedRowRange({bufferRange: [[6, 0], [6, 0]], startOffset: 0, endOffset: 0})), + new Deletion(new IndexedRowRange({bufferRange: [[7, 0], [10, 0]], startOffset: 0, endOffset: 0})), + new NoNewline(new IndexedRowRange({bufferRange: [[12, 0], [12, 0]], startOffset: 0, endOffset: 0})), ], - noNewline: new IndexedRowRange({bufferRange: [[12, 0], [12, 0]], startOffset: 0, endOffset: 0}), }); const inverted = original.invert(); @@ -139,9 +135,9 @@ describe('Hunk', function() { assert.strictEqual(inverted.getOldRowCount(), 3); assert.strictEqual(inverted.getNewRowCount(), 2); assert.strictEqual(inverted.getSectionHeading(), 'the-heading'); - assert.lengthOf(inverted.additions, 1); - assert.lengthOf(inverted.deletions, 2); - assert.isTrue(inverted.noNewline.isPresent()); + assert.lengthOf(inverted.getAdditions(), 1); + assert.lengthOf(inverted.getDeletions(), 2); + assert.isTrue(inverted.getNoNewline().isPresent()); }); describe('toStringIn()', function() { @@ -152,9 +148,7 @@ describe('Hunk', function() { newStartRow: 1, oldRowCount: 2, newRowCount: 3, - additions: [], - deletions: [], - noNewline: nullIndexedRowRange, + changes: [], }); assert.strictEqual(h.toStringIn(''), '@@ -0,2 +1,3 @@\n'); @@ -178,16 +172,14 @@ describe('Hunk', function() { startOffset: 5, endOffset: 91, }), - additions: [ - new IndexedRowRange({bufferRange: [[2, 0], [3, 0]], startOffset: 10, endOffset: 20}), - new IndexedRowRange({bufferRange: [[7, 0], [7, 0]], startOffset: 35, endOffset: 40}), - new IndexedRowRange({bufferRange: [[10, 0], [10, 0]], startOffset: 50, endOffset: 55}), + changes: [ + new Addition(new IndexedRowRange({bufferRange: [[2, 0], [3, 0]], startOffset: 10, endOffset: 20})), + new Deletion(new IndexedRowRange({bufferRange: [[5, 0], [5, 0]], startOffset: 25, endOffset: 30})), + new Addition(new IndexedRowRange({bufferRange: [[7, 0], [7, 0]], startOffset: 35, endOffset: 40})), + new Deletion(new IndexedRowRange({bufferRange: [[8, 0], [9, 0]], startOffset: 40, endOffset: 50})), + new Addition(new IndexedRowRange({bufferRange: [[10, 0], [10, 0]], startOffset: 50, endOffset: 55})), + new NoNewline(new IndexedRowRange({bufferRange: [[13, 0], [13, 0]], startOffset: 65, endOffset: 91})), ], - deletions: [ - new IndexedRowRange({bufferRange: [[5, 0], [5, 0]], startOffset: 25, endOffset: 30}), - new IndexedRowRange({bufferRange: [[8, 0], [9, 0]], startOffset: 40, endOffset: 50}), - ], - noNewline: new IndexedRowRange({bufferRange: [[13, 0], [13, 0]], startOffset: 65, endOffset: 91}), }); assert.strictEqual(h.toStringIn(buffer), [ @@ -204,7 +196,7 @@ describe('Hunk', function() { '+1000\n', ' 1111\n', ' 1222\n', - '\\No newline at end of file\n', + '\\ No newline at end of file\n', ].join('')); }); @@ -218,13 +210,10 @@ describe('Hunk', function() { oldRowCount: 1, newRowCount: 1, rowRange: new IndexedRowRange({bufferRange: [[0, 0], [3, 0]], startOffset: 0, endOffset: 20}), - additions: [ - new IndexedRowRange({bufferRange: [[1, 0], [1, 0]], startOffset: 5, endOffset: 10}), - ], - deletions: [ - new IndexedRowRange({bufferRange: [[2, 0], [2, 0]], startOffset: 10, endOffset: 15}), + changes: [ + new Addition(new IndexedRowRange({bufferRange: [[1, 0], [1, 0]], startOffset: 5, endOffset: 10})), + new Deletion(new IndexedRowRange({bufferRange: [[2, 0], [2, 0]], startOffset: 10, endOffset: 15})), ], - noNewline: nullIndexedRowRange, }); assert.strictEqual(h.toStringIn(buffer), [ From 5a2716f198508126ae628e22365e97170f489695 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 15 Aug 2018 09:59:29 -0400 Subject: [PATCH 0148/4252] Keep the NoNewline status marker as '\\' without the space --- lib/models/patch/change.js | 2 +- test/models/patch/change.test.js | 4 ++-- test/models/patch/hunk.test.js | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/models/patch/change.js b/lib/models/patch/change.js index 5f75f9e0a0..3442b8c0e4 100644 --- a/lib/models/patch/change.js +++ b/lib/models/patch/change.js @@ -54,7 +54,7 @@ export class Deletion extends Change { } export class NoNewline extends Change { - static origin = '\\ '; + static origin = '\\'; isNoNewline() { return true; diff --git a/test/models/patch/change.test.js b/test/models/patch/change.test.js index dfab23f61c..bab482f9b5 100644 --- a/test/models/patch/change.test.js +++ b/test/models/patch/change.test.js @@ -155,8 +155,8 @@ describe('Changes', function() { assert.isUndefined(result); }); - it('uses "\\ " as a prefix for toStringIn()', function() { - assert.strictEqual(noNewline.toStringIn(buffer), '\\ 1111\n\\ 2222\n\\ 3333\n'); + it('uses "\\" as a prefix for toStringIn()', function() { + assert.strictEqual(noNewline.toStringIn(buffer), '\\1111\n\\2222\n\\3333\n'); }); it('inverts as itself', function() { diff --git a/test/models/patch/hunk.test.js b/test/models/patch/hunk.test.js index 28ac982057..6118cf4e80 100644 --- a/test/models/patch/hunk.test.js +++ b/test/models/patch/hunk.test.js @@ -158,8 +158,8 @@ describe('Hunk', function() { const buffer = '0000\n0111\n0222\n0333\n0444\n0555\n0666\n0777\n0888\n0999\n' + '1000\n1111\n1222\n' + - 'No newline at end of file\n'; - // 0000.0111.0222.0333.0444.0555.0666.0777.0888.0999.1000.1111.1222.No newline at end of file. + ' No newline at end of file\n'; + // 0000.0111.0222.0333.0444.0555.0666.0777.0888.0999.1000.1111.1222. No newline at end of file. const h = new Hunk({ ...attrs, @@ -178,7 +178,7 @@ describe('Hunk', function() { new Addition(new IndexedRowRange({bufferRange: [[7, 0], [7, 0]], startOffset: 35, endOffset: 40})), new Deletion(new IndexedRowRange({bufferRange: [[8, 0], [9, 0]], startOffset: 40, endOffset: 50})), new Addition(new IndexedRowRange({bufferRange: [[10, 0], [10, 0]], startOffset: 50, endOffset: 55})), - new NoNewline(new IndexedRowRange({bufferRange: [[13, 0], [13, 0]], startOffset: 65, endOffset: 91})), + new NoNewline(new IndexedRowRange({bufferRange: [[13, 0], [13, 0]], startOffset: 65, endOffset: 92})), ], }); From e67ab801c5d762a83f7432453d9c874f35cab96e Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 15 Aug 2018 09:59:51 -0400 Subject: [PATCH 0149/4252] Update the PatchBufferAssertion helper to test Changes --- test/helpers.js | 29 ++++++++--------------------- 1 file changed, 8 insertions(+), 21 deletions(-) diff --git a/test/helpers.js b/test/helpers.js index 00a47c7e7c..c1a2ca2c0d 100644 --- a/test/helpers.js +++ b/test/helpers.js @@ -159,34 +159,21 @@ class PatchBufferAssertions { this.patch = patch; } - hunkChanges(changes, expectedStrings, expectedRanges) { - const actualStrings = changes.map(change => change.toStringIn(this.patch.getBufferText(), '*')); - const actualRanges = changes.map(change => change.bufferRange.serialize()); - - assert.deepEqual( - {strings: actualStrings, ranges: actualRanges}, - {strings: expectedStrings, ranges: expectedRanges}, - ); - } - - hunk(hunkIndex, {startRow, endRow, header, deletions, additions, noNewline}) { + hunk(hunkIndex, {startRow, endRow, header, changes}) { const hunk = this.patch.getHunks()[hunkIndex]; assert.isDefined(hunk); - deletions = deletions !== undefined ? deletions : {strings: [], ranges: []}; - additions = additions !== undefined ? additions : {strings: [], ranges: []}; - assert.deepEqual(hunk.getRowRange().serialize().bufferRange, [[startRow, 0], [endRow, 0]]); assert.strictEqual(hunk.getHeader(), header); + assert.lengthOf(hunk.getChanges(), changes.length); - this.hunkChanges(hunk.getDeletions(), deletions.strings, deletions.ranges); - this.hunkChanges(hunk.getAdditions(), additions.strings, additions.ranges); + for (let i = 0; i < changes.length; i++) { + const change = hunk.getChanges()[i]; + const spec = changes[i]; - const noNewlineChange = hunk.getNoNewline(); - if (noNewlineChange.isPresent()) { - this.hunkChanges([noNewlineChange], [noNewline.string], [noNewline.range]); - } else { - assert.isUndefined(noNewline); + assert.strictEqual(change.constructor.name.toLowerCase(), spec.kind); + assert.strictEqual(change.toStringIn(this.patch.getBufferText()), spec.string); + assert.deepEqual(change.getRange().bufferRange.serialize(), spec.range); } } From d7815a2598e62a5d77ca8d866c39c87b4144c9ab Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 15 Aug 2018 10:00:06 -0400 Subject: [PATCH 0150/4252] Construct Changes in Builder --- lib/models/patch/builder.js | 63 +++++++++------------ test/models/patch/builder.test.js | 94 ++++++++++++------------------- 2 files changed, 63 insertions(+), 94 deletions(-) diff --git a/lib/models/patch/builder.js b/lib/models/patch/builder.js index b611f711f6..ac697d304f 100644 --- a/lib/models/patch/builder.js +++ b/lib/models/patch/builder.js @@ -1,7 +1,8 @@ import Hunk from './hunk'; import File, {nullFile} from './file'; import Patch, {nullPatch} from './patch'; -import IndexedRowRange, {nullIndexedRowRange} from '../indexed-row-range'; +import IndexedRowRange from '../indexed-row-range'; +import {Addition, Deletion, NoNewline} from './change'; import FilePatch from './file-patch'; export default function buildFilePatch(diffs) { @@ -84,11 +85,11 @@ function dualDiffFilePatch(diff1, diff2) { return new FilePatch(oldFile, newFile, patch); } -const STATUS = { - '+': 'added', - '-': 'deleted', - ' ': 'unchanged', - '\\': 'nonewline', +const CHANGEKIND = { + '+': Addition, + '-': Deletion, + ' ': null, + '\\': NoNewline, }; function buildHunks(diff) { @@ -103,25 +104,26 @@ function buildHunks(diff) { const bufferStartRow = bufferRow; const bufferStartOffset = bufferOffset; - const additions = []; - const deletions = []; - const noNewlines = []; + const changes = []; - let lastStatus = null; + let LastChangeKind = null; let currentRangeStart = bufferRow; const finishCurrentRange = () => { - const ranges = { - added: additions, - deleted: deletions, - nonewline: noNewlines, - }[lastStatus]; - if (ranges !== undefined) { - ranges.push(new IndexedRowRange({ - bufferRange: [[currentRangeStart, 0], [bufferRow - 1, 0]], - startOffset, - endOffset: bufferOffset, - })); + if (currentRangeStart === bufferRow) { + return; + } + + if (LastChangeKind !== null) { + changes.push( + new LastChangeKind( + new IndexedRowRange({ + bufferRange: [[currentRangeStart, 0], [bufferRow - 1, 0]], + startOffset, + endOffset: bufferOffset, + }), + ), + ); } startOffset = bufferOffset; currentRangeStart = bufferRow; @@ -131,28 +133,21 @@ function buildHunks(diff) { const bufferLine = lineText.slice(1) + '\n'; bufferText += bufferLine; - const status = STATUS[lineText[0]]; - if (status === undefined) { + const ChangeKind = CHANGEKIND[lineText[0]]; + if (ChangeKind === undefined) { throw new Error(`Unknown diff status character: "${lineText[0]}"`); } - if (status !== lastStatus && lastStatus !== null) { + if (ChangeKind !== LastChangeKind) { finishCurrentRange(); } - lastStatus = status; + LastChangeKind = ChangeKind; bufferOffset += bufferLine.length; bufferRow++; } finishCurrentRange(); - let noNewline = nullIndexedRowRange; - if (noNewlines.length === 1) { - noNewline = noNewlines[0]; - } else if (noNewlines.length > 1) { - throw new Error('Multiple nonewline lines encountered in diff'); - } - hunks.push(new Hunk({ oldStartRow: hunkData.oldStartLine, newStartRow: hunkData.newStartLine, @@ -164,9 +159,7 @@ function buildHunks(diff) { startOffset: bufferStartOffset, endOffset: bufferOffset, }), - additions, - deletions, - noNewline, + changes, })); } diff --git a/test/models/patch/builder.test.js b/test/models/patch/builder.test.js index a77bbe351b..3bd1db0bbb 100644 --- a/test/models/patch/builder.test.js +++ b/test/models/patch/builder.test.js @@ -78,39 +78,30 @@ describe('buildFilePatch', function() { assertInPatch(p).hunks( { startRow: 0, + endRow: 8, header: '@@ -0,7 +0,6 @@', - deletions: { - strings: ['*line-1\n*line-2\n*line-3\n'], - ranges: [[[1, 0], [3, 0]]], - }, - additions: { - strings: ['*line-5\n*line-6\n'], - ranges: [[[5, 0], [6, 0]]], - }, + changes: [ + {kind: 'deletion', string: '-line-1\n-line-2\n-line-3\n', range: [[1, 0], [3, 0]]}, + {kind: 'addition', string: '+line-5\n+line-6\n', range: [[5, 0], [6, 0]]}, + ], }, { startRow: 9, + endRow: 12, header: '@@ -10,3 +11,3 @@', - deletions: { - strings: ['*line-9\n'], - ranges: [[[9, 0], [9, 0]]], - }, - additions: { - strings: ['*line-12\n'], - ranges: [[[12, 0], [12, 0]]], - }, + changes: [ + {kind: 'deletion', string: '-line-9\n', range: [[9, 0], [9, 0]]}, + {kind: 'addition', string: '+line-12\n', range: [[12, 0], [12, 0]]}, + ], }, { startRow: 13, + endRow: 18, header: '@@ -20,4 +21,4 @@', - deletions: { - strings: ['*line-14\n*line-15\n'], - ranges: [[[14, 0], [15, 0]]], - }, - additions: { - strings: ['*line-16\n*line-17\n'], - ranges: [[[16, 0], [17, 0]]], - }, + changes: [ + {kind: 'deletion', string: '-line-14\n-line-15\n', range: [[14, 0], [15, 0]]}, + {kind: 'addition', string: '+line-16\n+line-17\n', range: [[16, 0], [17, 0]]}, + ], }, ); }); @@ -206,35 +197,23 @@ describe('buildFilePatch', function() { newMode: '100644', status: 'modified', hunks: [{oldStartLine: 0, newStartLine: 0, oldLineCount: 1, newLineCount: 1, lines: [ - '+line-0', '-line-1', '\\No newline at end of file', + '+line-0', '-line-1', '\\ No newline at end of file', ]}], }]); - assert.strictEqual(p.getBufferText(), 'line-0\nline-1\nNo newline at end of file\n'); + assert.strictEqual(p.getBufferText(), 'line-0\nline-1\n No newline at end of file\n'); assertInPatch(p).hunks({ startRow: 0, + endRow: 2, header: '@@ -0,1 +0,1 @@', - additions: {strings: ['*line-0\n'], ranges: [[[0, 0], [0, 0]]]}, - deletions: {strings: ['*line-1\n'], ranges: [[[1, 0], [1, 0]]]}, - noNewline: {string: '*No newline at end of file\n', range: [[2, 0], [2, 0]]}, + changes: [ + {kind: 'addition', string: '+line-0\n', range: [[0, 0], [0, 0]]}, + {kind: 'deletion', string: '-line-1\n', range: [[1, 0], [1, 0]]}, + {kind: 'nonewline', string: '\\ No newline at end of file\n', range: [[2, 0], [2, 0]]}, + ], }); }); - - it('throws an error when multiple no-newline markers are encountered', function() { - assert.throws(() => { - buildFilePatch([{ - oldPath: 'old/path', - oldMode: '100644', - newPath: 'new/path', - newMode: '100644', - status: 'modified', - hunks: [{oldStartLine: 0, newStartLine: 0, oldLineCount: 1, newLineCount: 1, lines: [ - '\\No newline at end of file', ' unchanged', '\\No newline at end of file', - ]}], - }]); - }, /Multiple nonewline/); - }); }); describe('with a mode change and a content diff', function() { @@ -285,12 +264,11 @@ describe('buildFilePatch', function() { assert.strictEqual(p.getBufferText(), 'line-0\nline-1\n'); assertInPatch(p).hunks({ startRow: 0, + endRow: 1, header: '@@ -0,0 +0,2 @@', - deletions: {strings: [], ranges: []}, - additions: { - strings: ['*line-0\n*line-1\n'], - ranges: [[[0, 0], [1, 0]]], - }, + changes: [ + {kind: 'addition', string: '+line-0\n+line-1\n', range: [[0, 0], [1, 0]]}, + ], }); }); @@ -341,12 +319,11 @@ describe('buildFilePatch', function() { assert.strictEqual(p.getBufferText(), 'line-0\nline-1\n'); assertInPatch(p).hunks({ startRow: 0, + endRow: 1, header: '@@ -0,2 +0,0 @@', - deletions: { - strings: ['*line-0\n*line-1\n'], - ranges: [[[0, 0], [1, 0]]], - }, - additions: {strings: [], ranges: []}, + changes: [ + {kind: 'deletion', string: '-line-0\n-line-1\n', range: [[0, 0], [1, 0]]}, + ], }); }); @@ -396,12 +373,11 @@ describe('buildFilePatch', function() { assert.strictEqual(p.getBufferText(), 'line-0\nline-1\n'); assertInPatch(p).hunks({ startRow: 0, + endRow: 1, header: '@@ -0,0 +0,2 @@', - deletions: {strings: [], ranges: []}, - additions: { - strings: ['*line-0\n*line-1\n'], - ranges: [[[0, 0], [1, 0]]], - }, + changes: [ + {kind: 'addition', string: '+line-0\n+line-1\n', range: [[0, 0], [1, 0]]}, + ], }); }); From 97e2775dc2065c8b8d4df942834149bfda090a5a Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 15 Aug 2018 10:11:10 -0400 Subject: [PATCH 0151/4252] Use .offsetBy() to translate an IndexedRowRange --- lib/models/indexed-row-range.js | 16 ++++++++++++++++ test/models/indexed-row-range.test.js | 27 +++++++++++++++++++++++++-- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/lib/models/indexed-row-range.js b/lib/models/indexed-row-range.js index 0911012e85..d8c74a06a1 100644 --- a/lib/models/indexed-row-range.js +++ b/lib/models/indexed-row-range.js @@ -81,6 +81,18 @@ export default class IndexedRowRange { return intersections; } + offsetBy(bufferOffset, rowOffset) { + if (bufferOffset === 0 && rowOffset === 0) { + return this; + } + + return new this.constructor({ + bufferRange: this.bufferRange.translate([rowOffset, 0]), + startOffset: this.startOffset + bufferOffset, + endOffset: this.endOffset + bufferOffset, + }); + } + serialize() { return { bufferRange: this.bufferRange.serialize(), @@ -111,6 +123,10 @@ export const nullIndexedRowRange = { return []; }, + offsetBy() { + return this; + }, + isPresent() { return false; }, diff --git a/test/models/indexed-row-range.test.js b/test/models/indexed-row-range.test.js index 1aa7a0331c..eb8ea8c138 100644 --- a/test/models/indexed-row-range.test.js +++ b/test/models/indexed-row-range.test.js @@ -143,9 +143,32 @@ describe('IndexedRowRange', function() { }); describe('offsetBy()', function() { - it('returns the receiver as-is when there is no change'); + let original; - it('modifies the buffer range and the buffer offset'); + beforeEach(function() { + original = new IndexedRowRange({ + bufferRange: [[3, 0], [5, 0]], + startOffset: 15, + endOffset: 25, + }); + }); + + it('returns the receiver as-is when there is no change', function() { + assert.strictEqual(original.offsetBy(0, 0), original); + }); + + it('modifies the buffer range and the buffer offset', function() { + const changed = original.offsetBy(10, 3); + assert.deepEqual(changed.serialize(), { + bufferRange: [[6, 0], [8, 0]], + startOffset: 25, + endOffset: 35, + }); + }); + + it('is a no-op on a nullIndexedRowRange', function() { + assert.strictEqual(nullIndexedRowRange.offsetBy(100, 200), nullIndexedRowRange); + }); }); it('returns appropriate values from nullIndexedRowRange methods', function() { From 4c18a50f4dc2941ffd3a85dd08663cca72c2a5fb Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 15 Aug 2018 11:12:42 -0400 Subject: [PATCH 0152/4252] Rename getRange() to getRowRange() --- lib/models/patch/change.js | 18 +++++++++++++++--- lib/models/patch/hunk.js | 4 ++-- test/helpers.js | 2 +- test/models/patch/change.test.js | 12 +++++++++--- 4 files changed, 27 insertions(+), 9 deletions(-) diff --git a/lib/models/patch/change.js b/lib/models/patch/change.js index 3442b8c0e4..339a35faf1 100644 --- a/lib/models/patch/change.js +++ b/lib/models/patch/change.js @@ -3,7 +3,7 @@ class Change { this.range = range; } - getRange() { + getRowRange() { return this.range; } @@ -19,6 +19,18 @@ class Change { return false; } + getBufferRows() { + return this.range.getBufferRows(); + } + + getStartRange() { + return this.range.getStartRange(); + } + + bufferRowCount() { + return this.range.bufferRowCount(); + } + when(callbacks) { const callback = callbacks[this.constructor.name.toLowerCase()] || callbacks.default || (() => undefined); return callback(); @@ -37,7 +49,7 @@ export class Addition extends Change { } invert() { - return new Deletion(this.getRange()); + return new Deletion(this.getRowRange()); } } @@ -49,7 +61,7 @@ export class Deletion extends Change { } invert() { - return new Addition(this.getRange()); + return new Addition(this.getRowRange()); } } diff --git a/lib/models/patch/hunk.js b/lib/models/patch/hunk.js index 97f89a330d..e955601b9e 100644 --- a/lib/models/patch/hunk.js +++ b/lib/models/patch/hunk.js @@ -84,7 +84,7 @@ export default class Hunk { changedLineCount() { return this.changes.reduce((count, change) => change.when({ nonewline: () => count, - default: () => count + change.getRange().bufferRowCount(), + default: () => count + change.getRowRange().bufferRowCount(), }), 0); } @@ -105,7 +105,7 @@ export default class Hunk { let currentOffset = this.rowRange.startOffset; for (const change of this.getChanges()) { - const range = change.getRange(); + const range = change.getRowRange(); if (range.startOffset !== currentOffset) { const unchanged = new IndexedRowRange({ bufferRange: [[0, 0], [0, 0]], diff --git a/test/helpers.js b/test/helpers.js index c1a2ca2c0d..8f0caf2fba 100644 --- a/test/helpers.js +++ b/test/helpers.js @@ -173,7 +173,7 @@ class PatchBufferAssertions { assert.strictEqual(change.constructor.name.toLowerCase(), spec.kind); assert.strictEqual(change.toStringIn(this.patch.getBufferText()), spec.string); - assert.deepEqual(change.getRange().bufferRange.serialize(), spec.range); + assert.deepEqual(change.getRowRange().bufferRange.serialize(), spec.range); } } diff --git a/test/models/patch/change.test.js b/test/models/patch/change.test.js index bab482f9b5..4f18998a48 100644 --- a/test/models/patch/change.test.js +++ b/test/models/patch/change.test.js @@ -17,7 +17,13 @@ describe('Changes', function() { }); it('has a range accessor', function() { - assert.strictEqual(addition.getRange(), range); + assert.strictEqual(addition.getRowRange(), range); + }); + + it('delegates some methods to its row range', function() { + assert.sameMembers(Array.from(addition.getBufferRows()), [1, 2, 3]); + assert.deepEqual(addition.getStartRange().serialize(), [[1, 0], [1, 0]]); + assert.strictEqual(addition.bufferRowCount(), 3); }); it('can be recognized by the isAddition predicate', function() { @@ -60,7 +66,7 @@ describe('Changes', function() { it('inverts to a deletion', function() { const inverted = addition.invert(); assert.isTrue(inverted.isDeletion()); - assert.strictEqual(inverted.getRange(), addition.getRange()); + assert.strictEqual(inverted.getRowRange(), addition.getRowRange()); }); }); @@ -111,7 +117,7 @@ describe('Changes', function() { it('inverts to an addition', function() { const inverted = deletion.invert(); assert.isTrue(inverted.isAddition()); - assert.strictEqual(inverted.getRange(), deletion.getRange()); + assert.strictEqual(inverted.getRowRange(), deletion.getRowRange()); }); }); From 0b494b3266a4526d4fcf76e033e98442b54b2626 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 16 Aug 2018 11:34:02 -0400 Subject: [PATCH 0153/4252] Offset an IndexedRowRange with separate start and end translations --- lib/models/indexed-row-range.js | 10 +++++----- test/models/indexed-row-range.test.js | 9 +++++++++ 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/lib/models/indexed-row-range.js b/lib/models/indexed-row-range.js index d8c74a06a1..87b9b6d673 100644 --- a/lib/models/indexed-row-range.js +++ b/lib/models/indexed-row-range.js @@ -81,15 +81,15 @@ export default class IndexedRowRange { return intersections; } - offsetBy(bufferOffset, rowOffset) { - if (bufferOffset === 0 && rowOffset === 0) { + offsetBy(startBufferOffset, startRowOffset, endBufferOffset = startBufferOffset, endRowOffset = startRowOffset) { + if (startBufferOffset === 0 && startRowOffset === 0 && endBufferOffset === 0 && endRowOffset === 0) { return this; } return new this.constructor({ - bufferRange: this.bufferRange.translate([rowOffset, 0]), - startOffset: this.startOffset + bufferOffset, - endOffset: this.endOffset + bufferOffset, + bufferRange: this.bufferRange.translate([startRowOffset, 0], [endRowOffset, 0]), + startOffset: this.startOffset + startBufferOffset, + endOffset: this.endOffset + endBufferOffset, }); } diff --git a/test/models/indexed-row-range.test.js b/test/models/indexed-row-range.test.js index eb8ea8c138..27daef0d27 100644 --- a/test/models/indexed-row-range.test.js +++ b/test/models/indexed-row-range.test.js @@ -166,6 +166,15 @@ describe('IndexedRowRange', function() { }); }); + it('may specify separate start and end offsets', function() { + const changed = original.offsetBy(10, 2, 30, 4); + assert.deepEqual(changed.serialize(), { + bufferRange: [[5, 0], [9, 0]], + startOffset: 25, + endOffset: 55, + }); + }); + it('is a no-op on a nullIndexedRowRange', function() { assert.strictEqual(nullIndexedRowRange.offsetBy(100, 200), nullIndexedRowRange); }); From 3f4b96cf26e957dad52f7a5459dd4f1eb7c0328c Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 16 Aug 2018 11:35:05 -0400 Subject: [PATCH 0154/4252] Rename Change to Region and add an Unchanged region --- lib/models/patch/{change.js => region.js} | 36 +++++++-- test/models/patch/hunk.test.js | 2 +- .../patch/{change.test.js => region.test.js} | 77 ++++++++++++++++++- 3 files changed, 106 insertions(+), 9 deletions(-) rename lib/models/patch/{change.js => region.js} (72%) rename test/models/patch/{change.test.js => region.test.js} (68%) diff --git a/lib/models/patch/change.js b/lib/models/patch/region.js similarity index 72% rename from lib/models/patch/change.js rename to lib/models/patch/region.js index 339a35faf1..4535ae9a07 100644 --- a/lib/models/patch/change.js +++ b/lib/models/patch/region.js @@ -1,4 +1,4 @@ -class Change { +class Region { constructor(range) { this.range = range; } @@ -15,6 +15,10 @@ class Change { return false; } + isUnchanged() { + return false; + } + isNoNewline() { return false; } @@ -39,9 +43,17 @@ class Change { toStringIn(buffer) { return this.range.toStringIn(buffer, this.constructor.origin); } + + invert() { + return this; + } + + isChange() { + return true; + } } -export class Addition extends Change { +export class Addition extends Region { static origin = '+'; isAddition() { @@ -53,7 +65,7 @@ export class Addition extends Change { } } -export class Deletion extends Change { +export class Deletion extends Region { static origin = '-'; isDeletion() { @@ -65,14 +77,26 @@ export class Deletion extends Change { } } -export class NoNewline extends Change { +export class Unchanged extends Region { + static origin = ' '; + + isUnchanged() { + return true; + } + + isChange() { + return false; + } +} + +export class NoNewline extends Region { static origin = '\\'; isNoNewline() { return true; } - invert() { - return this; + isChange() { + return false; } } diff --git a/test/models/patch/hunk.test.js b/test/models/patch/hunk.test.js index 6118cf4e80..ba412defca 100644 --- a/test/models/patch/hunk.test.js +++ b/test/models/patch/hunk.test.js @@ -1,6 +1,6 @@ import Hunk from '../../../lib/models/patch/hunk'; import IndexedRowRange from '../../../lib/models/indexed-row-range'; -import {Addition, Deletion, NoNewline} from '../../../lib/models/patch/change'; +import {Addition, Deletion, NoNewline} from '../../../lib/models/patch/region'; describe('Hunk', function() { const attrs = { diff --git a/test/models/patch/change.test.js b/test/models/patch/region.test.js similarity index 68% rename from test/models/patch/change.test.js rename to test/models/patch/region.test.js index 4f18998a48..c7ca5dd6b6 100644 --- a/test/models/patch/change.test.js +++ b/test/models/patch/region.test.js @@ -1,7 +1,7 @@ -import {Addition, Deletion, NoNewline} from '../../../lib/models/patch/change'; +import {Addition, Deletion, NoNewline, Unchanged} from '../../../lib/models/patch/region'; import IndexedRowRange from '../../../lib/models/indexed-row-range'; -describe('Changes', function() { +describe('Regions', function() { let buffer, range; beforeEach(function() { @@ -29,13 +29,17 @@ describe('Changes', function() { it('can be recognized by the isAddition predicate', function() { assert.isTrue(addition.isAddition()); assert.isFalse(addition.isDeletion()); + assert.isFalse(addition.isUnchanged()); assert.isFalse(addition.isNoNewline()); + + assert.isTrue(addition.isChange()); }); it('executes the "addition" branch of a when() call', function() { const result = addition.when({ addition: () => 'correct', deletion: () => 'wrong: deletion', + unchanged: () => 'wrong: unchanged', nonewline: () => 'wrong: nonewline', default: () => 'wrong: default', }); @@ -45,6 +49,7 @@ describe('Changes', function() { it('executes the "default" branch of a when() call when no "addition" is provided', function() { const result = addition.when({ deletion: () => 'wrong: deletion', + unchanged: () => 'wrong: unchanged', nonewline: () => 'wrong: nonewline', default: () => 'correct', }); @@ -54,6 +59,7 @@ describe('Changes', function() { it('returns undefined from when() if neither "addition" nor "default" are provided', function() { const result = addition.when({ deletion: () => 'wrong: deletion', + unchanged: () => 'wrong: unchanged', nonewline: () => 'wrong: nonewline', }); assert.isUndefined(result); @@ -80,13 +86,17 @@ describe('Changes', function() { it('can be recognized by the isDeletion predicate', function() { assert.isFalse(deletion.isAddition()); assert.isTrue(deletion.isDeletion()); + assert.isFalse(deletion.isUnchanged()); assert.isFalse(deletion.isNoNewline()); + + assert.isTrue(deletion.isChange()); }); it('executes the "deletion" branch of a when() call', function() { const result = deletion.when({ addition: () => 'wrong: addition', deletion: () => 'correct', + unchanged: () => 'wrong: unchanged', nonewline: () => 'wrong: nonewline', default: () => 'wrong: default', }); @@ -96,6 +106,7 @@ describe('Changes', function() { it('executes the "default" branch of a when() call when no "deletion" is provided', function() { const result = deletion.when({ addition: () => 'wrong: addition', + unchanged: () => 'wrong: unchanged', nonewline: () => 'wrong: nonewline', default: () => 'correct', }); @@ -105,6 +116,7 @@ describe('Changes', function() { it('returns undefined from when() if neither "deletion" nor "default" are provided', function() { const result = deletion.when({ addition: () => 'wrong: addition', + unchanged: () => 'wrong: unchanged', nonewline: () => 'wrong: nonewline', }); assert.isUndefined(result); @@ -121,6 +133,61 @@ describe('Changes', function() { }); }); + describe('Unchanged', function() { + let unchanged; + + beforeEach(function() { + unchanged = new Unchanged(range); + }); + + it('can be recognized by the isUnchanged predicate', function() { + assert.isFalse(unchanged.isAddition()); + assert.isFalse(unchanged.isDeletion()); + assert.isTrue(unchanged.isUnchanged()); + assert.isFalse(unchanged.isNoNewline()); + + assert.isFalse(unchanged.isChange()); + }); + + it('executes the "unchanged" branch of a when() call', function() { + const result = unchanged.when({ + addition: () => 'wrong: addition', + deletion: () => 'wrong: deletion', + unchanged: () => 'correct', + nonewline: () => 'wrong: nonewline', + default: () => 'wrong: default', + }); + assert.strictEqual(result, 'correct'); + }); + + it('executes the "default" branch of a when() call when no "unchanged" is provided', function() { + const result = unchanged.when({ + addition: () => 'wrong: addition', + deletion: () => 'wrong: deletion', + nonewline: () => 'wrong: nonewline', + default: () => 'correct', + }); + assert.strictEqual(result, 'correct'); + }); + + it('returns undefined from when() if neither "unchanged" nor "default" are provided', function() { + const result = unchanged.when({ + addition: () => 'wrong: addition', + deletion: () => 'wrong: deletion', + nonewline: () => 'wrong: nonewline', + }); + assert.isUndefined(result); + }); + + it('uses " " as a prefix for toStringIn()', function() { + assert.strictEqual(unchanged.toStringIn(buffer), ' 1111\n 2222\n 3333\n'); + }); + + it('inverts as itself', function() { + assert.strictEqual(unchanged.invert(), unchanged); + }); + }); + describe('NoNewline', function() { let noNewline; @@ -131,13 +198,17 @@ describe('Changes', function() { it('can be recognized by the isNoNewline predicate', function() { assert.isFalse(noNewline.isAddition()); assert.isFalse(noNewline.isDeletion()); + assert.isFalse(noNewline.isUnchanged()); assert.isTrue(noNewline.isNoNewline()); + + assert.isFalse(noNewline.isChange()); }); it('executes the "nonewline" branch of a when() call', function() { const result = noNewline.when({ addition: () => 'wrong: addition', deletion: () => 'wrong: deletion', + unchanged: () => 'wrong: unchanged', nonewline: () => 'correct', default: () => 'wrong: default', }); @@ -148,6 +219,7 @@ describe('Changes', function() { const result = noNewline.when({ addition: () => 'wrong: addition', deletion: () => 'wrong: deletion', + unchanged: () => 'wrong: unchanged', default: () => 'correct', }); assert.strictEqual(result, 'correct'); @@ -157,6 +229,7 @@ describe('Changes', function() { const result = noNewline.when({ addition: () => 'wrong: addition', deletion: () => 'wrong: deletion', + unchanged: () => 'wrong: unchanged', }); assert.isUndefined(result); }); From 69094aa27737e499347c82111b9759aba088b6b5 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 16 Aug 2018 11:36:20 -0400 Subject: [PATCH 0155/4252] Iterate over all contiguous Regions within a Hunk --- lib/models/patch/hunk.js | 77 +++++++++++++++++++------------- test/models/patch/hunk.test.js | 80 +++++++++++++++++++++++++++++++--- 2 files changed, 120 insertions(+), 37 deletions(-) diff --git a/lib/models/patch/hunk.js b/lib/models/patch/hunk.js index e955601b9e..1b4290d20e 100644 --- a/lib/models/patch/hunk.js +++ b/lib/models/patch/hunk.js @@ -1,4 +1,5 @@ import IndexedRowRange, {nullIndexedRowRange} from '../indexed-row-range'; +import {Unchanged} from './region'; export default class Hunk { constructor({ @@ -48,18 +49,55 @@ export default class Hunk { return this.changes; } - getAdditions() { - return this.changes.filter(change => change.isAddition()).map(change => change.getRange()); + getRegions() { + const regions = []; + let currentRow = this.rowRange.bufferRange.start.row; + let currentPosition = this.rowRange.startOffset; + + for (const change of this.changes) { + const startRow = change.getRowRange().bufferRange.start.row; + const startPosition = change.getRowRange().startOffset; + + if (currentRow !== startRow) { + regions.push(new Unchanged(new IndexedRowRange({ + bufferRange: [[currentRow, 0], [startRow - 1, 0]], + startOffset: currentPosition, + endOffset: startPosition, + }))); + } + + regions.push(change); + + currentRow = change.getRowRange().bufferRange.end.row + 1; + currentPosition = change.getRowRange().endOffset; + } + + const endRow = this.rowRange.bufferRange.end.row; + const endPosition = this.rowRange.endOffset; + + if (currentRow <= endRow) { + regions.push(new Unchanged(new IndexedRowRange({ + bufferRange: [[currentRow, 0], [endRow, 0]], + startOffset: currentPosition, + endOffset: endPosition, + }))); + } + + return regions; + } + + getAdditionRanges() { + return this.changes.filter(change => change.isAddition()).map(change => change.getRowRange()); } - getDeletions() { - return this.changes.filter(change => change.isDeletion()).map(change => change.getRange()); + getDeletionRanges() { + return this.changes.filter(change => change.isDeletion()).map(change => change.getRowRange()); } - getNoNewline() { + getNoNewlineRange() { const lastChange = this.changes[this.changes.length - 1]; if (lastChange && lastChange.isNoNewline()) { - return lastChange.getRange(); + return lastChange.getRowRange(); } else { return nullIndexedRowRange; } @@ -102,32 +140,9 @@ export default class Hunk { toStringIn(bufferText) { let str = this.getHeader() + '\n'; - - let currentOffset = this.rowRange.startOffset; - for (const change of this.getChanges()) { - const range = change.getRowRange(); - if (range.startOffset !== currentOffset) { - const unchanged = new IndexedRowRange({ - bufferRange: [[0, 0], [0, 0]], - startOffset: currentOffset, - endOffset: range.startOffset, - }); - str += unchanged.toStringIn(bufferText, ' '); - } - - str += change.toStringIn(bufferText); - currentOffset = range.endOffset; - } - - if (currentOffset !== this.rowRange.endOffset) { - const unchanged = new IndexedRowRange({ - bufferRange: [[0, 0], [0, 0]], - startOffset: currentOffset, - endOffset: this.rowRange.endOffset, - }); - str += unchanged.toStringIn(bufferText, ' '); + for (const region of this.getRegions()) { + str += region.toStringIn(bufferText); } - return str; } } diff --git a/test/models/patch/hunk.test.js b/test/models/patch/hunk.test.js index ba412defca..671e9232bc 100644 --- a/test/models/patch/hunk.test.js +++ b/test/models/patch/hunk.test.js @@ -52,9 +52,9 @@ describe('Hunk', function() { }); assert.strictEqual(h.bufferRowCount(), 11); assert.lengthOf(h.getChanges(), 3); - assert.lengthOf(h.getAdditions(), 1); - assert.lengthOf(h.getDeletions(), 2); - assert.isFalse(h.getNoNewline().isPresent()); + assert.lengthOf(h.getAdditionRanges(), 1); + assert.lengthOf(h.getDeletionRanges(), 2); + assert.isFalse(h.getNoNewlineRange().isPresent()); }); it('creates its start range for decoration placement', function() { @@ -82,6 +82,74 @@ describe('Hunk', function() { assert.strictEqual(h.getHeader(), '@@ -0,2 +1,3 @@'); }); + it('returns a full set of covered regions, including unchanged', function() { + const h = new Hunk({ + ...attrs, + rowRange: new IndexedRowRange({ + bufferRange: [[0, 0], [11, 0]], + startOffset: 0, + endOffset: 120, + }), + changes: [ + new Addition(new IndexedRowRange({bufferRange: [[1, 0], [3, 0]], startOffset: 10, endOffset: 40})), + new Deletion(new IndexedRowRange({bufferRange: [[5, 0], [6, 0]], startOffset: 50, endOffset: 70})), + new Deletion(new IndexedRowRange({bufferRange: [[7, 0], [9, 0]], startOffset: 70, endOffset: 100})), + ], + }); + + const regions = h.getRegions(); + assert.lengthOf(regions, 6); + + assert.isTrue(regions[0].isUnchanged()); + assert.deepEqual(regions[0].range.serialize(), {bufferRange: [[0, 0], [0, 0]], startOffset: 0, endOffset: 10}); + + assert.isTrue(regions[1].isAddition()); + assert.deepEqual(regions[1].range.serialize(), {bufferRange: [[1, 0], [3, 0]], startOffset: 10, endOffset: 40}); + + assert.isTrue(regions[2].isUnchanged()); + assert.deepEqual(regions[2].range.serialize(), {bufferRange: [[4, 0], [4, 0]], startOffset: 40, endOffset: 50}); + + assert.isTrue(regions[3].isDeletion()); + assert.deepEqual(regions[3].range.serialize(), {bufferRange: [[5, 0], [6, 0]], startOffset: 50, endOffset: 70}); + + assert.isTrue(regions[4].isDeletion()); + assert.deepEqual(regions[4].range.serialize(), {bufferRange: [[7, 0], [9, 0]], startOffset: 70, endOffset: 100}); + + assert.isTrue(regions[5].isUnchanged()); + assert.deepEqual(regions[5].range.serialize(), {bufferRange: [[10, 0], [11, 0]], startOffset: 100, endOffset: 120}); + }); + + it('omits empty regions at the hunk beginning and end', function() { + const h = new Hunk({ + ...attrs, + rowRange: new IndexedRowRange({ + bufferRange: [[1, 0], [9, 0]], + startOffset: 10, + endOffset: 100, + }), + changes: [ + new Addition(new IndexedRowRange({bufferRange: [[1, 0], [3, 0]], startOffset: 10, endOffset: 40})), + new Deletion(new IndexedRowRange({bufferRange: [[5, 0], [6, 0]], startOffset: 50, endOffset: 70})), + new Deletion(new IndexedRowRange({bufferRange: [[7, 0], [9, 0]], startOffset: 70, endOffset: 100})), + ], + }); + + const regions = h.getRegions(); + assert.lengthOf(regions, 4); + + assert.isTrue(regions[0].isAddition()); + assert.deepEqual(regions[0].range.serialize(), {bufferRange: [[1, 0], [3, 0]], startOffset: 10, endOffset: 40}); + + assert.isTrue(regions[1].isUnchanged()); + assert.deepEqual(regions[1].range.serialize(), {bufferRange: [[4, 0], [4, 0]], startOffset: 40, endOffset: 50}); + + assert.isTrue(regions[2].isDeletion()); + assert.deepEqual(regions[2].range.serialize(), {bufferRange: [[5, 0], [6, 0]], startOffset: 50, endOffset: 70}); + + assert.isTrue(regions[3].isDeletion()); + assert.deepEqual(regions[3].range.serialize(), {bufferRange: [[7, 0], [9, 0]], startOffset: 70, endOffset: 100}); + }); + it('returns a set of covered buffer rows', function() { const h = new Hunk({ ...attrs, @@ -135,9 +203,9 @@ describe('Hunk', function() { assert.strictEqual(inverted.getOldRowCount(), 3); assert.strictEqual(inverted.getNewRowCount(), 2); assert.strictEqual(inverted.getSectionHeading(), 'the-heading'); - assert.lengthOf(inverted.getAdditions(), 1); - assert.lengthOf(inverted.getDeletions(), 2); - assert.isTrue(inverted.getNoNewline().isPresent()); + assert.lengthOf(inverted.getAdditionRanges(), 1); + assert.lengthOf(inverted.getDeletionRanges(), 2); + assert.isTrue(inverted.getNoNewlineRange().isPresent()); }); describe('toStringIn()', function() { From 48382dca2449860c4fd3859d531ce81fe762d5a0 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 16 Aug 2018 11:37:48 -0400 Subject: [PATCH 0156/4252] New tests for stage patch generation --- lib/models/patch/patch.js | 166 ++++++++++++++++++------ test/models/patch/patch.test.js | 217 ++++++++++++++++++++++++++++++-- 2 files changed, 335 insertions(+), 48 deletions(-) diff --git a/lib/models/patch/patch.js b/lib/models/patch/patch.js index aebdd83a94..161547cff7 100644 --- a/lib/models/patch/patch.js +++ b/lib/models/patch/patch.js @@ -1,4 +1,65 @@ import Hunk from './hunk'; +import {Addition, Deletion, NoNewline} from './region'; + +class BufferBuilder { + constructor(original) { + this.originalBufferText = original; + this.bufferText = ''; + this.positionOffset = 0; + this.rowOffset = 0; + + this.hunkBufferText = ''; + this.hunkRowCount = 0; + this.hunkStartPositionOffset = 0; + this.hunkStartRowOffset = 0; + + this.lastOffset = 0; + } + + append(rowRange) { + this.hunkBufferText += this.originalBufferText.slice(rowRange.startOffset, rowRange.endOffset); + this.hunkRowCount += rowRange.bufferRowCount(); + } + + remove(rowRange) { + this.rowOffset -= rowRange.bufferRowCount(); + this.positionOffset -= rowRange.endOffset - rowRange.startOffset; + } + + latestHunkWasIncluded() { + this.bufferText += this.hunkBufferText; + + this.hunkBufferText = ''; + this.hunkRowCount = 0; + this.hunkStartPositionOffset = this.positionOffset; + this.hunkStartRowOffset = this.rowOffset; + } + + latestHunkWasDiscarded() { + this.rowOffset -= this.hunkRowCount; + this.positionOffset -= this.hunkBufferText.length; + + this.hunkBufferText = ''; + this.hunkRowCount = 0; + this.hunkStartPositionOffset = this.positionOffset; + this.hunkStartRowOffset = this.rowOffset; + } + + applyOffsetTo(rowRange) { + return rowRange.offsetBy(this.positionOffset, this.rowOffset); + } + + applyHunkOffsetsTo(rowRange) { + return rowRange.offsetBy( + this.hunkStartPositionOffset, this.hunkStartRowOffset, + this.positionOffset, this.rowOffset, + ); + } + + getBufferText() { + return this.bufferText; + } +} export default class Patch { constructor({status, hunks, bufferText}) { @@ -37,58 +98,93 @@ export default class Patch { }); } - getStagePatchForLines(lineSet) { + getStagePatchForLines(rowSet) { + const builder = new BufferBuilder(this.getBufferText()); const hunks = []; - let delta = 0; + + let newRowDelta = 0; for (const hunk of this.getHunks()) { - const additions = []; - const deletions = []; - let notAddedRowCount = 0; - let deletedRowCount = 0; - let notDeletedRowCount = 0; + const changes = []; + let noNewlineChange = null; + let selectedDeletionRowCount = 0; + let noNewlineRowCount = 0; - for (const change of hunk.getAdditions()) { - notAddedRowCount += change.bufferRowCount(); - for (const intersection of change.intersectRowsIn(lineSet, this.getBufferText())) { - notAddedRowCount -= intersection.bufferRowCount(); - additions.push(intersection); + for (const region of hunk.getRegions()) { + for (const {intersection, gap} of region.getRowRange().intersectRowsIn(rowSet, this.getBufferText(), true)) { + region.when({ + addition: () => { + if (gap) { + // Unselected addition: omit from new buffer + builder.remove(intersection); + } else { + // Selected addition: include in new patch + builder.append(intersection); + changes.push(new Addition( + builder.applyOffsetTo(intersection), + )); + } + }, + deletion: () => { + if (gap) { + // Unselected deletion: convert to context row + builder.append(intersection); + } else { + // Selected deletion: include in new patch + builder.append(intersection); + changes.push(new Deletion( + builder.applyOffsetTo(intersection), + )); + selectedDeletionRowCount += intersection.bufferRowCount(); + } + }, + unchanged: () => { + // Untouched context line: include in new patch + builder.append(intersection); + }, + nonewline: () => { + builder.append(intersection); + noNewlineChange = new NoNewline( + builder.applyOffsetTo(intersection), + ); + noNewlineRowCount += intersection.bufferRowCount(); + }, + }); } } - for (const change of hunk.getDeletions()) { - notDeletedRowCount += change.bufferRowCount(); - for (const intersection of change.intersectRowsIn(lineSet, this.getBufferText())) { - deletedRowCount += intersection.bufferRowCount(); - notDeletedRowCount -= intersection.bufferRowCount(); - deletions.push(intersection); + if (changes.length > 0) { + // Hunk contains at least one selected line + if (noNewlineChange !== null) { + changes.push(noNewlineChange); } - } - if (additions.length > 0 || deletions.length > 0) { - // Hunk contains at least one selected line + const rowRange = builder.applyHunkOffsetsTo(hunk.getRowRange()); + const newStartRow = hunk.getNewStartRow() + newRowDelta; + const newRowCount = rowRange.bufferRowCount() - selectedDeletionRowCount - noNewlineRowCount; + hunks.push(new Hunk({ oldStartRow: hunk.getOldStartRow(), - newStartRow: hunk.getNewStartRow() + delta, oldRowCount: hunk.getOldRowCount(), - newRowCount: hunk.bufferRowCount() - deletedRowCount, + newStartRow, + newRowCount, sectionHeading: hunk.getSectionHeading(), - rowRange: hunk.getRowRange(), - additions, - deletions, - noNewline: hunk.getNoNewline(), + rowRange, + changes, })); - } - delta += notDeletedRowCount - notAddedRowCount; - } + newRowDelta += newRowCount - hunk.getNewRowCount(); - if (this.getStatus() === 'deleted') { - // Set status to modified - return this.clone({hunks, status: 'modified'}); - } else { - return this.clone({hunks}); + builder.latestHunkWasIncluded(); + } else { + newRowDelta += hunk.getOldRowCount() - hunk.getNewRowCount(); + + builder.latestHunkWasDiscarded(); + } } + + const status = this.getStatus() === 'deleted' ? 'modified' : this.getStatus(); + return this.clone({hunks, status, bufferText: builder.getBufferText()}); } getUnstagePatchForLines(lineSet) { diff --git a/test/models/patch/patch.test.js b/test/models/patch/patch.test.js index 131ae9faa1..5801e0c873 100644 --- a/test/models/patch/patch.test.js +++ b/test/models/patch/patch.test.js @@ -1,6 +1,8 @@ import Patch, {nullPatch} from '../../../lib/models/patch/patch'; import Hunk from '../../../lib/models/patch/hunk'; -import IndexedRowRange, {nullIndexedRowRange} from '../../../lib/models/indexed-row-range'; +import IndexedRowRange from '../../../lib/models/indexed-row-range'; +import {Addition, Deletion, NoNewline} from '../../../lib/models/patch/region'; +import {assertInPatch} from '../../helpers'; describe('Patch', function() { it('has some standard accessors', function() { @@ -16,6 +18,39 @@ describe('Patch', function() { assert.strictEqual(p.getByteSize(), 12); }); + it('computes the total changed line count', function() { + const hunks = [ + new Hunk({ + oldStartRow: 0, + newStartRow: 0, + oldRowCount: 1, + newRowCount: 1, + sectionHeading: 'zero', + rowRange: new IndexedRowRange({bufferRange: [[0, 0], [5, 0]], startOffset: 0, endOffset: 30}), + changes: [ + new Addition(new IndexedRowRange({bufferRange: [[1, 0], [1, 0]], startOffset: 5, endOffset: 10})), + new Deletion(new IndexedRowRange({bufferRange: [[3, 0], [4, 0]], startOffset: 15, endOffset: 25})), + ], + }), + new Hunk({ + oldStartRow: 0, + newStartRow: 0, + oldRowCount: 1, + newRowCount: 1, + sectionHeading: 'one', + rowRange: new IndexedRowRange({bufferRange: [[6, 0], [15, 0]], startOffset: 30, endOffset: 80}), + changes: [ + new Deletion(new IndexedRowRange({bufferRange: [[7, 0], [7, 0]], startOffset: 35, endOffset: 40})), + new Deletion(new IndexedRowRange({bufferRange: [[9, 0], [11, 0]], startOffset: 45, endOffset: 60})), + new Addition(new IndexedRowRange({bufferRange: [[12, 0], [14, 0]], startOffset: 60, endOffset: 75})), + ], + }), + ]; + const p = new Patch({status: 'modified', hunks, bufferText: 'bufferText'}); + + assert.strictEqual(p.getChangedLineCount(), 10); + }); + it('clones itself with optionally overridden properties', function() { const original = new Patch({status: 'modified', hunks: [], bufferText: 'bufferText'}); @@ -31,7 +66,7 @@ describe('Patch', function() { assert.deepEqual(dup1.getHunks(), []); assert.strictEqual(dup1.getBufferText(), 'bufferText'); - const hunks = [new Hunk({})]; + const hunks = [new Hunk({changes: []})]; const dup2 = original.clone({hunks}); assert.notStrictEqual(dup2, original); assert.strictEqual(dup2.getStatus(), 'modified'); @@ -56,7 +91,7 @@ describe('Patch', function() { assert.deepEqual(dup0.getHunks(), []); assert.strictEqual(dup0.getBufferText(), ''); - const hunks = [new Hunk({})]; + const hunks = [new Hunk({changes: []})]; const dup1 = nullPatch.clone({hunks}); assert.notStrictEqual(dup1, nullPatch); assert.isNull(dup1.getStatus()); @@ -70,11 +105,109 @@ describe('Patch', function() { assert.strictEqual(dup2.getBufferText(), 'changed'); }); + describe('stage patch generation', function() { + it('creates a patch that applies selected lines from only a single hunk', function() { + const patch = buildPatchFixture(); + const stagePatch = patch.getStagePatchForLines(new Set([8, 13, 14, 16])); + // buffer rows: 0 1 2 3 4 5 6 7 8 9 + const expectedBufferText = '0007\n0008\n0010\n0011\n0012\n0013\n0014\n0015\n0016\n0018\n'; + assert.strictEqual(stagePatch.getBufferText(), expectedBufferText); + assertInPatch(stagePatch).hunks( + { + startRow: 0, + endRow: 9, + header: '@@ -12,9 +12,7 @@', + changes: [ + {kind: 'addition', string: '+0008\n', range: [[1, 0], [1, 0]]}, + {kind: 'deletion', string: '-0013\n-0014\n', range: [[5, 0], [6, 0]]}, + {kind: 'deletion', string: '-0016\n', range: [[8, 0], [8, 0]]}, + ], + }, + ); + }); + + it('creates a patch that applies selected lines from several hunks', function() { + const patch = buildPatchFixture(); + const stagePatch = patch.getStagePatchForLines(new Set([1, 5, 15, 16, 17, 25])); + const expectedBufferText = + // buffer rows + // 0 1 2 3 4 + '0000\n0001\n0002\n0005\n0006\n' + + // 5 6 7 8 9 10 11 12 13 14 + '0007\n0010\n0011\n0012\n0013\n0014\n0015\n0016\n0017\n0018\n' + + // 15 16 17 + '0024\n0025\n No newline at end of file\n'; + assert.strictEqual(stagePatch.getBufferText(), expectedBufferText); + assertInPatch(stagePatch).hunks( + { + startRow: 0, + endRow: 4, + header: '@@ -3,4 +3,4 @@', + changes: [ + {kind: 'deletion', string: '-0001\n', range: [[1, 0], [1, 0]]}, + {kind: 'addition', string: '+0005\n', range: [[3, 0], [3, 0]]}, + ], + }, + { + startRow: 5, + endRow: 14, + header: '@@ -12,9 +12,8 @@', + changes: [ + {kind: 'deletion', string: '-0015\n-0016\n', range: [[11, 0], [12, 0]]}, + {kind: 'addition', string: '+0017\n', range: [[13, 0], [13, 0]]}, + ], + }, + { + startRow: 15, + endRow: 17, + header: '@@ -31,1 +30,2 @@', + changes: [ + {kind: 'addition', string: '+0025\n', range: [[16, 0], [16, 0]]}, + {kind: 'nonewline', string: '\\ No newline at end of file\n', range: [[17, 0], [17, 0]]}, + ], + }, + ); + }); + + it('returns a modification patch if original patch is a deletion', function() { + const bufferText = 'line-0\nline-1\nline-2\nline-3\nline-4\nline-5\n'; + + const hunks = [ + new Hunk({ + oldStartRow: 1, + oldRowCount: 5, + newStartRow: 1, + newRowCount: 0, + sectionHeading: 'zero', + rowRange: new IndexedRowRange({bufferRange: [[0, 0], [5, 0]], startOffset: 0, endOffset: 43}), + changes: [ + new Deletion(new IndexedRowRange({bufferRange: [[0, 0], [5, 0]], startOffset: 0, endOffset: 43})), + ], + }), + ]; + + const patch = new Patch({status: 'deleted', hunks, bufferText}); + + const stagedPatch = patch.getStagePatchForLines(new Set([1, 3, 4])); + assert.strictEqual(stagedPatch.getStatus(), 'modified'); + assertInPatch(stagedPatch).hunks( + { + startRow: 0, + endRow: 5, + header: '@@ -1,5 +1,3 @@', + changes: [ + {kind: 'deletion', string: '-line-1\n', range: [[1, 0], [1, 0]]}, + {kind: 'deletion', string: '-line-3\n-line-4\n', range: [[3, 0], [4, 0]]}, + ], + }, + ); + }); + }); it('prints itself as an apply-ready string', function() { const bufferText = '0000\n1111\n2222\n3333\n4444\n5555\n6666\n7777\n8888\n9999\n'; // old: 0000.2222.3333.4444.5555.6666.7777.8888.9999. // new: 0000.1111.2222.3333.4444.5555.6666.9999. - // 0000.1111.2222.3333.4444.5555.6666.7777.8888.9999. + // patch buffer: 0000.1111.2222.3333.4444.5555.6666.7777.8888.9999. const hunk0 = new Hunk({ oldStartRow: 0, @@ -83,11 +216,9 @@ describe('Patch', function() { newRowCount: 3, sectionHeading: 'zero', rowRange: new IndexedRowRange({bufferRange: [[0, 0], [2, 0]], startOffset: 0, endOffset: 15}), - additions: [ - new IndexedRowRange({bufferRange: [[1, 0], [1, 0]], startOffset: 5, endOffset: 10}), + changes: [ + new Addition(new IndexedRowRange({bufferRange: [[1, 0], [1, 0]], startOffset: 5, endOffset: 10})), ], - deletions: [], - noNewline: nullIndexedRowRange, }); const hunk1 = new Hunk({ @@ -96,12 +227,10 @@ describe('Patch', function() { oldRowCount: 4, newRowCount: 2, sectionHeading: 'one', - rowRange: new IndexedRowRange({bufferRange: [[6, 0], [10, 0]], startOffset: 30, endOffset: 55}), - additions: [], - deletions: [ - new IndexedRowRange({bufferRange: [[7, 0], [8, 0]], startOffset: 35, endOffset: 45}), + rowRange: new IndexedRowRange({bufferRange: [[6, 0], [9, 0]], startOffset: 30, endOffset: 50}), + changes: [ + new Deletion(new IndexedRowRange({bufferRange: [[7, 0], [8, 0]], startOffset: 35, endOffset: 45})), ], - noNewline: nullIndexedRowRange, }); const p = new Patch({status: 'modified', hunks: [hunk0, hunk1], bufferText}); @@ -127,3 +256,65 @@ describe('Patch', function() { assert.isFalse(nullPatch.isPresent()); }); }); + +function buildPatchFixture() { + const bufferText = + '0000\n0001\n0002\n0003\n0004\n0005\n0006\n0007\n0008\n0009\n' + + '0010\n0011\n0012\n0013\n0014\n0015\n0016\n0017\n0018\n0019\n' + + '0020\n0021\n0022\n0023\n0024\n0025\n' + + ' No newline at end of file\n'; + + const hunks = [ + new Hunk({ + oldStartRow: 3, + oldRowCount: 4, + newStartRow: 3, + newRowCount: 5, + sectionHeading: 'zero', + rowRange: new IndexedRowRange({bufferRange: [[0, 0], [6, 0]], startOffset: 0, endOffset: 35}), + changes: [ + new Deletion(new IndexedRowRange({bufferRange: [[1, 0], [2, 0]], startOffset: 5, endOffset: 15})), + new Addition(new IndexedRowRange({bufferRange: [[3, 0], [5, 0]], startOffset: 15, endOffset: 30})), + ], + }), + new Hunk({ + oldStartRow: 12, + oldRowCount: 9, + newStartRow: 13, + newRowCount: 7, + sectionHeading: 'one', + rowRange: new IndexedRowRange({bufferRange: [[7, 0], [18, 0]], startOffset: 35, endOffset: 95}), + changes: [ + new Addition(new IndexedRowRange({bufferRange: [[8, 0], [9, 0]], startOffset: 40, endOffset: 50})), + new Deletion(new IndexedRowRange({bufferRange: [[12, 0], [16, 0]], startOffset: 60, endOffset: 85})), + new Addition(new IndexedRowRange({bufferRange: [[17, 0], [17, 0]], startOffset: 85, endOffset: 90})), + ], + }), + new Hunk({ + oldStartRow: 26, + oldRowCount: 3, + newStartRow: 25, + newRowCount: 4, + sectionHeading: 'two', + rowRange: new IndexedRowRange({bufferRange: [[19, 0], [23, 0]], startOffset: 95, endOffset: 120}), + changes: [ + new Addition(new IndexedRowRange({bufferRange: [[20, 0], [20, 0]], startOffset: 100, endOffset: 105})), + new Deletion(new IndexedRowRange({bufferRange: [[21, 0], [22, 0]], startOffset: 105, endOffset: 115})), + ], + }), + new Hunk({ + oldStartRow: 31, + oldRowCount: 1, + newStartRow: 31, + newRowCount: 2, + sectionHeading: 'three', + rowRange: new IndexedRowRange({bufferRange: [[24, 0], [26, 0]], startOffset: 120, endOffset: 157}), + changes: [ + new Addition(new IndexedRowRange({bufferRange: [[25, 0], [25, 0]], startOffset: 125, endOffset: 130})), + new NoNewline(new IndexedRowRange({bufferRange: [[26, 0], [26, 0]], startOffset: 130, endOffset: 157})), + ], + }), + ]; + + return new Patch({status: 'modified', hunks, bufferText}); +} From b8fa5b18e9a26825094c4cf9acd3de12b44f5949 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 16 Aug 2018 15:52:20 -0400 Subject: [PATCH 0157/4252] Unstage patch generation --- lib/models/patch/builder.js | 2 +- lib/models/patch/patch.js | 121 ++++++++++++++++++++---------- test/models/patch/patch.test.js | 126 ++++++++++++++++++++++++++++++-- 3 files changed, 205 insertions(+), 44 deletions(-) diff --git a/lib/models/patch/builder.js b/lib/models/patch/builder.js index ac697d304f..457d02ef46 100644 --- a/lib/models/patch/builder.js +++ b/lib/models/patch/builder.js @@ -2,7 +2,7 @@ import Hunk from './hunk'; import File, {nullFile} from './file'; import Patch, {nullPatch} from './patch'; import IndexedRowRange from '../indexed-row-range'; -import {Addition, Deletion, NoNewline} from './change'; +import {Addition, Deletion, NoNewline} from './region'; import FilePatch from './file-patch'; export default function buildFilePatch(diffs) { diff --git a/lib/models/patch/patch.js b/lib/models/patch/patch.js index 161547cff7..b81bf96f44 100644 --- a/lib/models/patch/patch.js +++ b/lib/models/patch/patch.js @@ -187,58 +187,91 @@ export default class Patch { return this.clone({hunks, status, bufferText: builder.getBufferText()}); } - getUnstagePatchForLines(lineSet) { - let delta = 0; + getUnstagePatchForLines(rowSet) { + const builder = new BufferBuilder(this.getBufferText()); const hunks = []; - let bufferText = this.getBufferText(); - let bufferOffset = 0; + let newRowDelta = 0; for (const hunk of this.getHunks()) { - const additions = []; - const deletions = []; - let notAddedRowCount = 0; - let addedRowCount = 0; - let notDeletedRowCount = 0; - - for (const change of hunk.getAdditions()) { - notDeletedRowCount += change.bufferRowCount(); - for (const intersection of change.intersectRowsIn(lineSet, bufferText)) { - notDeletedRowCount -= intersection.bufferRowCount(); - deletions.push(intersection); - } - } + const changes = []; + let noNewlineChange = null; + let contextRowCount = 0; + let additionRowCount = 0; + let deletionRowCount = 0; - for (const change of hunk.getDeletions()) { - notAddedRowCount = change.bufferRowCount(); - for (const intersection of change.intersectRowsIn(lineSet, bufferText)) { - addedRowCount += intersection.bufferRowCount(); - notAddedRowCount -= intersection.bufferRowCount(); - additions.push(intersection); + for (const region of hunk.getRegions()) { + for (const {intersection, gap} of region.getRowRange().intersectRowsIn(rowSet, this.getBufferText(), true)) { + region.when({ + addition: () => { + if (gap) { + // Unselected addition: become a context line. + builder.append(intersection); + contextRowCount += intersection.bufferRowCount(); + } else { + // Selected addition: become a deletion. + builder.append(intersection); + changes.push(new Deletion( + builder.applyOffsetTo(intersection), + )); + deletionRowCount += intersection.bufferRowCount(); + } + }, + deletion: () => { + if (gap) { + // Non-selected deletion: omit from new buffer. + builder.remove(intersection); + } else { + // Selected deletion: becomes an addition + builder.append(intersection); + changes.push(new Addition( + builder.applyOffsetTo(intersection), + )); + additionRowCount += intersection.bufferRowCount(); + } + }, + unchanged: () => { + // Untouched context line: include in new patch. + builder.append(intersection); + contextRowCount += intersection.bufferRowCount(); + }, + nonewline: () => { + // Nonewline marker: include in new patch. + builder.append(intersection); + noNewlineChange = new NoNewline( + builder.applyOffsetTo(intersection), + ); + }, + }); } } - if (additions.length > 0 || deletions.length > 0) { + if (changes.length > 0) { // Hunk contains at least one selected line + if (noNewlineChange !== null) { + changes.push(noNewlineChange); + } + hunks.push(new Hunk({ - oldStartRow: hunk.getOldStartRow() + delta, - newStartRow: hunk.getNewStartRow(), - oldRowCount: hunk.bufferRowCount() - addedRowCount, - newRowCount: hunk.getNewRowCount(), + oldStartRow: hunk.getNewStartRow(), + oldRowCount: contextRowCount + deletionRowCount, + newStartRow: hunk.getNewStartRow() + newRowDelta, + newRowCount: contextRowCount + additionRowCount, sectionHeading: hunk.getSectionHeading(), - rowRange: hunk.getRowRange(), - additions, - deletions, - noNewline: hunk.getNoNewline(), + rowRange: builder.applyHunkOffsetsTo(hunk.getRowRange()), + changes, })); + + builder.latestHunkWasIncluded(); + } else { + builder.latestHunkWasDiscarded(); } - delta += notAddedRowCount - notDeletedRowCount; - } - if (this.getStatus() === 'added') { - return this.clone({hunks, bufferText, status: 'modified'}); - } else { - return this.clone({hunks, bufferText}); + // (contextRowCount + additionRowCount) - (contextRowCount + deletionRowCount) + newRowDelta += additionRowCount - deletionRowCount; } + + const status = this.getStatus() === 'added' ? 'modified' : this.getStatus(); + return this.clone({hunks, status, bufferText: builder.getBufferText()}); } toString() { @@ -282,6 +315,18 @@ export const nullPatch = { } }, + getStagePatchForLines() { + return this; + }, + + getUnstagePatchForLines() { + return this; + }, + + toString() { + return ''; + }, + isPresent() { return false; }, diff --git a/test/models/patch/patch.test.js b/test/models/patch/patch.test.js index 5801e0c873..c6304f62d5 100644 --- a/test/models/patch/patch.test.js +++ b/test/models/patch/patch.test.js @@ -160,7 +160,7 @@ describe('Patch', function() { { startRow: 15, endRow: 17, - header: '@@ -31,1 +30,2 @@', + header: '@@ -32,1 +31,2 @@', changes: [ {kind: 'addition', string: '+0025\n', range: [[16, 0], [16, 0]]}, {kind: 'nonewline', string: '\\ No newline at end of file\n', range: [[17, 0], [17, 0]]}, @@ -202,7 +202,122 @@ describe('Patch', function() { }, ); }); + + it('returns a nullPatch as a nullPatch', function() { + assert.strictEqual(nullPatch.getStagePatchForLines(new Set([1, 2, 3])), nullPatch); + }); }); + + describe('unstage patch generation', function() { + it('creates a patch that updates the index to unapply selected lines from a single hunk', function() { + const patch = buildPatchFixture(); + const unstagePatch = patch.getUnstagePatchForLines(new Set([8, 12, 13])); + assert.strictEqual( + unstagePatch.getBufferText(), + // 0 1 2 3 4 5 6 7 8 + '0007\n0008\n0009\n0010\n0011\n0012\n0013\n0017\n0018\n', + ); + assertInPatch(unstagePatch).hunks( + { + startRow: 0, + endRow: 8, + header: '@@ -13,7 +13,8 @@', + changes: [ + {kind: 'deletion', string: '-0008\n', range: [[1, 0], [1, 0]]}, + {kind: 'addition', string: '+0012\n+0013\n', range: [[5, 0], [6, 0]]}, + ], + }, + ); + }); + + it('creates a patch that updates the index to unapply lines from several hunks', function() { + const patch = buildPatchFixture(); + const unstagePatch = patch.getUnstagePatchForLines(new Set([1, 4, 5, 16, 17, 20, 25])); + assert.strictEqual( + unstagePatch.getBufferText(), + // 0 1 2 3 4 5 + '0000\n0001\n0003\n0004\n0005\n0006\n' + + // 6 7 8 9 10 11 12 13 + '0007\n0008\n0009\n0010\n0011\n0016\n0017\n0018\n' + + // 14 15 16 + '0019\n0020\n0023\n' + + // 17 18 19 + '0024\n0025\n No newline at end of file\n', + ); + assertInPatch(unstagePatch).hunks( + { + startRow: 0, + endRow: 5, + header: '@@ -3,5 +3,4 @@', + changes: [ + {kind: 'addition', string: '+0001\n', range: [[1, 0], [1, 0]]}, + {kind: 'deletion', string: '-0004\n-0005\n', range: [[3, 0], [4, 0]]}, + ], + }, + { + startRow: 6, + endRow: 13, + header: '@@ -13,7 +12,7 @@', + changes: [ + {kind: 'addition', string: '+0016\n', range: [[11, 0], [11, 0]]}, + {kind: 'deletion', string: '-0017\n', range: [[12, 0], [12, 0]]}, + ], + }, + { + startRow: 14, + endRow: 16, + header: '@@ -25,3 +24,2 @@', + changes: [ + {kind: 'deletion', string: '-0020\n', range: [[15, 0], [15, 0]]}, + ], + }, + { + startRow: 17, + endRow: 19, + header: '@@ -30,2 +28,1 @@', + changes: [ + {kind: 'deletion', string: '-0025\n', range: [[18, 0], [18, 0]]}, + {kind: 'nonewline', string: '\\ No newline at end of file\n', range: [[19, 0], [19, 0]]}, + ], + }, + ); + }); + + it('returns a modification if original patch is an addition', function() { + const bufferText = '0000\n0001\n0002\n'; + const hunks = [ + new Hunk({ + oldStartRow: 1, + oldRowCount: 0, + newStartRow: 1, + newRowCount: 3, + rowRange: new IndexedRowRange({bufferRange: [[0, 0], [2, 0]], startOffset: 0, endOffset: 15}), + changes: [ + new Addition(new IndexedRowRange({bufferRange: [[0, 0], [2, 0]], startOffset: 0, endOffset: 15})), + ], + }), + ]; + const patch = new Patch({status: 'added', hunks, bufferText}); + const unstagePatch = patch.getUnstagePatchForLines(new Set([1, 2])); + assert.strictEqual(unstagePatch.getStatus(), 'modified'); + assert.strictEqual(unstagePatch.getBufferText(), '0000\n0001\n0002\n'); + assertInPatch(unstagePatch).hunks( + { + startRow: 0, + endRow: 2, + header: '@@ -1,3 +1,1 @@', + changes: [ + {kind: 'deletion', string: '-0001\n-0002\n', range: [[1, 0], [2, 0]]}, + ], + }, + ); + }); + + it('returns a nullPatch as a nullPatch', function() { + assert.strictEqual(nullPatch.getUnstagePatchForLines(new Set([1, 2, 3])), nullPatch); + }); + }); + it('prints itself as an apply-ready string', function() { const bufferText = '0000\n1111\n2222\n3333\n4444\n5555\n6666\n7777\n8888\n9999\n'; // old: 0000.2222.3333.4444.5555.6666.7777.8888.9999. @@ -254,6 +369,7 @@ describe('Patch', function() { assert.strictEqual(nullPatch.getBufferText(), ''); assert.strictEqual(nullPatch.getByteSize(), 0); assert.isFalse(nullPatch.isPresent()); + assert.strictEqual(nullPatch.toString(), ''); }); }); @@ -292,9 +408,9 @@ function buildPatchFixture() { }), new Hunk({ oldStartRow: 26, - oldRowCount: 3, + oldRowCount: 4, newStartRow: 25, - newRowCount: 4, + newRowCount: 3, sectionHeading: 'two', rowRange: new IndexedRowRange({bufferRange: [[19, 0], [23, 0]], startOffset: 95, endOffset: 120}), changes: [ @@ -303,9 +419,9 @@ function buildPatchFixture() { ], }), new Hunk({ - oldStartRow: 31, + oldStartRow: 32, oldRowCount: 1, - newStartRow: 31, + newStartRow: 30, newRowCount: 2, sectionHeading: 'three', rowRange: new IndexedRowRange({bufferRange: [[24, 0], [26, 0]], startOffset: 120, endOffset: 157}), From 15b692504f9f1387038a1c40a7ec342751fc9e10 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 16 Aug 2018 15:54:00 -0400 Subject: [PATCH 0158/4252] Delete some unused files --- lib/models/file-patch.js | 372 --------------------------------------- notes.txt | 66 ------- stage.patch | 20 --- unstage.patch | 15 -- 4 files changed, 473 deletions(-) delete mode 100644 lib/models/file-patch.js delete mode 100644 notes.txt delete mode 100644 stage.patch delete mode 100644 unstage.patch diff --git a/lib/models/file-patch.js b/lib/models/file-patch.js deleted file mode 100644 index 6641974de7..0000000000 --- a/lib/models/file-patch.js +++ /dev/null @@ -1,372 +0,0 @@ -import Hunk from './hunk'; -import {toGitPathSep} from '../helpers'; - -class File { - static empty() { - return new File({path: null, mode: null, symlink: null}); - } - - constructor({path, mode, symlink}) { - this.path = path; - this.mode = mode; - this.symlink = symlink; - } - - getPath() { - return this.path; - } - - getMode() { - return this.mode; - } - - isSymlink() { - return this.getMode() === '120000'; - } - - isRegularFile() { - return this.getMode() === '100644' || this.getMode() === '100755'; - } - - getSymlink() { - return this.symlink; - } - - clone(opts = {}) { - return new File({ - path: opts.path !== undefined ? opts.path : this.path, - mode: opts.mode !== undefined ? opts.mode : this.mode, - symlink: opts.symlink !== undefined ? opts.symlink : this.symlink, - }); - } -} - -class Patch { - constructor({status, hunks}) { - this.status = status; - this.hunks = hunks; - } - - getStatus() { - return this.status; - } - - getHunks() { - return this.hunks; - } - - getByteSize() { - return this.getHunks().reduce((acc, hunk) => acc + hunk.getByteSize(), 0); - } - - clone(opts = {}) { - return new Patch({ - status: opts.status !== undefined ? opts.status : this.status, - hunks: opts.hunks !== undefined ? opts.hunks : this.hunks, - }); - } -} - -export default class FilePatch { - static File = File; - static Patch = Patch; - - constructor(oldFile, newFile, patch) { - this.oldFile = oldFile; - this.newFile = newFile; - this.patch = patch; - - this.changedLineCount = this.getHunks().reduce((acc, hunk) => { - return acc + hunk.getLines().filter(line => line.isChanged()).length; - }, 0); - } - - clone(opts = {}) { - const oldFile = opts.oldFile !== undefined ? opts.oldFile : this.getOldFile(); - const newFile = opts.newFile !== undefined ? opts.newFile : this.getNewFile(); - const patch = opts.patch !== undefined ? opts.patch : this.patch; - return new FilePatch(oldFile, newFile, patch); - } - - getOldFile() { - return this.oldFile; - } - - getNewFile() { - return this.newFile; - } - - getPatch() { - return this.patch; - } - - getOldPath() { - return this.getOldFile().getPath(); - } - - getNewPath() { - return this.getNewFile().getPath(); - } - - getOldMode() { - return this.getOldFile().getMode(); - } - - getNewMode() { - return this.getNewFile().getMode(); - } - - getOldSymlink() { - return this.getOldFile().getSymlink(); - } - - getNewSymlink() { - return this.getNewFile().getSymlink(); - } - - getByteSize() { - return this.getPatch().getByteSize(); - } - - didChangeExecutableMode() { - const oldMode = this.getOldMode(); - const newMode = this.getNewMode(); - - if (!oldMode || !newMode) { - // Addition or deletion - return false; - } - - return oldMode === '100755' && newMode !== '100755' || - oldMode !== '100755' && newMode === '100755'; - } - - didChangeSymlinkMode() { - const oldMode = this.getOldMode(); - const newMode = this.getNewMode(); - return oldMode === '120000' && newMode !== '120000' || - oldMode !== '120000' && newMode === '120000'; - } - - hasSymlink() { - return this.getOldFile().getSymlink() || this.getNewFile().getSymlink(); - } - - hasTypechange() { - const oldFile = this.getOldFile(); - const newFile = this.getNewFile(); - return (oldFile.isSymlink() && newFile.isRegularFile()) || - (newFile.isSymlink() && oldFile.isRegularFile()); - } - - getPath() { - return this.getOldPath() || this.getNewPath(); - } - - getStatus() { - return this.getPatch().getStatus(); - } - - getHunks() { - return this.getPatch().getHunks(); - } - - getStagePatchForHunk(selectedHunk) { - return this.getStagePatchForLines(new Set(selectedHunk.getLines())); - } - - getStagePatchForLines(selectedLines) { - const wholeFileSelected = this.changedLineCount === [...selectedLines].filter(line => line.isChanged()).length; - if (wholeFileSelected) { - if (this.hasTypechange() && this.getStatus() === 'deleted') { - // handle special case when symlink is created where a file was deleted. In order to stage the file deletion, - // we must ensure that the created file patch has no new file - return this.clone({ - newFile: File.empty(), - }); - } else { - return this; - } - } else { - const hunks = this.getStagePatchHunks(selectedLines); - if (this.getStatus() === 'deleted') { - // Set status to modified - return this.clone({ - newFile: this.getOldFile(), - patch: this.getPatch().clone({hunks, status: 'modified'}), - }); - } else { - return this.clone({ - patch: this.getPatch().clone({hunks}), - }); - } - } - } - - getStagePatchHunks(selectedLines) { - let delta = 0; - const hunks = []; - for (const hunk of this.getHunks()) { - const newStartRow = (hunk.getNewStartRow() || 1) + delta; - let newLineNumber = newStartRow; - const lines = []; - let hunkContainsSelectedLines = false; - for (const line of hunk.getLines()) { - if (line.getStatus() === 'nonewline') { - lines.push(line.copy({oldLineNumber: -1, newLineNumber: -1})); - } else if (selectedLines.has(line)) { - hunkContainsSelectedLines = true; - if (line.getStatus() === 'deleted') { - lines.push(line.copy()); - } else { - lines.push(line.copy({newLineNumber: newLineNumber++})); - } - } else if (line.getStatus() === 'deleted') { - lines.push(line.copy({newLineNumber: newLineNumber++, status: 'unchanged'})); - } else if (line.getStatus() === 'unchanged') { - lines.push(line.copy({newLineNumber: newLineNumber++})); - } - } - const newRowCount = newLineNumber - newStartRow; - if (hunkContainsSelectedLines) { - // eslint-disable-next-line max-len - hunks.push(new Hunk(hunk.getOldStartRow(), newStartRow, hunk.getOldRowCount(), newRowCount, hunk.getSectionHeading(), lines)); - } - delta += newRowCount - hunk.getNewRowCount(); - } - return hunks; - } - - getUnstagePatch() { - let invertedStatus; - switch (this.getStatus()) { - case 'modified': - invertedStatus = 'modified'; - break; - case 'added': - invertedStatus = 'deleted'; - break; - case 'deleted': - invertedStatus = 'added'; - break; - default: - // throw new Error(`Unknown Status: ${this.getStatus()}`); - } - const invertedHunks = this.getHunks().map(h => h.invert()); - return this.clone({ - oldFile: this.getNewFile(), - newFile: this.getOldFile(), - patch: this.getPatch().clone({ - status: invertedStatus, - hunks: invertedHunks, - }), - }); - } - - getUnstagePatchForHunk(hunk) { - return this.getUnstagePatchForLines(new Set(hunk.getLines())); - } - - getUnstagePatchForLines(selectedLines) { - if (this.changedLineCount === [...selectedLines].filter(line => line.isChanged()).length) { - if (this.hasTypechange() && this.getStatus() === 'added') { - // handle special case when a file was created after a symlink was deleted. - // In order to unstage the file creation, we must ensure that the unstage patch has no new file, - // so when the patch is applied to the index, there file will be removed from the index - return this.clone({ - oldFile: File.empty(), - }).getUnstagePatch(); - } else { - return this.getUnstagePatch(); - } - } - - const hunks = this.getUnstagePatchHunks(selectedLines); - if (this.getStatus() === 'added') { - return this.clone({ - oldFile: this.getNewFile(), - patch: this.getPatch().clone({hunks, status: 'modified'}), - }).getUnstagePatch(); - } else { - return this.clone({ - patch: this.getPatch().clone({hunks}), - }).getUnstagePatch(); - } - } - - getUnstagePatchHunks(selectedLines) { - let delta = 0; - const hunks = []; - for (const hunk of this.getHunks()) { - const oldStartRow = (hunk.getOldStartRow() || 1) + delta; - let oldLineNumber = oldStartRow; - const lines = []; - let hunkContainsSelectedLines = false; - for (const line of hunk.getLines()) { - if (line.getStatus() === 'nonewline') { - lines.push(line.copy({oldLineNumber: -1, newLineNumber: -1})); - } else if (selectedLines.has(line)) { - hunkContainsSelectedLines = true; - if (line.getStatus() === 'added') { - lines.push(line.copy()); - } else { - lines.push(line.copy({oldLineNumber: oldLineNumber++})); - } - } else if (line.getStatus() === 'added') { - lines.push(line.copy({oldLineNumber: oldLineNumber++, status: 'unchanged'})); - } else if (line.getStatus() === 'unchanged') { - lines.push(line.copy({oldLineNumber: oldLineNumber++})); - } - } - const oldRowCount = oldLineNumber - oldStartRow; - if (hunkContainsSelectedLines) { - // eslint-disable-next-line max-len - hunks.push(new Hunk(oldStartRow, hunk.getNewStartRow(), oldRowCount, hunk.getNewRowCount(), hunk.getSectionHeading(), lines)); - } - delta += oldRowCount - hunk.getOldRowCount(); - } - return hunks; - } - - toString() { - if (this.hasTypechange()) { - const left = this.clone({ - newFile: File.empty(), - patch: this.getOldSymlink() ? new Patch({status: 'deleted', hunks: []}) : this.getPatch(), - }); - const right = this.clone({ - oldFile: File.empty(), - patch: this.getNewSymlink() ? new Patch({status: 'added', hunks: []}) : this.getPatch(), - }); - - return left.toString() + right.toString(); - } else if (this.getStatus() === 'added' && this.getNewFile().isSymlink()) { - const symlinkPath = this.getNewSymlink(); - return this.getHeaderString() + `@@ -0,0 +1 @@\n+${symlinkPath}\n\\ No newline at end of file\n`; - } else if (this.getStatus() === 'deleted' && this.getOldFile().isSymlink()) { - const symlinkPath = this.getOldSymlink(); - return this.getHeaderString() + `@@ -1 +0,0 @@\n-${symlinkPath}\n\\ No newline at end of file\n`; - } else { - return this.getHeaderString() + this.getHunks().map(h => h.toString()).join(''); - } - } - - getHeaderString() { - const fromPath = this.getOldPath() || this.getNewPath(); - const toPath = this.getNewPath() || this.getOldPath(); - let header = `diff --git a/${toGitPathSep(fromPath)} b/${toGitPathSep(toPath)}`; - header += '\n'; - if (this.getStatus() === 'added') { - header += `new file mode ${this.getNewMode()}`; - header += '\n'; - } else if (this.getStatus() === 'deleted') { - header += `deleted file mode ${this.getOldMode()}`; - header += '\n'; - } - header += this.getOldPath() ? `--- a/${toGitPathSep(this.getOldPath())}` : '--- /dev/null'; - header += '\n'; - header += this.getNewPath() ? `+++ b/${toGitPathSep(this.getNewPath())}` : '+++ /dev/null'; - header += '\n'; - return header; - } -} diff --git a/notes.txt b/notes.txt deleted file mode 100644 index c323737af0..0000000000 --- a/notes.txt +++ /dev/null @@ -1,66 +0,0 @@ -file in index: ------------------ -00: aaa -01: line-0 -02: line-1 -03: line-2 -04: bbb -05: ccc -06: ddd -07: line-3 -08: line-6 -09: line-7 -10: line-8 -11: eee -12: fff -13: ggg -14: hhh -15: iii -16: jjj -17: kkk -18: lll -19: mmm -20: nnn -21: line-12 -22: line-13 ------------------ - -file on HEAD: ------------------ -00: aaa -01: line-2 -02: bbb -03: ccc -04: ddd -05: line-3 -06: line-4 -07: line-5 -08: line-9 -09: line-10 -10: eee -11: fff -12: ggg -13: hhh -14: iii -15: jjj -16: kkk -17: lll -18: mmm -19: nnn -20: line-11 -21: line-13 ------------------ - -unstage patch ------------------ -@@ -7,4 +7,4 @@ - line-3 -+line-4 -+line-5 --line-6 --line-7 - line-8 -@@ -21,2 +21,3 @@ -+line-11 - line-12 - line-13 diff --git a/stage.patch b/stage.patch deleted file mode 100644 index d820eaef0d..0000000000 --- a/stage.patch +++ /dev/null @@ -1,20 +0,0 @@ -dif --git a/a.txt b/a.txt ---- a/a.txt -+++ b/a.txt -@@ -2,1 +2,3 @@ -+line-0 -+line-1 - line-2 -@@ -6,5 +8,4 @@ - line-3 --line-4 - --line-5 - -+line-6 - -+line-7 - -+line-8 --line-9 --line-10 -@@ -20,2 +21,2 @@ --line-11 - -+line-12 - - line-13 - diff --git a/unstage.patch b/unstage.patch deleted file mode 100644 index d66cfb3978..0000000000 --- a/unstage.patch +++ /dev/null @@ -1,15 +0,0 @@ -dif --git a/a.txt b/a.txt ---- a/a.txt -+++ b/a.txt -@@ -8,4 +8,4 @@ - line-3 -+line-4 -+line-5 --line-6 --line-7 - line-8 -@@ -22,2 +22,2 @@ -+line-11 --line-12 - line-13 -\ No newline at end of file From 40d345ca5f928795c7d49c85e7b8d24cab767076 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 16 Aug 2018 16:12:00 -0400 Subject: [PATCH 0159/4252] Start FilePatch tests from scratch --- test/models/patch/file-patch.test.js | 285 ++++----------------------- 1 file changed, 42 insertions(+), 243 deletions(-) diff --git a/test/models/patch/file-patch.test.js b/test/models/patch/file-patch.test.js index 594beb7732..4be79c00cf 100644 --- a/test/models/patch/file-patch.test.js +++ b/test/models/patch/file-patch.test.js @@ -1,263 +1,62 @@ -import {buildFilePatch} from '../../../lib/models/patch'; +import FilePatch from '../../../lib/models/patch/file-patch'; +import File from '../../../lib/models/patch/file'; +import Patch from '../../../lib/models/patch/patch'; +import Hunk from '../../../lib/models/patch/hunk'; +import {Addition, Deletion} from '../../../lib/models/patch/region'; +import IndexedRowRange from '../../../lib/models/indexed-row-range'; import {assertInFilePatch} from '../../helpers'; describe('FilePatch', function() { describe('getStagePatchForLines()', function() { it('returns a new FilePatch that applies only the selected lines', function() { - const filePatch = buildFilePatch([{ - oldPath: 'a.txt', - oldMode: '100644', - newPath: 'a.txt', - newMode: '100644', - hunks: [ - { - oldStartLine: 1, - oldLineCount: 1, - newStartLine: 1, - newLineCount: 3, - lines: [ - '+line-0', - '+line-1', - ' line-2', - ], - }, - { - oldStartLine: 5, - oldLineCount: 5, - newStartLine: 7, - newLineCount: 4, - lines: [ - ' line-3', - '-line-4', - '-line-5', - '+line-6', - '+line-7', - '+line-8', - '-line-9', - '-line-10', - ], - }, - { - oldStartLine: 20, - oldLineCount: 2, - newStartLine: 19, - newLineCount: 2, - lines: [ - '-line-11', - '+line-12', - ' line-13', - '\\No newline at end of file', - ], - }, - ], - }]); - - assert.strictEqual( - filePatch.getBufferText(), - 'line-0\nline-1\nline-2\nline-3\nline-4\nline-5\nline-6\nline-7\nline-8\nline-9\nline-10\n' + - 'line-11\nline-12\nline-13\nNo newline at end of file\n', - ); - - const stagePatch0 = filePatch.getStagePatchForLines(new Set([4, 5, 6])); - assertInFilePatch(stagePatch0).hunks( - { - startRow: 3, - endRow: 10, - header: '@@ -5,5 +5,6 @@', - deletions: {strings: ['*line-4\n*line-5\n'], ranges: [[[4, 0], [5, 0]]]}, - additions: {strings: ['*line-6\n'], ranges: [[[6, 0], [6, 0]]]}, - }, - ); - - const stagePatch1 = filePatch.getStagePatchForLines(new Set([0, 4, 5, 6, 11])); - assertInFilePatch(stagePatch1).hunks( + const bufferText = '0000\n0001\n0002\n0003\n0004\n'; + const hunks = [ + new Hunk({ + oldStartRow: 5, + oldRowCount: 3, + newStartRow: 5, + newRowCount: 4, + rowRange: new IndexedRowRange({bufferRange: [[0, 0], [4, 0]], startOffset: 0, endOffset: 25}), + changes: [ + new Addition(new IndexedRowRange({bufferRange: [[1, 0], [2, 0]], startOffset: 5, endOffset: 15})), + new Deletion(new IndexedRowRange({bufferRange: [[3, 0], [3, 0]], startOffset: 15, endOffset: 20})), + ], + }), + ]; + const patch = new Patch({status: 'modified', hunks, bufferText}); + const oldFile = new File({path: 'file.txt', mode: '100644'}); + const newFile = new File({path: 'file.txt', mode: '100644'}); + const filePatch = new FilePatch(oldFile, newFile, patch); + + const stagedPatch = filePatch.getStagePatchForLines(new Set([1, 3])); + assert.strictEqual(stagedPatch.getStatus(), 'modified'); + assert.strictEqual(stagedPatch.getOldPath(), 'file.txt'); + assert.strictEqual(stagedPatch.getOldMode(), '100644'); + assert.strictEqual(stagedPatch.getNewPath(), 'file.txt'); + assert.strictEqual(stagedPatch.getNewMode(), '100644'); + assert.strictEqual(stagedPatch.getBufferText(), '0000\n0001\n0003\n0004\n'); + assertInFilePatch(stagedPatch).hunks( { startRow: 0, - endRow: 2, - header: '@@ -1,1 +1,3 @@', - additions: {strings: ['*line-0\n'], ranges: [[[0, 0], [0, 0]]]}, - }, - { - startRow: 3, - endRow: 10, - header: '@@ -5,5 +6,6 @@', - deletions: {strings: ['*line-4\n*line-5\n'], ranges: [[[4, 0], [5, 0]]]}, - additions: {strings: ['*line-6\n'], ranges: [[[6, 0], [6, 0]]]}, - }, - { - startRow: 11, - endRow: 14, - header: '@@ -20,2 +18,3 @@', - deletions: {strings: ['*line-11\n'], ranges: [[[11, 0], [11, 0]]]}, - noNewline: {string: '*No newline at end of file\n', range: [[14, 0], [14, 0]]}, + endRow: 3, + header: '@@ -5,3 +5,3 @@', + changes: [ + {kind: 'addition', string: '+0001\n', range: [[1, 0], [1, 0]]}, + {kind: 'deletion', string: '-0003\n', range: [[2, 0], [2, 0]]}, + ], }, ); }); describe('staging lines from deleted files', function() { - it('handles staging part of the file', function() { - const filePatch = buildFilePatch([{ - oldPath: 'a.txt', - oldMode: '100644', - newPath: null, - newMode: null, - status: 'deleted', - hunks: [ - { - oldStartLine: 1, - newStartLine: 0, - oldLineCount: 3, - newLineCount: 0, - lines: [ - '-line-1', - '-line-2', - '-line-3', - ], - }, - { - oldStartLine: 19, - newStartLine: 21, - oldLineCount: 2, - newLineCount: 2, - lines: [ - '-line-13', - '+line-12', - ' line-14', - '\\No newline at end of file', - ], - }, - ], - }]); + it('handles staging part of the file'); - assert.strictEqual(filePatch.getBufferText(), - 'line-1\nline-2\nline-3\nline-13\nline-12\nline-14\n' + - 'No newline at end of file\n'); - - const stagePatch = filePatch.getStagePatchForLines(new Set([0, 1])); - assertInFilePatch(stagePatch).hunks( - { - startRow: 0, - endRow: 2, - header: '@@ -1,3 +0,1 @@', - deletions: {strings: ['*line-1\n*line-2\n'], ranges: [[[0, 0], [1, 0]]]}, - }, - ); - }); - - it('handles staging all lines, leaving nothing unstaged', function() { - const filePatch = buildFilePatch([{ - oldPath: 'a.txt', - oldMode: '100644', - newPath: null, - newMode: null, - status: 'deleted', - hunks: [ - { - oldStartLine: 1, - oldLineCount: 3, - newStartLine: 1, - newLineCount: 0, - lines: [ - '-line-1', - '-line-2', - '-line-3', - ], - }, - ], - }]); - - assert.strictEqual(filePatch.getBufferText(), 'line-1\nline-2\nline-3\n'); - - const stagePatch = filePatch.getStagePatchForLines(new Set([0, 1, 2])); - assertInFilePatch(stagePatch).hunks( - { - startRow: 0, - endRow: 2, - header: '@@ -1,3 +1,0 @@', - deletions: {strings: ['*line-1\n*line-2\n*line-3\n'], ranges: [[[0, 0], [2, 0]]]}, - }, - ); - }); + it('handles staging all lines, leaving nothing unstaged'); }); }); describe('getUnstagePatchForLines()', function() { - it('returns a new FilePatch that unstages only the specified lines', function() { - const filePatch = buildFilePatch([{ - oldPath: 'a.txt', - oldMode: '100644', - newPath: 'a.txt', - newMode: '100644', - status: 'modified', - hunks: [ - { - oldStartLine: 1, - oldLineCount: 1, - newStartLine: 1, - newLineCount: 3, - lines: [ - '+line-0', - '+line-1', - ' line-2', - ], - }, - { - oldStartLine: 5, - oldLineCount: 5, - newStartLine: 7, - newLineCount: 4, - lines: [ - ' line-3', - '-line-4', - '-line-5', - '+line-6', - '+line-7', - '+line-8', - '-line-9', - '-line-10', - ], - }, - { - oldStartLine: 20, - oldLineCount: 2, - newStartLine: 21, - newLineCount: 2, - lines: [ - '-line-11', - '+line-12', - ' line-13', - '\\No newline at end of file', - ], - }, - ], - }]); - - assert.strictEqual( - filePatch.getBufferText(), - 'line-0\nline-1\nline-2\nline-3\nline-4\nline-5\nline-6\nline-7\nline-8\nline-9\nline-10\n' + - 'line-11\nline-12\nline-13\nNo newline at end of file\n', - ); - - const unstagedPatch0 = filePatch.getUnstagePatchForLines(new Set([4, 5, 6, 7, 11, 12, 13])); - console.log(unstagedPatch0.toString()); - assertInFilePatch(unstagedPatch0).hunks( - { - startRow: 3, - endRow: 10, - header: '@@ -7,4 +7,4 @@', - additions: {strings: ['*line-4\n*line-5\n'], ranges: [[[4, 0], [5, 0]]]}, - deletions: {strings: ['*line-6\n*line-7\n'], ranges: [[[5, 0], [6, 0]]]}, - }, - { - startRow: 11, - endRow: 14, - header: '@@ -19,2 +21,2 @@', - additions: {strings: ['*line-11\n'], ranges: [[[11, 0], [11, 0]]]}, - deletions: {strings: ['*line-12\n'], ranges: [[[12, 0], [12, 0]]]}, - noNewline: {string: '*No newline at end of file\n', range: [[14, 0], [14, 0]]}, - }, - ); - }); + it('returns a new FilePatch that unstages only the specified lines'); describe('unstaging lines from an added file', function() { it('handles unstaging part of the file'); From fa47e6624c34ff80b6cf5b176a711c1c5c2c143c Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 17 Aug 2018 14:24:35 -0400 Subject: [PATCH 0160/4252] Return Atom Ranges from Hunk .getXyzRange() methods --- lib/models/patch/hunk.js | 22 +++++----------------- test/models/patch/hunk.test.js | 4 ++-- 2 files changed, 7 insertions(+), 19 deletions(-) diff --git a/lib/models/patch/hunk.js b/lib/models/patch/hunk.js index 1b4290d20e..cf33d955e2 100644 --- a/lib/models/patch/hunk.js +++ b/lib/models/patch/hunk.js @@ -1,4 +1,4 @@ -import IndexedRowRange, {nullIndexedRowRange} from '../indexed-row-range'; +import IndexedRowRange from '../indexed-row-range'; import {Unchanged} from './region'; export default class Hunk { @@ -87,19 +87,19 @@ export default class Hunk { } getAdditionRanges() { - return this.changes.filter(change => change.isAddition()).map(change => change.getRowRange()); + return this.changes.filter(change => change.isAddition()).map(change => change.getRowRange().bufferRange); } getDeletionRanges() { - return this.changes.filter(change => change.isDeletion()).map(change => change.getRowRange()); + return this.changes.filter(change => change.isDeletion()).map(change => change.getRowRange().bufferRange); } getNoNewlineRange() { const lastChange = this.changes[this.changes.length - 1]; if (lastChange && lastChange.isNoNewline()) { - return lastChange.getRowRange(); + return lastChange.getRowRange().bufferRange; } else { - return nullIndexedRowRange; + return null; } } @@ -126,18 +126,6 @@ export default class Hunk { }), 0); } - invert() { - return new Hunk({ - oldStartRow: this.getNewStartRow(), - newStartRow: this.getOldStartRow(), - oldRowCount: this.getNewRowCount(), - newRowCount: this.getOldRowCount(), - sectionHeading: this.getSectionHeading(), - rowRange: this.rowRange, - changes: this.getChanges().map(change => change.invert()), - }); - } - toStringIn(bufferText) { let str = this.getHeader() + '\n'; for (const region of this.getRegions()) { diff --git a/test/models/patch/hunk.test.js b/test/models/patch/hunk.test.js index 671e9232bc..9a07d6e85b 100644 --- a/test/models/patch/hunk.test.js +++ b/test/models/patch/hunk.test.js @@ -54,7 +54,7 @@ describe('Hunk', function() { assert.lengthOf(h.getChanges(), 3); assert.lengthOf(h.getAdditionRanges(), 1); assert.lengthOf(h.getDeletionRanges(), 2); - assert.isFalse(h.getNoNewlineRange().isPresent()); + assert.isNull(h.getNoNewlineRange()); }); it('creates its start range for decoration placement', function() { @@ -205,7 +205,7 @@ describe('Hunk', function() { assert.strictEqual(inverted.getSectionHeading(), 'the-heading'); assert.lengthOf(inverted.getAdditionRanges(), 1); assert.lengthOf(inverted.getDeletionRanges(), 2); - assert.isTrue(inverted.getNoNewlineRange().isPresent()); + assert.deepEqual(inverted.getNoNewlineRange().serialize(), [[12, 0], [12, 0]]); }); describe('toStringIn()', function() { From 8494eec6465ebe1e0d7c5a40acd6b071c7a4ff87 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 17 Aug 2018 14:25:35 -0400 Subject: [PATCH 0161/4252] Return the correct status when staging or unstaging an entire Patch --- lib/models/patch/patch.js | 10 ++++++-- test/models/patch/patch.test.js | 43 +++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/lib/models/patch/patch.js b/lib/models/patch/patch.js index b81bf96f44..48906ef19b 100644 --- a/lib/models/patch/patch.js +++ b/lib/models/patch/patch.js @@ -183,7 +183,8 @@ export default class Patch { } } - const status = this.getStatus() === 'deleted' ? 'modified' : this.getStatus(); + const wholeFile = rowSet.size === this.changedLineCount; + const status = this.getStatus() === 'deleted' && !wholeFile ? 'modified' : this.getStatus(); return this.clone({hunks, status, bufferText: builder.getBufferText()}); } @@ -270,7 +271,12 @@ export default class Patch { newRowDelta += additionRowCount - deletionRowCount; } - const status = this.getStatus() === 'added' ? 'modified' : this.getStatus(); + const wholeFile = rowSet.size === this.changedLineCount; + let status = this.getStatus(); + if (this.getStatus() === 'added') { + status = wholeFile ? 'deleted' : 'modified'; + } + return this.clone({hunks, status, bufferText: builder.getBufferText()}); } diff --git a/test/models/patch/patch.test.js b/test/models/patch/patch.test.js index c6304f62d5..d2f8308695 100644 --- a/test/models/patch/patch.test.js +++ b/test/models/patch/patch.test.js @@ -203,6 +203,26 @@ describe('Patch', function() { ); }); + it('returns an deletion when staging an entire deletion patch', function() { + const bufferText = '0000\n0001\n0002\n'; + const hunks = [ + new Hunk({ + oldStartRow: 1, + oldRowCount: 3, + newStartRow: 1, + newRowCount: 0, + rowRange: new IndexedRowRange({bufferRange: [[0, 0], [2, 0]], startOffset: 0, endOffset: 15}), + changes: [ + new Deletion(new IndexedRowRange({bufferRange: [[0, 0], [2, 0]], startOffset: 0, endOffset: 15})), + ], + }), + ]; + const patch = new Patch({status: 'deleted', hunks, bufferText}); + + const unstagePatch0 = patch.getUnstagePatchForLines(new Set([0, 1, 2])); + assert.strictEqual(unstagePatch0.getStatus(), 'deleted'); + }); + it('returns a nullPatch as a nullPatch', function() { assert.strictEqual(nullPatch.getStagePatchForLines(new Set([1, 2, 3])), nullPatch); }); @@ -313,6 +333,29 @@ describe('Patch', function() { ); }); + it('returns a deletion when unstaging an entire addition patch', function() { + const bufferText = '0000\n0001\n0002\n'; + const hunks = [ + new Hunk({ + oldStartRow: 1, + oldRowCount: 0, + newStartRow: 1, + newRowCount: 3, + rowRange: new IndexedRowRange({bufferRange: [[0, 0], [2, 0]], startOffset: 0, endOffset: 15}), + changes: [ + new Addition(new IndexedRowRange({bufferRange: [[0, 0], [2, 0]], startOffset: 0, endOffset: 15})), + ], + }), + ]; + const patch = new Patch({status: 'added', hunks, bufferText}); + + const unstagePatch0 = patch.getUnstagePatchForLines(new Set([0, 1, 2])); + assert.strictEqual(unstagePatch0.getStatus(), 'deleted'); + + const unstagePatch1 = patch.getFullUnstagedPatch(); + assert.strictEqual(unstagePatch1.getStatus(), 'deleted'); + }); + it('returns a nullPatch as a nullPatch', function() { assert.strictEqual(nullPatch.getUnstagePatchForLines(new Set([1, 2, 3])), nullPatch); }); From 485444615ddf45c9182d4d31c0fda5a3a76ad97d Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 17 Aug 2018 14:26:01 -0400 Subject: [PATCH 0162/4252] Unstage an entire Patch in one gulp --- lib/models/patch/patch.js | 19 ++++++++++++++ test/models/patch/patch.test.js | 46 +++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/lib/models/patch/patch.js b/lib/models/patch/patch.js index 48906ef19b..4c330feb8c 100644 --- a/lib/models/patch/patch.js +++ b/lib/models/patch/patch.js @@ -280,6 +280,25 @@ export default class Patch { return this.clone({hunks, status, bufferText: builder.getBufferText()}); } + getFullUnstagedPatch() { + let newRowDelta = 0; + const hunks = this.getHunks().map(hunk => { + const changes = hunk.getChanges().map(change => change.invert()); + const newHunk = new Hunk({ + oldStartRow: hunk.getNewStartRow(), + oldRowCount: hunk.getNewRowCount(), + newStartRow: hunk.getNewStartRow() + newRowDelta, + newRowCount: hunk.getOldRowCount(), + rowRange: hunk.getRowRange(), + changes, + }); + newRowDelta += newHunk.getNewRowCount() - newHunk.getOldRowCount(); + return newHunk; + }); + const status = this.getStatus() === 'added' ? 'deleted' : this.getStatus(); + return this.clone({hunks, status}); + } + toString() { return this.getHunks().reduce((str, hunk) => { str += hunk.toStringIn(this.getBufferText()); diff --git a/test/models/patch/patch.test.js b/test/models/patch/patch.test.js index d2f8308695..67f46683a6 100644 --- a/test/models/patch/patch.test.js +++ b/test/models/patch/patch.test.js @@ -303,6 +303,52 @@ describe('Patch', function() { ); }); + it('unstages an entire patch at once', function() { + const patch = buildPatchFixture(); + const unstagedPatch = patch.getFullUnstagedPatch(); + + assert.strictEqual(unstagedPatch.getBufferText(), patch.getBufferText()); + assertInPatch(unstagedPatch).hunks( + { + startRow: 0, + endRow: 6, + header: '@@ -3,5 +3,4 @@', + changes: [ + {kind: 'addition', string: '+0001\n+0002\n', range: [[1, 0], [2, 0]]}, + {kind: 'deletion', string: '-0003\n-0004\n-0005\n', range: [[3, 0], [5, 0]]}, + ], + }, + { + startRow: 7, + endRow: 18, + header: '@@ -13,7 +12,9 @@', + changes: [ + {kind: 'deletion', string: '-0008\n-0009\n', range: [[8, 0], [9, 0]]}, + {kind: 'addition', string: '+0012\n+0013\n+0014\n+0015\n+0016\n', range: [[12, 0], [16, 0]]}, + {kind: 'deletion', string: '-0017\n', range: [[17, 0], [17, 0]]}, + ], + }, + { + startRow: 19, + endRow: 23, + header: '@@ -25,3 +26,4 @@', + changes: [ + {kind: 'deletion', string: '-0020\n', range: [[20, 0], [20, 0]]}, + {kind: 'addition', string: '+0021\n+0022\n', range: [[21, 0], [22, 0]]}, + ], + }, + { + startRow: 24, + endRow: 26, + header: '@@ -30,2 +32,1 @@', + changes: [ + {kind: 'deletion', string: '-0025\n', range: [[25, 0], [25, 0]]}, + {kind: 'nonewline', string: '\\ No newline at end of file\n', range: [[26, 0], [26, 0]]}, + ], + }, + ); + }); + it('returns a modification if original patch is an addition', function() { const bufferText = '0000\n0001\n0002\n'; const hunks = [ From c09829b1ff3a14459c92287296f641dee28651fa Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 17 Aug 2018 14:26:16 -0400 Subject: [PATCH 0163/4252] File::isExecutable() for consistency --- lib/models/patch/file.js | 8 ++++++++ test/models/patch/file.test.js | 7 +++++++ 2 files changed, 15 insertions(+) diff --git a/lib/models/patch/file.js b/lib/models/patch/file.js index a3eda44239..c9017ec56f 100644 --- a/lib/models/patch/file.js +++ b/lib/models/patch/file.js @@ -25,6 +25,10 @@ export default class File { return this.getMode() === '100644' || this.getMode() === '100755'; } + isExecutable() { + return this.getMode() === '100755'; + } + isPresent() { return true; } @@ -62,6 +66,10 @@ export const nullFile = { return false; }, + isExecutable() { + return false; + }, + isPresent() { return false; }, diff --git a/test/models/patch/file.test.js b/test/models/patch/file.test.js index 38bb449e78..452cb8c6e6 100644 --- a/test/models/patch/file.test.js +++ b/test/models/patch/file.test.js @@ -14,6 +14,13 @@ describe('File', function() { assert.isFalse(nullFile.isRegularFile()); }); + it("detects when it's executable", function() { + assert.isTrue(new File({path: 'path', mode: '100755', symlink: null}).isExecutable()); + assert.isFalse(new File({path: 'path', mode: '100644', symlink: null}).isExecutable()); + assert.isFalse(new File({path: 'path', mode: '120000', symlink: null}).isExecutable()); + assert.isFalse(nullFile.isExecutable()); + }); + it('clones itself with possible overrides', function() { const original = new File({path: 'original', mode: '100644', symlink: null}); From dbb1a92f21568e476fe04e6b0e5410abc7ab5170 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 17 Aug 2018 14:26:33 -0400 Subject: [PATCH 0164/4252] Reimplement and retest FilePatch --- lib/models/patch/file-patch.js | 164 +++---- test/models/patch/file-patch.test.js | 637 ++++++++++++++++++++++++++- 2 files changed, 673 insertions(+), 128 deletions(-) diff --git a/lib/models/patch/file-patch.js b/lib/models/patch/file-patch.js index d765ca82c7..3b57951624 100644 --- a/lib/models/patch/file-patch.js +++ b/lib/models/patch/file-patch.js @@ -1,5 +1,4 @@ -import Hunk from './hunk'; -import File, {nullFile} from './file'; +import {nullFile} from './file'; import Patch from './patch'; import {toGitPathSep} from '../../helpers'; @@ -54,57 +53,55 @@ export default class FilePatch { return this.getPatch().getBufferText(); } - getHunkStartPositions() { - return this.getHunks().map(hunk => hunk.getBufferStartPosition()); - } - - getAddedBufferPositions() { + getAdditionRanges() { return this.getHunks().reduce((acc, hunk) => { - acc.push(...hunk.getAddedBufferPositions()); + acc.push(...hunk.getAdditionRanges()); return acc; }, []); } - getBufferDeletedPositions() { + getDeletionRanges() { return this.getHunks().reduce((acc, hunk) => { - acc.push(...hunk.getDeletedBufferPositions()); + acc.push(...hunk.getDeletionRanges()); return acc; }, []); } - getBufferNoNewlinePosition() { - return this.getHunks().reduce((acc, hunk) => { - const position = hunk.getBufferNoNewlinePosition(); - if (position.isPresent()) { - acc.push(position); - } - return acc; - }, []); + getNoNewlineRanges() { + const hunks = this.getHunks(); + const lastHunk = hunks[hunks.length - 1]; + if (!lastHunk) { + return []; + } + + const range = lastHunk.getNoNewlineRange(); + if (!range) { + return []; + } + + return [range]; } didChangeExecutableMode() { - const oldMode = this.getOldMode(); - const newMode = this.getNewMode(); - return oldMode === '100755' && newMode !== '100755' || - oldMode !== '100755' && newMode === '100755'; - } + if (!this.oldFile.isPresent() || !this.newFile.isPresent()) { + return false; + } - didChangeSymlinkMode() { - const oldMode = this.getOldMode(); - const newMode = this.getNewMode(); - return oldMode === '120000' && newMode !== '120000' || - oldMode !== '120000' && newMode === '120000'; + return this.oldFile.isExecutable() && !this.newFile.isExecutable() || + !this.oldFile.isExecutable() && this.newFile.isExecutable(); } hasSymlink() { - return this.getOldFile().getSymlink() || this.getNewFile().getSymlink(); + return Boolean(this.getOldFile().getSymlink() || this.getNewFile().getSymlink()); } hasTypechange() { - const oldFile = this.getOldFile(); - const newFile = this.getNewFile(); - return (oldFile.isSymlink() && newFile.isRegularFile()) || - (newFile.isSymlink() && oldFile.isRegularFile()); + if (!this.oldFile.isPresent() || !this.newFile.isPresent()) { + return false; + } + + return this.oldFile.isSymlink() && !this.newFile.isSymlink() || + !this.oldFile.isSymlink() && this.newFile.isSymlink(); } getPath() { @@ -119,7 +116,7 @@ export default class FilePatch { return this.getPatch().getHunks(); } - clone(opts) { + clone(opts = {}) { return new this.constructor( opts.oldFile !== undefined ? opts.oldFile : this.oldFile, opts.newFile !== undefined ? opts.newFile : this.newFile, @@ -127,13 +124,8 @@ export default class FilePatch { ); } - getStagePatchForHunk(selectedHunk) { - return this.getStagePatchForLines(new Set(selectedHunk.getBufferRows())); - } - getStagePatchForLines(selectedLineSet) { - const wholeFileSelected = this.patch.getChangedLineCount() === selectedLineSet.size; - if (wholeFileSelected) { + if (this.patch.getChangedLineCount() === selectedLineSet.size) { if (this.hasTypechange() && this.getStatus() === 'deleted') { // handle special case when symlink is created where a file was deleted. In order to stage the file deletion, // we must ensure that the created file patch has no new file @@ -145,103 +137,51 @@ export default class FilePatch { const patch = this.patch.getStagePatchForLines(selectedLineSet); if (this.getStatus() === 'deleted') { // Populate newFile - return this.clone({ - newFile: this.getOldFile(), - patch, - }); + return this.clone({newFile: this.getOldFile(), patch}); } else { return this.clone({patch}); } } } - getUnstagePatchForHunk(hunk) { - return this.getUnstagePatchForLines(new Set(hunk.getBufferRows())); + getStagePatchForHunk(selectedHunk) { + return this.getStagePatchForLines(new Set(selectedHunk.getBufferRows())); } - getUnstagePatchForLines(selectedRowSet) { - if (this.changedLineCount === selectedRowSet.size) { + getUnstagePatchForLines(selectedLineSet) { + if (this.patch.getChangedLineCount() === selectedLineSet.size) { + const patch = this.patch.getFullUnstagedPatch(); if (this.hasTypechange() && this.getStatus() === 'added') { // handle special case when a file was created after a symlink was deleted. // In order to unstage the file creation, we must ensure that the unstage patch has no new file, - // so when the patch is applied to the index, there file will be removed from the index - return this.clone({ - oldFile: File.empty(), - }).getUnstagePatch(); + // so when the patch is applied to the index, there file will be removed from the index. + return this.clone({oldFile: nullFile, patch}); } else { - return this.getUnstagePatch(); + return this.clone({patch}); } - } - - const hunks = this.getUnstagePatchHunks(selectedRowSet); - if (this.getStatus() === 'added') { - return this.clone({ - oldFile: this.getNewFile(), - patch: this.getPatch().clone({hunks, status: 'modified'}), - }).getUnstagePatch(); } else { - return this.clone({ - patch: this.getPatch().clone({hunks}), - }).getUnstagePatch(); + const patch = this.patch.getUnstagePatchForLines(selectedLineSet); + if (this.getStatus() === 'added') { + return this.clone({oldFile: this.getNewFile(), patch}); + } else { + return this.clone({patch}); + } } } - getUnstagePatchHunks(selectedRowSet) { - let delta = 0; - const hunks = []; - for (const hunk of this.getHunks()) { - const additions = []; - const deletions = []; - let notAddedRowCount = 0; - let addedRowCount = 0; - let notDeletedRowCount = 0; - let deletedRowCount = 0; - - for (const change of hunk.getAdditions()) { - notDeletedRowCount += change.bufferRowCount(); - for (const intersection of change.intersectRowsIn(selectedRowSet, this.getBufferText())) { - deletedRowCount += intersection.bufferRowCount(); - notDeletedRowCount -= intersection.bufferRowCount(); - deletions.push(intersection); - } - } - - for (const change of hunk.getDeletions()) { - notAddedRowCount = change.bufferRowCount(); - for (const intersection of change.intersectRowsIn(selectedRowSet, this.getBufferText())) { - addedRowCount += intersection.bufferRowCount(); - notAddedRowCount -= intersection.bufferRowCount(); - additions.push(intersection); - } - } - - if (additions.length > 0 || deletions.length > 0) { - // Hunk contains at least one selected line - hunks.push(new Hunk({ - oldStartRow: hunk.getOldStartRow() + delta, - newStartRow: hunk.getNewStartRow(), - oldRowCount: hunk.bufferRowCount() - addedRowCount, - newRowCount: hunk.getNewRowCount(), - sectionHeading: hunk.getSectionHeading(), - rowRange: hunk.getRowRange(), - additions, - deletions, - noNewline: hunk.getNoNewline(), - })); - } - delta += notDeletedRowCount - notAddedRowCount; - } - return hunks; + getUnstagePatchForHunk(hunk) { + return this.getUnstagePatchForLines(new Set(hunk.getBufferRows())); } toString() { if (this.hasTypechange()) { const left = this.clone({ - newFile: File.empty(), + newFile: nullFile, patch: this.getOldSymlink() ? new Patch({status: 'deleted', hunks: []}) : this.getPatch(), }); + const right = this.clone({ - oldFile: File.empty(), + oldFile: nullFile, patch: this.getNewSymlink() ? new Patch({status: 'added', hunks: []}) : this.getPatch(), }); diff --git a/test/models/patch/file-patch.test.js b/test/models/patch/file-patch.test.js index 4be79c00cf..dc266ada09 100644 --- a/test/models/patch/file-patch.test.js +++ b/test/models/patch/file-patch.test.js @@ -1,12 +1,210 @@ import FilePatch from '../../../lib/models/patch/file-patch'; -import File from '../../../lib/models/patch/file'; +import File, {nullFile} from '../../../lib/models/patch/file'; import Patch from '../../../lib/models/patch/patch'; import Hunk from '../../../lib/models/patch/hunk'; -import {Addition, Deletion} from '../../../lib/models/patch/region'; +import {Addition, Deletion, NoNewline} from '../../../lib/models/patch/region'; import IndexedRowRange from '../../../lib/models/indexed-row-range'; import {assertInFilePatch} from '../../helpers'; describe('FilePatch', function() { + it('delegates methods to its files and patch', function() { + const bufferText = '0000\n0001\n'; + const hunks = [ + new Hunk({ + oldStartRow: 2, + oldRowCount: 1, + newStartRow: 2, + newRowCount: 2, + rowRange: new IndexedRowRange({bufferRange: [[0, 0], [1, 0]], startOffset: 0, endOffset: 10}), + changes: [ + new Addition(new IndexedRowRange({bufferRange: [[1, 0], [1, 0]], startOffset: 5, endOffset: 10})), + ], + }), + ]; + const patch = new Patch({status: 'modified', hunks, bufferText}); + const oldFile = new File({path: 'a.txt', mode: '120000', symlink: 'dest.txt'}); + const newFile = new File({path: 'b.txt', mode: '100755'}); + const filePatch = new FilePatch(oldFile, newFile, patch); + + assert.strictEqual(filePatch.getOldPath(), 'a.txt'); + assert.strictEqual(filePatch.getOldMode(), '120000'); + assert.strictEqual(filePatch.getOldSymlink(), 'dest.txt'); + + assert.strictEqual(filePatch.getNewPath(), 'b.txt'); + assert.strictEqual(filePatch.getNewMode(), '100755'); + assert.isUndefined(filePatch.getNewSymlink()); + + assert.strictEqual(filePatch.getByteSize(), 10); + assert.strictEqual(filePatch.getBufferText(), bufferText); + }); + + it('accesses a file path from either side of the patch', function() { + const oldFile = new File({path: 'old-file.txt', mode: '100644'}); + const newFile = new File({path: 'new-file.txt', mode: '100644'}); + const patch = new Patch({status: 'modified', hunks: [], bufferText: ''}); + + assert.strictEqual(new FilePatch(oldFile, newFile, patch).getPath(), 'old-file.txt'); + assert.strictEqual(new FilePatch(oldFile, nullFile, patch).getPath(), 'old-file.txt'); + assert.strictEqual(new FilePatch(nullFile, newFile, patch).getPath(), 'new-file.txt'); + assert.isNull(new FilePatch(nullFile, nullFile, patch).getPath()); + }); + + it('iterates addition and deletion ranges from all hunks', function() { + const bufferText = '0000\n0001\n0002\n0003\n0004\n0005\n0006\n0007\n0008\n0009\n'; + const hunks = [ + new Hunk({ + oldStartRow: 1, + oldRowCount: 0, + newStartRow: 1, + newRowCount: 0, + rowRange: new IndexedRowRange({bufferRange: [[0, 0], [9, 0]], startOffset: 0, endOffset: 50}), + changes: [ + new Addition(new IndexedRowRange({bufferRange: [[1, 0], [1, 0]], startOffset: 5, endOffset: 10})), + new Addition(new IndexedRowRange({bufferRange: [[3, 0], [3, 0]], startOffset: 15, endOffset: 20})), + new Deletion(new IndexedRowRange({bufferRange: [[4, 0], [4, 0]], startOffset: 20, endOffset: 25})), + new Addition(new IndexedRowRange({bufferRange: [[5, 0], [6, 0]], startOffset: 25, endOffset: 35})), + new Deletion(new IndexedRowRange({bufferRange: [[7, 0], [7, 0]], startOffset: 35, endOffset: 40})), + new Addition(new IndexedRowRange({bufferRange: [[8, 0], [8, 0]], startOffset: 40, endOffset: 45})), + ], + }), + ]; + const patch = new Patch({status: 'modified', hunks, bufferText}); + const oldFile = new File({path: 'a.txt', mode: '100644'}); + const newFile = new File({path: 'a.txt', mode: '100644'}); + const filePatch = new FilePatch(oldFile, newFile, patch); + + const additionRanges = filePatch.getAdditionRanges(); + assert.deepEqual(additionRanges.map(range => range.serialize()), [ + [[1, 0], [1, 0]], + [[3, 0], [3, 0]], + [[5, 0], [6, 0]], + [[8, 0], [8, 0]], + ]); + + const deletionRanges = filePatch.getDeletionRanges(); + assert.deepEqual(deletionRanges.map(range => range.serialize()), [ + [[4, 0], [4, 0]], + [[7, 0], [7, 0]], + ]); + + const noNewlineRanges = filePatch.getNoNewlineRanges(); + assert.lengthOf(noNewlineRanges, 0); + }); + + it('returns an empty nonewline range if no hunks are present', function() { + const patch = new Patch({status: 'modified', hunks: [], bufferText: ''}); + const oldFile = new File({path: 'a.txt', mode: '100644'}); + const newFile = new File({path: 'a.txt', mode: '100644'}); + const filePatch = new FilePatch(oldFile, newFile, patch); + + assert.lengthOf(filePatch.getNoNewlineRanges(), 0); + }); + + it('returns a nonewline range if one is present', function() { + const bufferText = '0000\n No newline at end of file\n'; + const hunks = [ + new Hunk({ + oldStartRow: 1, + oldRowCount: 0, + newStartRow: 1, + newRowCount: 0, + rowRange: new IndexedRowRange({bufferRange: [[0, 0], [1, 0]], startOffset: 0, endOffset: 32}), + changes: [ + new Addition(new IndexedRowRange({bufferRange: [[0, 0], [0, 0]], startOffset: 0, endOffset: 5})), + new NoNewline(new IndexedRowRange({bufferRange: [[1, 0], [1, 0]], startOffset: 5, endOffset: 32})), + ], + }), + ]; + const patch = new Patch({status: 'modified', hunks, bufferText}); + const oldFile = new File({path: 'a.txt', mode: '100644'}); + const newFile = new File({path: 'a.txt', mode: '100644'}); + const filePatch = new FilePatch(oldFile, newFile, patch); + + const noNewlineRanges = filePatch.getNoNewlineRanges(); + assert.deepEqual(noNewlineRanges.map(range => range.serialize()), [ + [[1, 0], [1, 0]], + ]); + }); + + describe('file-level change detection', function() { + let emptyPatch; + + beforeEach(function() { + emptyPatch = new Patch({status: 'modified', hunks: [], bufferText: ''}); + }); + + it('detects changes in executable mode', function() { + const executableFile = new File({path: 'file.txt', mode: '100755'}); + const nonExecutableFile = new File({path: 'file.txt', mode: '100644'}); + + assert.isTrue(new FilePatch(nonExecutableFile, executableFile, emptyPatch).didChangeExecutableMode()); + assert.isTrue(new FilePatch(executableFile, nonExecutableFile, emptyPatch).didChangeExecutableMode()); + assert.isFalse(new FilePatch(nonExecutableFile, nonExecutableFile, emptyPatch).didChangeExecutableMode()); + assert.isFalse(new FilePatch(executableFile, executableFile, emptyPatch).didChangeExecutableMode()); + assert.isFalse(new FilePatch(nullFile, nonExecutableFile).didChangeExecutableMode()); + assert.isFalse(new FilePatch(nullFile, executableFile).didChangeExecutableMode()); + }); + + it('detects changes in symlink mode', function() { + const symlinkFile = new File({path: 'file.txt', mode: '120000', symlink: 'dest.txt'}); + const nonSymlinkFile = new File({path: 'file.txt', mode: '100644'}); + + assert.isTrue(new FilePatch(nonSymlinkFile, symlinkFile, emptyPatch).hasTypechange()); + assert.isTrue(new FilePatch(symlinkFile, nonSymlinkFile, emptyPatch).hasTypechange()); + assert.isFalse(new FilePatch(nonSymlinkFile, nonSymlinkFile, emptyPatch).hasTypechange()); + assert.isFalse(new FilePatch(symlinkFile, symlinkFile, emptyPatch).hasTypechange()); + assert.isFalse(new FilePatch(nullFile, nonSymlinkFile).hasTypechange()); + assert.isFalse(new FilePatch(nullFile, symlinkFile).hasTypechange()); + }); + + it('detects when either file has a symlink destination', function() { + const symlinkFile = new File({path: 'file.txt', mode: '120000', symlink: 'dest.txt'}); + const nonSymlinkFile = new File({path: 'file.txt', mode: '100644'}); + + assert.isTrue(new FilePatch(nonSymlinkFile, symlinkFile, emptyPatch).hasSymlink()); + assert.isTrue(new FilePatch(symlinkFile, nonSymlinkFile, emptyPatch).hasSymlink()); + assert.isFalse(new FilePatch(nonSymlinkFile, nonSymlinkFile, emptyPatch).hasSymlink()); + assert.isTrue(new FilePatch(symlinkFile, symlinkFile, emptyPatch).hasSymlink()); + assert.isFalse(new FilePatch(nullFile, nonSymlinkFile).hasSymlink()); + assert.isTrue(new FilePatch(nullFile, symlinkFile).hasSymlink()); + }); + }); + + it('clones itself and overrides select properties', function() { + const file00 = new File({path: 'file-00.txt', mode: '100644'}); + const file01 = new File({path: 'file-01.txt', mode: '100644'}); + const file10 = new File({path: 'file-10.txt', mode: '100644'}); + const file11 = new File({path: 'file-11.txt', mode: '100644'}); + const patch0 = new Patch({status: 'modified', hunks: [], bufferText: '0'}); + const patch1 = new Patch({status: 'modified', hunks: [], bufferText: '1'}); + + const original = new FilePatch(file00, file01, patch0); + + const clone0 = original.clone(); + assert.notStrictEqual(clone0, original); + assert.strictEqual(clone0.getOldFile(), file00); + assert.strictEqual(clone0.getNewFile(), file01); + assert.strictEqual(clone0.getPatch(), patch0); + + const clone1 = original.clone({oldFile: file10}); + assert.notStrictEqual(clone1, original); + assert.strictEqual(clone1.getOldFile(), file10); + assert.strictEqual(clone1.getNewFile(), file01); + assert.strictEqual(clone1.getPatch(), patch0); + + const clone2 = original.clone({newFile: file11}); + assert.notStrictEqual(clone2, original); + assert.strictEqual(clone2.getOldFile(), file00); + assert.strictEqual(clone2.getNewFile(), file11); + assert.strictEqual(clone2.getPatch(), patch0); + + const clone3 = original.clone({patch: patch1}); + assert.notStrictEqual(clone3, original); + assert.strictEqual(clone3.getOldFile(), file00); + assert.strictEqual(clone3.getNewFile(), file01); + assert.strictEqual(clone3.getPatch(), patch1); + }); + describe('getStagePatchForLines()', function() { it('returns a new FilePatch that applies only the selected lines', function() { const bufferText = '0000\n0001\n0002\n0003\n0004\n'; @@ -49,38 +247,445 @@ describe('FilePatch', function() { }); describe('staging lines from deleted files', function() { - it('handles staging part of the file'); + let deletionPatch; + + beforeEach(function() { + const bufferText = '0000\n0001\n0002\n'; + const hunks = [ + new Hunk({ + oldStartRow: 1, + oldRowCount: 3, + newStartRow: 1, + newRowCount: 0, + rowRange: new IndexedRowRange({bufferRange: [[0, 0], [2, 0]], startOffset: 0, endOffset: 15}), + changes: [ + new Deletion(new IndexedRowRange({bufferRange: [[0, 0], [2, 0]], startOffset: 0, endOffset: 15})), + ], + }), + ]; + const patch = new Patch({status: 'deleted', hunks, bufferText}); + const oldFile = new File({path: 'file.txt', mode: '100644'}); + deletionPatch = new FilePatch(oldFile, nullFile, patch); + }); + + it('handles staging part of the file', function() { + const stagedPatch = deletionPatch.getStagePatchForLines(new Set([1, 2])); - it('handles staging all lines, leaving nothing unstaged'); + assert.strictEqual(stagedPatch.getStatus(), 'modified'); + assert.strictEqual(stagedPatch.getOldPath(), 'file.txt'); + assert.strictEqual(stagedPatch.getOldMode(), '100644'); + assert.strictEqual(stagedPatch.getNewPath(), 'file.txt'); + assert.strictEqual(stagedPatch.getNewMode(), '100644'); + assert.strictEqual(stagedPatch.getBufferText(), '0000\n0001\n0002\n'); + assertInFilePatch(stagedPatch).hunks( + { + startRow: 0, + endRow: 2, + header: '@@ -1,3 +1,1 @@', + changes: [ + {kind: 'deletion', string: '-0001\n-0002\n', range: [[1, 0], [2, 0]]}, + ], + }, + ); + }); + + it('handles staging all lines, leaving nothing unstaged', function() { + const stagedPatch = deletionPatch.getStagePatchForLines(new Set([1, 2, 3])); + assert.strictEqual(stagedPatch.getStatus(), 'deleted'); + assert.strictEqual(stagedPatch.getOldPath(), 'file.txt'); + assert.strictEqual(stagedPatch.getOldMode(), '100644'); + assert.isFalse(stagedPatch.getNewFile().isPresent()); + assert.strictEqual(stagedPatch.getBufferText(), '0000\n0001\n0002\n'); + assertInFilePatch(stagedPatch).hunks( + { + startRow: 0, + endRow: 2, + header: '@@ -1,3 +1,0 @@', + changes: [ + {kind: 'deletion', string: '-0000\n-0001\n-0002\n', range: [[0, 0], [2, 0]]}, + ], + }, + ); + }); + + it('unsets the newFile when a symlink is created where a file was deleted', function() { + const bufferText = '0000\n0001\n0002\n'; + const hunks = [ + new Hunk({ + oldStartRow: 1, + oldRowCount: 3, + newStartRow: 1, + newRowCount: 0, + rowRange: new IndexedRowRange({bufferRange: [[0, 0], [2, 0]], startOffset: 0, endOffset: 15}), + changes: [ + new Deletion(new IndexedRowRange({bufferRange: [[0, 0], [2, 0]], startOffset: 0, endOffset: 15})), + ], + }), + ]; + const patch = new Patch({status: 'deleted', hunks, bufferText}); + const oldFile = new File({path: 'file.txt', mode: '100644'}); + const newFile = new File({path: 'file.txt', mode: '120000'}); + const replacePatch = new FilePatch(oldFile, newFile, patch); + + const stagedPatch = replacePatch.getStagePatchForLines(new Set([0, 1, 2])); + assert.isTrue(stagedPatch.getOldFile().isPresent()); + assert.isFalse(stagedPatch.getNewFile().isPresent()); + }); }); }); + it('stages an entire hunk at once', function() { + const bufferText = '0000\n0001\n0002\n0003\n0004\n0005\n'; + const hunks = [ + new Hunk({ + oldStartRow: 10, + oldRowCount: 2, + newStartRow: 10, + newRowCount: 3, + rowRange: new IndexedRowRange({bufferRange: [[0, 0], [2, 0]], startOffset: 0, endOffset: 15}), + changes: [ + new Addition(new IndexedRowRange({bufferRange: [[1, 0], [1, 0]], startOffset: 5, endOffset: 10})), + ], + }), + new Hunk({ + oldStartRow: 20, + oldRowCount: 3, + newStartRow: 19, + newRowCount: 2, + rowRange: new IndexedRowRange({bufferRange: [[3, 0], [5, 0]], startOffset: 15, endOffset: 35}), + changes: [ + new Deletion(new IndexedRowRange({bufferRange: [[4, 0], [4, 0]], startOffset: 20, endOffset: 25})), + ], + }), + ]; + const patch = new Patch({status: 'modified', hunks, bufferText}); + const oldFile = new File({path: 'file.txt', mode: '100644'}); + const newFile = new File({path: 'file.txt', mode: '100644'}); + const filePatch = new FilePatch(oldFile, newFile, patch); + + const stagedPatch = filePatch.getStagePatchForHunk(hunks[1]); + assert.strictEqual(stagedPatch.getBufferText(), '0003\n0004\n0005\n'); + assertInFilePatch(stagedPatch).hunks( + { + startRow: 0, + endRow: 2, + header: '@@ -20,3 +18,2 @@', + changes: [ + {kind: 'deletion', string: '-0004\n', range: [[1, 0], [1, 0]]}, + ], + }, + ); + }); + describe('getUnstagePatchForLines()', function() { - it('returns a new FilePatch that unstages only the specified lines'); + it('returns a new FilePatch that unstages only the specified lines', function() { + const bufferText = '0000\n0001\n0002\n0003\n0004\n'; + const hunks = [ + new Hunk({ + oldStartRow: 5, + oldRowCount: 3, + newStartRow: 5, + newRowCount: 4, + rowRange: new IndexedRowRange({bufferRange: [[0, 0], [4, 0]], startOffset: 0, endOffset: 25}), + changes: [ + new Addition(new IndexedRowRange({bufferRange: [[1, 0], [2, 0]], startOffset: 5, endOffset: 15})), + new Deletion(new IndexedRowRange({bufferRange: [[3, 0], [3, 0]], startOffset: 15, endOffset: 20})), + ], + }), + ]; + const patch = new Patch({status: 'modified', hunks, bufferText}); + const oldFile = new File({path: 'file.txt', mode: '100644'}); + const newFile = new File({path: 'file.txt', mode: '100644'}); + const filePatch = new FilePatch(oldFile, newFile, patch); + + const unstagedPatch = filePatch.getUnstagePatchForLines(new Set([1, 3])); + assert.strictEqual(unstagedPatch.getStatus(), 'modified'); + assert.strictEqual(unstagedPatch.getOldPath(), 'file.txt'); + assert.strictEqual(unstagedPatch.getOldMode(), '100644'); + assert.strictEqual(unstagedPatch.getNewPath(), 'file.txt'); + assert.strictEqual(unstagedPatch.getNewMode(), '100644'); + assert.strictEqual(unstagedPatch.getBufferText(), '0000\n0001\n0002\n0003\n0004\n'); + assertInFilePatch(unstagedPatch).hunks( + { + startRow: 0, + endRow: 4, + header: '@@ -5,4 +5,4 @@', + changes: [ + {kind: 'deletion', string: '-0001\n', range: [[1, 0], [1, 0]]}, + {kind: 'addition', string: '+0003\n', range: [[3, 0], [3, 0]]}, + ], + }, + ); + }); describe('unstaging lines from an added file', function() { - it('handles unstaging part of the file'); + let newFile, addedPatch, addedFilePatch; + + beforeEach(function() { + const bufferText = '0000\n0001\n0002\n'; + const hunks = [ + new Hunk({ + oldStartRow: 1, + oldRowCount: 0, + newStartRow: 1, + newRowCount: 3, + rowRange: new IndexedRowRange({bufferRange: [[0, 0], [2, 0]], startOffset: 0, endOffset: 15}), + changes: [ + new Addition(new IndexedRowRange({bufferRange: [[0, 0], [2, 0]], startOffset: 0, endOffset: 15})), + ], + }), + ]; + newFile = new File({path: 'file.txt', mode: '100644'}); + addedPatch = new Patch({status: 'added', hunks, bufferText}); + addedFilePatch = new FilePatch(nullFile, newFile, addedPatch); + }); + + it('handles unstaging part of the file', function() { + const unstagePatch = addedFilePatch.getUnstagePatchForLines(new Set([2])); + assert.strictEqual(unstagePatch.getStatus(), 'modified'); + assertInFilePatch(unstagePatch).hunks( + { + startRow: 0, + endRow: 2, + header: '@@ -1,3 +1,2 @@', + changes: [ + {kind: 'deletion', string: '-0002\n', range: [[2, 0], [2, 0]]}, + ], + }, + ); + }); - it('handles unstaging all lines, leaving nothing staged'); + it('handles unstaging all lines, leaving nothing staged', function() { + const unstagePatch = addedFilePatch.getUnstagePatchForLines(new Set([0, 1, 2])); + assert.strictEqual(unstagePatch.getStatus(), 'deleted'); + assertInFilePatch(unstagePatch).hunks( + { + startRow: 0, + endRow: 2, + header: '@@ -1,3 +1,0 @@', + changes: [ + {kind: 'deletion', string: '-0000\n-0001\n-0002\n', range: [[0, 0], [2, 0]]}, + ], + }, + ); + }); + + it('unsets the oldFile when a symlink is deleted and a file is created in its place', function() { + const oldSymlink = new File({path: 'file.txt', mode: '120000', symlink: 'wat.txt'}); + const patch = new FilePatch(oldSymlink, newFile, addedPatch); + const unstagePatch = patch.getUnstagePatchForLines(new Set([0, 1, 2])); + assert.isFalse(unstagePatch.getOldFile().isPresent()); + assertInFilePatch(unstagePatch).hunks( + { + startRow: 0, + endRow: 2, + header: '@@ -1,3 +1,0 @@', + changes: [ + {kind: 'deletion', string: '-0000\n-0001\n-0002\n', range: [[0, 0], [2, 0]]}, + ], + }, + ); + }); }); }); - it('handles newly added files'); + it('unstages an entire hunk at once', function() { + const bufferText = '0000\n0001\n0002\n0003\n0004\n0005\n'; + const hunks = [ + new Hunk({ + oldStartRow: 10, + oldRowCount: 2, + newStartRow: 10, + newRowCount: 3, + rowRange: new IndexedRowRange({bufferRange: [[0, 0], [2, 0]], startOffset: 0, endOffset: 15}), + changes: [ + new Addition(new IndexedRowRange({bufferRange: [[1, 0], [1, 0]], startOffset: 5, endOffset: 10})), + ], + }), + new Hunk({ + oldStartRow: 20, + oldRowCount: 3, + newStartRow: 19, + newRowCount: 2, + rowRange: new IndexedRowRange({bufferRange: [[3, 0], [5, 0]], startOffset: 15, endOffset: 35}), + changes: [ + new Deletion(new IndexedRowRange({bufferRange: [[4, 0], [4, 0]], startOffset: 20, endOffset: 25})), + ], + }), + ]; + const patch = new Patch({status: 'modified', hunks, bufferText}); + const oldFile = new File({path: 'file.txt', mode: '100644'}); + const newFile = new File({path: 'file.txt', mode: '100644'}); + const filePatch = new FilePatch(oldFile, newFile, patch); + + const unstagedPatch = filePatch.getUnstagePatchForHunk(hunks[0]); + assert.strictEqual(unstagedPatch.getBufferText(), '0000\n0001\n0002\n'); + assertInFilePatch(unstagedPatch).hunks( + { + startRow: 0, + endRow: 2, + header: '@@ -10,3 +10,2 @@', + changes: [ + {kind: 'deletion', string: '-0001\n', range: [[1, 0], [1, 0]]}, + ], + }, + ); + }); describe('toString()', function() { - it('converts the patch to the standard textual format'); + it('converts the patch to the standard textual format', function() { + const bufferText = '0000\n0001\n0002\n0003\n0004\n0005\n0006\n0007\n'; + const hunks = [ + new Hunk({ + oldStartRow: 10, + oldRowCount: 4, + newStartRow: 10, + newRowCount: 3, + rowRange: new IndexedRowRange({bufferRange: [[0, 0], [4, 0]], startOffset: 0, endOffset: 25}), + changes: [ + new Addition(new IndexedRowRange({bufferRange: [[1, 0], [1, 0]], startOffset: 5, endOffset: 10})), + new Deletion(new IndexedRowRange({bufferRange: [[2, 0], [3, 0]], startOffset: 10, endOffset: 20})), + ], + }), + new Hunk({ + oldStartRow: 20, + oldRowCount: 2, + newStartRow: 20, + newRowCount: 3, + rowRange: new IndexedRowRange({bufferRange: [[5, 0], [7, 0]], startOffset: 25, endOffset: 40}), + changes: [ + new Addition(new IndexedRowRange({bufferRange: [[6, 0], [6, 0]], startOffset: 30, endOffset: 35})), + ], + }), + ]; + const patch = new Patch({status: 'modified', hunks, bufferText}); + const oldFile = new File({path: 'a.txt', mode: '100644'}); + const newFile = new File({path: 'b.txt', mode: '100755'}); + const filePatch = new FilePatch(oldFile, newFile, patch); - it('correctly formats new files with no newline at the end'); + const expectedString = + 'diff --git a/a.txt b/b.txt\n' + + '--- a/a.txt\n' + + '+++ b/b.txt\n' + + '@@ -10,4 +10,3 @@\n' + + ' 0000\n' + + '+0001\n' + + '-0002\n' + + '-0003\n' + + ' 0004\n' + + '@@ -20,2 +20,3 @@\n' + + ' 0005\n' + + '+0006\n' + + ' 0007\n'; + assert.strictEqual(filePatch.toString(), expectedString); + }); - describe('typechange file patches', function() { - it('handles typechange patches for a symlink replaced with a file'); + it('correctly formats a file with no newline at the end', function() { + const bufferText = '0000\n0001\n No newline at end of file\n'; + const hunks = [ + new Hunk({ + oldStartRow: 1, + oldRowCount: 1, + newStartRow: 1, + newRowCount: 2, + rowRange: new IndexedRowRange({bufferRange: [[0, 0], [2, 0]], startOffset: 0, endOffset: 37}), + changes: [ + new Addition(new IndexedRowRange({bufferRange: [[1, 0], [1, 0]], startOffset: 5, endOffset: 10})), + new NoNewline(new IndexedRowRange({bufferRange: [[2, 0], [2, 0]], startOffset: 10, endOffset: 37})), + ], + }), + ]; + const patch = new Patch({status: 'modified', hunks, bufferText}); + const oldFile = new File({path: 'a.txt', mode: '100644'}); + const newFile = new File({path: 'b.txt', mode: '100755'}); + const filePatch = new FilePatch(oldFile, newFile, patch); - it('handles typechange patches for a file replaced with a symlink'); + const expectedString = + 'diff --git a/a.txt b/b.txt\n' + + '--- a/a.txt\n' + + '+++ b/b.txt\n' + + '@@ -1,1 +1,2 @@\n' + + ' 0000\n' + + '+0001\n' + + '\\ No newline at end of file\n'; + assert.strictEqual(filePatch.toString(), expectedString); }); - }); - describe('getHeaderString()', function() { - it('formats paths with git path separators'); + describe('typechange file patches', function() { + it('handles typechange patches for a symlink replaced with a file', function() { + const bufferText = '0000\n0001\n'; + const hunks = [ + new Hunk({ + oldStartRow: 1, + oldRowCount: 0, + newStartRow: 1, + newRowCount: 2, + rowRange: new IndexedRowRange({bufferRange: [[0, 0], [2, 0]], startOffset: 0, endOffset: 10}), + changes: [ + new Addition(new IndexedRowRange({bufferRange: [[0, 0], [2, 0]], startOffset: 0, endOffset: 10})), + ], + }), + ]; + const patch = new Patch({status: 'added', hunks, bufferText}); + const oldFile = new File({path: 'a.txt', mode: '120000', symlink: 'dest.txt'}); + const newFile = new File({path: 'a.txt', mode: '100644'}); + const filePatch = new FilePatch(oldFile, newFile, patch); + + const expectedString = + 'diff --git a/a.txt b/a.txt\n' + + 'deleted file mode 120000\n' + + '--- a/a.txt\n' + + '+++ /dev/null\n' + + '@@ -1 +0,0 @@\n' + + '-dest.txt\n' + + '\\ No newline at end of file\n' + + 'diff --git a/a.txt b/a.txt\n' + + 'new file mode 100644\n' + + '--- /dev/null\n' + + '+++ b/a.txt\n' + + '@@ -1,0 +1,2 @@\n' + + '+0000\n' + + '+0001\n'; + assert.strictEqual(filePatch.toString(), expectedString); + }); + + it('handles typechange patches for a file replaced with a symlink', function() { + const bufferText = '0000\n0001\n'; + const hunks = [ + new Hunk({ + oldStartRow: 1, + oldRowCount: 2, + newStartRow: 1, + newRowCount: 0, + rowRange: new IndexedRowRange({bufferRange: [[0, 0], [2, 0]], startOffset: 0, endOffset: 10}), + changes: [ + new Deletion(new IndexedRowRange({bufferRange: [[0, 0], [2, 0]], startOffset: 0, endOffset: 10})), + ], + }), + ]; + const patch = new Patch({status: 'deleted', hunks, bufferText}); + const oldFile = new File({path: 'a.txt', mode: '100644'}); + const newFile = new File({path: 'a.txt', mode: '120000', symlink: 'dest.txt'}); + const filePatch = new FilePatch(oldFile, newFile, patch); + + const expectedString = + 'diff --git a/a.txt b/a.txt\n' + + 'deleted file mode 100644\n' + + '--- a/a.txt\n' + + '+++ /dev/null\n' + + '@@ -1,2 +1,0 @@\n' + + '-0000\n' + + '-0001\n' + + 'diff --git a/a.txt b/a.txt\n' + + 'new file mode 120000\n' + + '--- /dev/null\n' + + '+++ b/a.txt\n' + + '@@ -0,0 +1 @@\n' + + '+dest.txt\n' + + '\\ No newline at end of file\n'; + assert.strictEqual(filePatch.toString(), expectedString); + }); + }); }); it('getByteSize() returns the size in bytes'); From 3a67c2c4c2d5d89501b4b6e6b6cbf81f560e79f6 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 17 Aug 2018 14:34:30 -0400 Subject: [PATCH 0165/4252] nullPatch method parity --- lib/models/patch/patch.js | 4 ++++ test/models/patch/patch.test.js | 1 + 2 files changed, 5 insertions(+) diff --git a/lib/models/patch/patch.js b/lib/models/patch/patch.js index 4c330feb8c..f10c942434 100644 --- a/lib/models/patch/patch.js +++ b/lib/models/patch/patch.js @@ -348,6 +348,10 @@ export const nullPatch = { return this; }, + getFullUnstagedPatch() { + return this; + }, + toString() { return ''; }, diff --git a/test/models/patch/patch.test.js b/test/models/patch/patch.test.js index 67f46683a6..f54237b022 100644 --- a/test/models/patch/patch.test.js +++ b/test/models/patch/patch.test.js @@ -404,6 +404,7 @@ describe('Patch', function() { it('returns a nullPatch as a nullPatch', function() { assert.strictEqual(nullPatch.getUnstagePatchForLines(new Set([1, 2, 3])), nullPatch); + assert.strictEqual(nullPatch.getFullUnstagedPatch(), nullPatch); }); }); From 0e77fb0de76598e5876546156a330eddeeeb93d5 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 17 Aug 2018 15:19:23 -0400 Subject: [PATCH 0166/4252] nullPatch completeness --- lib/models/patch/patch.js | 4 ++++ test/models/patch/patch.test.js | 1 + 2 files changed, 5 insertions(+) diff --git a/lib/models/patch/patch.js b/lib/models/patch/patch.js index f10c942434..1b9be2bbe3 100644 --- a/lib/models/patch/patch.js +++ b/lib/models/patch/patch.js @@ -328,6 +328,10 @@ export const nullPatch = { return 0; }, + getChangedLineCount() { + return 0; + }, + clone(opts = {}) { if (opts.status === undefined && opts.hunks === undefined && opts.bufferText === undefined) { return this; diff --git a/test/models/patch/patch.test.js b/test/models/patch/patch.test.js index f54237b022..d4b4746f0c 100644 --- a/test/models/patch/patch.test.js +++ b/test/models/patch/patch.test.js @@ -460,6 +460,7 @@ describe('Patch', function() { assert.strictEqual(nullPatch.getByteSize(), 0); assert.isFalse(nullPatch.isPresent()); assert.strictEqual(nullPatch.toString(), ''); + assert.strictEqual(nullPatch.getChangedLineCount(), 0); }); }); From e27b2dd7e8bb70b0ccea466371f0db515742824a Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 17 Aug 2018 15:19:51 -0400 Subject: [PATCH 0167/4252] Implement a nullFilePatch --- lib/models/patch/file-patch.js | 12 +++++++++- test/models/patch/file-patch.test.js | 35 ++++++++++++++++++++++++++-- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/lib/models/patch/file-patch.js b/lib/models/patch/file-patch.js index 3b57951624..188cea71d3 100644 --- a/lib/models/patch/file-patch.js +++ b/lib/models/patch/file-patch.js @@ -1,5 +1,5 @@ import {nullFile} from './file'; -import Patch from './patch'; +import Patch, {nullPatch} from './patch'; import {toGitPathSep} from '../../helpers'; export default class FilePatch { @@ -9,6 +9,10 @@ export default class FilePatch { this.patch = patch; } + isPresent() { + return this.oldFile.isPresent() || this.newFile.isPresent() || this.patch.isPresent(); + } + getOldFile() { return this.oldFile; } @@ -174,6 +178,10 @@ export default class FilePatch { } toString() { + if (!this.isPresent()) { + return ''; + } + if (this.hasTypechange()) { const left = this.clone({ newFile: nullFile, @@ -216,3 +224,5 @@ export default class FilePatch { return header; } } + +export const nullFilePatch = new FilePatch(nullFile, nullFile, nullPatch); diff --git a/test/models/patch/file-patch.test.js b/test/models/patch/file-patch.test.js index dc266ada09..c21204f4a8 100644 --- a/test/models/patch/file-patch.test.js +++ b/test/models/patch/file-patch.test.js @@ -1,4 +1,4 @@ -import FilePatch from '../../../lib/models/patch/file-patch'; +import FilePatch, {nullFilePatch} from '../../../lib/models/patch/file-patch'; import File, {nullFile} from '../../../lib/models/patch/file'; import Patch from '../../../lib/models/patch/patch'; import Hunk from '../../../lib/models/patch/hunk'; @@ -26,6 +26,8 @@ describe('FilePatch', function() { const newFile = new File({path: 'b.txt', mode: '100755'}); const filePatch = new FilePatch(oldFile, newFile, patch); + assert.isTrue(filePatch.isPresent()); + assert.strictEqual(filePatch.getOldPath(), 'a.txt'); assert.strictEqual(filePatch.getOldMode(), '120000'); assert.strictEqual(filePatch.getOldSymlink(), 'dest.txt'); @@ -688,5 +690,34 @@ describe('FilePatch', function() { }); }); - it('getByteSize() returns the size in bytes'); + it('has a nullFilePatch that stubs all FilePatch methods', function() { + const rowRange = new IndexedRowRange({bufferRange: [[0, 0], [1, 0]], startOffset: 0, endOffset: 10}); + + assert.isFalse(nullFilePatch.isPresent()); + assert.isFalse(nullFilePatch.getOldFile().isPresent()); + assert.isFalse(nullFilePatch.getNewFile().isPresent()); + assert.isFalse(nullFilePatch.getPatch().isPresent()); + assert.isNull(nullFilePatch.getOldPath()); + assert.isNull(nullFilePatch.getNewPath()); + assert.isNull(nullFilePatch.getOldMode()); + assert.isNull(nullFilePatch.getNewMode()); + assert.isNull(nullFilePatch.getOldSymlink()); + assert.isNull(nullFilePatch.getNewSymlink()); + assert.strictEqual(nullFilePatch.getByteSize(), 0); + assert.strictEqual(nullFilePatch.getBufferText(), ''); + assert.lengthOf(nullFilePatch.getAdditionRanges(), 0); + assert.lengthOf(nullFilePatch.getDeletionRanges(), 0); + assert.lengthOf(nullFilePatch.getNoNewlineRanges(), 0); + assert.isFalse(nullFilePatch.didChangeExecutableMode()); + assert.isFalse(nullFilePatch.hasSymlink()); + assert.isFalse(nullFilePatch.hasTypechange()); + assert.isNull(nullFilePatch.getPath()); + assert.isNull(nullFilePatch.getStatus()); + assert.lengthOf(nullFilePatch.getHunks(), 0); + assert.isFalse(nullFilePatch.getStagePatchForLines(new Set([0])).isPresent()); + assert.isFalse(nullFilePatch.getStagePatchForHunk(new Hunk({changes: [], rowRange})).isPresent()); + assert.isFalse(nullFilePatch.getUnstagePatchForLines(new Set([0])).isPresent()); + assert.isFalse(nullFilePatch.getUnstagePatchForHunk(new Hunk({changes: [], rowRange})).isPresent()); + assert.strictEqual(nullFilePatch.toString(), ''); + }); }); From 6e11034e076be601247d33402c3719a9e77d2f9e Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 17 Aug 2018 15:33:03 -0400 Subject: [PATCH 0168/4252] Return nullFilePatch from getFilePatchForPath() --- lib/models/repository-states/state.js | 3 ++- test/models/repository.test.js | 18 ++++++++++++++---- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/lib/models/repository-states/state.js b/lib/models/repository-states/state.js index 9c66928e7b..c9d5c82b55 100644 --- a/lib/models/repository-states/state.js +++ b/lib/models/repository-states/state.js @@ -2,6 +2,7 @@ import {nullCommit} from '../commit'; import BranchSet from '../branch-set'; import RemoteSet from '../remote-set'; import {nullOperationStates} from '../operation-states'; +import {nullFilePatch} from '../patch/file-patch'; /** * Map of registered subclasses to allow states to transition to one another without circular dependencies. @@ -274,7 +275,7 @@ export default class State { } getFilePatchForPath(filePath, options = {}) { - return Promise.resolve(null); + return Promise.resolve(nullFilePatch); } readFileFromIndex(filePath) { diff --git a/test/models/repository.test.js b/test/models/repository.test.js index 3003496211..be105cd803 100644 --- a/test/models/repository.test.js +++ b/test/models/repository.test.js @@ -71,7 +71,7 @@ describe('Repository', function() { // Methods that resolve to null for (const method of [ - 'getFilePatchForPath', 'getAheadCount', 'getBehindCount', 'getConfig', 'getLastHistorySnapshots', 'getCache', + 'getAheadCount', 'getBehindCount', 'getConfig', 'getLastHistorySnapshots', 'getCache', ]) { assert.isNull(await repository[method]()); } @@ -110,6 +110,7 @@ describe('Repository', function() { assert.strictEqual(await repository.getHeadDescription(), '(no repository)'); assert.strictEqual(await repository.getOperationStates(), nullOperationStates); assert.strictEqual(await repository.getCommitMessage(), ''); + assert.isFalse((await repository.getFilePatchForPath('anything.txt')).isPresent()); }); it('returns a rejecting promise', async function() { @@ -402,6 +403,15 @@ describe('Repository', function() { assert.notEqual(await repo.getFilePatchForPath('a.txt'), filePatchA); assert.deepEqual(await repo.getFilePatchForPath('a.txt'), filePatchA); }); + + it('returns a nullFilePatch for unknown paths', async function() { + const workingDirPath = await cloneRepository('multiple-commits'); + const repo = new Repository(workingDirPath); + await repo.getLoadPromise(); + + const patch = await repo.getFilePatchForPath('no.txt'); + assert.isFalse(patch.isPresent()); + }); }); describe('isPartiallyStaged(filePath)', function() { @@ -468,7 +478,7 @@ describe('Repository', function() { assert.deepEqual(unstagedChanges, [path.join('subdir-1', 'a.txt')]); assert.deepEqual(stagedChanges, [path.join('subdir-1', 'a.txt')]); - await repo.applyPatchToIndex(unstagedPatch1.getUnstagePatch()); + await repo.applyPatchToIndex(unstagedPatch1.getUnstagePatchForLines(new Set([0, 1, 2]))); repo.refresh(); const unstagedPatch3 = await repo.getFilePatchForPath(path.join('subdir-1', 'a.txt')); assert.deepEqual(unstagedPatch3, unstagedPatch2); @@ -1513,7 +1523,7 @@ describe('Repository', function() { await repository.getLoadPromise(); await fs.writeFile(path.join(workdir, 'a.txt'), 'foo\nfoo-1\n', {encoding: 'utf8'}); - const patch = (await repository.getFilePatchForPath('a.txt')).getUnstagePatch(); + const patch = (await repository.getFilePatchForPath('a.txt')).getUnstagePatchForLines(new Set([0, 1])); await assertCorrectInvalidation({repository}, async () => { await repository.applyPatchToWorkdir(patch); @@ -1792,7 +1802,7 @@ describe('Repository', function() { const {repository, observer} = await wireUpObserver(); await fs.writeFile(path.join(workdir, 'a.txt'), 'boop\n', {encoding: 'utf8'}); - const patch = (await repository.getFilePatchForPath('a.txt')).getUnstagePatch(); + const patch = (await repository.getFilePatchForPath('a.txt')).getUnstagePatchForLines(new Set([0])); await assertCorrectInvalidation({repository}, async () => { await observer.start(); From 75ec2cb1a689f147abaee65fa780d55c98a79628 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 17 Aug 2018 15:59:06 -0400 Subject: [PATCH 0169/4252] Capture MarkerLayers as well as IDs --- lib/atom/marker-layer.js | 6 ++++++ test/atom/marker-layer.test.js | 18 +++++++++++++----- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/lib/atom/marker-layer.js b/lib/atom/marker-layer.js index 9c483d7582..8cfc381daa 100644 --- a/lib/atom/marker-layer.js +++ b/lib/atom/marker-layer.js @@ -20,10 +20,12 @@ class BareMarkerLayer extends React.Component { editor: PropTypes.object, children: PropTypes.node, handleID: PropTypes.func, + handleLayer: PropTypes.func, }; static defaultProps = { handleID: () => {}, + handleLayer: () => {}, } constructor(props) { @@ -75,6 +77,8 @@ class BareMarkerLayer extends React.Component { componentWillUnmount() { this.layerHolder.map(layer => layer.destroy()); + this.layerHolder.setter(null); + this.props.handleLayer(undefined); this.sub.dispose(); } @@ -91,6 +95,8 @@ class BareMarkerLayer extends React.Component { this.layerHolder.setter( this.state.editorHolder.map(editor => editor.addMarkerLayer(options)).getOr(null), ); + + this.props.handleLayer(this.layerHolder.getOr(null)); this.props.handleID(this.getID()); } diff --git a/test/atom/marker-layer.test.js b/test/atom/marker-layer.test.js index 6fe699f2a1..1f140c3098 100644 --- a/test/atom/marker-layer.test.js +++ b/test/atom/marker-layer.test.js @@ -5,7 +5,7 @@ import MarkerLayer from '../../lib/atom/marker-layer'; import AtomTextEditor from '../../lib/atom/atom-text-editor'; describe('MarkerLayer', function() { - let atomEnv, editor, layerID; + let atomEnv, editor, layer, layerID; beforeEach(async function() { atomEnv = global.buildAtomEnvironment(); @@ -16,6 +16,10 @@ describe('MarkerLayer', function() { atomEnv.destroy(); }); + function setLayer(object) { + layer = object; + } + function setLayerID(id) { layerID = id; } @@ -27,20 +31,24 @@ describe('MarkerLayer', function() { maintainHistory={true} persistent={true} handleID={setLayerID} + handleLayer={setLayer} />, ); - const layer = editor.getMarkerLayer(layerID); - assert.isTrue(layer.bufferMarkerLayer.maintainHistory); - assert.isTrue(layer.bufferMarkerLayer.persistent); + const theLayer = editor.getMarkerLayer(layerID); + assert.strictEqual(theLayer, layer); + assert.isTrue(theLayer.bufferMarkerLayer.maintainHistory); + assert.isTrue(theLayer.bufferMarkerLayer.persistent); }); it('removes its layer on unmount', function() { - const wrapper = mount(); + const wrapper = mount(); assert.isDefined(editor.getMarkerLayer(layerID)); + assert.isDefined(layer); wrapper.unmount(); assert.isUndefined(editor.getMarkerLayer(layerID)); + assert.isUndefined(layer); }); it('inherits an editor from a parent node', function() { From 26f7cbec8e5c10415f233f8594ac5fb6add49a3b Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 17 Aug 2018 20:32:08 -0400 Subject: [PATCH 0170/4252] Get the Marker model --- lib/atom/marker.js | 5 +++++ test/atom/marker.test.js | 42 +++++++++++++++++++++++++--------------- 2 files changed, 31 insertions(+), 16 deletions(-) diff --git a/lib/atom/marker.js b/lib/atom/marker.js index 1ebef279e6..05cbb3d989 100644 --- a/lib/atom/marker.js +++ b/lib/atom/marker.js @@ -29,11 +29,13 @@ class BareMarker extends React.Component { children: PropTypes.node, onDidChange: PropTypes.func, handleID: PropTypes.func, + handleMarker: PropTypes.func, } static defaultProps = { onDidChange: () => {}, handleID: () => {}, + handleMarker: () => {}, } constructor(props) { @@ -43,6 +45,9 @@ class BareMarker extends React.Component { this.subs = new CompositeDisposable(); this.markerHolder = new RefHolder(); + this.markerHolder.observe(marker => { + this.props.handleMarker(marker); + }); this.decorable = { holder: this.markerHolder, diff --git a/test/atom/marker.test.js b/test/atom/marker.test.js index 9d304bd2ee..7bfc30f626 100644 --- a/test/atom/marker.test.js +++ b/test/atom/marker.test.js @@ -7,7 +7,7 @@ import AtomTextEditor from '../../lib/atom/atom-text-editor'; import MarkerLayer from '../../lib/atom/marker-layer'; describe('Marker', function() { - let atomEnv, editor, markerID; + let atomEnv, editor, marker, markerID; beforeEach(async function() { atomEnv = global.buildAtomEnvironment(); @@ -18,19 +18,29 @@ describe('Marker', function() { atomEnv.destroy(); }); + function setMarker(m) { + marker = m; + } + function setMarkerID(id) { markerID = id; } it('adds its marker on mount with default properties', function() { mount( - , + , ); - const marker = editor.getMarker(markerID); - assert.isTrue(marker.getBufferRange().isEqual([[0, 0], [10, 0]])); - assert.strictEqual(marker.bufferMarker.invalidate, 'overlap'); - assert.isFalse(marker.isReversed()); + const theMarker = editor.getMarker(markerID); + assert.strictEqual(theMarker, marker); + assert.isTrue(theMarker.getBufferRange().isEqual([[0, 0], [10, 0]])); + assert.strictEqual(theMarker.bufferMarker.invalidate, 'overlap'); + assert.isFalse(theMarker.isReversed()); }); it('configures its marker', function() { @@ -45,10 +55,10 @@ describe('Marker', function() { />, ); - const marker = editor.getMarker(markerID); - assert.isTrue(marker.getBufferRange().isEqual([[1, 2], [4, 5]])); - assert.isTrue(marker.isReversed()); - assert.strictEqual(marker.bufferMarker.invalidate, 'never'); + const theMarker = editor.getMarker(markerID); + assert.isTrue(theMarker.getBufferRange().isEqual([[1, 2], [4, 5]])); + assert.isTrue(theMarker.isReversed()); + assert.strictEqual(theMarker.bufferMarker.invalidate, 'never'); }); it('prefers marking a MarkerLayer to a TextEditor', function() { @@ -63,8 +73,8 @@ describe('Marker', function() { />, ); - const marker = layer.getMarker(markerID); - assert.strictEqual(marker.layer, layer); + const theMarker = layer.getMarker(markerID); + assert.strictEqual(theMarker.layer, layer); }); it('destroys its marker on unmount', function() { @@ -85,8 +95,8 @@ describe('Marker', function() { ); const theEditor = wrapper.instance().getModel(); - const marker = theEditor.getMarker(markerID); - assert.isTrue(marker.getBufferRange().isEqual([[0, 0], [0, 0]])); + const theMarker = theEditor.getMarker(markerID); + assert.isTrue(theMarker.getBufferRange().isEqual([[0, 0], [0, 0]])); }); it('marks a marker layer from a parent node', function() { @@ -101,7 +111,7 @@ describe('Marker', function() { const theEditor = wrapper.instance().getModel(); const layer = theEditor.getMarkerLayer(layerID); - const marker = layer.getMarker(markerID); - assert.isTrue(marker.getBufferRange().isEqual([[0, 0], [0, 0]])); + const theMarker = layer.getMarker(markerID); + assert.isTrue(theMarker.getBufferRange().isEqual([[0, 0], [0, 0]])); }); }); From 6b9ec47928fde6929de3c09daf1c297a2c3fbd30 Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Sat, 18 Aug 2018 13:03:08 +0000 Subject: [PATCH 0171/4252] chore(package): update mocha-appveyor-reporter to version 0.4.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a0f1028595..2887e75120 100644 --- a/package.json +++ b/package.json @@ -83,7 +83,7 @@ "hock": "1.3.3", "lodash.isequal": "4.5.0", "mkdirp": "0.5.1", - "mocha-appveyor-reporter": "0.4.0", + "mocha-appveyor-reporter": "0.4.1", "mocha-multi-reporters": "^1.1.7", "mocha-stress": "1.0.0", "node-fetch": "2.2.0", From 97d4ee23a84f40fd3151d03e8d89fbabea5c7d48 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 20 Aug 2018 09:08:19 -0400 Subject: [PATCH 0172/4252] IndexedRowRange.includesRow() --- lib/models/indexed-row-range.js | 8 ++++++++ test/models/indexed-row-range.test.js | 15 +++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/lib/models/indexed-row-range.js b/lib/models/indexed-row-range.js index 87b9b6d673..58089ac7be 100644 --- a/lib/models/indexed-row-range.js +++ b/lib/models/indexed-row-range.js @@ -23,6 +23,10 @@ export default class IndexedRowRange { return this.bufferRange.getRowCount(); } + includesRow(bufferRow) { + return this.bufferRange.intersectsRow(bufferRow); + } + toStringIn(buffer, prefix) { return buffer.slice(this.startOffset, this.endOffset).replace(/(^|\n)(?!$)/g, '$&' + prefix); } @@ -115,6 +119,10 @@ export const nullIndexedRowRange = { return 0; }, + includesRow() { + return false; + }, + toStringIn() { return ''; }, diff --git a/test/models/indexed-row-range.test.js b/test/models/indexed-row-range.test.js index 27daef0d27..bfda49a929 100644 --- a/test/models/indexed-row-range.test.js +++ b/test/models/indexed-row-range.test.js @@ -20,6 +20,20 @@ describe('IndexedRowRange', function() { assert.sameMembers(range.getBufferRows(), [2, 3, 4, 5, 6, 7, 8]); }); + it('has a buffer row inclusion predicate', function() { + const range = new IndexedRowRange({ + bufferRange: [[2, 0], [4, 0]], + startOffset: 0, + endOffset: 10, + }); + + assert.isFalse(range.includesRow(1)); + assert.isTrue(range.includesRow(2)); + assert.isTrue(range.includesRow(3)); + assert.isTrue(range.includesRow(4)); + assert.isFalse(range.includesRow(5)); + }); + it('creates a Range from its first line', function() { const range = new IndexedRowRange({ bufferRange: [[2, 0], [8, 0]], @@ -184,6 +198,7 @@ describe('IndexedRowRange', function() { assert.deepEqual(nullIndexedRowRange.intersectRowsIn(new Set([0, 1, 2]), ''), []); assert.strictEqual(nullIndexedRowRange.toStringIn('', '+'), ''); assert.strictEqual(nullIndexedRowRange.bufferRowCount(), 0); + assert.isFalse(nullIndexedRowRange.includesRow(4)); assert.isFalse(nullIndexedRowRange.isPresent()); }); }); From a271293846d1cb7a9555164df39cb841476046bf Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 20 Aug 2018 09:08:43 -0400 Subject: [PATCH 0173/4252] nullIndexedRowRange should implement getBufferRows --- lib/models/indexed-row-range.js | 4 ++++ test/models/indexed-row-range.test.js | 1 + 2 files changed, 5 insertions(+) diff --git a/lib/models/indexed-row-range.js b/lib/models/indexed-row-range.js index 58089ac7be..40bdd01ec8 100644 --- a/lib/models/indexed-row-range.js +++ b/lib/models/indexed-row-range.js @@ -119,6 +119,10 @@ export const nullIndexedRowRange = { return 0; }, + getBufferRows() { + return []; + }, + includesRow() { return false; }, diff --git a/test/models/indexed-row-range.test.js b/test/models/indexed-row-range.test.js index bfda49a929..81999d2bd6 100644 --- a/test/models/indexed-row-range.test.js +++ b/test/models/indexed-row-range.test.js @@ -198,6 +198,7 @@ describe('IndexedRowRange', function() { assert.deepEqual(nullIndexedRowRange.intersectRowsIn(new Set([0, 1, 2]), ''), []); assert.strictEqual(nullIndexedRowRange.toStringIn('', '+'), ''); assert.strictEqual(nullIndexedRowRange.bufferRowCount(), 0); + assert.lengthOf(nullIndexedRowRange.getBufferRows(), 0); assert.isFalse(nullIndexedRowRange.includesRow(4)); assert.isFalse(nullIndexedRowRange.isPresent()); }); From b3a1b4ab1fdf599cd3af01342b3f10e26cf3864a Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 20 Aug 2018 09:09:12 -0400 Subject: [PATCH 0174/4252] Hunk.includesBufferRow() predicate --- lib/models/patch/hunk.js | 4 ++++ test/models/patch/hunk.test.js | 17 +++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/lib/models/patch/hunk.js b/lib/models/patch/hunk.js index cf33d955e2..fcce5a2e84 100644 --- a/lib/models/patch/hunk.js +++ b/lib/models/patch/hunk.js @@ -119,6 +119,10 @@ export default class Hunk { return this.rowRange.bufferRowCount(); } + includesBufferRow(row) { + return this.rowRange.includesRow(row); + } + changedLineCount() { return this.changes.reduce((count, change) => change.when({ nonewline: () => count, diff --git a/test/models/patch/hunk.test.js b/test/models/patch/hunk.test.js index 9a07d6e85b..147cb14a08 100644 --- a/test/models/patch/hunk.test.js +++ b/test/models/patch/hunk.test.js @@ -162,6 +162,23 @@ describe('Hunk', function() { assert.sameMembers(Array.from(h.getBufferRows()), [6, 7, 8, 9, 10]); }); + it('determines if a buffer row is part of this hunk', function() { + const h = new Hunk({ + ...attrs, + rowRange: new IndexedRowRange({ + bufferRange: [[3, 0], [5, 0]], + startOffset: 30, + endOffset: 55, + }), + }); + + assert.isFalse(h.includesBufferRow(2)); + assert.isTrue(h.includesBufferRow(3)); + assert.isTrue(h.includesBufferRow(4)); + assert.isTrue(h.includesBufferRow(5)); + assert.isFalse(h.includesBufferRow(6)); + }); + it('computes the total number of changed lines', function() { const h0 = new Hunk({ ...attrs, From 7436a7a1976d2f81699a64af087e2ec853266642 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 20 Aug 2018 09:09:32 -0400 Subject: [PATCH 0175/4252] Cover the other branch of Hunk.getNoNewlineRange() --- test/models/patch/hunk.test.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/test/models/patch/hunk.test.js b/test/models/patch/hunk.test.js index 147cb14a08..16d7ed8261 100644 --- a/test/models/patch/hunk.test.js +++ b/test/models/patch/hunk.test.js @@ -57,6 +57,21 @@ describe('Hunk', function() { assert.isNull(h.getNoNewlineRange()); }); + it('returns the range of a no-newline region', function() { + const h = new Hunk({ + ...attrs, + changes: [ + new Addition(new IndexedRowRange({bufferRange: [[1, 0], [2, 0]], startOffset: 6, endOffset: 7})), + new Deletion(new IndexedRowRange({bufferRange: [[4, 0], [5, 0]], startOffset: 8, endOffset: 9})), + new NoNewline(new IndexedRowRange({bufferRange: [[10, 0], [10, 0]], startOffset: 100, endOffset: 120})), + ], + }); + + const nl = h.getNoNewlineRange(); + assert.isNotNull(nl); + assert.deepEqual(nl.serialize(), [[10, 0], [10, 0]]); + }); + it('creates its start range for decoration placement', function() { const h = new Hunk({ ...attrs, From af6bd60839e53ded988cc1b5ebd4a7a8f33ca629 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 20 Aug 2018 09:09:53 -0400 Subject: [PATCH 0176/4252] We didn't need Hunk::invert() any more --- test/models/patch/hunk.test.js | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/test/models/patch/hunk.test.js b/test/models/patch/hunk.test.js index 16d7ed8261..67574067b4 100644 --- a/test/models/patch/hunk.test.js +++ b/test/models/patch/hunk.test.js @@ -213,33 +213,6 @@ describe('Hunk', function() { assert.strictEqual(h1.changedLineCount(), 0); }); - it('computes an inverted hunk', function() { - const original = new Hunk({ - ...attrs, - oldStartRow: 0, - newStartRow: 1, - oldRowCount: 2, - newRowCount: 3, - sectionHeading: 'the-heading', - changes: [ - new Addition(new IndexedRowRange({bufferRange: [[2, 0], [4, 0]], startOffset: 0, endOffset: 0})), - new Addition(new IndexedRowRange({bufferRange: [[6, 0], [6, 0]], startOffset: 0, endOffset: 0})), - new Deletion(new IndexedRowRange({bufferRange: [[7, 0], [10, 0]], startOffset: 0, endOffset: 0})), - new NoNewline(new IndexedRowRange({bufferRange: [[12, 0], [12, 0]], startOffset: 0, endOffset: 0})), - ], - }); - - const inverted = original.invert(); - assert.strictEqual(inverted.getOldStartRow(), 1); - assert.strictEqual(inverted.getNewStartRow(), 0); - assert.strictEqual(inverted.getOldRowCount(), 3); - assert.strictEqual(inverted.getNewRowCount(), 2); - assert.strictEqual(inverted.getSectionHeading(), 'the-heading'); - assert.lengthOf(inverted.getAdditionRanges(), 1); - assert.lengthOf(inverted.getDeletionRanges(), 2); - assert.deepEqual(inverted.getNoNewlineRange().serialize(), [[12, 0], [12, 0]]); - }); - describe('toStringIn()', function() { it('prints its header', function() { const h = new Hunk({ From 57c2db07f4dedfc67ccf031d4fadef22e6978a80 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 20 Aug 2018 09:12:41 -0400 Subject: [PATCH 0177/4252] We want to decorate the full row range of a Hunk --- lib/models/patch/hunk.js | 4 ++-- test/models/patch/hunk.test.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/models/patch/hunk.js b/lib/models/patch/hunk.js index fcce5a2e84..cfb9c38822 100644 --- a/lib/models/patch/hunk.js +++ b/lib/models/patch/hunk.js @@ -107,8 +107,8 @@ export default class Hunk { return this.rowRange; } - getStartRange() { - return this.rowRange.getStartRange(); + getBufferRange() { + return this.rowRange.bufferRange; } getBufferRows() { diff --git a/test/models/patch/hunk.test.js b/test/models/patch/hunk.test.js index 67574067b4..f04129bef3 100644 --- a/test/models/patch/hunk.test.js +++ b/test/models/patch/hunk.test.js @@ -72,7 +72,7 @@ describe('Hunk', function() { assert.deepEqual(nl.serialize(), [[10, 0], [10, 0]]); }); - it('creates its start range for decoration placement', function() { + it('creates its row range for decoration placement', function() { const h = new Hunk({ ...attrs, rowRange: new IndexedRowRange({ @@ -82,7 +82,7 @@ describe('Hunk', function() { }), }); - assert.deepEqual(h.getStartRange().serialize(), [[3, 0], [3, 0]]); + assert.deepEqual(h.getBufferRange().serialize(), [[3, 0], [6, 0]]); }); it('generates a patch section header', function() { From 7b530df5810dfac7ccc2abcdc0a4024bf111120a Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 20 Aug 2018 09:21:51 -0400 Subject: [PATCH 0178/4252] Compute old and new diff rows corresponding to a buffer row --- lib/models/patch/hunk.js | 8 +++++++ test/models/patch/hunk.test.js | 38 ++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/lib/models/patch/hunk.js b/lib/models/patch/hunk.js index cfb9c38822..c9c1495e5a 100644 --- a/lib/models/patch/hunk.js +++ b/lib/models/patch/hunk.js @@ -123,6 +123,14 @@ export default class Hunk { return this.rowRange.includesRow(row); } + getOldRowAt(row) { + return this.oldStartRow + (row - this.rowRange.bufferRange.start.row); + } + + getNewRowAt(row) { + return this.newStartRow + (row - this.rowRange.bufferRange.start.row); + } + changedLineCount() { return this.changes.reduce((count, change) => change.when({ nonewline: () => count, diff --git a/test/models/patch/hunk.test.js b/test/models/patch/hunk.test.js index f04129bef3..77c21cc407 100644 --- a/test/models/patch/hunk.test.js +++ b/test/models/patch/hunk.test.js @@ -194,6 +194,44 @@ describe('Hunk', function() { assert.isFalse(h.includesBufferRow(6)); }); + it('computes the old file row for a buffer row', function() { + const h = new Hunk({ + ...attrs, + rowRange: new IndexedRowRange({ + bufferRange: [[2, 0], [10, 0]], + startOffset: 10, + endOffset: 100, + }), + oldStartRow: 10, + oldRowCount: 4, + newStartRow: 20, + newRowCount: 6, + }); + + assert.strictEqual(h.getOldRowAt(2), 10); + assert.strictEqual(h.getOldRowAt(3), 11); + assert.strictEqual(h.getOldRowAt(10), 18); + }); + + it('computes the new file row for a buffer row', function() { + const h = new Hunk({ + ...attrs, + rowRange: new IndexedRowRange({ + bufferRange: [[2, 0], [10, 0]], + startOffset: 10, + endOffset: 100, + }), + oldStartRow: 10, + oldRowCount: 4, + newStartRow: 20, + newRowCount: 6, + }); + + assert.strictEqual(h.getNewRowAt(2), 20); + assert.strictEqual(h.getNewRowAt(3), 21); + assert.strictEqual(h.getNewRowAt(10), 28); + }); + it('computes the total number of changed lines', function() { const h0 = new Hunk({ ...attrs, From 0e9b51593c49e84b04c8742f97be084a16823e9e Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 20 Aug 2018 09:38:53 -0400 Subject: [PATCH 0179/4252] Measure the maximum number of digits in a diff line number --- lib/models/patch/file-patch.js | 4 ++++ lib/models/patch/hunk.js | 7 +++++++ lib/models/patch/patch.js | 5 +++++ test/models/patch/file-patch.test.js | 1 + test/models/patch/hunk.test.js | 20 ++++++++++++++++++++ test/models/patch/patch.test.js | 28 ++++++++++++++++++++++++++++ 6 files changed, 65 insertions(+) diff --git a/lib/models/patch/file-patch.js b/lib/models/patch/file-patch.js index 188cea71d3..df93c80eed 100644 --- a/lib/models/patch/file-patch.js +++ b/lib/models/patch/file-patch.js @@ -57,6 +57,10 @@ export default class FilePatch { return this.getPatch().getBufferText(); } + getMaxLineNumberWidth() { + return this.getPatch().getMaxLineNumberWidth(); + } + getAdditionRanges() { return this.getHunks().reduce((acc, hunk) => { acc.push(...hunk.getAdditionRanges()); diff --git a/lib/models/patch/hunk.js b/lib/models/patch/hunk.js index c9c1495e5a..3236d7a9cd 100644 --- a/lib/models/patch/hunk.js +++ b/lib/models/patch/hunk.js @@ -131,6 +131,13 @@ export default class Hunk { return this.newStartRow + (row - this.rowRange.bufferRange.start.row); } + getMaxLineNumberWidth() { + return Math.max( + (this.oldStartRow + this.oldRowCount).toString().length, + (this.newStartRow + this.newRowCount).toString().length, + ); + } + changedLineCount() { return this.changes.reduce((count, change) => change.when({ nonewline: () => count, diff --git a/lib/models/patch/patch.js b/lib/models/patch/patch.js index 1b9be2bbe3..3e23f94955 100644 --- a/lib/models/patch/patch.js +++ b/lib/models/patch/patch.js @@ -90,6 +90,11 @@ export default class Patch { return this.changedLineCount; } + getMaxLineNumberWidth() { + const lastHunk = this.hunks[this.hunks.length - 1]; + return lastHunk ? lastHunk.getMaxLineNumberWidth() : 0; + } + clone(opts = {}) { return new this.constructor({ status: opts.status !== undefined ? opts.status : this.getStatus(), diff --git a/test/models/patch/file-patch.test.js b/test/models/patch/file-patch.test.js index c21204f4a8..81ad43e0a9 100644 --- a/test/models/patch/file-patch.test.js +++ b/test/models/patch/file-patch.test.js @@ -38,6 +38,7 @@ describe('FilePatch', function() { assert.strictEqual(filePatch.getByteSize(), 10); assert.strictEqual(filePatch.getBufferText(), bufferText); + assert.strictEqual(filePatch.getMaxLineNumberWidth(), 1); }); it('accesses a file path from either side of the patch', function() { diff --git a/test/models/patch/hunk.test.js b/test/models/patch/hunk.test.js index 77c21cc407..63610a8fb9 100644 --- a/test/models/patch/hunk.test.js +++ b/test/models/patch/hunk.test.js @@ -251,6 +251,26 @@ describe('Hunk', function() { assert.strictEqual(h1.changedLineCount(), 0); }); + it('determines the maximum number of digits necessary to represent a diff line number', function() { + const h0 = new Hunk({ + ...attrs, + oldStartRow: 200, + oldRowCount: 10, + newStartRow: 999, + newRowCount: 1, + }); + assert.strictEqual(h0.getMaxLineNumberWidth(), 4); + + const h1 = new Hunk({ + ...attrs, + oldStartRow: 5000, + oldRowCount: 10, + newStartRow: 20000, + newRowCount: 20, + }); + assert.strictEqual(h1.getMaxLineNumberWidth(), 5); + }); + describe('toStringIn()', function() { it('prints its header', function() { const h = new Hunk({ diff --git a/test/models/patch/patch.test.js b/test/models/patch/patch.test.js index d4b4746f0c..398bb95b01 100644 --- a/test/models/patch/patch.test.js +++ b/test/models/patch/patch.test.js @@ -51,6 +51,34 @@ describe('Patch', function() { assert.strictEqual(p.getChangedLineCount(), 10); }); + it('computes the maximum number of digits needed to display a diff line number', function() { + const hunks = [ + new Hunk({ + oldStartRow: 0, + oldRowCount: 1, + newStartRow: 0, + newRowCount: 1, + sectionHeading: 'zero', + rowRange: new IndexedRowRange({bufferRange: [[0, 0], [5, 0]], startOffset: 0, endOffset: 30}), + changes: [], + }), + new Hunk({ + oldStartRow: 98, + oldRowCount: 5, + newStartRow: 95, + newRowCount: 3, + sectionHeading: 'one', + rowRange: new IndexedRowRange({bufferRange: [[6, 0], [15, 0]], startOffset: 30, endOffset: 80}), + changes: [], + }), + ]; + const p0 = new Patch({status: 'modified', hunks, bufferText: 'bufferText'}); + assert.strictEqual(p0.getMaxLineNumberWidth(), 3); + + const p1 = new Patch({status: 'deleted', hunks: [], bufferText: ''}); + assert.strictEqual(p1.getMaxLineNumberWidth(), 0); + }); + it('clones itself with optionally overridden properties', function() { const original = new Patch({status: 'modified', hunks: [], bufferText: 'bufferText'}); From 8db5e339801e778b3a5004de2ef74bbc6d132338 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 20 Aug 2018 10:47:48 -0400 Subject: [PATCH 0180/4252] Remove unused IndexedRowRange::getStartRange() --- lib/models/indexed-row-range.js | 3 --- test/models/indexed-row-range.test.js | 9 --------- 2 files changed, 12 deletions(-) diff --git a/lib/models/indexed-row-range.js b/lib/models/indexed-row-range.js index 40bdd01ec8..c5fb21f0ca 100644 --- a/lib/models/indexed-row-range.js +++ b/lib/models/indexed-row-range.js @@ -14,9 +14,6 @@ export default class IndexedRowRange { return this.bufferRange.getRows(); } - getStartRange() { - const start = this.bufferRange.start; - return new Range(start, start); } bufferRowCount() { diff --git a/test/models/indexed-row-range.test.js b/test/models/indexed-row-range.test.js index 81999d2bd6..6aca39c5e7 100644 --- a/test/models/indexed-row-range.test.js +++ b/test/models/indexed-row-range.test.js @@ -34,15 +34,6 @@ describe('IndexedRowRange', function() { assert.isFalse(range.includesRow(5)); }); - it('creates a Range from its first line', function() { - const range = new IndexedRowRange({ - bufferRange: [[2, 0], [8, 0]], - startOffset: 0, - endOffset: 10, - }); - assert.deepEqual(range.getStartRange().serialize(), [[2, 0], [2, 0]]); - }); - it('extracts its offset range from buffer text with toStringIn()', function() { const buffer = '0000\n1111\n2222\n3333\n4444\n5555\n'; const range = new IndexedRowRange({ From 27e60ab7f8ccb912ecb7263f8d5ee31811809172 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 20 Aug 2018 10:48:09 -0400 Subject: [PATCH 0181/4252] IndexedRowRange accessor for the starting buffer row --- lib/models/indexed-row-range.js | 14 ++++++++++++-- test/models/indexed-row-range.test.js | 17 ++++++++++++++--- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/lib/models/indexed-row-range.js b/lib/models/indexed-row-range.js index c5fb21f0ca..aa080338a0 100644 --- a/lib/models/indexed-row-range.js +++ b/lib/models/indexed-row-range.js @@ -10,10 +10,12 @@ export default class IndexedRowRange { this.endOffset = endOffset; } - getBufferRows() { - return this.bufferRange.getRows(); + getStartBufferRow() { + return this.bufferRange.start.row; } + getBufferRows() { + return this.bufferRange.getRows(); } bufferRowCount() { @@ -112,6 +114,10 @@ export const nullIndexedRowRange = { endOffset: Infinity, + getStartBufferRow() { + return null; + }, + bufferRowCount() { return 0; }, @@ -136,6 +142,10 @@ export const nullIndexedRowRange = { return this; }, + serialize() { + return null; + }, + isPresent() { return false; }, diff --git a/test/models/indexed-row-range.test.js b/test/models/indexed-row-range.test.js index 6aca39c5e7..99730df4c9 100644 --- a/test/models/indexed-row-range.test.js +++ b/test/models/indexed-row-range.test.js @@ -11,6 +11,15 @@ describe('IndexedRowRange', function() { assert.deepEqual(range.bufferRowCount(), 2); }); + it('returns its starting buffer row', function() { + const range = new IndexedRowRange({ + bufferRange: [[2, 0], [8, 0]], + startOffset: 0, + endOffset: 10, + }); + assert.strictEqual(range.getStartBufferRow(), 2); + }); + it('returns an array of the covered rows', function() { const range = new IndexedRowRange({ bufferRange: [[2, 0], [8, 0]], @@ -186,11 +195,13 @@ describe('IndexedRowRange', function() { }); it('returns appropriate values from nullIndexedRowRange methods', function() { - assert.deepEqual(nullIndexedRowRange.intersectRowsIn(new Set([0, 1, 2]), ''), []); - assert.strictEqual(nullIndexedRowRange.toStringIn('', '+'), ''); - assert.strictEqual(nullIndexedRowRange.bufferRowCount(), 0); + assert.isNull(nullIndexedRowRange.getStartBufferRow()); assert.lengthOf(nullIndexedRowRange.getBufferRows(), 0); + assert.strictEqual(nullIndexedRowRange.bufferRowCount(), 0); assert.isFalse(nullIndexedRowRange.includesRow(4)); + assert.strictEqual(nullIndexedRowRange.toStringIn('', '+'), ''); + assert.deepEqual(nullIndexedRowRange.intersectRowsIn(new Set([0, 1, 2]), ''), []); + assert.isNull(nullIndexedRowRange.serialize()); assert.isFalse(nullIndexedRowRange.isPresent()); }); }); From fc64d1f6b30db14827ab2e642aef578fe411ea64 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 20 Aug 2018 10:48:31 -0400 Subject: [PATCH 0182/4252] Delegate some Region methods to its IndexedRowRange --- lib/models/patch/region.js | 12 ++++++++---- test/models/patch/region.test.js | 5 +++-- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/lib/models/patch/region.js b/lib/models/patch/region.js index 4535ae9a07..ce021b1185 100644 --- a/lib/models/patch/region.js +++ b/lib/models/patch/region.js @@ -7,6 +7,14 @@ class Region { return this.range; } + getStartBufferRow() { + return this.range.getStartBufferRow(); + } + + includesBufferRow(row) { + return this.range.includesRow(row); + } + isAddition() { return false; } @@ -27,10 +35,6 @@ class Region { return this.range.getBufferRows(); } - getStartRange() { - return this.range.getStartRange(); - } - bufferRowCount() { return this.range.bufferRowCount(); } diff --git a/test/models/patch/region.test.js b/test/models/patch/region.test.js index c7ca5dd6b6..5be6612b12 100644 --- a/test/models/patch/region.test.js +++ b/test/models/patch/region.test.js @@ -16,14 +16,15 @@ describe('Regions', function() { addition = new Addition(range); }); - it('has a range accessor', function() { + it('has range accessors', function() { assert.strictEqual(addition.getRowRange(), range); + assert.strictEqual(addition.getStartBufferRow(), 1); }); it('delegates some methods to its row range', function() { assert.sameMembers(Array.from(addition.getBufferRows()), [1, 2, 3]); - assert.deepEqual(addition.getStartRange().serialize(), [[1, 0], [1, 0]]); assert.strictEqual(addition.bufferRowCount(), 3); + assert.isTrue(addition.includesBufferRow(2)); }); it('can be recognized by the isAddition predicate', function() { From 940711c90e1e4ecfce1a6a858b24b2663ad57de3 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 20 Aug 2018 10:48:59 -0400 Subject: [PATCH 0183/4252] Skip regions when computing old and new diff line numbers --- lib/models/patch/hunk.js | 48 +++++++++++++++++++++++++++-- test/models/patch/hunk.test.js | 56 +++++++++++++++++++++++----------- 2 files changed, 85 insertions(+), 19 deletions(-) diff --git a/lib/models/patch/hunk.js b/lib/models/patch/hunk.js index 3236d7a9cd..3f577e5643 100644 --- a/lib/models/patch/hunk.js +++ b/lib/models/patch/hunk.js @@ -124,11 +124,55 @@ export default class Hunk { } getOldRowAt(row) { - return this.oldStartRow + (row - this.rowRange.bufferRange.start.row); + let current = this.oldStartRow; + + for (const region of this.getRegions()) { + if (region.range.includesRow(row)) { + const offset = row - region.getStartBufferRow(); + + return region.when({ + unchanged: () => current + offset, + addition: () => null, + deletion: () => current + offset, + nonewline: () => null, + }); + } else { + current += region.when({ + unchanged: () => region.bufferRowCount(), + addition: () => 0, + deletion: () => region.bufferRowCount(), + nonewline: () => 0, + }); + } + } + + return null; } getNewRowAt(row) { - return this.newStartRow + (row - this.rowRange.bufferRange.start.row); + let current = this.newStartRow; + + for (const region of this.getRegions()) { + if (region.includesBufferRow(row)) { + const offset = row - region.getStartBufferRow(); + + return region.when({ + unchanged: () => current + offset, + addition: () => current + offset, + deletion: () => null, + nonewline: () => null, + }); + } else { + current += region.when({ + unchanged: () => region.bufferRowCount(), + addition: () => region.bufferRowCount(), + deletion: () => 0, + nonewline: () => 0, + }); + } + } + + return null; } getMaxLineNumberWidth() { diff --git a/test/models/patch/hunk.test.js b/test/models/patch/hunk.test.js index 63610a8fb9..faf10cc104 100644 --- a/test/models/patch/hunk.test.js +++ b/test/models/patch/hunk.test.js @@ -197,39 +197,61 @@ describe('Hunk', function() { it('computes the old file row for a buffer row', function() { const h = new Hunk({ ...attrs, - rowRange: new IndexedRowRange({ - bufferRange: [[2, 0], [10, 0]], - startOffset: 10, - endOffset: 100, - }), oldStartRow: 10, - oldRowCount: 4, + oldRowCount: 6, newStartRow: 20, - newRowCount: 6, + newRowCount: 7, + rowRange: new IndexedRowRange({bufferRange: [[2, 0], [12, 0]], startOffset: 0, endOffset: 0}), + changes: [ + new Addition(new IndexedRowRange({bufferRange: [[3, 0], [5, 0]], startOffset: 0, endOffset: 0})), + new Deletion(new IndexedRowRange({bufferRange: [[7, 0], [9, 0]], startOffset: 0, endOffset: 0})), + new Addition(new IndexedRowRange({bufferRange: [[11, 0], [11, 0]], startOffset: 0, endOffset: 0})), + new NoNewline(new IndexedRowRange({bufferRange: [[12, 0], [12, 0]], startOffset: 0, endOffset: 0})), + ], }); assert.strictEqual(h.getOldRowAt(2), 10); - assert.strictEqual(h.getOldRowAt(3), 11); - assert.strictEqual(h.getOldRowAt(10), 18); + assert.isNull(h.getOldRowAt(3)); + assert.isNull(h.getOldRowAt(4)); + assert.isNull(h.getOldRowAt(5)); + assert.strictEqual(h.getOldRowAt(6), 11); + assert.strictEqual(h.getOldRowAt(7), 12); + assert.strictEqual(h.getOldRowAt(8), 13); + assert.strictEqual(h.getOldRowAt(9), 14); + assert.strictEqual(h.getOldRowAt(10), 15); + assert.isNull(h.getOldRowAt(11)); + assert.isNull(h.getOldRowAt(12)); + assert.isNull(h.getOldRowAt(13)); }); it('computes the new file row for a buffer row', function() { const h = new Hunk({ ...attrs, - rowRange: new IndexedRowRange({ - bufferRange: [[2, 0], [10, 0]], - startOffset: 10, - endOffset: 100, - }), oldStartRow: 10, - oldRowCount: 4, + oldRowCount: 6, newStartRow: 20, - newRowCount: 6, + newRowCount: 7, + rowRange: new IndexedRowRange({bufferRange: [[2, 0], [12, 0]], startOffset: 0, endOffset: 0}), + changes: [ + new Addition(new IndexedRowRange({bufferRange: [[3, 0], [5, 0]], startOffset: 0, endOffset: 0})), + new Deletion(new IndexedRowRange({bufferRange: [[7, 0], [9, 0]], startOffset: 0, endOffset: 0})), + new Addition(new IndexedRowRange({bufferRange: [[11, 0], [11, 0]], startOffset: 0, endOffset: 0})), + new NoNewline(new IndexedRowRange({bufferRange: [[12, 0], [12, 0]], startOffset: 0, endOffset: 0})), + ], }); assert.strictEqual(h.getNewRowAt(2), 20); assert.strictEqual(h.getNewRowAt(3), 21); - assert.strictEqual(h.getNewRowAt(10), 28); + assert.strictEqual(h.getNewRowAt(4), 22); + assert.strictEqual(h.getNewRowAt(5), 23); + assert.strictEqual(h.getNewRowAt(6), 24); + assert.isNull(h.getNewRowAt(7)); + assert.isNull(h.getNewRowAt(8)); + assert.isNull(h.getNewRowAt(9)); + assert.strictEqual(h.getNewRowAt(10), 25); + assert.strictEqual(h.getNewRowAt(11), 26); + assert.isNull(h.getNewRowAt(12)); + assert.isNull(h.getNewRowAt(13)); }); it('computes the total number of changed lines', function() { From 4133f2dda10a6d672d11b1641310559d82f06516 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 20 Aug 2018 12:27:09 -0400 Subject: [PATCH 0184/4252] getRepositoryForWorkdir is no longer required for RootController --- lib/controllers/root-controller.js | 7 +------ test/controllers/root-controller.test.js | 4 +--- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/lib/controllers/root-controller.js b/lib/controllers/root-controller.js index 5184328e4b..bd61553dfe 100644 --- a/lib/controllers/root-controller.js +++ b/lib/controllers/root-controller.js @@ -40,7 +40,6 @@ export default class RootController extends React.Component { project: PropTypes.object.isRequired, loginModel: PropTypes.object.isRequired, confirm: PropTypes.func.isRequired, - getRepositoryForWorkdir: PropTypes.func.isRequired, createRepositoryForProjectPath: PropTypes.func, cloneRepositoryForProjectPath: PropTypes.func, repository: PropTypes.object.isRequired, @@ -63,7 +62,7 @@ export default class RootController extends React.Component { super(props, context); autobind( this, - 'installReactDevTools', 'getRepositoryForWorkdir', 'clearGithubToken', 'initializeRepo', 'showOpenIssueishDialog', + 'installReactDevTools', 'clearGithubToken', 'initializeRepo', 'showOpenIssueishDialog', 'showWaterfallDiagnostics', 'showCacheDiagnostics', 'acceptClone', 'cancelClone', 'acceptInit', 'cancelInit', 'acceptOpenIssueish', 'cancelOpenIssueish', 'surfaceFromFileAtPath', 'destroyFilePatchPaneItems', 'destroyEmptyFilePatchPaneItems', 'openCloneDialog', 'quietlySelectItem', 'viewUnstagedChangesForCurrentFile', @@ -377,10 +376,6 @@ export default class RootController extends React.Component { devTools.default(devTools.REACT_DEVELOPER_TOOLS); } - getRepositoryForWorkdir(workdir) { - return this.props.getRepositoryForWorkdir(workdir); - } - componentWillUnmount() { this.subscription.dispose(); } diff --git a/test/controllers/root-controller.test.js b/test/controllers/root-controller.test.js index fd6d134dfa..59bf57c7b3 100644 --- a/test/controllers/root-controller.test.js +++ b/test/controllers/root-controller.test.js @@ -22,7 +22,7 @@ import RootController from '../../lib/controllers/root-controller'; describe('RootController', function() { let atomEnv, app; let workspace, commandRegistry, notificationManager, tooltips, config, confirm, deserializers, grammars, project; - let workdirContextPool, getRepositoryForWorkdir; + let workdirContextPool; beforeEach(function() { atomEnv = global.buildAtomEnvironment(); @@ -36,7 +36,6 @@ describe('RootController', function() { project = atomEnv.project; workdirContextPool = new WorkdirContextPool(); - getRepositoryForWorkdir = sinon.stub(); const loginModel = new GithubLoginModel(InMemoryStrategy); const absentRepository = Repository.absent(); @@ -60,7 +59,6 @@ describe('RootController', function() { startOpen={false} startRevealed={false} workdirContextPool={workdirContextPool} - getRepositoryForWorkdir={getRepositoryForWorkdir} /> ); }); From 71c22771677d48ef167e2e558346f74133c71c60 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 20 Aug 2018 12:27:43 -0400 Subject: [PATCH 0185/4252] Update RootController to use getBufferRows() instead of getLines() --- test/controllers/root-controller.test.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/test/controllers/root-controller.test.js b/test/controllers/root-controller.test.js index 59bf57c7b3..55ded4449e 100644 --- a/test/controllers/root-controller.test.js +++ b/test/controllers/root-controller.test.js @@ -503,7 +503,7 @@ describe('RootController', function() { sinon.stub(repository, 'applyPatchToWorkdir'); sinon.stub(notificationManager, 'addError'); // unmodified buffer - const hunkLines = unstagedFilePatch.getHunks()[0].getLines(); + const hunkLines = unstagedFilePatch.getHunks()[0].getBufferRows(); await wrapper.instance().discardLines(unstagedFilePatch, new Set([hunkLines[0]])); assert.isTrue(repository.applyPatchToWorkdir.calledOnce); assert.isFalse(notificationManager.addError.called); @@ -511,7 +511,7 @@ describe('RootController', function() { // modified buffer repository.applyPatchToWorkdir.reset(); editor.setText('modify contents'); - await wrapper.instance().discardLines(unstagedFilePatch, new Set(unstagedFilePatch.getHunks()[0].getLines())); + await wrapper.instance().discardLines(unstagedFilePatch, new Set(unstagedFilePatch.getHunks()[0].getBufferRows())); assert.isFalse(repository.applyPatchToWorkdir.called); const notificationArgs = notificationManager.addError.args[0]; assert.equal(notificationArgs[0], 'Cannot discard lines.'); @@ -573,14 +573,14 @@ describe('RootController', function() { it('reverses last discard for file path', async () => { const contents1 = fs.readFileSync(absFilePath, 'utf8'); - await wrapper.instance().discardLines(unstagedFilePatch, new Set(unstagedFilePatch.getHunks()[0].getLines().slice(0, 2))); + await wrapper.instance().discardLines(unstagedFilePatch, new Set(unstagedFilePatch.getHunks()[0].getBufferRows().slice(0, 2))); const contents2 = fs.readFileSync(absFilePath, 'utf8'); assert.notEqual(contents1, contents2); await repository.refresh(); unstagedFilePatch = await repository.getFilePatchForPath('sample.js'); wrapper.setState({filePatch: unstagedFilePatch}); - await wrapper.instance().discardLines(unstagedFilePatch, new Set(unstagedFilePatch.getHunks()[0].getLines().slice(2, 4))); + await wrapper.instance().discardLines(unstagedFilePatch, new Set(unstagedFilePatch.getHunks()[0].getBufferRows().slice(2, 4))); const contents3 = fs.readFileSync(absFilePath, 'utf8'); assert.notEqual(contents2, contents3); @@ -592,7 +592,7 @@ describe('RootController', function() { it('does not undo if buffer is modified', async () => { const contents1 = fs.readFileSync(absFilePath, 'utf8'); - await wrapper.instance().discardLines(unstagedFilePatch, new Set(unstagedFilePatch.getHunks()[0].getLines().slice(0, 2))); + await wrapper.instance().discardLines(unstagedFilePatch, new Set(unstagedFilePatch.getHunks()[0].getBufferRows().slice(0, 2))); const contents2 = fs.readFileSync(absFilePath, 'utf8'); assert.notEqual(contents1, contents2); @@ -616,7 +616,7 @@ describe('RootController', function() { describe('when file content has changed since last discard', () => { it('successfully undoes discard if changes do not conflict', async () => { const contents1 = fs.readFileSync(absFilePath, 'utf8'); - await wrapper.instance().discardLines(unstagedFilePatch, new Set(unstagedFilePatch.getHunks()[0].getLines().slice(0, 2))); + await wrapper.instance().discardLines(unstagedFilePatch, new Set(unstagedFilePatch.getHunks()[0].getBufferRows().slice(0, 2))); const contents2 = fs.readFileSync(absFilePath, 'utf8'); assert.notEqual(contents1, contents2); @@ -636,7 +636,7 @@ describe('RootController', function() { await repository.git.exec(['config', 'merge.conflictstyle', 'diff3']); const contents1 = fs.readFileSync(absFilePath, 'utf8'); - await wrapper.instance().discardLines(unstagedFilePatch, new Set(unstagedFilePatch.getHunks()[0].getLines().slice(0, 2))); + await wrapper.instance().discardLines(unstagedFilePatch, new Set(unstagedFilePatch.getHunks()[0].getBufferRows().slice(0, 2))); const contents2 = fs.readFileSync(absFilePath, 'utf8'); assert.notEqual(contents1, contents2); @@ -699,11 +699,11 @@ describe('RootController', function() { it('clears the discard history if the last blob is no longer valid', async () => { // this would occur in the case of garbage collection cleaning out the blob - await wrapper.instance().discardLines(unstagedFilePatch, new Set(unstagedFilePatch.getHunks()[0].getLines().slice(0, 2))); + await wrapper.instance().discardLines(unstagedFilePatch, new Set(unstagedFilePatch.getHunks()[0].getBufferRows().slice(0, 2))); await repository.refresh(); unstagedFilePatch = await repository.getFilePatchForPath('sample.js'); wrapper.setState({filePatch: unstagedFilePatch}); - const {beforeSha} = await wrapper.instance().discardLines(unstagedFilePatch, new Set(unstagedFilePatch.getHunks()[0].getLines().slice(2, 4))); + const {beforeSha} = await wrapper.instance().discardLines(unstagedFilePatch, new Set(unstagedFilePatch.getHunks()[0].getBufferRows().slice(2, 4))); // remove blob from git object store fs.unlinkSync(path.join(repository.getGitDirectoryPath(), 'objects', beforeSha.slice(0, 2), beforeSha.slice(2))); From 47542e5a95110dd3f04d904c467f70df9c2f6161 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 20 Aug 2018 12:38:20 -0400 Subject: [PATCH 0186/4252] Pass additional props to render the FilePatchItem --- test/items/file-patch-item.test.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/items/file-patch-item.test.js b/test/items/file-patch-item.test.js index d588233824..57960ca33b 100644 --- a/test/items/file-patch-item.test.js +++ b/test/items/file-patch-item.test.js @@ -29,7 +29,10 @@ describe('FilePatchItem', function() { function buildPaneApp(overrideProps = {}) { const props = { workdirContextPool: pool, + workspace: atomEnv.workspace, tooltips: atomEnv.tooltips, + discardLines: () => {}, + undoLastDiscard: () => {}, ...overrideProps, }; From 33cc8c17242979412bc566c59e5c124a1272c8b5 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 20 Aug 2018 16:04:19 -0400 Subject: [PATCH 0187/4252] Pass additional required props in FilePatchContainer tests --- test/containers/file-patch-container.test.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/containers/file-patch-container.test.js b/test/containers/file-patch-container.test.js index 02fdd93eed..0fcaee7717 100644 --- a/test/containers/file-patch-container.test.js +++ b/test/containers/file-patch-container.test.js @@ -34,6 +34,11 @@ describe('FilePatchContainer', function() { repository, stagingStatus: 'unstaged', relPath: 'a.txt', + workspace: atomEnv.workspace, + tooltips: atomEnv.tooltips, + discardLines: () => {}, + undoLastDiscard: () => {}, + destroy: () => {}, ...overrideProps, }; @@ -45,11 +50,6 @@ describe('FilePatchContainer', function() { assert.isTrue(wrapper.find('LoadingView').exists()); }); - it('renders a message for an empty patch', async function() { - const wrapper = mount(buildApp({relPath: 'c.txt'})); - await assert.async.isTrue(wrapper.update().find('span.icon-info').exists()); - }); - it('renders a FilePatchView', async function() { const wrapper = mount(buildApp({relPath: 'a.txt', stagingStatus: 'unstaged'})); await assert.async.isTrue(wrapper.update().find('FilePatchView').exists()); From c6e982c2066e1abb12ca2a56dc8a2c3662d6d1df Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 21 Aug 2018 09:55:20 -0400 Subject: [PATCH 0188/4252] Revamp FilePatchSelection to understand line selections by buffer row --- lib/models/file-patch-selection.js | 91 +- test/models/file-patch-selection.test.js | 778 ++++++++++++++ .../file-patch-selection.test.pending.js | 965 ------------------ 3 files changed, 844 insertions(+), 990 deletions(-) create mode 100644 test/models/file-patch-selection.test.js delete mode 100644 test/models/file-patch-selection.test.pending.js diff --git a/lib/models/file-patch-selection.js b/lib/models/file-patch-selection.js index 091bd27fd8..d84110b860 100644 --- a/lib/models/file-patch-selection.js +++ b/lib/models/file-patch-selection.js @@ -9,19 +9,23 @@ export default class FilePatchSelection { this.mode = 'hunk'; this.hunksByLine = new Map(); + this.changedLines = new Set(); + const lines = []; for (const hunk of hunks) { - for (const line of hunk.lines) { - lines.push(line); - this.hunksByLine.set(line, hunk); + for (const region of hunk.getRegions()) { + for (const line of region.getBufferRows()) { + lines.push(line); + this.hunksByLine.set(line, hunk); + if (region.isChange()) { + this.changedLines.add(line); + } + } } } this.hunksSelection = new ListSelection({items: hunks}); - this.linesSelection = new ListSelection({ - items: lines, - isItemSelectable: line => line.isChanged(), - }); + this.linesSelection = new ListSelection({items: lines, isItemSelectable: line => this.changedLines.has(line)}); this.resolveNextUpdatePromise = () => {}; } else { // Copy from options. *Only* reachable from the copy() method because no other module has visibility to @@ -33,6 +37,7 @@ export default class FilePatchSelection { this.linesSelection = options.linesSelection; this.resolveNextUpdatePromise = options.resolveNextUpdatePromise; this.hunksByLine = options.hunksByLine; + this.changedLines = options.changedLines; } } @@ -42,6 +47,7 @@ export default class FilePatchSelection { let linesSelection = options.linesSelection || this.linesSelection; let hunksByLine = null; + let changedLines = null; if (options.hunks) { // Update hunks const oldHunks = this.hunksSelection.getItems(); @@ -66,10 +72,16 @@ export default class FilePatchSelection { const newLines = []; hunksByLine = new Map(); + changedLines = new Set(); for (const hunk of newHunks) { - for (const line of hunk.lines) { - newLines.push(line); - hunksByLine.set(line, hunk); + for (const region of hunk.getRegions()) { + for (const line of region.getBufferRows()) { + newLines.push(line); + hunksByLine.set(line, hunk); + if (region.isChange()) { + changedLines.add(line); + } + } } } @@ -79,14 +91,18 @@ export default class FilePatchSelection { const oldSelectionStartIndex = this.linesSelection.getMostRecentSelectionStartIndex(); let changedLineCount = 0; for (let i = 0; i < oldSelectionStartIndex; i++) { - if (oldLines[i].isChanged()) { changedLineCount++; } + if (this.changedLines.has(oldLines[i])) { + changedLineCount++; + } } for (let i = 0; i < newLines.length; i++) { const line = newLines[i]; - if (line.isChanged()) { + if (changedLines.has(line)) { newSelectedLine = line; - if (changedLineCount === 0) { break; } + if (changedLineCount === 0) { + break; + } changedLineCount--; } } @@ -100,8 +116,9 @@ export default class FilePatchSelection { this.resolveNextUpdatePromise(); } } else { - // Hunks are unchanged. Don't recompute hunksByLine. + // Hunks are unchanged. Don't recompute hunksByLine or changedLines. hunksByLine = this.hunksByLine; + changedLines = this.changedLines; } return new FilePatchSelection({ @@ -110,15 +127,16 @@ export default class FilePatchSelection { hunksSelection, linesSelection, hunksByLine, + changedLines, resolveNextUpdatePromise: options.resolveNextUpdatePromise || this.resolveNextUpdatePromise, }); } toggleMode() { if (this.mode === 'hunk') { - const firstLineOfSelectedHunk = this.getHeadHunk().lines[0]; + const firstLineOfSelectedHunk = this.getHeadHunk().getBufferRange().start.row; const selection = this.selectLine(firstLineOfSelectedHunk); - if (!firstLineOfSelectedHunk.isChanged()) { + if (!this.changedLines.has(firstLineOfSelectedHunk)) { return selection.selectNextLine(); } else { return selection; @@ -236,8 +254,9 @@ export default class FilePatchSelection { getSelectedHunks() { if (this.mode === 'line') { const selectedHunks = new Set(); - const selectedLines = this.getSelectedLines(); - selectedLines.forEach(line => selectedHunks.add(this.hunksByLine.get(line))); + for (const line of this.getSelectedLines()) { + selectedHunks.add(this.hunksByLine.get(line)); + } return selectedHunks; } else { return this.hunksSelection.getSelectedItems(); @@ -304,11 +323,13 @@ export default class FilePatchSelection { getSelectedLines() { if (this.mode === 'hunk') { const selectedLines = new Set(); - this.getSelectedHunks().forEach(hunk => { - for (const line of hunk.lines) { - if (line.isChanged()) { selectedLines.add(line); } + for (const hunk of this.getSelectedHunks()) { + for (const change of hunk.getChanges()) { + for (const line of change.getBufferRows()) { + selectedLines.add(line); + } } - }); + } return selectedLines; } else { return this.linesSelection.getSelectedItems(); @@ -341,6 +362,7 @@ export default class FilePatchSelection { } goToDiffLine(lineNumber) { + // console.log(`<<< finding closest line to ${lineNumber}`); const lines = this.linesSelection.getItems(); let closestLine; @@ -348,20 +370,39 @@ export default class FilePatchSelection { for (let i = 0; i < lines.length; i++) { const line = lines[i]; - if (!this.linesSelection.isItemSelectable(line)) { continue; } - if (line.newLineNumber === lineNumber) { + // console.log(`considering line = ${line}`); + if (!this.linesSelection.isItemSelectable(line)) { + // console.log('... not selectable'); + continue; + } + + const hunk = this.hunksByLine.get(line); + const newLineNumber = hunk.getNewRowAt(line); + if (newLineNumber === null) { + // console.log('... deleted line'); + continue; + } + + // console.log(` new line number = ${newLineNumber}`); + + if (newLineNumber === lineNumber) { + // console.log('>>> exact match'); return this.selectLine(line); } else { - const newDistance = Math.abs(line.newLineNumber - lineNumber); + const newDistance = Math.abs(newLineNumber - lineNumber); + // console.log(` distance = ${newDistance} vs. closest = ${closestLineDistance}`); if (newDistance < closestLineDistance) { closestLineDistance = newDistance; closestLine = line; + // console.log(` new closest line = ${closestLine}`); } else { + // console.log(`>>> increasing distance. choosing previous closest line = ${closestLine}`); return this.selectLine(closestLine); } } } + // console.log(`>>> choosing closest line = ${closestLine}`); return this.selectLine(closestLine); } } diff --git a/test/models/file-patch-selection.test.js b/test/models/file-patch-selection.test.js new file mode 100644 index 0000000000..cb0c6679d2 --- /dev/null +++ b/test/models/file-patch-selection.test.js @@ -0,0 +1,778 @@ +import FilePatchSelection from '../../lib/models/file-patch-selection'; +import buildFilePatch from '../../lib/models/patch/builder'; +import IndexedRowRange from '../../lib/models/indexed-row-range'; +import Hunk from '../../lib/models/patch/hunk'; +import {Addition, Deletion} from '../../lib/models/patch/region'; + +describe('FilePatchSelection', function() { + describe('line selection', function() { + it('starts a new line selection with selectLine and updates an existing selection when preserveTail is true', function() { + const patch = buildPatchFixture(); + + const selection0 = new FilePatchSelection(patch.getHunks()); + + const selection1 = selection0.selectLine(2); + assert.sameMembers(Array.from(selection1.getSelectedLines()), [2]); + + const selection2 = selection1.selectLine(7, true); + assert.sameMembers(Array.from(selection2.getSelectedLines()), [2, 3, 6, 7]); + + const selection3 = selection2.selectLine(6, true); + assert.sameMembers(Array.from(selection3.getSelectedLines()), [2, 3, 6]); + + const selection4 = selection3.selectLine(1, true); + assert.sameMembers(Array.from(selection4.getSelectedLines()), [1, 2]); + + const selection5 = selection4.selectLine(7); + assert.sameMembers(Array.from(selection5.getSelectedLines()), [7]); + }); + + it('adds a new line selection when calling addOrSubtractLineSelection with an unselected line and always updates the head of the most recent line selection', function() { + const patch = buildPatchFixture(); + + const selection0 = new FilePatchSelection(patch.getHunks()) + .selectLine(2) + .selectLine(3, true) + .addOrSubtractLineSelection(7) + .selectLine(8, true); + + assert.sameMembers(Array.from(selection0.getSelectedLines()), [2, 3, 7, 8]); + + const selection1 = selection0.selectLine(1, true); + assert.sameMembers(Array.from(selection1.getSelectedLines()), [1, 2, 3, 6, 7]); + }); + + it('subtracts from existing selections when calling addOrSubtractLineSelection with a selected line', function() { + const patch = buildPatchFixture(); + + const selection0 = new FilePatchSelection(patch.getHunks()) + .selectLine(2) + .selectLine(7, true); + + assert.sameMembers(Array.from(selection0.getSelectedLines()), [2, 3, 6, 7]); + + const selection1 = selection0.addOrSubtractLineSelection(6); + assert.sameMembers(Array.from(selection1.getSelectedLines()), [2, 3, 7]); + + const selection2 = selection1.selectLine(8, true); + assert.sameMembers(Array.from(selection2.getSelectedLines()), [2, 3]); + + const selection3 = selection2.selectLine(2, true); + assert.sameMembers(Array.from(selection3.getSelectedLines()), [7]); + }); + + it('allows the next or previous line to be selected', function() { + const patch = buildPatchFixture(); + + const selection0 = new FilePatchSelection(patch.getHunks()) + .selectLine(1) + .selectNextLine(); + assert.sameMembers(Array.from(selection0.getSelectedLines()), [2]); + + const selection1 = selection0.selectNextLine(); + assert.sameMembers(Array.from(selection1.getSelectedLines()), [3]); + + const selection2 = selection1.selectNextLine(); + assert.sameMembers(Array.from(selection2.getSelectedLines()), [6]); + + const selection3 = selection2.selectPreviousLine(); + assert.sameMembers(Array.from(selection3.getSelectedLines()), [3]); + + const selection4 = selection3.selectPreviousLine(); + assert.sameMembers(Array.from(selection4.getSelectedLines()), [2]); + + const selection5 = selection4.selectPreviousLine(); + assert.sameMembers(Array.from(selection5.getSelectedLines()), [1]); + + const selection6 = selection5.selectNextLine(true); + assert.sameMembers(Array.from(selection6.getSelectedLines()), [1, 2]); + + const selection7 = selection6.selectNextLine().selectNextLine().selectPreviousLine(true); + assert.sameMembers(Array.from(selection7.getSelectedLines()), [3, 6]); + }); + + it('allows the first/last changed line to be selected', function() { + const patch = buildPatchFixture(); + + const selection0 = new FilePatchSelection(patch.getHunks()).selectLastLine(); + assert.sameMembers(Array.from(selection0.getSelectedLines()), [8]); + + const selection1 = selection0.selectFirstLine(); + assert.sameMembers(Array.from(selection1.getSelectedLines()), [1]); + + const selection2 = selection1.selectLastLine(true); + assert.sameMembers(Array.from(selection2.getSelectedLines()), [1, 2, 3, 6, 7, 8]); + + const selection3 = selection2.selectLastLine(); + assert.sameMembers(Array.from(selection3.getSelectedLines()), [8]); + + const selection4 = selection3.selectFirstLine(true); + assert.sameMembers(Array.from(selection4.getSelectedLines()), [1, 2, 3, 6, 7, 8]); + + const selection5 = selection4.selectFirstLine(); + assert.sameMembers(Array.from(selection5.getSelectedLines()), [1]); + }); + + it('allows all lines to be selected', function() { + const patch = buildPatchFixture(); + + const selection0 = new FilePatchSelection(patch.getHunks()).selectAllLines(); + assert.sameMembers(Array.from(selection0.getSelectedLines()), [1, 2, 3, 6, 7, 8]); + }); + + it('defaults to the first/last changed line when selecting next / previous with no current selection', function() { + const patch = buildPatchFixture(); + + const selection0 = new FilePatchSelection(patch.getHunks()) + .selectLine(1) + .addOrSubtractLineSelection(1) + .coalesce(); + assert.sameMembers(Array.from(selection0.getSelectedLines()), []); + + const selection1 = selection0.selectNextLine(); + assert.sameMembers(Array.from(selection1.getSelectedLines()), [1]); + + const selection2 = selection1.addOrSubtractLineSelection(1).coalesce(); + assert.sameMembers(Array.from(selection2.getSelectedLines()), []); + + const selection3 = selection2.selectPreviousLine(); + assert.sameMembers(Array.from(selection3.getSelectedLines()), [8]); + }); + + it('collapses multiple selections down to one line when selecting next or previous', function() { + const patch = buildPatchFixture(); + + const selection0 = new FilePatchSelection(patch.getHunks()) + .selectLine(1) + .addOrSubtractLineSelection(2) + .selectNextLine(true); + assert.sameMembers(Array.from(selection0.getSelectedLines()), [1, 2, 3]); + + const selection1 = selection0.selectNextLine(); + assert.sameMembers(Array.from(selection1.getSelectedLines()), [6]); + + const selection2 = selection1.selectLine(1) + .addOrSubtractLineSelection(2) + .selectPreviousLine(true); + assert.sameMembers(Array.from(selection2.getSelectedLines()), [1, 2]); + + const selection3 = selection2.selectPreviousLine(); + assert.sameMembers(Array.from(selection3.getSelectedLines()), [1]); + }); + + describe('coalescing', function() { + it('merges overlapping selections', function() { + const patch = buildAllAddedPatchFixture(); + + const selection0 = new FilePatchSelection(patch.getHunks()) + .selectLine(3) + .selectLine(5, true) + .addOrSubtractLineSelection(0) + .selectLine(4, true) + .coalesce() + .selectPreviousLine(true); + assert.sameMembers(Array.from(selection0.getSelectedLines()), [0, 1, 2, 3, 4]); + + const selection1 = selection0.addOrSubtractLineSelection(7) + .selectLine(3, true) + .coalesce() + .selectNextLine(true); + assert.sameMembers(Array.from(selection1.getSelectedLines()), [1, 2, 3, 4, 5, 6, 7]); + }); + + it('merges adjacent selections', function() { + const patch = buildAllAddedPatchFixture(); + + const selection0 = new FilePatchSelection(patch.getHunks()) + .selectLine(3) + .selectLine(5, true) + .addOrSubtractLineSelection(1) + .selectLine(2, true) + .coalesce() + .selectPreviousLine(true); + assert.sameMembers(Array.from(selection0.getSelectedLines()), [1, 2, 3, 4]); + + const selection1 = selection0.addOrSubtractLineSelection(6) + .selectLine(5, true) + .coalesce() + .selectNextLine(true); + assert.sameMembers(Array.from(selection1.getSelectedLines()), [2, 3, 4, 5, 6]); + }); + + it('expands selections to contain all adjacent context lines', function() { + const patch = buildPatchFixture(); + + const selection0 = new FilePatchSelection(patch.getHunks()) + .selectLine(7) + .selectLine(6, true) + .addOrSubtractLineSelection(2) + .selectLine(1, true) + .coalesce() + .selectNext(true); + + assert.sameMembers(Array.from(selection0.getSelectedLines()), [2, 6, 7]); + }); + + it('truncates or splits selections where they overlap a negative selection', function() { + const patch = buildAllAddedPatchFixture(); + + const selection0 = new FilePatchSelection(patch.getHunks()) + .selectLine(0) + .selectLine(7, true) + .addOrSubtractLineSelection(3) + .selectLine(4, true) + .coalesce() + .selectPrevious(true); + assert.sameMembers(Array.from(selection0.getSelectedLines()), [0, 1, 2, 5, 6]); + }); + + it('does not blow up when coalescing with no selections', function() { + const patch = buildAllAddedPatchFixture(); + + const selection0 = new FilePatchSelection(patch.getHunks()) + .selectLine(0) + .addOrSubtractLineSelection(0); + assert.lengthOf(Array.from(selection0.getSelectedLines()), 0); + + const selection1 = selection0.coalesce(); + assert.lengthOf(Array.from(selection1.getSelectedLines()), 0); + }); + }); + }); + + describe('hunk selection', function() { + it('selects the first hunk by default', function() { + const hunks = buildPatchFixture().getHunks(); + const selection0 = new FilePatchSelection(hunks); + assert.sameMembers(Array.from(selection0.getSelectedHunks()), [hunks[0]]); + }); + + it('starts a new hunk selection with selectHunk and updates an existing selection when preserveTail is true', function() { + const hunks = buildFourHunksPatchFixture().getHunks(); + const selection0 = new FilePatchSelection(hunks) + .selectHunk(hunks[1]); + assert.sameMembers(Array.from(selection0.getSelectedHunks()), [hunks[1]]); + + const selection1 = selection0.selectHunk(hunks[3], true); + assert.sameMembers(Array.from(selection1.getSelectedHunks()), [hunks[1], hunks[2], hunks[3]]); + + const selection2 = selection1.selectHunk(hunks[0], true); + assert.sameMembers(Array.from(selection2.getSelectedHunks()), [hunks[0], hunks[1]]); + }); + + it('adds a new hunk selection with addOrSubtractHunkSelection and always updates the head of the most recent hunk selection', function() { + const hunks = buildFourHunksPatchFixture().getHunks(); + const selection0 = new FilePatchSelection(hunks) + .addOrSubtractHunkSelection(hunks[2]); + assert.sameMembers(Array.from(selection0.getSelectedHunks()), [hunks[0], hunks[2]]); + + const selection1 = selection0.selectHunk(hunks[3], true); + assert.sameMembers(Array.from(selection1.getSelectedHunks()), [hunks[0], hunks[2], hunks[3]]); + + const selection2 = selection1.selectHunk(hunks[1], true); + assert.sameMembers(Array.from(selection2.getSelectedHunks()), [hunks[0], hunks[1], hunks[2]]); + }); + + it('allows the next or previous hunk to be selected', function() { + const hunks = buildFourHunksPatchFixture().getHunks(); + const selection0 = new FilePatchSelection(hunks) + .selectNextHunk(); + assert.sameMembers(Array.from(selection0.getSelectedHunks()), [hunks[1]]); + + const selection1 = selection0.selectNextHunk(); + assert.sameMembers(Array.from(selection1.getSelectedHunks()), [hunks[2]]); + + const selection2 = selection1.selectNextHunk() + .selectNextHunk(); + assert.sameMembers(Array.from(selection2.getSelectedHunks()), [hunks[3]]); + + const selection3 = selection2.selectPreviousHunk(); + assert.sameMembers(Array.from(selection3.getSelectedHunks()), [hunks[2]]); + + const selection4 = selection3.selectPreviousHunk(); + assert.sameMembers(Array.from(selection4.getSelectedHunks()), [hunks[1]]); + + const selection5 = selection4.selectPreviousHunk() + .selectPreviousHunk(); + assert.sameMembers(Array.from(selection5.getSelectedHunks()), [hunks[0]]); + + const selection6 = selection5.selectNextHunk() + .selectNextHunk(true); + assert.sameMembers(Array.from(selection6.getSelectedHunks()), [hunks[1], hunks[2]]); + + const selection7 = selection6.selectPreviousHunk(true); + assert.sameMembers(Array.from(selection7.getSelectedHunks()), [hunks[1]]); + + const selection8 = selection7.selectPreviousHunk(true); + assert.sameMembers(Array.from(selection8.getSelectedHunks()), [hunks[0], hunks[1]]); + }); + + it('allows all hunks to be selected', function() { + const hunks = buildFourHunksPatchFixture().getHunks(); + + const selection0 = new FilePatchSelection(hunks) + .selectAllHunks(); + assert.sameMembers(Array.from(selection0.getSelectedHunks()), [hunks[0], hunks[1], hunks[2], hunks[3]]); + }); + }); + + describe('selection modes', function() { + it('allows the selection mode to be toggled between hunks and lines', function() { + const hunks = buildPatchFixture().getHunks(); + const selection0 = new FilePatchSelection(hunks); + + assert.strictEqual(selection0.getMode(), 'hunk'); + assert.sameMembers(Array.from(selection0.getSelectedHunks()), [hunks[0]]); + assert.sameMembers(Array.from(selection0.getSelectedLines()), [1, 2, 3]); + + const selection1 = selection0.selectNext(); + assert.strictEqual(selection1.getMode(), 'hunk'); + assert.sameMembers(Array.from(selection1.getSelectedHunks()), [hunks[1]]); + assert.sameMembers(Array.from(selection1.getSelectedLines()), [6, 7, 8]); + + const selection2 = selection1.toggleMode(); + assert.strictEqual(selection2.getMode(), 'line'); + assert.sameMembers(Array.from(selection2.getSelectedHunks()), [hunks[1]]); + assert.sameMembers(Array.from(selection2.getSelectedLines()), [6]); + + const selection3 = selection2.selectNext(); + assert.sameMembers(Array.from(selection3.getSelectedHunks()), [hunks[1]]); + assert.sameMembers(Array.from(selection3.getSelectedLines()), [7]); + + const selection4 = selection3.toggleMode(); + assert.strictEqual(selection4.getMode(), 'hunk'); + assert.sameMembers(Array.from(selection4.getSelectedHunks()), [hunks[1]]); + assert.sameMembers(Array.from(selection4.getSelectedLines()), [6, 7, 8]); + + const selection5 = selection4.selectLine(1); + assert.strictEqual(selection5.getMode(), 'line'); + assert.sameMembers(Array.from(selection5.getSelectedHunks()), [hunks[0]]); + assert.sameMembers(Array.from(selection5.getSelectedLines()), [1]); + + const selection6 = selection5.selectHunk(hunks[1]); + assert.strictEqual(selection6.getMode(), 'hunk'); + assert.sameMembers(Array.from(selection6.getSelectedHunks()), [hunks[1]]); + assert.sameMembers(Array.from(selection6.getSelectedLines()), [6, 7, 8]); + }); + }); + + describe('updateHunks(hunks)', function() { + it('collapses the line selection to a single line following the previous selected range with the highest start index', function() { + const oldHunks = [ + new Hunk({ + oldStartRow: 1, oldRowCount: 1, newStartRow: 1, newRowCount: 3, + rowRange: new IndexedRowRange({bufferRange: [[0, 0], [2, 0]], startOffset: 0, endOffset: 0}), + changes: [ + new Addition(new IndexedRowRange({bufferRange: [[0, 0], [1, 0]], startOffset: 0, endOffset: 0})), + ], + }), + new Hunk({ + oldStartRow: 5, oldRowCount: 7, newStartRow: 5, newRowCount: 4, + rowRange: new IndexedRowRange({bufferRange: [[3, 0], [10, 0]], startOffset: 0, endOffset: 0}), + changes: [ + new Deletion(new IndexedRowRange({bufferRange: [[4, 0], [5, 0]], startOffset: 0, endOffset: 0})), + new Addition(new IndexedRowRange({bufferRange: [[6, 0], [8, 0]], startOffset: 0, endOffset: 0})), + new Deletion(new IndexedRowRange({bufferRange: [[9, 0], [10, 0]], startOffset: 0, endOffset: 0})), + ], + }), + ]; + + const selection0 = new FilePatchSelection(oldHunks) + .selectLine(5) + .selectLine(7, true); + + const newHunks = [ + new Hunk({ + oldStartRow: 1, oldRowCount: 1, newStartRow: 1, newRowCount: 3, + rowRange: new IndexedRowRange({bufferRange: [[0, 0], [2, 0]], startOffset: 0, endOffset: 0}), + changes: [ + new Addition(new IndexedRowRange({bufferRange: [[0, 0], [1, 0]], startOffset: 0, endOffset: 0})), + ], + }), + new Hunk({ + oldStartRow: 5, oldRowCount: 7, newStartRow: 3, newRowCount: 2, + rowRange: new IndexedRowRange({bufferRange: [[3, 0], [5, 0]], startOffset: 0, endOffset: 0}), + changes: [ + new Deletion(new IndexedRowRange({bufferRange: [[4, 0], [4, 0]], startOffset: 0, endOffset: 0})), + ], + }), + new Hunk({ + oldStartRow: 9, oldRowCount: 10, newStartRow: 3, newRowCount: 2, + rowRange: new IndexedRowRange({bufferRange: [[6, 0], [9, 0]], startOffset: 0, endOffset: 0}), + changes: [ + new Addition(new IndexedRowRange({bufferRange: [[7, 0], [7, 0]], startOffset: 0, endOffset: 0})), + new Deletion(new IndexedRowRange({bufferRange: [[8, 0], [9, 0]], startOffset: 0, endOffset: 0})), + ], + }), + ]; + const selection1 = selection0.updateHunks(newHunks); + + assert.sameMembers(Array.from(selection1.getSelectedLines()), [7]); + }); + + it('collapses the line selection to the line preceding the previous selected line if it was the *last* line', function() { + const oldHunks = [ + new Hunk({ + oldStartRow: 1, oldRowCount: 1, newStartRow: 1, newRowCount: 4, + rowRange: new IndexedRowRange({bufferRange: [[0, 0], [3, 0]], startOffset: 0, endOffset: 0}), + changes: [ + new Addition(new IndexedRowRange({bufferRange: [[0, 0], [2, 0]], startOffset: 0, endOffset: 0})), + ], + }), + ]; + + const selection0 = new FilePatchSelection(oldHunks) + .selectLine(2); + + const newHunks = [ + new Hunk({ + oldStartRow: 1, oldRowCount: 1, newStartRow: 1, newRowCount: 4, + rowRange: new IndexedRowRange({bufferRange: [[0, 0], [3, 0]], startOffset: 0, endOffset: 0}), + changes: [ + new Addition(new IndexedRowRange({bufferRange: [[0, 0], [1, 0]], startOffset: 0, endOffset: 0})), + ], + }), + ]; + const selection1 = selection0.updateHunks(newHunks); + assert.sameMembers(Array.from(selection1.getSelectedLines()), [1]); + }); + + it('updates the hunk selection if it exceeds the new length of the hunks list', function() { + const oldHunks = [ + new Hunk({ + oldStartRow: 1, oldRowCount: 0, newStartRow: 1, newRowCount: 1, + rowRange: new IndexedRowRange({bufferRange: [[0, 0], [0, 0]], startOffset: 0, endOffset: 0}), + changes: [ + new Addition(new IndexedRowRange({bufferRange: [[0, 0], [0, 0]], startOffset: 0, endOffset: 0})), + ], + }), + new Hunk({ + oldStartRow: 5, oldRowCount: 0, newStartRow: 6, newRowCount: 1, + rowRange: new IndexedRowRange({bufferRange: [[1, 0], [1, 0]], startOffset: 0, endOffset: 0}), + changes: [ + new Addition(new IndexedRowRange({bufferRange: [[1, 0], [1, 0]], startOffset: 0, endOffset: 0})), + ], + }), + ]; + const selection0 = new FilePatchSelection(oldHunks) + .selectHunk(oldHunks[1]); + + const newHunks = [ + new Hunk({ + oldStartRow: 1, oldRowCount: 0, newStartRow: 1, newRowCount: 1, + rowRange: new IndexedRowRange({bufferRange: [[0, 0], [0, 0]], startOffset: 0, endOffset: 0}), + changes: [ + new Addition(new IndexedRowRange({bufferRange: [[0, 0], [0, 0]], startOffset: 0, endOffset: 0})), + ], + }), + ]; + const selection1 = selection0.updateHunks(newHunks); + assert.sameMembers(Array.from(selection1.getSelectedHunks()), [newHunks[0]]); + }); + + it('deselects if updating with an empty hunk array', function() { + const oldHunks = [ + new Hunk({ + oldStartRow: 1, oldRowCount: 1, newStartRow: 1, newRowCount: 3, + rowRange: new IndexedRowRange({bufferRange: [[0, 0], [1, 0]], startOffset: 0, endOffset: 0}), + changes: [ + new Addition(new IndexedRowRange({bufferRange: [[0, 0], [1, 0]], startOffset: 0, endOffset: 0})), + ], + }), + ]; + + const selection0 = new FilePatchSelection(oldHunks) + .selectLine(1) + .updateHunks([]); + assert.lengthOf(Array.from(selection0.getSelectedLines()), []); + }); + + it('resolves the getNextUpdatePromise the next time hunks are changed', async function() { + const hunk0 = new Hunk({ + oldStartRow: 1, oldRowCount: 0, newStartRow: 1, newRowCount: 1, + rowRange: new IndexedRowRange({bufferRange: [[0, 0], [0, 0]], startOffset: 0, endOffset: 0}), + changes: [ + new Addition(new IndexedRowRange({bufferRange: [[0, 0], [0, 0]], startOffset: 0, endOffset: 0})), + ], + }); + const hunk1 = new Hunk({ + oldStartRow: 5, oldRowCount: 0, newStartRow: 6, newRowCount: 1, + rowRange: new IndexedRowRange({bufferRange: [[1, 0], [1, 0]], startOffset: 0, endOffset: 0}), + changes: [ + new Addition(new IndexedRowRange({bufferRange: [[1, 0], [1, 0]], startOffset: 0, endOffset: 0})), + ], + }); + const existingHunks = [hunk0, hunk1]; + const selection0 = new FilePatchSelection(existingHunks); + + let wasResolved = false; + const promise = selection0.getNextUpdatePromise().then(() => { wasResolved = true; }); + + const unchangedHunks = [hunk0, hunk1]; + const selection1 = selection0.updateHunks(unchangedHunks); + + assert.isFalse(wasResolved); + + const hunk2 = new Hunk({ + oldStartRow: 6, oldRowCount: 1, newStartRow: 4, newRowCount: 3, + rowRange: new IndexedRowRange({bufferRange: [[1, 0], [2, 0]], startOffset: 0, endOffset: 0}), + changes: [ + new Addition(new IndexedRowRange({bufferRange: [[1, 0], [2, 0]], startOffset: 0, endOffset: 0})), + ], + }); + const changedHunks = [hunk0, hunk2]; + selection1.updateHunks(changedHunks); + + await promise; + assert.isTrue(wasResolved); + }); + }); + + describe('jumpToNextHunk() and jumpToPreviousHunk()', function() { + it('selects the next/previous hunk', function() { + const hunks = buildThreeHunkPatchFixture().getHunks(); + const selection0 = new FilePatchSelection(hunks); + + // in hunk mode, selects the entire next/previous hunk + assert.strictEqual(selection0.getMode(), 'hunk'); + assert.sameMembers(Array.from(selection0.getSelectedHunks()), [hunks[0]]); + + const selection1 = selection0.jumpToNextHunk(); + assert.sameMembers(Array.from(selection1.getSelectedHunks()), [hunks[1]]); + + const selection2 = selection1.jumpToNextHunk(); + assert.sameMembers(Array.from(selection2.getSelectedHunks()), [hunks[2]]); + + const selection3 = selection2.jumpToNextHunk(); + assert.sameMembers(Array.from(selection3.getSelectedHunks()), [hunks[2]]); + + const selection4 = selection3.jumpToPreviousHunk(); + assert.sameMembers(Array.from(selection4.getSelectedHunks()), [hunks[1]]); + + const selection5 = selection4.jumpToPreviousHunk(); + assert.sameMembers(Array.from(selection5.getSelectedHunks()), [hunks[0]]); + + const selection6 = selection5.jumpToPreviousHunk(); + assert.sameMembers(Array.from(selection6.getSelectedHunks()), [hunks[0]]); + + // in line selection mode, the first changed line of the next/previous hunk is selected + const selection7 = selection6.toggleMode(); + assert.strictEqual(selection7.getMode(), 'line'); + assert.sameMembers(Array.from(selection7.getSelectedLines()), [1]); + + const selection8 = selection7.jumpToNextHunk(); + assert.sameMembers(Array.from(selection8.getSelectedLines()), [6]); + + const selection9 = selection8.jumpToNextHunk(); + assert.sameMembers(Array.from(selection9.getSelectedLines()), [12]); + + const selection10 = selection9.jumpToNextHunk(); + assert.sameMembers(Array.from(selection10.getSelectedLines()), [12]); + + const selection11 = selection10.jumpToPreviousHunk(); + assert.sameMembers(Array.from(selection11.getSelectedLines()), [6]); + + const selection12 = selection11.jumpToPreviousHunk(); + assert.sameMembers(Array.from(selection12.getSelectedLines()), [1]); + + const selection13 = selection12.jumpToPreviousHunk(); + assert.sameMembers(Array.from(selection13.getSelectedLines()), [1]); + }); + }); + + describe('goToDiffLine(lineNumber)', function() { + it('selects the closest selectable hunk line', function() { + const hunks = buildPatchFixture().getHunks(); + + const selection0 = new FilePatchSelection(hunks); + const selection1 = selection0.goToDiffLine(11); + assert.sameMembers(Array.from(selection1.getSelectedLines()), [1]); + + const selection2 = selection1.goToDiffLine(26); + assert.sameMembers(Array.from(selection2.getSelectedLines()), [7]); + + // selects closest added hunk line + const selection3 = selection2.goToDiffLine(27); + assert.sameMembers(Array.from(selection3.getSelectedLines()), [7]); + + const selection4 = selection3.goToDiffLine(18); + assert.sameMembers(Array.from(selection4.getSelectedLines()), [1]); + + const selection5 = selection4.goToDiffLine(19); + assert.sameMembers(Array.from(selection5.getSelectedLines()), [6]); + }); + }); +}); + +function buildPatchFixture() { + return buildFilePatch([{ + oldPath: 'a.txt', + oldMode: '100644', + newPath: 'a.txt', + newMode: '100644', + status: 'modified', + hunks: [ + { + oldStartLine: 10, + oldLineCount: 4, + newStartLine: 10, + newLineCount: 3, + lines: [ + ' 0000', + '+0001', + '-0002', + '-0003', + ' 0004', + ], + }, + { + oldStartLine: 25, + oldLineCount: 3, + newStartLine: 24, + newLineCount: 4, + lines: [ + ' 0005', + '+0006', + '+0007', + '-0008', + ' 0009', + ], + }, + ], + }]); +} + +function buildThreeHunkPatchFixture() { + return buildFilePatch([{ + oldPath: 'a.txt', + oldMode: '100644', + newPath: 'a.txt', + newMode: '100644', + status: 'modified', + hunks: [ + { + oldStartLine: 10, + oldLineCount: 4, + newStartLine: 10, + newLineCount: 3, + lines: [ + ' 0000', + '+0001', + '-0002', + '-0003', + ' 0004', + ], + }, + { + oldStartLine: 25, + oldLineCount: 3, + newStartLine: 24, + newLineCount: 4, + lines: [ + ' 0005', + '+0006', + '+0007', + '-0008', + ' 0009', + ], + }, + { + oldStartLine: 40, + oldLineCount: 5, + newStartLine: 44, + newLineCount: 3, + lines: [ + ' 0010', + ' 0011', + '-0012', + '-0013', + ' 0014', + ], + }, + ], + }]); +} + +function buildAllAddedPatchFixture() { + return buildFilePatch([{ + oldPath: 'a.txt', + oldMode: '100644', + newPath: 'a.txt', + newMode: '100644', + status: 'modified', + hunks: [ + { + oldStartLine: 1, + oldLineCount: 0, + newStartLine: 1, + newLineCount: 4, + lines: [ + '+0000', + '+0001', + '+0002', + '+0003', + ], + }, + { + oldStartLine: 1, + oldLineCount: 0, + newStartLine: 5, + newLineCount: 4, + lines: [ + '+0004', + '+0005', + '+0006', + '+0007', + ], + }, + ], + }]); +} + +function buildFourHunksPatchFixture() { + return buildFilePatch([{ + oldPath: 'a.txt', + oldMode: '100644', + newPath: 'a.txt', + newMode: '100644', + status: 'modified', + hunks: [ + { + oldStartLine: 1, + oldLineCount: 0, + newStartLine: 1, + newLineCount: 1, + lines: [ + '+0000', + ], + }, + { + oldStartLine: 2, + oldLineCount: 0, + newStartLine: 2, + newLineCount: 1, + lines: [ + '+0001', + ], + }, + { + oldStartLine: 3, + oldLineCount: 0, + newStartLine: 3, + newLineCount: 1, + lines: [ + '+0002', + ], + }, + { + oldStartLine: 4, + oldLineCount: 0, + newStartLine: 4, + newLineCount: 1, + lines: [ + '+0004', + ], + }, + ], + }]); +} diff --git a/test/models/file-patch-selection.test.pending.js b/test/models/file-patch-selection.test.pending.js deleted file mode 100644 index 6ea83f436f..0000000000 --- a/test/models/file-patch-selection.test.pending.js +++ /dev/null @@ -1,965 +0,0 @@ -import FilePatchSelection from '../../lib/models/file-patch-selection'; -import Hunk from '../../lib/models/hunk'; -import HunkLine from '../../lib/models/hunk-line'; -import {assertEqualSets} from '../helpers'; - -describe('FilePatchSelection', function() { - describe('line selection', function() { - it('starts a new line selection with selectLine and updates an existing selection when preserveTail is true', function() { - const hunks = [ - new Hunk(1, 1, 1, 3, '', [ - new HunkLine('line-1', 'added', -1, 1), - new HunkLine('line-2', 'added', -1, 2), - new HunkLine('line-3', 'unchanged', 1, 3), - ]), - new Hunk(5, 7, 5, 4, '', [ - new HunkLine('line-4', 'unchanged', 5, 7), - new HunkLine('line-5', 'deleted', 6, -1), - new HunkLine('line-6', 'deleted', 7, -1), - new HunkLine('line-7', 'added', -1, 8), - new HunkLine('line-8', 'added', -1, 9), - new HunkLine('line-9', 'added', -1, 10), - new HunkLine('line-10', 'deleted', 8, -1), - new HunkLine('line-11', 'deleted', 9, -1), - ]), - ]; - const selection0 = new FilePatchSelection(hunks); - - const selection1 = selection0.selectLine(hunks[0].lines[1]); - assertEqualSets(selection1.getSelectedLines(), new Set([ - hunks[0].lines[1], - ])); - - const selection2 = selection1.selectLine(hunks[1].lines[2], true); - assertEqualSets(selection2.getSelectedLines(), new Set([ - hunks[0].lines[1], - hunks[1].lines[1], - hunks[1].lines[2], - ])); - - const selection3 = selection2.selectLine(hunks[1].lines[1], true); - assertEqualSets(selection3.getSelectedLines(), new Set([ - hunks[0].lines[1], - hunks[1].lines[1], - ])); - - const selection4 = selection3.selectLine(hunks[0].lines[0], true); - assertEqualSets(selection4.getSelectedLines(), new Set([ - hunks[0].lines[0], - hunks[0].lines[1], - ])); - - const selection5 = selection4.selectLine(hunks[1].lines[2]); - assertEqualSets(selection5.getSelectedLines(), new Set([ - hunks[1].lines[2], - ])); - }); - - it('adds a new line selection when calling addOrSubtractLineSelection with an unselected line and always updates the head of the most recent line selection', function() { - const hunks = [ - new Hunk(1, 1, 1, 3, '', [ - new HunkLine('line-1', 'added', -1, 1), - new HunkLine('line-2', 'added', -1, 2), - new HunkLine('line-3', 'unchanged', 1, 3), - ]), - new Hunk(5, 7, 5, 4, '', [ - new HunkLine('line-4', 'unchanged', 5, 7), - new HunkLine('line-5', 'deleted', 6, -1), - new HunkLine('line-6', 'deleted', 7, -1), - new HunkLine('line-7', 'added', -1, 8), - new HunkLine('line-8', 'added', -1, 9), - new HunkLine('line-9', 'added', -1, 10), - new HunkLine('line-10', 'deleted', 8, -1), - new HunkLine('line-11', 'deleted', 9, -1), - ]), - ]; - const selection0 = new FilePatchSelection(hunks) - .selectLine(hunks[0].lines[1]) - .selectLine(hunks[1].lines[1], true) - .addOrSubtractLineSelection(hunks[1].lines[3]) - .selectLine(hunks[1].lines[4], true); - - assertEqualSets(selection0.getSelectedLines(), new Set([ - hunks[0].lines[1], - hunks[1].lines[1], - hunks[1].lines[3], - hunks[1].lines[4], - ])); - - const selection1 = selection0.selectLine(hunks[0].lines[0], true); - assertEqualSets(selection1.getSelectedLines(), new Set([ - hunks[0].lines[0], - hunks[0].lines[1], - hunks[1].lines[1], - hunks[1].lines[2], - hunks[1].lines[3], - ])); - }); - - it('subtracts from existing selections when calling addOrSubtractLineSelection with a selected line', function() { - const hunks = [ - new Hunk(1, 1, 2, 4, '', [ - new HunkLine('line-1', 'unchanged', 1, 1), - new HunkLine('line-2', 'added', -1, 2), - new HunkLine('line-3', 'added', -1, 3), - new HunkLine('line-4', 'unchanged', 2, 4), - ]), - new Hunk(5, 7, 1, 4, '', [ - new HunkLine('line-5', 'unchanged', 5, 7), - new HunkLine('line-6', 'added', -1, 8), - new HunkLine('line-7', 'added', -1, 9), - new HunkLine('line-8', 'added', -1, 10), - ]), - ]; - const selection0 = new FilePatchSelection(hunks) - .selectLine(hunks[0].lines[2]) - .selectLine(hunks[1].lines[2], true); - assertEqualSets(selection0.getSelectedLines(), new Set([ - hunks[0].lines[2], - hunks[1].lines[1], - hunks[1].lines[2], - ])); - - const selection1 = selection0.addOrSubtractLineSelection(hunks[1].lines[1]); - assertEqualSets(selection1.getSelectedLines(), new Set([ - hunks[0].lines[2], - hunks[1].lines[2], - ])); - - const selection2 = selection1.selectLine(hunks[1].lines[3], true); - assertEqualSets(selection2.getSelectedLines(), new Set([ - hunks[0].lines[2], - ])); - - const selection3 = selection2.selectLine(hunks[0].lines[1], true); - assertEqualSets(selection3.getSelectedLines(), new Set([ - hunks[1].lines[2], - ])); - }); - - it('allows the next or previous line to be selected', function() { - const hunks = [ - new Hunk(1, 1, 2, 4, '', [ - new HunkLine('line-1', 'unchanged', 1, 1), - new HunkLine('line-2', 'added', -1, 2), - new HunkLine('line-3', 'added', -1, 3), - new HunkLine('line-4', 'unchanged', 2, 4), - ]), - new Hunk(5, 7, 3, 4, '', [ - new HunkLine('line-5', 'unchanged', 5, 7), - new HunkLine('line-6', 'unchanged', 6, 8), - new HunkLine('line-7', 'added', -1, 9), - new HunkLine('line-8', 'unchanged', 7, 10), - ]), - ]; - const selection0 = new FilePatchSelection(hunks) - .selectLine(hunks[0].lines[1]) - .selectNextLine(); - assertEqualSets(selection0.getSelectedLines(), new Set([ - hunks[0].lines[2], - ])); - - const selection1 = selection0.selectNextLine(); - assertEqualSets(selection1.getSelectedLines(), new Set([ - hunks[1].lines[2], - ])); - - const selection2 = selection1.selectNextLine(); - assertEqualSets(selection2.getSelectedLines(), new Set([ - hunks[1].lines[2], - ])); - - const selection3 = selection2.selectPreviousLine(); - assertEqualSets(selection3.getSelectedLines(), new Set([ - hunks[0].lines[2], - ])); - - const selection4 = selection3.selectPreviousLine(); - assertEqualSets(selection4.getSelectedLines(), new Set([ - hunks[0].lines[1], - ])); - - const selection5 = selection4.selectPreviousLine(); - assertEqualSets(selection5.getSelectedLines(), new Set([ - hunks[0].lines[1], - ])); - - const selection6 = selection5.selectNextLine(true); - assertEqualSets(selection6.getSelectedLines(), new Set([ - hunks[0].lines[1], - hunks[0].lines[2], - ])); - - const selection7 = selection6.selectNextLine().selectPreviousLine(true); - assertEqualSets(selection7.getSelectedLines(), new Set([ - hunks[0].lines[2], - hunks[1].lines[2], - ])); - }); - - it('allows the first/last changed line to be selected', function() { - const hunks = [ - new Hunk(1, 1, 2, 4, '', [ - new HunkLine('line-1', 'unchanged', 1, 1), - new HunkLine('line-2', 'added', -1, 2), - new HunkLine('line-3', 'added', -1, 3), - new HunkLine('line-4', 'unchanged', 2, 4), - ]), - new Hunk(5, 7, 3, 4, '', [ - new HunkLine('line-5', 'unchanged', 5, 7), - new HunkLine('line-6', 'unchanged', 6, 8), - new HunkLine('line-7', 'added', -1, 9), - new HunkLine('line-8', 'unchanged', 7, 10), - ]), - ]; - - const selection0 = new FilePatchSelection(hunks).selectLastLine(); - assertEqualSets(selection0.getSelectedLines(), new Set([ - hunks[1].lines[2], - ])); - - const selection1 = selection0.selectFirstLine(); - assertEqualSets(selection1.getSelectedLines(), new Set([ - hunks[0].lines[1], - ])); - - const selection2 = selection1.selectLastLine(true); - assertEqualSets(selection2.getSelectedLines(), new Set([ - hunks[0].lines[1], - hunks[0].lines[2], - hunks[1].lines[2], - ])); - - const selection3 = selection2.selectLastLine(); - assertEqualSets(selection3.getSelectedLines(), new Set([ - hunks[1].lines[2], - ])); - - const selection4 = selection3.selectFirstLine(true); - assertEqualSets(selection4.getSelectedLines(), new Set([ - hunks[0].lines[1], - hunks[0].lines[2], - hunks[1].lines[2], - ])); - - const selection5 = selection4.selectFirstLine(); - assertEqualSets(selection5.getSelectedLines(), new Set([ - hunks[0].lines[1], - ])); - }); - - it('allows all lines to be selected', function() { - const hunks = [ - new Hunk(1, 1, 2, 4, '', [ - new HunkLine('line-1', 'unchanged', 1, 1), - new HunkLine('line-2', 'added', -1, 2), - new HunkLine('line-3', 'added', -1, 3), - new HunkLine('line-4', 'unchanged', 2, 4), - ]), - new Hunk(5, 7, 3, 4, '', [ - new HunkLine('line-5', 'unchanged', 5, 7), - new HunkLine('line-6', 'unchanged', 6, 8), - new HunkLine('line-7', 'added', -1, 9), - new HunkLine('line-8', 'unchanged', 7, 10), - ]), - ]; - - const selection0 = new FilePatchSelection(hunks).selectAllLines(); - assertEqualSets(selection0.getSelectedLines(), new Set([ - hunks[0].lines[1], - hunks[0].lines[2], - hunks[1].lines[2], - ])); - }); - - it('defaults to the first/last changed line when selecting next / previous with no current selection', function() { - const hunks = [ - new Hunk(1, 1, 2, 4, '', [ - new HunkLine('line-1', 'unchanged', 1, 1), - new HunkLine('line-2', 'added', -1, 2), - new HunkLine('line-3', 'added', -1, 3), - new HunkLine('line-4', 'unchanged', 2, 4), - ]), - ]; - - const selection0 = new FilePatchSelection(hunks) - .selectLine(hunks[0].lines[1]) - .addOrSubtractLineSelection(hunks[0].lines[1]) - .coalesce(); - assertEqualSets(selection0.getSelectedLines(), new Set()); - - const selection1 = selection0.selectNextLine(); - assertEqualSets(selection1.getSelectedLines(), new Set([hunks[0].lines[1]])); - - const selection2 = selection1.addOrSubtractLineSelection(hunks[0].lines[1]).coalesce(); - assertEqualSets(selection2.getSelectedLines(), new Set()); - - const selection3 = selection2.selectPreviousLine(); - assertEqualSets(selection3.getSelectedLines(), new Set([hunks[0].lines[2]])); - }); - - it('collapses multiple selections down to one line when selecting next or previous', function() { - const hunks = [ - new Hunk(1, 1, 2, 4, '', [ - new HunkLine('line-1', 'unchanged', 1, 1), - new HunkLine('line-2', 'added', -1, 2), - new HunkLine('line-3', 'added', -1, 3), - new HunkLine('line-4', 'unchanged', 2, 4), - ]), - new Hunk(5, 7, 3, 4, '', [ - new HunkLine('line-5', 'unchanged', 5, 7), - new HunkLine('line-6', 'unchanged', 6, 8), - new HunkLine('line-7', 'added', -1, 9), - new HunkLine('line-8', 'unchanged', 7, 10), - ]), - ]; - - const selection0 = new FilePatchSelection(hunks) - .selectLine(hunks[0].lines[1]) - .addOrSubtractLineSelection(hunks[0].lines[2]) - .selectNextLine(true); - assertEqualSets(selection0.getSelectedLines(), new Set([ - hunks[0].lines[1], - hunks[0].lines[2], - hunks[1].lines[2], - ])); - - const selection1 = selection0.selectNextLine(); - assertEqualSets(selection1.getSelectedLines(), new Set([ - hunks[1].lines[2], - ])); - - const selection2 = selection1.selectLine(hunks[0].lines[1]) - .addOrSubtractLineSelection(hunks[0].lines[2]) - .selectPreviousLine(true); - assertEqualSets(selection2.getSelectedLines(), new Set([ - hunks[0].lines[1], - hunks[0].lines[2], - ])); - - const selection3 = selection2.selectPreviousLine(); - assertEqualSets(selection3.getSelectedLines(), new Set([ - hunks[0].lines[1], - ])); - }); - - describe('coalescing', function() { - it('merges overlapping selections', function() { - const hunks = [ - new Hunk(1, 1, 0, 4, '', [ - new HunkLine('line-1', 'added', -1, 1), - new HunkLine('line-2', 'added', -1, 2), - new HunkLine('line-3', 'added', -1, 3), - new HunkLine('line-4', 'added', -1, 4), - ]), - new Hunk(5, 7, 0, 4, '', [ - new HunkLine('line-5', 'added', -1, 7), - new HunkLine('line-6', 'added', -1, 8), - new HunkLine('line-7', 'added', -1, 9), - new HunkLine('line-8', 'added', -1, 10), - ]), - ]; - - const selection0 = new FilePatchSelection(hunks) - .selectLine(hunks[0].lines[2]) - .selectLine(hunks[1].lines[1], true) - .addOrSubtractLineSelection(hunks[0].lines[0]) - .selectLine(hunks[1].lines[0], true) - .coalesce() - .selectPreviousLine(true); - assertEqualSets(selection0.getSelectedLines(), new Set([ - hunks[0].lines[0], - hunks[0].lines[1], - hunks[0].lines[2], - hunks[0].lines[3], - hunks[1].lines[0], - ])); - - const selection1 = selection0.addOrSubtractLineSelection(hunks[1].lines[3]) - .selectLine(hunks[0].lines[3], true) - .coalesce() - .selectNextLine(true); - assertEqualSets(selection1.getSelectedLines(), new Set([ - hunks[0].lines[1], - hunks[0].lines[2], - hunks[0].lines[3], - hunks[1].lines[0], - hunks[1].lines[1], - hunks[1].lines[2], - hunks[1].lines[3], - ])); - }); - - it('merges adjacent selections', function() { - const hunks = [ - new Hunk(1, 1, 0, 4, '', [ - new HunkLine('line-1', 'added', -1, 1), - new HunkLine('line-2', 'added', -1, 2), - new HunkLine('line-3', 'added', -1, 3), - new HunkLine('line-4', 'added', -1, 4), - ]), - new Hunk(5, 7, 0, 4, '', [ - new HunkLine('line-5', 'added', -1, 7), - new HunkLine('line-6', 'added', -1, 8), - new HunkLine('line-7', 'added', -1, 9), - new HunkLine('line-8', 'added', -1, 10), - ]), - ]; - - const selection0 = new FilePatchSelection(hunks) - .selectLine(hunks[0].lines[3]) - .selectLine(hunks[1].lines[1], true) - .addOrSubtractLineSelection(hunks[0].lines[1]) - .selectLine(hunks[0].lines[2], true) - .coalesce() - .selectPreviousLine(true); - assertEqualSets(selection0.getSelectedLines(), new Set([ - hunks[0].lines[1], - hunks[0].lines[2], - hunks[0].lines[3], - hunks[1].lines[0], - ])); - - const selection1 = selection0.addOrSubtractLineSelection(hunks[1].lines[2]) - .selectLine(hunks[1].lines[1], true) - .coalesce() - .selectNextLine(true); - assertEqualSets(selection1.getSelectedLines(), new Set([ - hunks[0].lines[2], - hunks[0].lines[3], - hunks[1].lines[0], - hunks[1].lines[1], - hunks[1].lines[2], - ])); - }); - - it('expands selections to contain all adjacent context lines', function() { - const hunks = [ - new Hunk(1, 1, 2, 4, '', [ - new HunkLine('line-1', 'added', -1, 1), - new HunkLine('line-2', 'added', -1, 2), - new HunkLine('line-3', 'unchanged', 1, 3), - new HunkLine('line-4', 'unchanged', 2, 4), - ]), - new Hunk(5, 7, 2, 4, '', [ - new HunkLine('line-5', 'unchanged', 5, 7), - new HunkLine('line-6', 'unchanged', 6, 8), - new HunkLine('line-7', 'added', -1, 9), - new HunkLine('line-8', 'added', -1, 10), - ]), - ]; - - const selection0 = new FilePatchSelection(hunks) - .selectLine(hunks[1].lines[3]) - .selectLine(hunks[1].lines[2], true) - .addOrSubtractLineSelection(hunks[0].lines[1]) - .selectLine(hunks[0].lines[0], true) - .coalesce() - .selectNext(true); - assertEqualSets(selection0.getSelectedLines(), new Set([ - hunks[0].lines[1], - hunks[1].lines[2], - hunks[1].lines[3], - ])); - }); - - it('truncates or splits selections where they overlap a negative selection', function() { - const hunks = [ - new Hunk(1, 1, 0, 4, '', [ - new HunkLine('line-1', 'added', -1, 1), - new HunkLine('line-2', 'added', -1, 2), - new HunkLine('line-3', 'added', -1, 3), - new HunkLine('line-4', 'added', -1, 4), - ]), - new Hunk(5, 7, 0, 4, '', [ - new HunkLine('line-5', 'added', -1, 7), - new HunkLine('line-6', 'added', -1, 8), - new HunkLine('line-7', 'added', -1, 9), - new HunkLine('line-8', 'added', -1, 10), - ]), - ]; - - const selection0 = new FilePatchSelection(hunks) - .selectLine(hunks[0].lines[0]) - .selectLine(hunks[1].lines[3], true) - .addOrSubtractLineSelection(hunks[0].lines[3]) - .selectLine(hunks[1].lines[0], true) - .coalesce() - .selectPrevious(true); - assertEqualSets(selection0.getSelectedLines(), new Set([ - hunks[0].lines[0], - hunks[0].lines[1], - hunks[0].lines[2], - hunks[1].lines[1], - hunks[1].lines[2], - ])); - }); - - it('does not blow up when coalescing with no selections', function() { - const hunks = [ - new Hunk(1, 1, 0, 1, '', [ - new HunkLine('line-1', 'added', -1, 1), - ]), - ]; - const selection0 = new FilePatchSelection(hunks) - .selectLine(hunks[0].lines[0]) - .addOrSubtractLineSelection(hunks[0].lines[0]); - assertEqualSets(selection0.getSelectedLines(), new Set()); - - selection0.coalesce(); - }); - }); - }); - - describe('hunk selection', function() { - it('selects the first hunk by default', function() { - const hunks = [ - new Hunk(1, 1, 0, 1, '', [ - new HunkLine('line-1', 'added', -1, 1), - ]), - new Hunk(5, 6, 0, 1, '', [ - new HunkLine('line-2', 'added', -1, 6), - ]), - ]; - const selection0 = new FilePatchSelection(hunks); - assertEqualSets(selection0.getSelectedHunks(), new Set([hunks[0]])); - }); - - it('starts a new hunk selection with selectHunk and updates an existing selection when preserveTail is true', function() { - const hunks = [ - new Hunk(1, 1, 0, 1, '', [ - new HunkLine('line-1', 'added', -1, 1), - ]), - new Hunk(5, 6, 0, 1, '', [ - new HunkLine('line-2', 'added', -1, 6), - ]), - new Hunk(10, 12, 0, 1, '', [ - new HunkLine('line-3', 'added', -1, 12), - ]), - new Hunk(15, 18, 0, 1, '', [ - new HunkLine('line-4', 'added', -1, 18), - ]), - ]; - const selection0 = new FilePatchSelection(hunks) - .selectHunk(hunks[1]); - assertEqualSets(selection0.getSelectedHunks(), new Set([hunks[1]])); - - const selection1 = selection0.selectHunk(hunks[3], true); - assertEqualSets(selection1.getSelectedHunks(), new Set([hunks[1], hunks[2], hunks[3]])); - - const selection2 = selection1.selectHunk(hunks[0], true); - assertEqualSets(selection2.getSelectedHunks(), new Set([hunks[0], hunks[1]])); - }); - - it('adds a new hunk selection with addOrSubtractHunkSelection and always updates the head of the most recent hunk selection', function() { - const hunks = [ - new Hunk(1, 1, 0, 1, '', [ - new HunkLine('line-1', 'added', -1, 1), - ]), - new Hunk(5, 6, 0, 1, '', [ - new HunkLine('line-2', 'added', -1, 6), - ]), - new Hunk(10, 12, 0, 1, '', [ - new HunkLine('line-3', 'added', -1, 12), - ]), - new Hunk(15, 18, 0, 1, '', [ - new HunkLine('line-4', 'added', -1, 18), - ]), - ]; - const selection0 = new FilePatchSelection(hunks) - .addOrSubtractHunkSelection(hunks[2]); - assertEqualSets(selection0.getSelectedHunks(), new Set([hunks[0], hunks[2]])); - - const selection1 = selection0.selectHunk(hunks[3], true); - assertEqualSets(selection1.getSelectedHunks(), new Set([hunks[0], hunks[2], hunks[3]])); - - const selection2 = selection1.selectHunk(hunks[1], true); - assertEqualSets(selection2.getSelectedHunks(), new Set([hunks[0], hunks[1], hunks[2]])); - }); - - it('allows the next or previous hunk to be selected', function() { - const hunks = [ - new Hunk(1, 1, 0, 1, '', [ - new HunkLine('line-1', 'added', -1, 1), - ]), - new Hunk(5, 6, 0, 1, '', [ - new HunkLine('line-2', 'added', -1, 6), - ]), - new Hunk(10, 12, 0, 1, '', [ - new HunkLine('line-3', 'added', -1, 12), - ]), - new Hunk(15, 18, 0, 1, '', [ - new HunkLine('line-4', 'added', -1, 18), - ]), - ]; - - const selection0 = new FilePatchSelection(hunks) - .selectNextHunk(); - assertEqualSets(selection0.getSelectedHunks(), new Set([hunks[1]])); - - const selection1 = selection0.selectNextHunk(); - assertEqualSets(selection1.getSelectedHunks(), new Set([hunks[2]])); - - const selection2 = selection1.selectNextHunk() - .selectNextHunk(); - assertEqualSets(selection2.getSelectedHunks(), new Set([hunks[3]])); - - const selection3 = selection2.selectPreviousHunk(); - assertEqualSets(selection3.getSelectedHunks(), new Set([hunks[2]])); - - const selection4 = selection3.selectPreviousHunk(); - assertEqualSets(selection4.getSelectedHunks(), new Set([hunks[1]])); - - const selection5 = selection4.selectPreviousHunk() - .selectPreviousHunk(); - assertEqualSets(selection5.getSelectedHunks(), new Set([hunks[0]])); - - const selection6 = selection5.selectNextHunk() - .selectNextHunk(true); - assertEqualSets(selection6.getSelectedHunks(), new Set([hunks[1], hunks[2]])); - - const selection7 = selection6.selectPreviousHunk(true); - assertEqualSets(selection7.getSelectedHunks(), new Set([hunks[1]])); - - const selection8 = selection7.selectPreviousHunk(true); - assertEqualSets(selection8.getSelectedHunks(), new Set([hunks[0], hunks[1]])); - }); - - it('allows all hunks to be selected', function() { - const hunks = [ - new Hunk(1, 1, 0, 1, '', [ - new HunkLine('line-1', 'added', -1, 1), - ]), - new Hunk(5, 6, 0, 1, '', [ - new HunkLine('line-2', 'added', -1, 6), - ]), - new Hunk(10, 12, 0, 1, '', [ - new HunkLine('line-3', 'added', -1, 12), - ]), - new Hunk(15, 18, 0, 1, '', [ - new HunkLine('line-4', 'added', -1, 18), - ]), - ]; - - const selection0 = new FilePatchSelection(hunks) - .selectAllHunks(); - assertEqualSets(selection0.getSelectedHunks(), new Set([hunks[0], hunks[1], hunks[2], hunks[3]])); - }); - }); - - describe('selection modes', function() { - it('allows the selection mode to be toggled between hunks and lines', function() { - const hunks = [ - new Hunk(1, 1, 1, 3, '', [ - new HunkLine('line-1', 'added', -1, 1), - new HunkLine('line-2', 'added', -1, 2), - new HunkLine('line-3', 'unchanged', 1, 3), - ]), - new Hunk(5, 7, 5, 4, '', [ - new HunkLine('line-4', 'unchanged', 5, 7), - new HunkLine('line-5', 'deleted', 6, -1), - new HunkLine('line-6', 'deleted', 7, -1), - new HunkLine('line-7', 'added', -1, 8), - new HunkLine('line-8', 'added', -1, 9), - new HunkLine('line-9', 'added', -1, 10), - new HunkLine('line-10', 'deleted', 8, -1), - new HunkLine('line-11', 'deleted', 9, -1), - ]), - ]; - const selection0 = new FilePatchSelection(hunks); - - assert.equal(selection0.getMode(), 'hunk'); - assertEqualSets(selection0.getSelectedHunks(), new Set([hunks[0]])); - assertEqualSets(selection0.getSelectedLines(), getChangedLines(hunks[0])); - - const selection1 = selection0.selectNext(); - assert.equal(selection1.getMode(), 'hunk'); - assertEqualSets(selection1.getSelectedHunks(), new Set([hunks[1]])); - assertEqualSets(selection1.getSelectedLines(), getChangedLines(hunks[1])); - - const selection2 = selection1.toggleMode(); - assert.equal(selection2.getMode(), 'line'); - assertEqualSets(selection2.getSelectedHunks(), new Set([hunks[1]])); - assertEqualSets(selection2.getSelectedLines(), new Set([hunks[1].lines[1]])); - - const selection3 = selection2.selectNext(); - assertEqualSets(selection3.getSelectedHunks(), new Set([hunks[1]])); - assertEqualSets(selection3.getSelectedLines(), new Set([hunks[1].lines[2]])); - - const selection4 = selection3.toggleMode(); - assert.equal(selection4.getMode(), 'hunk'); - assertEqualSets(selection4.getSelectedHunks(), new Set([hunks[1]])); - assertEqualSets(selection4.getSelectedLines(), getChangedLines(hunks[1])); - - const selection5 = selection4.selectLine(hunks[0].lines[1]); - assert.equal(selection5.getMode(), 'line'); - assertEqualSets(selection5.getSelectedHunks(), new Set([hunks[0]])); - assertEqualSets(selection5.getSelectedLines(), new Set([hunks[0].lines[1]])); - - const selection6 = selection5.selectHunk(hunks[1]); - assert.equal(selection6.getMode(), 'hunk'); - assertEqualSets(selection6.getSelectedHunks(), new Set([hunks[1]])); - assertEqualSets(selection6.getSelectedLines(), getChangedLines(hunks[1])); - }); - }); - - describe('updateHunks(hunks)', function() { - it('collapses the line selection to a single line following the previous selected range with the highest start index', function() { - const oldHunks = [ - new Hunk(1, 1, 1, 3, '', [ - new HunkLine('line-1', 'added', -1, 1), - new HunkLine('line-2', 'added', -1, 2), - new HunkLine('line-3', 'unchanged', 1, 3), - ]), - new Hunk(5, 7, 5, 4, '', [ - new HunkLine('line-4', 'unchanged', 5, 7), - new HunkLine('line-5', 'deleted', 6, -1), - new HunkLine('line-6', 'deleted', 7, -1), - new HunkLine('line-7', 'added', -1, 8), - new HunkLine('line-8', 'added', -1, 9), - new HunkLine('line-9', 'added', -1, 10), - new HunkLine('line-10', 'deleted', 8, -1), - new HunkLine('line-11', 'deleted', 9, -1), - ]), - ]; - const selection0 = new FilePatchSelection(oldHunks) - .selectLine(oldHunks[1].lines[2]) - .selectLine(oldHunks[1].lines[4], true); - - const newHunks = [ - new Hunk(1, 1, 1, 3, '', [ - new HunkLine('line-1', 'added', -1, 1), - new HunkLine('line-2', 'added', -1, 2), - new HunkLine('line-3', 'unchanged', 1, 3), - ]), - new Hunk(5, 7, 3, 2, '', [ - new HunkLine('line-4', 'unchanged', 5, 7), - new HunkLine('line-5', 'deleted', 6, -1), - new HunkLine('line-6', 'unchanged', 7, 8), - ]), - new Hunk(9, 10, 3, 2, '', [ - new HunkLine('line-8', 'unchanged', 9, 10), - new HunkLine('line-9', 'added', -1, 11), - new HunkLine('line-10', 'deleted', 10, -1), - new HunkLine('line-11', 'deleted', 11, -1), - ]), - ]; - const selection1 = selection0.updateHunks(newHunks); - - assertEqualSets(selection1.getSelectedLines(), new Set([ - newHunks[2].lines[1], - ])); - }); - - it('collapses the line selection to the line preceding the previous selected line if it was the *last* line', function() { - const oldHunks = [ - new Hunk(1, 1, 1, 3, '', [ - new HunkLine('line-1', 'added', -1, 1), - new HunkLine('line-2', 'added', -1, 2), - new HunkLine('line-3', 'unchanged', 1, 3), - ]), - ]; - - const selection0 = new FilePatchSelection(oldHunks); - selection0.selectLine(oldHunks[0].lines[1]); - - const newHunks = [ - new Hunk(1, 1, 1, 3, '', [ - new HunkLine('line-1', 'added', -1, 1), - new HunkLine('line-2', 'unchanged', 1, 2), - new HunkLine('line-3', 'unchanged', 2, 3), - ]), - ]; - const selection1 = selection0.updateHunks(newHunks); - - assertEqualSets(selection1.getSelectedLines(), new Set([ - newHunks[0].lines[0], - ])); - }); - - it('updates the hunk selection if it exceeds the new length of the hunks list', function() { - const oldHunks = [ - new Hunk(1, 1, 0, 1, '', [ - new HunkLine('line-1', 'added', -1, 1), - ]), - new Hunk(5, 6, 0, 1, '', [ - new HunkLine('line-2', 'added', -1, 6), - ]), - ]; - const selection0 = new FilePatchSelection(oldHunks) - .selectHunk(oldHunks[1]); - - const newHunks = [ - new Hunk(1, 1, 0, 1, '', [ - new HunkLine('line-1', 'added', -1, 1), - ]), - ]; - const selection1 = selection0.updateHunks(newHunks); - - assertEqualSets(selection1.getSelectedHunks(), new Set([newHunks[0]])); - }); - - it('deselects if updating with an empty hunk array', function() { - const oldHunks = [ - new Hunk(1, 1, 1, 3, '', [ - new HunkLine('line-1', 'added', -1, 1), - new HunkLine('line-2', 'added', -1, 2), - ]), - ]; - - const selection0 = new FilePatchSelection(oldHunks) - .selectLine(oldHunks[0], oldHunks[0].lines[1]) - .updateHunks([]); - assertEqualSets(selection0.getSelectedLines(), new Set()); - }); - - it('resolves the getNextUpdatePromise the next time hunks are changed', async function() { - const hunk0 = new Hunk(1, 1, 1, 3, '', [ - new HunkLine('line-1', 'added', -1, 1), - new HunkLine('line-2', 'added', -1, 2), - ]); - const hunk1 = new Hunk(4, 4, 1, 3, '', [ - new HunkLine('line-4', 'added', -1, 1), - new HunkLine('line-7', 'added', -1, 2), - ]); - - const existingHunks = [hunk0, hunk1]; - const selection0 = new FilePatchSelection(existingHunks); - - let wasResolved = false; - selection0.getNextUpdatePromise().then(() => { wasResolved = true; }); - - const unchangedHunks = [hunk0, hunk1]; - const selection1 = selection0.updateHunks(unchangedHunks); - - assert.isFalse(wasResolved); - - const hunk2 = new Hunk(6, 4, 1, 3, '', [ - new HunkLine('line-12', 'added', -1, 1), - new HunkLine('line-77', 'added', -1, 2), - ]); - const changedHunks = [hunk0, hunk2]; - selection1.updateHunks(changedHunks); - - await assert.async.isTrue(wasResolved); - }); - }); - - describe('jumpToNextHunk() and jumpToPreviousHunk()', function() { - it('selects the next/previous hunk', function() { - const hunks = [ - new Hunk(1, 1, 1, 3, '', [ - new HunkLine('line-1', 'added', -1, 1), - new HunkLine('line-2', 'added', -1, 2), - new HunkLine('line-3', 'unchanged', 1, 3), - ]), - new Hunk(5, 7, 3, 2, '', [ - new HunkLine('line-4', 'unchanged', 5, 7), - new HunkLine('line-5', 'deleted', 6, -1), - new HunkLine('line-6', 'unchanged', 7, 8), - ]), - new Hunk(9, 10, 3, 2, '', [ - new HunkLine('line-8', 'unchanged', 9, 10), - new HunkLine('line-9', 'added', -1, 11), - new HunkLine('line-10', 'deleted', 10, -1), - new HunkLine('line-11', 'deleted', 11, -1), - ]), - ]; - const selection0 = new FilePatchSelection(hunks); - - // in hunk mode, selects the entire next/previous hunk - assert.equal(selection0.getMode(), 'hunk'); - assertEqualSets(selection0.getSelectedHunks(), new Set([hunks[0]])); - - const selection1 = selection0.jumpToNextHunk(); - assertEqualSets(selection1.getSelectedHunks(), new Set([hunks[1]])); - - const selection2 = selection1.jumpToNextHunk(); - assertEqualSets(selection2.getSelectedHunks(), new Set([hunks[2]])); - - const selection3 = selection2.jumpToNextHunk(); - assertEqualSets(selection3.getSelectedHunks(), new Set([hunks[2]])); - - const selection4 = selection3.jumpToPreviousHunk(); - assertEqualSets(selection4.getSelectedHunks(), new Set([hunks[1]])); - - const selection5 = selection4.jumpToPreviousHunk(); - assertEqualSets(selection5.getSelectedHunks(), new Set([hunks[0]])); - - const selection6 = selection5.jumpToPreviousHunk(); - assertEqualSets(selection6.getSelectedHunks(), new Set([hunks[0]])); - - // in line selection mode, the first changed line of the next/previous hunk is selected - const selection7 = selection6.toggleMode(); - assert.equal(selection7.getMode(), 'line'); - assertEqualSets(selection7.getSelectedLines(), new Set([getFirstChangedLine(hunks[0])])); - - const selection8 = selection7.jumpToNextHunk(); - assertEqualSets(selection8.getSelectedLines(), new Set([getFirstChangedLine(hunks[1])])); - - const selection9 = selection8.jumpToNextHunk(); - assertEqualSets(selection9.getSelectedLines(), new Set([getFirstChangedLine(hunks[2])])); - - const selection10 = selection9.jumpToNextHunk(); - assertEqualSets(selection10.getSelectedLines(), new Set([getFirstChangedLine(hunks[2])])); - - const selection11 = selection10.jumpToPreviousHunk(); - assertEqualSets(selection11.getSelectedLines(), new Set([getFirstChangedLine(hunks[1])])); - - const selection12 = selection11.jumpToPreviousHunk(); - assertEqualSets(selection12.getSelectedLines(), new Set([getFirstChangedLine(hunks[0])])); - - const selection13 = selection12.jumpToPreviousHunk(); - assertEqualSets(selection13.getSelectedLines(), new Set([getFirstChangedLine(hunks[0])])); - }); - }); - - describe('goToDiffLine(lineNumber)', function() { - it('selects the closest selectable hunk line', function() { - const hunks = [ - new Hunk(1, 1, 2, 4, '', [ - new HunkLine('line-1', 'unchanged', 1, 1), - new HunkLine('line-2', 'added', -1, 2), - new HunkLine('line-3', 'added', -1, 3), - new HunkLine('line-4', 'unchanged', 2, 4), - ]), - new Hunk(5, 7, 3, 4, '', [ - new HunkLine('line-7', 'unchanged', 5, 7), - new HunkLine('line-8', 'unchanged', 6, 8), - new HunkLine('line-9', 'added', -1, 9), - new HunkLine('line-10', 'unchanged', 7, 10), - ]), - ]; - - const selection0 = new FilePatchSelection(hunks); - const selection1 = selection0.goToDiffLine(2); - assert.equal(Array.from(selection1.getSelectedLines())[0].getText(), 'line-2'); - assertEqualSets(selection1.getSelectedLines(), new Set([hunks[0].lines[1]])); - - const selection2 = selection1.goToDiffLine(9); - assert.equal(Array.from(selection2.getSelectedLines())[0].getText(), 'line-9'); - assertEqualSets(selection2.getSelectedLines(), new Set([hunks[1].lines[2]])); - - // selects closest added hunk line - const selection3 = selection2.goToDiffLine(5); - assert.equal(Array.from(selection3.getSelectedLines())[0].getText(), 'line-3'); - assertEqualSets(selection3.getSelectedLines(), new Set([hunks[0].lines[2]])); - - const selection4 = selection3.goToDiffLine(8); - assert.equal(Array.from(selection4.getSelectedLines())[0].getText(), 'line-9'); - assertEqualSets(selection4.getSelectedLines(), new Set([hunks[1].lines[2]])); - - const selection5 = selection4.goToDiffLine(11); - assert.equal(Array.from(selection5.getSelectedLines())[0].getText(), 'line-9'); - assertEqualSets(selection5.getSelectedLines(), new Set([hunks[1].lines[2]])); - }); - }); -}); - -function getChangedLines(hunk) { - return new Set(hunk.getLines().filter(l => l.isChanged())); -} - -function getFirstChangedLine(hunk) { - return hunk.getLines().find(l => l.isChanged()); -} From bce2f0d3200053939b07e046a76bdd3268986007 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 21 Aug 2018 10:56:25 -0400 Subject: [PATCH 0189/4252] Use nullFiles when constructing FilePatches --- lib/models/patch/builder.js | 8 ++- test/models/patch/builder.test.js | 87 +++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 2 deletions(-) diff --git a/lib/models/patch/builder.js b/lib/models/patch/builder.js index 457d02ef46..5270aeebe5 100644 --- a/lib/models/patch/builder.js +++ b/lib/models/patch/builder.js @@ -37,8 +37,12 @@ function singleDiffFilePatch(diff) { newSymlink = diff.hunks[0].lines[2].slice(1); } - const oldFile = new File({path: diff.oldPath, mode: diff.oldMode, symlink: oldSymlink}); - const newFile = new File({path: diff.newPath, mode: diff.newMode, symlink: newSymlink}); + const oldFile = diff.oldPath !== null || diff.oldMode !== null + ? new File({path: diff.oldPath, mode: diff.oldMode, symlink: oldSymlink}) + : nullFile; + const newFile = diff.newPath !== null || diff.newMode !== null + ? new File({path: diff.newPath, mode: diff.newMode, symlink: newSymlink}) + : nullFile; const patch = new Patch({status: diff.status, hunks, bufferText}); return new FilePatch(oldFile, newFile, patch); diff --git a/test/models/patch/builder.test.js b/test/models/patch/builder.test.js index 3bd1db0bbb..6539a4e824 100644 --- a/test/models/patch/builder.test.js +++ b/test/models/patch/builder.test.js @@ -176,6 +176,93 @@ describe('buildFilePatch', function() { assert.strictEqual(p.getNewSymlink(), 'new/destination'); }); + it('assembles a patch from a file deletion', function() { + const p = buildFilePatch([{ + oldPath: 'old/path', + oldMode: '100644', + newPath: null, + newMode: null, + status: 'deleted', + hunks: [ + { + oldStartLine: 1, + oldLineCount: 4, + newStartLine: 0, + newLineCount: 0, + lines: [ + '-line-0', + '-line-1', + '-line-2', + '-line-3', + ], + }, + ], + }]); + + assert.isTrue(p.getOldFile().isPresent()); + assert.strictEqual(p.getOldPath(), 'old/path'); + assert.strictEqual(p.getOldMode(), '100644'); + assert.isFalse(p.getNewFile().isPresent()); + assert.strictEqual(p.getPatch().getStatus(), 'deleted'); + + const buffer = 'line-0\nline-1\nline-2\nline-3\n'; + assert.strictEqual(p.getBufferText(), buffer); + + assertInPatch(p).hunks( + { + startRow: 0, + endRow: 3, + header: '@@ -1,4 +0,0 @@', + changes: [ + {kind: 'deletion', string: '-line-0\n-line-1\n-line-2\n-line-3\n', range: [[0, 0], [3, 0]]}, + ], + }, + ); + }); + + it('assembles a patch from a file addition', function() { + const p = buildFilePatch([{ + oldPath: null, + oldMode: null, + newPath: 'new/path', + newMode: '100755', + status: 'added', + hunks: [ + { + oldStartLine: 0, + oldLineCount: 0, + newStartLine: 1, + newLineCount: 3, + lines: [ + '+line-0', + '+line-1', + '+line-2', + ], + }, + ], + }]); + + assert.isFalse(p.getOldFile().isPresent()); + assert.isTrue(p.getNewFile().isPresent()); + assert.strictEqual(p.getNewPath(), 'new/path'); + assert.strictEqual(p.getNewMode(), '100755'); + assert.strictEqual(p.getPatch().getStatus(), 'added'); + + const buffer = 'line-0\nline-1\nline-2\n'; + assert.strictEqual(p.getBufferText(), buffer); + + assertInPatch(p).hunks( + { + startRow: 0, + endRow: 2, + header: '@@ -0,0 +1,3 @@', + changes: [ + {kind: 'addition', string: '+line-0\n+line-1\n+line-2\n', range: [[0, 0], [2, 0]]}, + ], + }, + ); + }); + it('throws an error with an unknown diff status character', function() { assert.throws(() => { buildFilePatch([{ From 44745833556eaeba519a045cf120ea10eb14028d Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 22 Aug 2018 09:48:17 -0400 Subject: [PATCH 0190/4252] Bypass readOnly when setting TextEditor contents --- lib/atom/atom-text-editor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/atom/atom-text-editor.js b/lib/atom/atom-text-editor.js index c8af6dbb22..fef2e5ca7b 100644 --- a/lib/atom/atom-text-editor.js +++ b/lib/atom/atom-text-editor.js @@ -82,7 +82,7 @@ export default class AtomTextEditor extends React.PureComponent { this.refElement.map(element => { const editor = element.getModel(); - editor.setText(this.props.text); + editor.setText(this.props.text, {bypassReadOnly: true}); this.refModel.setter(editor); From 6b21f428378e662a600a25db7a92bacf5bb15f03 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 22 Aug 2018 09:48:33 -0400 Subject: [PATCH 0191/4252] nullPatch needs to implement getMaxLineNumberWidth() --- lib/models/patch/patch.js | 4 ++++ test/models/patch/patch.test.js | 1 + 2 files changed, 5 insertions(+) diff --git a/lib/models/patch/patch.js b/lib/models/patch/patch.js index 3e23f94955..0e5289ba85 100644 --- a/lib/models/patch/patch.js +++ b/lib/models/patch/patch.js @@ -361,6 +361,10 @@ export const nullPatch = { return this; }, + getMaxLineNumberWidth() { + return 0; + }, + toString() { return ''; }, diff --git a/test/models/patch/patch.test.js b/test/models/patch/patch.test.js index 398bb95b01..e3b3350d21 100644 --- a/test/models/patch/patch.test.js +++ b/test/models/patch/patch.test.js @@ -489,6 +489,7 @@ describe('Patch', function() { assert.isFalse(nullPatch.isPresent()); assert.strictEqual(nullPatch.toString(), ''); assert.strictEqual(nullPatch.getChangedLineCount(), 0); + assert.strictEqual(nullPatch.getMaxLineNumberWidth(), 0); }); }); From 890c6f6b55460030754846eef4df1625de4b7455 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 22 Aug 2018 16:24:32 -0400 Subject: [PATCH 0192/4252] Pass missing props in FilePatchController tests --- test/controllers/file-patch-controller.test.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/controllers/file-patch-controller.test.js b/test/controllers/file-patch-controller.test.js index ad18b3cb4d..03410e9bdc 100644 --- a/test/controllers/file-patch-controller.test.js +++ b/test/controllers/file-patch-controller.test.js @@ -27,9 +27,16 @@ describe('FilePatchController', function() { function buildApp(overrideProps = {}) { const props = { + repository, stagingStatus: 'unstaged', + relPath: 'a.txt', isPartiallyStaged: false, filePatch, + workspace: atomEnv.workspace, + tooltips: atomEnv.tooltips, + destroy: () => {}, + discardLines: () => {}, + undoLastDiscard: () => {}, ...overrideProps, }; From 1cc0665f9a2a19e3c93533ac1593142d892d041c Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 22 Aug 2018 16:25:04 -0400 Subject: [PATCH 0193/4252] Adapt FilePatchView to the model changes --- lib/views/file-patch-view.js | 235 ++++++++++++++++++++++------------- 1 file changed, 149 insertions(+), 86 deletions(-) diff --git a/lib/views/file-patch-view.js b/lib/views/file-patch-view.js index a13ccbf64a..37ba4beb71 100644 --- a/lib/views/file-patch-view.js +++ b/lib/views/file-patch-view.js @@ -1,6 +1,7 @@ import React, {Fragment} from 'react'; import PropTypes from 'prop-types'; import cx from 'classnames'; +import {Range} from 'atom'; import {autobind} from '../helpers'; import AtomTextEditor from '../atom/atom-text-editor'; @@ -11,6 +12,7 @@ import Gutter from '../atom/gutter'; import FilePatchHeaderView from './file-patch-header-view'; import FilePatchMetaView from './file-patch-meta-view'; import HunkHeaderView from './hunk-header-view'; +import RefHolder from '../models/ref-holder'; const executableText = { 100644: 'non executable', @@ -55,36 +57,32 @@ export default class FilePatchView extends React.Component { 'oldLineNumberLabel', 'newLineNumberLabel', ); - const presentedFilePatch = this.props.filePatch.present(); - const selectedLines = this.props.selection.getSelectedLines(); this.state = { lastSelection: this.props.selection, selectedHunks: this.props.selection.getSelectedHunks(), - selectedLines, - presentedFilePatch, - selectedLinePositions: Array.from(selectedLines, line => presentedFilePatch.getPositionForLine(line)), + selectedLines: this.props.selection.getSelectedLines(), + selectedLineRanges: Array.from( + this.props.selection.getSelectedLines(), + line => Range.fromObject([[line, 0], [line, 0]]), + ), }; this.lastMouseMoveLine = null; + this.hunksByMarkerID = new Map(); + this.hunkMarkerLayerHolder = new RefHolder(); } static getDerivedStateFromProps(props, state) { const nextState = {}; - let currentPresentedFilePatch = state.presentedFilePatch; - - if (props.filePatch !== state.presentedFilePatch.getFilePatch()) { - currentPresentedFilePatch = props.filePatch.present(); - nextState.presentedFilePatch = currentPresentedFilePatch; - } if (props.selection !== state.lastSelection) { nextState.lastSelection = props.selection; nextState.selectedHunks = props.selection.getSelectedHunks(); nextState.selectedLines = props.selection.getSelectedLines(); - - nextState.selectedLinePositions = Array.from(nextState.selectedLines, line => { - return currentPresentedFilePatch.getPositionForLine(line); - }); + nextState.selectedLineRanges = Array.from( + props.selection.getSelectedLines(), + line => Range.fromObject([[line, 0], [line, 0]]), + ); } return nextState; @@ -121,11 +119,12 @@ export default class FilePatchView extends React.Component {
+ autoHeight={false} + readOnly={true}> { - const isSelected = selectedHunks.has(hunk); - let buttonSuffix = (isHunkSelectionMode || !isSelected) ? ' Hunk' : ' Selection'; - if (isSelected && selectedHunks.size > 1) { - buttonSuffix += 's'; - } - const toggleSelectionLabel = `${toggleVerb}${buttonSuffix}`; - const discardSelectionLabel = `Discard${buttonSuffix}`; - - const onDidChange = event => { - hunkStartPositions[index] = event.newPosition; - }; - - return ( - - - - this.toggleSelection(hunk)} - discardSelection={() => this.discardSelection(hunk)} - mouseDown={this.props.mouseDownOnHeader} - /> - - - ); - }); + return ( + + {/* + The markers on this layer are used to efficiently locate Hunks based on buffer row. + See .getHunkAt(). + */} + + {this.props.filePatch.getHunks().map((hunk, index) => { + return ( + { this.hunksByMarkerID.set(id, hunk); }} + /> + ); + })} + + {/* + These markers are decorated to position hunk headers as block decorations. + */} + + {this.props.filePatch.getHunks().map((hunk, index) => { + const isSelected = selectedHunks.has(hunk); + let buttonSuffix = (isHunkSelectionMode || !isSelected) ? ' Hunk' : ' Selection'; + if (isSelected && selectedHunks.size > 1) { + buttonSuffix += 's'; + } + const toggleSelectionLabel = `${toggleVerb}${buttonSuffix}`; + const discardSelectionLabel = `Discard${buttonSuffix}`; + + const startPoint = hunk.getBufferRange().start; + const startRange = new Range(startPoint, startPoint); + + return ( + + + this.toggleSelection(hunk)} + discardSelection={() => this.discardSelection(hunk)} + mouseDown={this.props.mouseDownOnHeader} + /> + + + ); + })} + + + ); } - renderLineDecorations(positions, lineClass, {line, gutter}) { - if (positions.length === 0) { + renderLineDecorations(ranges, lineClass, {line, gutter}) { + if (ranges.length === 0) { return null; } return ( - {positions.map((position, index) => { - const onDidChange = event => { - positions[index] = event.newPosition; - }; - + {ranges.map((range, index) => { return ( ); })} - {line && } + {line && } {gutter && ( - - + + )} @@ -383,10 +404,9 @@ export default class FilePatchView extends React.Component { } didMouseDownOnLineNumber(event) { - const line = this.state.presentedFilePatch.getLineAt(event.bufferRow); - const hunk = this.state.presentedFilePatch.getHunkAt(event.bufferRow); - - if (line === undefined || hunk === undefined) { + const line = event.bufferRow; + const hunk = this.getHunkAt(event.bufferRow); + if (hunk === undefined) { return; } @@ -394,13 +414,13 @@ export default class FilePatchView extends React.Component { } didMouseMoveOnLineNumber(event) { - const line = this.state.presentedFilePatch.getLineAt(event.bufferRow); + const line = event.bufferRow; if (this.lastMouseMoveLine === line || line === undefined) { return; } this.lastMouseMoveLine = line; - const hunk = this.state.presentedFilePatch.getHunkAt(event.bufferRow); + const hunk = this.getHunkAt(event.bufferRow); if (hunk === undefined) { return; } @@ -412,12 +432,31 @@ export default class FilePatchView extends React.Component { this.props.mouseUp(); } - oldLineNumberLabel({bufferRow}) { - return this.pad(this.state.presentedFilePatch.getOldLineNumberAt(bufferRow)); + oldLineNumberLabel({bufferRow, softWrapped}) { + const hunk = this.getHunkAt(bufferRow); + if (hunk === undefined) { + return this.pad(''); + } + + const oldRow = hunk.getOldRowAt(bufferRow); + if (softWrapped) { + return this.pad(oldRow === null ? '' : '•'); + } + + return this.pad(oldRow); } - newLineNumberLabel({bufferRow}) { - return this.pad(this.state.presentedFilePatch.getNewLineNumberAt(bufferRow)); + newLineNumberLabel({bufferRow, softWrapped}) { + const hunk = this.getHunkAt(bufferRow); + if (hunk === undefined) { + return ''; + } + + const newRow = hunk.getNewRowAt(bufferRow); + if (softWrapped) { + return this.pad(newRow === null ? '' : '•'); + } + return this.pad(newRow); } toggleSelection(hunk) { @@ -436,9 +475,33 @@ export default class FilePatchView extends React.Component { } } + getHunkAt(bufferRow) { + const hunkFromMarker = this.hunkMarkerLayerHolder.map(layer => { + const markers = layer.findMarkers({intersectsRow: bufferRow}); + if (markers.length === 0) { + return null; + } + return this.hunksByMarkerID.get(markers[0].id); + }).getOr(null); + + if (hunkFromMarker !== null) { + return hunkFromMarker; + } + + // Fall back to a linear hunk scan. + for (const hunk of this.props.filePatch.getHunks()) { + if (hunk.includesBufferRow(bufferRow)) { + return hunk; + } + } + + // Hunk not found. + return undefined; + } + pad(num) { - const maxDigits = this.state.presentedFilePatch.getMaxLineNumberWidth(); - if (num === undefined || num === -1) { + const maxDigits = this.props.filePatch.getMaxLineNumberWidth(); + if (num === null) { return NBSP_CHARACTER.repeat(maxDigits); } else { return NBSP_CHARACTER.repeat(maxDigits - num.toString().length) + num.toString(); From 2c91e572ba9ba16a5b4e5da86a8cbd01f9c9e16f Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 22 Aug 2018 16:25:19 -0400 Subject: [PATCH 0194/4252] FilePatchView tests :white_check_mark: --- test/views/file-patch-view.test.js | 310 +++++++++++++++++++++ test/views/file-patch-view.test.pending.js | 239 ---------------- 2 files changed, 310 insertions(+), 239 deletions(-) create mode 100644 test/views/file-patch-view.test.js delete mode 100644 test/views/file-patch-view.test.pending.js diff --git a/test/views/file-patch-view.test.js b/test/views/file-patch-view.test.js new file mode 100644 index 0000000000..a8fa414c74 --- /dev/null +++ b/test/views/file-patch-view.test.js @@ -0,0 +1,310 @@ +import path from 'path'; +import fs from 'fs-extra'; +import React from 'react'; +import {shallow, mount} from 'enzyme'; + +import {cloneRepository, buildRepository} from '../helpers'; +import FilePatchView from '../../lib/views/file-patch-view'; +import FilePatchSelection from '../../lib/models/file-patch-selection'; +import {nullFile} from '../../lib/models/patch/file'; +import Hunk from '../../lib/models/patch/hunk'; +import {Addition, Deletion, NoNewline} from '../../lib/models/patch/region'; +import IndexedRowRange from '../../lib/models/indexed-row-range'; + +describe('FilePatchView', function() { + let atomEnv, repository, filePatch; + + beforeEach(async function() { + atomEnv = global.buildAtomEnvironment(); + + const workdirPath = await cloneRepository(); + repository = await buildRepository(workdirPath); + + // a.txt: unstaged changes + await fs.writeFile(path.join(workdirPath, 'a.txt'), 'changed\n'); + filePatch = await repository.getFilePatchForPath('a.txt', {staged: false}); + }); + + afterEach(function() { + atomEnv.destroy(); + }); + + function buildApp(overrideProps = {}) { + const props = { + relPath: 'a.txt', + stagingStatus: 'unstaged', + isPartiallyStaged: false, + filePatch, + selection: new FilePatchSelection(filePatch.getHunks()), + repository, + + tooltips: atomEnv.tooltips, + + mouseDownOnHeader: () => {}, + mouseDownOnLineNumber: () => {}, + mouseMoveOnLineNumber: () => {}, + mouseUp: () => {}, + + diveIntoMirrorPatch: () => {}, + openFile: () => {}, + toggleFile: () => {}, + selectAndToggleHunk: () => {}, + toggleLines: () => {}, + toggleModeChange: () => {}, + toggleSymlinkChange: () => {}, + undoLastDiscard: () => {}, + discardLines: () => {}, + selectAndDiscardHunk: () => {}, + + ...overrideProps, + }; + + return ; + } + + it('renders the file header', function() { + const wrapper = shallow(buildApp()); + assert.isTrue(wrapper.find('FilePatchHeaderView').exists()); + }); + + it('renders the file patch within an editor', function() { + const wrapper = mount(buildApp()); + + const editor = wrapper.find('AtomTextEditor'); + assert.strictEqual(editor.instance().getModel().getText(), filePatch.getBufferText()); + }); + + describe('executable mode changes', function() { + it('does not render if the mode has not changed', function() { + const fp = filePatch.clone({ + oldFile: filePatch.getOldFile().clone({mode: '100644'}), + newFile: filePatch.getNewFile().clone({mode: '100644'}), + }); + + const wrapper = shallow(buildApp({filePatch: fp})); + assert.isFalse(wrapper.find('FilePatchMetaView[title="Mode change"]').exists()); + }); + + it('renders change details within a meta container', function() { + const fp = filePatch.clone({ + oldFile: filePatch.getOldFile().clone({mode: '100644'}), + newFile: filePatch.getNewFile().clone({mode: '100755'}), + }); + + const wrapper = mount(buildApp({filePatch: fp, stagingStatus: 'unstaged'})); + + const meta = wrapper.find('FilePatchMetaView[title="Mode change"]'); + assert.strictEqual(meta.prop('actionIcon'), 'icon-move-down'); + assert.strictEqual(meta.prop('actionText'), 'Stage Mode Change'); + + const details = meta.find('.github-FilePatchView-metaDetails'); + assert.strictEqual(details.text(), 'File changed modefrom non executable 100644to executable 100755'); + }); + + it("stages or unstages the mode change when the meta container's action is triggered", function() { + const fp = filePatch.clone({ + oldFile: filePatch.getOldFile().clone({mode: '100644'}), + newFile: filePatch.getNewFile().clone({mode: '100755'}), + }); + + const toggleModeChange = sinon.stub(); + const wrapper = shallow(buildApp({filePatch: fp, stagingStatus: 'staged', toggleModeChange})); + + const meta = wrapper.find('FilePatchMetaView[title="Mode change"]'); + assert.isTrue(meta.exists()); + assert.strictEqual(meta.prop('actionIcon'), 'icon-move-up'); + assert.strictEqual(meta.prop('actionText'), 'Unstage Mode Change'); + + meta.prop('action')(); + assert.isTrue(toggleModeChange.called); + }); + }); + + describe('symlink changes', function() { + it('does not render if the symlink status is unchanged', function() { + const fp = filePatch.clone({ + oldFile: filePatch.getOldFile().clone({mode: '100644'}), + newFile: filePatch.getNewFile().clone({mode: '100755'}), + }); + + const wrapper = mount(buildApp({filePatch: fp})); + assert.lengthOf(wrapper.find('FilePatchMetaView').filterWhere(v => v.prop('title').startsWith('Symlink')), 0); + }); + + it('renders symlink change information within a meta container', function() { + const fp = filePatch.clone({ + oldFile: filePatch.getOldFile().clone({mode: '120000', symlink: '/old.txt'}), + newFile: filePatch.getNewFile().clone({mode: '120000', symlink: '/new.txt'}), + }); + + const wrapper = mount(buildApp({filePatch: fp, stagingStatus: 'unstaged'})); + const meta = wrapper.find('FilePatchMetaView[title="Symlink changed"]'); + assert.isTrue(meta.exists()); + assert.strictEqual(meta.prop('actionIcon'), 'icon-move-down'); + assert.strictEqual(meta.prop('actionText'), 'Stage Symlink Change'); + assert.strictEqual( + meta.find('.github-FilePatchView-metaDetails').text(), + 'Symlink changedfrom /old.txtto /new.txt.', + ); + }); + + it('stages or unstages the symlink change', function() { + const toggleSymlinkChange = sinon.stub(); + const fp = filePatch.clone({ + oldFile: filePatch.getOldFile().clone({mode: '120000', symlink: '/old.txt'}), + newFile: filePatch.getNewFile().clone({mode: '120000', symlink: '/new.txt'}), + }); + + const wrapper = mount(buildApp({filePatch: fp, stagingStatus: 'staged', toggleSymlinkChange})); + const meta = wrapper.find('FilePatchMetaView[title="Symlink changed"]'); + assert.isTrue(meta.exists()); + assert.strictEqual(meta.prop('actionIcon'), 'icon-move-up'); + assert.strictEqual(meta.prop('actionText'), 'Unstage Symlink Change'); + + meta.find('button.icon-move-up').simulate('click'); + assert.isTrue(toggleSymlinkChange.called); + }); + + it('renders details for a symlink deletion', function() { + const fp = filePatch.clone({ + oldFile: filePatch.getOldFile().clone({mode: '120000', symlink: '/old.txt'}), + newFile: nullFile, + }); + + const wrapper = mount(buildApp({filePatch: fp})); + const meta = wrapper.find('FilePatchMetaView[title="Symlink deleted"]'); + assert.isTrue(meta.exists()); + assert.strictEqual( + meta.find('.github-FilePatchView-metaDetails').text(), + 'Symlinkto /old.txtdeleted.', + ); + }); + + it('renders details for a symlink creation', function() { + const fp = filePatch.clone({ + oldFile: nullFile, + newFile: filePatch.getOldFile().clone({mode: '120000', symlink: '/new.txt'}), + }); + + const wrapper = mount(buildApp({filePatch: fp})); + const meta = wrapper.find('FilePatchMetaView[title="Symlink created"]'); + assert.isTrue(meta.exists()); + assert.strictEqual( + meta.find('.github-FilePatchView-metaDetails').text(), + 'Symlinkto /new.txtcreated.', + ); + }); + }); + + it('renders a header for each hunk', function() { + const bufferText = '0000\n0001\n0002\n0003\n0004\n0005\n'; + const hunks = [ + new Hunk({ + oldStartRow: 1, oldRowCount: 2, newStartRow: 1, newRowCount: 3, + sectionHeading: 'first hunk', + rowRange: new IndexedRowRange({bufferRange: [[0, 0], [2, 0]], startOffset: 0, endOffset: 15}), + changes: [ + new Addition(new IndexedRowRange({bufferRange: [[1, 0], [1, 0]], startOffset: 5, endOffset: 10})), + ], + }), + new Hunk({ + oldStartRow: 10, oldRowCount: 3, newStartRow: 11, newRowCount: 2, + sectionHeading: 'second hunk', + rowRange: new IndexedRowRange({bufferRange: [[3, 0], [5, 0]], startOffset: 15, endOffset: 30}), + changes: [ + new Deletion(new IndexedRowRange({bufferRange: [[4, 0], [4, 0]], startOffset: 5, endOffset: 10})), + ], + }), + ]; + const fp = filePatch.clone({ + patch: filePatch.getPatch().clone({hunks, bufferText}), + }); + const wrapper = mount(buildApp({filePatch: fp})); + assert.isTrue(wrapper.find('HunkHeaderView').someWhere(h => h.prop('hunk') === hunks[0])); + assert.isTrue(wrapper.find('HunkHeaderView').someWhere(h => h.prop('hunk') === hunks[1])); + }); + + describe('hunk lines', function() { + let linesPatch; + + beforeEach(function() { + const bufferText = + '0000\n0001\n0002\n0003\n0004\n0005\n0006\n0007\n0008\n0009\n' + + '0010\n0011\n0012\n0013\n0014\n0015\n0016\n' + + ' No newline at end of file\n'; + const hunks = [ + new Hunk({ + oldStartRow: 1, oldRowCount: 3, newStartRow: 1, newRowCount: 6, + sectionHeading: 'first hunk', + rowRange: new IndexedRowRange({bufferRange: [[0, 0], [6, 0]], startOffset: 0, endOffset: 35}), + changes: [ + new Addition(new IndexedRowRange({bufferRange: [[1, 0], [2, 0]], startOffset: 5, endOffset: 15})), + new Deletion(new IndexedRowRange({bufferRange: [[3, 0], [3, 0]], startOffset: 15, endOffset: 20})), + new Addition(new IndexedRowRange({bufferRange: [[4, 0], [5, 0]], startOffset: 20, endOffset: 30})), + ], + }), + new Hunk({ + oldStartRow: 10, oldRowCount: 0, newStartRow: 13, newRowCount: 0, + sectionHeading: 'second hunk', + rowRange: new IndexedRowRange({bufferRange: [[7, 0], [17, 0]], startOffset: 35, endOffset: 112}), + changes: [ + new Deletion(new IndexedRowRange({bufferRange: [[8, 0], [10, 0]], startOffset: 40, endOffset: 55})), + new Addition(new IndexedRowRange({bufferRange: [[12, 0], [14, 0]], startOffset: 60, endOffset: 75})), + new Deletion(new IndexedRowRange({bufferRange: [[15, 0], [15, 0]], startOffset: 75, endOffset: 80})), + new NoNewline(new IndexedRowRange({bufferRange: [[17, 0], [17, 0]], startOffset: 85, endOffset: 112})), + ], + }), + ]; + + linesPatch = filePatch.clone({ + patch: filePatch.getPatch().clone({hunks, bufferText}), + }); + }); + + it('decorates added lines', function() { + const wrapper = mount(buildApp({filePatch: linesPatch})); + + const decorationSelector = 'Decoration[type="line"][className="github-FilePatchView-line--added"]'; + const decoration = wrapper.find(decorationSelector); + assert.isTrue(decoration.exists()); + + const layer = wrapper.find('MarkerLayer').filterWhere(each => each.find(decorationSelector).exists()); + const markers = layer.find('Marker').map(marker => marker.prop('bufferRange').serialize()); + assert.deepEqual(markers, [ + [[1, 0], [2, 0]], + [[4, 0], [5, 0]], + [[12, 0], [14, 0]], + ]); + }); + + it('decorates deleted lines', function() { + const wrapper = mount(buildApp({filePatch: linesPatch})); + + const decorationSelector = 'Decoration[type="line"][className="github-FilePatchView-line--deleted"]'; + const decoration = wrapper.find(decorationSelector); + assert.isTrue(decoration.exists()); + + const layer = wrapper.find('MarkerLayer').filterWhere(each => each.find(decorationSelector).exists()); + const markers = layer.find('Marker').map(marker => marker.prop('bufferRange').serialize()); + assert.deepEqual(markers, [ + [[3, 0], [3, 0]], + [[8, 0], [10, 0]], + [[15, 0], [15, 0]], + ]); + }); + + it('decorates the nonewline line', function() { + const wrapper = mount(buildApp({filePatch: linesPatch})); + + const decorationSelector = 'Decoration[type="line"][className="github-FilePatchView-line--nonewline"]'; + const decoration = wrapper.find(decorationSelector); + assert.isTrue(decoration.exists()); + + const layer = wrapper.find('MarkerLayer').filterWhere(each => each.find(decorationSelector).exists()); + const markers = layer.find('Marker').map(marker => marker.prop('bufferRange').serialize()); + assert.deepEqual(markers, [ + [[17, 0], [17, 0]], + ]); + }); + }); +}); diff --git a/test/views/file-patch-view.test.pending.js b/test/views/file-patch-view.test.pending.js deleted file mode 100644 index 2209313379..0000000000 --- a/test/views/file-patch-view.test.pending.js +++ /dev/null @@ -1,239 +0,0 @@ -import path from 'path'; -import fs from 'fs-extra'; -import React from 'react'; -import {shallow, mount} from 'enzyme'; - -import {cloneRepository, buildRepository} from '../helpers'; -import Hunk from '../../lib/models/hunk'; -import HunkLine from '../../lib/models/hunk-line'; -import FilePatchView from '../../lib/views/file-patch-view'; - -describe('FilePatchView', function() { - let atomEnv, repository, filePatch; - - beforeEach(async function() { - atomEnv = global.buildAtomEnvironment(); - - const workdirPath = await cloneRepository(); - repository = await buildRepository(workdirPath); - - // a.txt: unstaged changes - await fs.writeFile(path.join(workdirPath, 'a.txt'), 'changed\n'); - filePatch = await repository.getFilePatchForPath('a.txt', {staged: false}); - }); - - afterEach(function() { - atomEnv.destroy(); - }); - - function buildApp(overrideProps = {}) { - const props = { - relPath: 'a.txt', - stagingStatus: 'unstaged', - isPartiallyStaged: false, - filePatch, - repository, - tooltips: atomEnv.tooltips, - - undoLastDiscard: () => {}, - diveIntoMirrorPatch: () => {}, - openFile: () => {}, - toggleFile: () => {}, - toggleModeChange: () => {}, - toggleSymlinkChange: () => {}, - - ...overrideProps, - }; - - return ; - } - - it('renders the file header', function() { - const wrapper = shallow(buildApp()); - assert.isTrue(wrapper.find('FilePatchHeaderView').exists()); - }); - - it('renders the file patch within an editor', function() { - const wrapper = mount(buildApp()); - - const editor = wrapper.find('AtomTextEditor'); - assert.strictEqual(editor.instance().getModel().getText(), filePatch.present().getText()); - }); - - describe('executable mode changes', function() { - it('does not render if the mode has not changed', function() { - sinon.stub(filePatch, 'getOldMode').returns('100644'); - sinon.stub(filePatch, 'getNewMode').returns('100644'); - - const wrapper = shallow(buildApp()); - assert.isFalse(wrapper.find('FilePatchMetaView[title="Mode change"]').exists()); - }); - - it('renders change details within a meta container', function() { - sinon.stub(filePatch, 'getOldMode').returns('100644'); - sinon.stub(filePatch, 'getNewMode').returns('100755'); - - const wrapper = mount(buildApp({stagingStatus: 'unstaged'})); - - const meta = wrapper.find('FilePatchMetaView[title="Mode change"]'); - assert.isTrue(meta.exists()); - assert.strictEqual(meta.prop('actionIcon'), 'icon-move-down'); - assert.strictEqual(meta.prop('actionText'), 'Stage Mode Change'); - - const details = meta.find('.github-FilePatchView-metaDetails'); - assert.strictEqual(details.text(), 'File changed modefrom non executable 100644to executable 100755'); - }); - - it("stages or unstages the mode change when the meta container's action is triggered", function() { - sinon.stub(filePatch, 'getOldMode').returns('100644'); - sinon.stub(filePatch, 'getNewMode').returns('100755'); - - const toggleModeChange = sinon.stub(); - const wrapper = shallow(buildApp({stagingStatus: 'staged', toggleModeChange})); - - const meta = wrapper.find('FilePatchMetaView[title="Mode change"]'); - assert.isTrue(meta.exists()); - assert.strictEqual(meta.prop('actionIcon'), 'icon-move-up'); - assert.strictEqual(meta.prop('actionText'), 'Unstage Mode Change'); - - meta.prop('action')(); - assert.isTrue(toggleModeChange.called); - }); - }); - - describe('symlink changes', function() { - it('does not render if the symlink status is unchanged', function() { - const wrapper = mount(buildApp()); - assert.lengthOf(wrapper.find('FilePatchMetaView').filterWhere(v => v.prop('title').startsWith('Symlink')), 0); - }); - - it('renders symlink change information within a meta container', function() { - sinon.stub(filePatch, 'hasSymlink').returns(true); - sinon.stub(filePatch, 'getOldSymlink').returns('/old.txt'); - sinon.stub(filePatch, 'getNewSymlink').returns('/new.txt'); - - const wrapper = mount(buildApp({stagingStatus: 'unstaged'})); - const meta = wrapper.find('FilePatchMetaView[title="Symlink changed"]'); - assert.isTrue(meta.exists()); - assert.strictEqual(meta.prop('actionIcon'), 'icon-move-down'); - assert.strictEqual(meta.prop('actionText'), 'Stage Symlink Change'); - assert.strictEqual( - meta.find('.github-FilePatchView-metaDetails').text(), - 'Symlink changedfrom /old.txtto /new.txt.', - ); - }); - - it('stages or unstages the symlink change', function() { - const toggleSymlinkChange = sinon.stub(); - sinon.stub(filePatch, 'hasSymlink').returns(true); - sinon.stub(filePatch, 'getOldSymlink').returns('/old.txt'); - sinon.stub(filePatch, 'getNewSymlink').returns('/new.txt'); - - const wrapper = mount(buildApp({stagingStatus: 'staged', toggleSymlinkChange})); - const meta = wrapper.find('FilePatchMetaView[title="Symlink changed"]'); - assert.isTrue(meta.exists()); - assert.strictEqual(meta.prop('actionIcon'), 'icon-move-up'); - assert.strictEqual(meta.prop('actionText'), 'Unstage Symlink Change'); - - meta.find('button.icon-move-up').simulate('click'); - assert.isTrue(toggleSymlinkChange.called); - }); - - it('renders details for a symlink deletion', function() { - sinon.stub(filePatch, 'hasSymlink').returns(true); - sinon.stub(filePatch, 'getOldSymlink').returns('/old.txt'); - sinon.stub(filePatch, 'getNewSymlink').returns(null); - - const wrapper = mount(buildApp()); - const meta = wrapper.find('FilePatchMetaView[title="Symlink deleted"]'); - assert.isTrue(meta.exists()); - assert.strictEqual( - meta.find('.github-FilePatchView-metaDetails').text(), - 'Symlinkto /old.txtdeleted.', - ); - }); - - it('renders details for a symlink creation', function() { - sinon.stub(filePatch, 'hasSymlink').returns(true); - sinon.stub(filePatch, 'getOldSymlink').returns(null); - sinon.stub(filePatch, 'getNewSymlink').returns('/new.txt'); - - const wrapper = mount(buildApp()); - const meta = wrapper.find('FilePatchMetaView[title="Symlink created"]'); - assert.isTrue(meta.exists()); - assert.strictEqual( - meta.find('.github-FilePatchView-metaDetails').text(), - 'Symlinkto /new.txtcreated.', - ); - }); - }); - - it('renders a header for each hunk', function() { - const hunks = [ - new Hunk(0, 0, 5, 5, 'hunk 0', []), - new Hunk(10, 10, 15, 15, 'hunk 1', []), - ]; - sinon.stub(filePatch, 'getHunks').returns(hunks); - - const wrapper = mount(buildApp()); - assert.isTrue(wrapper.find('HunkHeaderView').someWhere(h => h.prop('hunk') === hunks[0])); - assert.isTrue(wrapper.find('HunkHeaderView').someWhere(h => h.prop('hunk') === hunks[1])); - }); - - describe('hunk lines', function() { - it('decorates added lines', function() { - const hunks = [ - new Hunk(0, 0, 1, 1, 'hunk 0', [ - new HunkLine('line 0', 'added', 0, 1, 0), - new HunkLine('line 1', 'deleted', 0, 1, 0), - ]), - ]; - sinon.stub(filePatch, 'getHunks').returns(hunks); - - const wrapper = mount(buildApp()); - assert.lengthOf( - wrapper.find('Decoration').filterWhere(h => { - return h.prop('type') === 'line' && h.prop('className') === 'github-FilePatchView-line--added'; - }), - 1, - ); - }); - - it('decorates deleted lines', function() { - const hunks = [ - new Hunk(0, 0, 1, 1, 'hunk 0', [ - new HunkLine('line 0', 'added', 0, 1, 0), - new HunkLine('line 1', 'deleted', 0, 1, 0), - ]), - ]; - sinon.stub(filePatch, 'getHunks').returns(hunks); - - const wrapper = mount(buildApp()); - assert.lengthOf( - wrapper.find('Decoration').filterWhere(h => { - return h.prop('type') === 'line' && h.prop('className') === 'github-FilePatchView-line--deleted'; - }), - 1, - ); - }); - - it('decorates the nonewline line', function() { - const hunks = [ - new Hunk(0, 0, 1, 1, 'hunk 0', [ - new HunkLine('line 0', 'added', 0, 1, 0), - new HunkLine('line 1', 'deleted', 0, 1, 0), - new HunkLine('no newline', 'nonewline', 0, 1, 0), - ]), - ]; - sinon.stub(filePatch, 'getHunks').returns(hunks); - - const wrapper = mount(buildApp()); - assert.lengthOf( - wrapper.find('Decoration').filterWhere(h => { - return h.prop('type') === 'line' && h.prop('className') === 'github-FilePatchView-line--nonewline'; - }), - 1, - ); - }); - }); -}); From 3eca8ce673b31f71902717bf6ceed328e5cbd27f Mon Sep 17 00:00:00 2001 From: Tilde Ann Thurium Date: Wed, 22 Aug 2018 14:42:43 -0700 Subject: [PATCH 0195/4252] add usage tracking for clicking the `ChangedFilesCount` component --- lib/views/changed-files-count-view.js | 8 +- package-lock.json | 3191 +++++++++---------- test/views/changed-files-count-view.test.js | 42 + 3 files changed, 1644 insertions(+), 1597 deletions(-) create mode 100644 test/views/changed-files-count-view.test.js diff --git a/lib/views/changed-files-count-view.js b/lib/views/changed-files-count-view.js index 051e824614..b3a1bbfea6 100644 --- a/lib/views/changed-files-count-view.js +++ b/lib/views/changed-files-count-view.js @@ -1,6 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import Octicon from '../atom/octicon'; +import {addEvent} from '../reporter-proxy'; export default class ChangedFilesCountView extends React.Component { static propTypes = { @@ -15,6 +16,11 @@ export default class ChangedFilesCountView extends React.Component { didClick: () => {}, } + handleClick() { + addEvent('click', {package: 'github', component: 'ChangedFileCountView'}); + this.props.didClick(); + } + render() { const label = (this.props.changedFilesCount === 1) @@ -24,7 +30,7 @@ export default class ChangedFilesCountView extends React.Component { -
- ); - } - - renderHunks() { - // Render hunks for symlink change only if 'typechange' (which indicates symlink change AND file content change) - const {symlinkChange} = this.props; - if (symlinkChange && !symlinkChange.typechange) { return null; } - - const selectedHunks = this.state.selection.getSelectedHunks(); - const selectedLines = this.state.selection.getSelectedLines(); - const headHunk = this.state.selection.getHeadHunk(); - const headLine = this.state.selection.getHeadLine(); - const hunkSelectionMode = this.state.selection.getMode() === 'hunk'; - - const unstaged = this.props.stagingStatus === 'unstaged'; - const stageButtonLabelPrefix = unstaged ? 'Stage' : 'Unstage'; - - if (this.props.hunks.length === 0) { - return this.renderEmptyDiffMessage(); - } - - return this.props.hunks.map(hunk => { - const isSelected = selectedHunks.has(hunk); - let stageButtonSuffix = (hunkSelectionMode || !isSelected) ? ' Hunk' : ' Selection'; - if (selectedHunks.size > 1 && selectedHunks.has(hunk)) { - stageButtonSuffix += 's'; - } - const stageButtonLabel = stageButtonLabelPrefix + stageButtonSuffix; - const discardButtonLabel = 'Discard' + stageButtonSuffix; - - return ( - this.mousedownOnHeader(e, hunk)} - mousedownOnLine={this.mousedownOnLine} - mousemoveOnLine={this.mousemoveOnLine} - contextMenuOnItem={this.contextMenuOnItem} - didClickStageButton={() => this.didClickStageButtonForHunk(hunk)} - didClickDiscardButton={() => this.didClickDiscardButtonForHunk(hunk)} - /> - ); - }); - - } - - render() { - const unstaged = this.props.stagingStatus === 'unstaged'; - return ( -
- - {this.registerCommands()} - -
- - {unstaged ? 'Unstaged Changes for ' : 'Staged Changes for '} - {this.props.filePath} - - {this.renderButtonGroup()} -
- -
- {this.props.executableModeChange && this.renderExecutableModeChange(unstaged)} - {this.props.symlinkChange && this.renderSymlinkChange(unstaged)} - {this.props.displayLargeDiffMessage ? this.renderLargeDiffMessage() : this.renderHunks()} -
-
- ); - } - - registerCommands() { - return ( -
- - - - - - - - - - - - - - - - - this.props.isPartiallyStaged && this.props.didDiveIntoCorrespondingFilePatch()} - /> - - this.props.hasUndoHistory && this.props.undoLastDiscard()} - /> - {this.props.executableModeChange && - } - {this.props.symlinkChange && - } - - - - this.props.hasUndoHistory && this.props.undoLastDiscard()} - /> - - -
- ); - } - - renderButtonGroup() { - const unstaged = this.props.stagingStatus === 'unstaged'; - - return ( - - {this.props.hasUndoHistory && unstaged ? ( - - ) : null} - {this.props.isPartiallyStaged || !this.props.hunks.length ? ( - - - ) : null } - - ); - } - - renderExecutableModeChange(unstaged) { - const {executableModeChange} = this.props; - return ( -
-
-
-

Mode change

-
- -
-
-
- File changed mode - - -
-
-
- ); - } - - renderSymlinkChange(unstaged) { - const {symlinkChange} = this.props; - const {oldSymlink, newSymlink} = symlinkChange; - - if (oldSymlink && !newSymlink) { - return ( -
-
-
-

Symlink deleted

-
- -
-
-
- Symlink - - to {oldSymlink} - - deleted. -
-
-
- ); - } else if (!oldSymlink && newSymlink) { - return ( -
-
-
-

Symlink added

-
- -
-
-
- Symlink - - to {newSymlink} - - created. -
-
-
- ); - } else if (oldSymlink && newSymlink) { - return ( -
-
-
-

Symlink changed

-
- -
-
-
- - from {oldSymlink} - - - to {newSymlink} - -
-
-
- ); - } else { - return new Error('Symlink change detected, but missing symlink paths'); - } - } - - componentWillUnmount() { - this.disposables.dispose(); - } - - contextMenuOnItem(event, hunk, line) { - const resend = () => { - const newEvent = new MouseEvent(event.type, event); - setImmediate(() => event.target.parentNode.dispatchEvent(newEvent)); - }; - - const mode = this.state.selection.getMode(); - if (mode === 'hunk' && !this.state.selection.getSelectedHunks().has(hunk)) { - event.stopPropagation(); - - this.setState(prevState => { - return {selection: prevState.selection.selectHunk(hunk, event.shiftKey)}; - }, resend); - } else if (mode === 'line' && !this.state.selection.getSelectedLines().has(line)) { - event.stopPropagation(); - - this.setState(prevState => { - return {selection: prevState.selection.selectLine(line, event.shiftKey)}; - }, resend); - } - } - - mousedownOnHeader(event, hunk) { - if (event.button !== 0) { return; } - const windows = process.platform === 'win32'; - if (event.ctrlKey && !windows) { return; } // simply open context menu - - this.mouseSelectionInProgress = true; - event.persist && event.persist(); - - this.setState(prevState => { - let selection = prevState.selection; - if (event.metaKey || (event.ctrlKey && windows)) { - if (selection.getMode() === 'hunk') { - selection = selection.addOrSubtractHunkSelection(hunk); - } else { - // TODO: optimize - selection = hunk.getLines().reduce( - (current, line) => current.addOrSubtractLineSelection(line).coalesce(), - selection, - ); - } - } else if (event.shiftKey) { - if (selection.getMode() === 'hunk') { - selection = selection.selectHunk(hunk, true); - } else { - const hunkLines = hunk.getLines(); - const tailIndex = selection.getLineSelectionTailIndex(); - const selectedHunkAfterTail = tailIndex < hunkLines[0].diffLineNumber; - if (selectedHunkAfterTail) { - selection = selection.selectLine(hunkLines[hunkLines.length - 1], true); - } else { - selection = selection.selectLine(hunkLines[0], true); - } - } - } else { - selection = selection.selectHunk(hunk, false); - } - - return {selection}; - }); - } - - mousedownOnLine(event, hunk, line) { - if (event.button !== 0) { return; } - const windows = process.platform === 'win32'; - if (event.ctrlKey && !windows) { return; } // simply open context menu - - this.mouseSelectionInProgress = true; - event.persist && event.persist(); - - this.setState(prevState => { - let selection = prevState.selection; - - if (event.metaKey || (event.ctrlKey && windows)) { - if (selection.getMode() === 'hunk') { - selection = selection.addOrSubtractHunkSelection(hunk); - } else { - selection = selection.addOrSubtractLineSelection(line); - } - } else if (event.shiftKey) { - if (selection.getMode() === 'hunk') { - selection = selection.selectHunk(hunk, true); - } else { - selection = selection.selectLine(line, true); - } - } else if (event.detail === 1) { - selection = selection.selectLine(line, false); - } else if (event.detail === 2) { - selection = selection.selectHunk(hunk, false); - } - - return {selection}; - }); - } - - mousemoveOnLine(event, hunk, line) { - if (!this.mouseSelectionInProgress) { return; } - - this.setState(prevState => { - let selection = null; - if (prevState.selection.getMode() === 'hunk') { - selection = prevState.selection.selectHunk(hunk, true); - } else { - selection = prevState.selection.selectLine(line, true); - } - return {selection}; - }); - } - - mouseup() { - this.mouseSelectionInProgress = false; - this.setState(prevState => { - return {selection: prevState.selection.coalesce()}; - }); - } - - togglePatchSelectionMode() { - this.setState(prevState => ({selection: prevState.selection.toggleMode()})); - } - - getPatchSelectionMode() { - return this.state.selection.getMode(); - } - - getSelectedHunks() { - return this.state.selection.getSelectedHunks(); - } - - getSelectedLines() { - return this.state.selection.getSelectedLines(); - } - - selectNext() { - this.setState(prevState => ({selection: prevState.selection.selectNext()})); - } - - selectNextElement() { - if (this.state.selection.isEmpty() && this.props.didSurfaceFile) { - this.props.didSurfaceFile(); - } else { - this.setState(prevState => ({selection: prevState.selection.jumpToNextHunk()})); - } - } - - selectToNext() { - this.setState(prevState => { - return {selection: prevState.selection.selectNext(true).coalesce()}; - }); - } - - selectPrevious() { - this.setState(prevState => ({selection: prevState.selection.selectPrevious()})); - } - - selectPreviousElement() { - if (this.state.selection.isEmpty() && this.props.didSurfaceFile) { - this.props.didSurfaceFile(); - } else { - this.setState(prevState => ({selection: prevState.selection.jumpToPreviousHunk()})); - } - } - - selectToPrevious() { - this.setState(prevState => { - return {selection: prevState.selection.selectPrevious(true).coalesce()}; - }); - } - - selectFirst() { - this.setState(prevState => ({selection: prevState.selection.selectFirst()})); - } - - selectToFirst() { - this.setState(prevState => ({selection: prevState.selection.selectFirst(true)})); - } - - selectLast() { - this.setState(prevState => ({selection: prevState.selection.selectLast()})); - } - - selectToLast() { - this.setState(prevState => ({selection: prevState.selection.selectLast(true)})); - } - - selectAll() { - return new Promise(resolve => { - this.setState(prevState => ({selection: prevState.selection.selectAll()}), resolve); - }); - } - - getNextHunkUpdatePromise() { - return this.state.selection.getNextUpdatePromise(); - } - - didClickStageButtonForHunk(hunk) { - if (this.state.selection.getSelectedHunks().has(hunk)) { - this.props.attemptLineStageOperation(this.state.selection.getSelectedLines()); - } else { - this.setState(prevState => ({selection: prevState.selection.selectHunk(hunk)}), () => { - this.props.attemptHunkStageOperation(hunk); - }); - } - } - - didClickDiscardButtonForHunk(hunk) { - if (this.state.selection.getSelectedHunks().has(hunk)) { - this.discardSelection(); - } else { - this.setState(prevState => ({selection: prevState.selection.selectHunk(hunk)}), () => { - this.discardSelection(); - }); - } - } - - didConfirm() { - return this.didClickStageButtonForHunk([...this.state.selection.getSelectedHunks()][0]); - } - - didMoveRight() { - if (this.props.didSurfaceFile) { - this.props.didSurfaceFile(); - } - } - - focus() { - this.refElement.get().focus(); - } - - openFile() { - let lineNumber = 0; - const firstSelectedLine = Array.from(this.state.selection.getSelectedLines())[0]; - if (firstSelectedLine && firstSelectedLine.newLineNumber > -1) { - lineNumber = firstSelectedLine.newLineNumber; - } else { - const firstSelectedHunk = Array.from(this.state.selection.getSelectedHunks())[0]; - lineNumber = firstSelectedHunk ? firstSelectedHunk.getNewStartRow() : 0; - } - return this.props.openCurrentFile({lineNumber}); - } - - stageOrUnstageAll() { - this.props.attemptFileStageOperation(); - } - - stageOrUnstageModeChange() { - this.props.attemptModeStageOperation(); - } - - stageOrUnstageSymlinkChange() { - this.props.attemptSymlinkStageOperation(); - } - - discardSelection() { - const selectedLines = this.state.selection.getSelectedLines(); - return selectedLines.size ? this.props.discardLines(selectedLines) : null; - } - - goToDiffLine(lineNumber) { - this.setState(prevState => ({selection: prevState.selection.goToDiffLine(lineNumber)})); - } -} diff --git a/lib/views/hunk-view.old.js b/lib/views/hunk-view.old.js deleted file mode 100644 index 794f4887f2..0000000000 --- a/lib/views/hunk-view.old.js +++ /dev/null @@ -1,143 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -import SimpleTooltip from '../atom/simple-tooltip'; -import ContextMenuInterceptor from '../context-menu-interceptor'; -import {autobind} from '../helpers'; - -export default class HunkView extends React.Component { - static propTypes = { - tooltips: PropTypes.object.isRequired, - hunk: PropTypes.object.isRequired, - headHunk: PropTypes.object, - headLine: PropTypes.object, - isSelected: PropTypes.bool.isRequired, - selectedLines: PropTypes.instanceOf(Set).isRequired, - hunkSelectionMode: PropTypes.bool.isRequired, - stageButtonLabel: PropTypes.string.isRequired, - discardButtonLabel: PropTypes.string.isRequired, - unstaged: PropTypes.bool.isRequired, - mousedownOnHeader: PropTypes.func.isRequired, - mousedownOnLine: PropTypes.func.isRequired, - mousemoveOnLine: PropTypes.func.isRequired, - contextMenuOnItem: PropTypes.func.isRequired, - didClickStageButton: PropTypes.func.isRequired, - didClickDiscardButton: PropTypes.func.isRequired, - } - - constructor(props, context) { - super(props, context); - autobind(this, 'mousedownOnLine', 'mousemoveOnLine', 'registerLineElement'); - - this.lineElements = new WeakMap(); - this.lastMousemoveLine = null; - } - - render() { - const hunkSelectedClass = this.props.isSelected ? 'is-selected' : ''; - const hunkModeClass = this.props.hunkSelectionMode ? 'is-hunkMode' : ''; - - return ( -
{ this.element = e; }}> -
this.props.mousedownOnHeader(e)}> - - {this.props.hunk.getHeader().trim()} {this.props.hunk.getSectionHeading().trim()} - - - {this.props.unstaged && - -
- {this.props.hunk.getLines().map((line, idx) => ( - this.props.contextMenuOnItem(e, this.props.hunk, clickedLine)} - /> - ))} -
- ); - } - - mousedownOnLine(event, line) { - this.props.mousedownOnLine(event, this.props.hunk, line); - } - - mousemoveOnLine(event, line) { - if (line !== this.lastMousemoveLine) { - this.lastMousemoveLine = line; - this.props.mousemoveOnLine(event, this.props.hunk, line); - } - } - - registerLineElement(line, element) { - this.lineElements.set(line, element); - } - - componentDidUpdate(prevProps) { - if (prevProps.headLine !== this.props.headLine) { - if (this.props.headLine && this.lineElements.has(this.props.headLine)) { - this.lineElements.get(this.props.headLine).scrollIntoViewIfNeeded(); - } - } - - if (prevProps.headHunk !== this.props.headHunk) { - if (this.props.headHunk === this.props.hunk) { - this.element.scrollIntoViewIfNeeded(); - } - } - } -} - -class LineView extends React.Component { - static propTypes = { - line: PropTypes.object.isRequired, - isSelected: PropTypes.bool.isRequired, - mousedown: PropTypes.func.isRequired, - mousemove: PropTypes.func.isRequired, - contextMenuOnItem: PropTypes.func.isRequired, - registerLineElement: PropTypes.func.isRequired, - } - - render() { - const line = this.props.line; - const oldLineNumber = line.getOldLineNumber() === -1 ? ' ' : line.getOldLineNumber(); - const newLineNumber = line.getNewLineNumber() === -1 ? ' ' : line.getNewLineNumber(); - const lineSelectedClass = this.props.isSelected ? 'is-selected' : ''; - - return ( - this.props.contextMenuOnItem(event, line)}> -
this.props.mousedown(event, line)} - onMouseMove={event => this.props.mousemove(event, line)} - ref={e => this.props.registerLineElement(line, e)}> -
{oldLineNumber}
-
{newLineNumber}
-
- {line.getOrigin()} - {line.getText()} -
-
-
- ); - } -} diff --git a/test/controllers/file-patch-controller.test.old.js b/test/controllers/file-patch-controller.test.old.js deleted file mode 100644 index a7b075e8ec..0000000000 --- a/test/controllers/file-patch-controller.test.old.js +++ /dev/null @@ -1,811 +0,0 @@ -import React from 'react'; -import {shallow, mount} from 'enzyme'; -import until from 'test-until'; - -import fs from 'fs'; -import path from 'path'; - -import {cloneRepository, buildRepository} from '../helpers'; -import FilePatch from '../../lib/models/file-patch'; -import FilePatchController from '../../lib/controllers/file-patch-controller'; -import Hunk from '../../lib/models/hunk'; -import HunkLine from '../../lib/models/hunk-line'; -import ResolutionProgress from '../../lib/models/conflicts/resolution-progress'; -import Switchboard from '../../lib/switchboard'; - -function createFilePatch(oldFilePath, newFilePath, status, hunks) { - const oldFile = new FilePatch.File({path: oldFilePath}); - const newFile = new FilePatch.File({path: newFilePath}); - const patch = new FilePatch.Patch({status, hunks}); - - return new FilePatch(oldFile, newFile, patch); -} - -let atomEnv, commandRegistry, tooltips, deserializers; -let switchboard, getFilePatchForPath; -let discardLines, didSurfaceFile, didDiveIntoFilePath, quietlySelectItem, undoLastDiscard, openFiles, getRepositoryForWorkdir; -let getSelectedStagingViewItems, resolutionProgress; - -function createComponent(repository, filePath) { - atomEnv = global.buildAtomEnvironment(); - commandRegistry = atomEnv.commands; - deserializers = atomEnv.deserializers; - tooltips = atomEnv.tooltips; - - switchboard = new Switchboard(); - - discardLines = sinon.spy(); - didSurfaceFile = sinon.spy(); - didDiveIntoFilePath = sinon.spy(); - quietlySelectItem = sinon.spy(); - undoLastDiscard = sinon.spy(); - openFiles = sinon.spy(); - getSelectedStagingViewItems = sinon.spy(); - - getRepositoryForWorkdir = () => repository; - resolutionProgress = new ResolutionProgress(); - - FilePatchController.resetConfirmedLargeFilePatches(); - - return ( - - ); -} - -async function refreshRepository(wrapper) { - const workDir = wrapper.prop('workingDirectoryPath'); - const repository = wrapper.prop('getRepositoryForWorkdir')(workDir); - - const promise = wrapper.prop('switchboard').getFinishRepositoryRefreshPromise(); - repository.refresh(); - await promise; - wrapper.update(); -} - -describe('FilePatchController', function() { - afterEach(function() { - atomEnv.destroy(); - }); - - describe('unit tests', function() { - let workdirPath, repository, filePath, component; - beforeEach(async function() { - workdirPath = await cloneRepository('multi-line-file'); - repository = await buildRepository(workdirPath); - filePath = 'sample.js'; - component = createComponent(repository, filePath); - - getFilePatchForPath = sinon.stub(repository, 'getFilePatchForPath'); - }); - - describe('when the FilePatch is too large', function() { - it('renders a confirmation widget', async function() { - const hunk1 = new Hunk(0, 0, 1, 1, '', [ - new HunkLine('line-1', 'added', 1, 1), - new HunkLine('line-2', 'added', 2, 2), - new HunkLine('line-3', 'added', 3, 3), - new HunkLine('line-4', 'added', 4, 4), - new HunkLine('line-5', 'added', 5, 5), - new HunkLine('line-6', 'added', 6, 6), - ]); - const filePatch = createFilePatch(filePath, filePath, 'modified', [hunk1]); - - getFilePatchForPath.returns(filePatch); - - const wrapper = mount(React.cloneElement(component, {largeDiffByteThreshold: 5})); - - await assert.async.match(wrapper.text(), /large .+ diff/); - }); - - it('renders the full diff when the confirmation is clicked', async function() { - const hunk = new Hunk(0, 0, 1, 1, '', [ - new HunkLine('line-1', 'added', 1, 1), - new HunkLine('line-2', 'added', 2, 2), - new HunkLine('line-3', 'added', 3, 3), - new HunkLine('line-4', 'added', 4, 4), - new HunkLine('line-5', 'added', 5, 5), - new HunkLine('line-6', 'added', 6, 6), - ]); - const filePatch = createFilePatch(filePath, filePath, 'modified', [hunk]); - getFilePatchForPath.returns(filePatch); - - const wrapper = mount(React.cloneElement(component, {largeDiffByteThreshold: 5})); - - await assert.async.isTrue(wrapper.update().find('.large-file-patch').exists()); - wrapper.find('.large-file-patch').find('button').simulate('click'); - - assert.isTrue(wrapper.find('HunkView').exists()); - }); - - it('renders the full diff if the file has been confirmed before', async function() { - const hunk = new Hunk(0, 0, 1, 1, '', [ - new HunkLine('line-1', 'added', 1, 1), - new HunkLine('line-2', 'added', 2, 2), - new HunkLine('line-3', 'added', 3, 3), - new HunkLine('line-4', 'added', 4, 4), - new HunkLine('line-5', 'added', 5, 5), - new HunkLine('line-6', 'added', 6, 6), - ]); - const filePatch1 = createFilePatch(filePath, filePath, 'modified', [hunk]); - const filePatch2 = createFilePatch('b.txt', 'b.txt', 'modified', [hunk]); - - getFilePatchForPath.returns(filePatch1); - - const wrapper = mount(React.cloneElement(component, { - filePath: filePatch1.getPath(), largeDiffByteThreshold: 5, - })); - - await assert.async.isTrue(wrapper.update().find('.large-file-patch').exists()); - wrapper.find('.large-file-patch').find('button').simulate('click'); - assert.isTrue(wrapper.find('HunkView').exists()); - - getFilePatchForPath.returns(filePatch2); - wrapper.setProps({filePath: filePatch2.getPath()}); - await assert.async.isTrue(wrapper.update().find('.large-file-patch').exists()); - - getFilePatchForPath.returns(filePatch1); - wrapper.setProps({filePath: filePatch1.getPath()}); - assert.isTrue(wrapper.update().find('HunkView').exists()); - }); - }); - - describe('onRepoRefresh', function() { - it('sets the correct FilePatch as state', async function() { - repository.getFilePatchForPath.restore(); - fs.writeFileSync(path.join(workdirPath, filePath), 'change', 'utf8'); - - const wrapper = mount(component); - - await assert.async.isNotNull(wrapper.state('filePatch')); - - const originalFilePatch = wrapper.state('filePatch'); - assert.equal(wrapper.state('stagingStatus'), 'unstaged'); - - fs.writeFileSync(path.join(workdirPath, 'file.txt'), 'change\nand again!', 'utf8'); - await refreshRepository(wrapper); - - assert.notEqual(originalFilePatch, wrapper.state('filePatch')); - assert.equal(wrapper.state('stagingStatus'), 'unstaged'); - }); - }); - - it('renders FilePatchView only if FilePatch has hunks', async function() { - const emptyFilePatch = createFilePatch(filePath, filePath, 'modified', []); - getFilePatchForPath.returns(emptyFilePatch); - - const wrapper = mount(component); - - assert.isTrue(wrapper.find('FilePatchView').exists()); - assert.isTrue(wrapper.find('FilePatchView').text().includes('File has no contents')); - - const hunk1 = new Hunk(0, 0, 1, 1, '', [new HunkLine('line-1', 'added', 1, 1)]); - const filePatch = createFilePatch(filePath, filePath, 'modified', [hunk1]); - getFilePatchForPath.returns(filePatch); - - wrapper.instance().onRepoRefresh(repository); - - await assert.async.isTrue(wrapper.update().find('HunkView').exists()); - assert.isTrue(wrapper.find('HunkView').text().includes('@@ -0,1 +0,1 @@')); - }); - - it('updates the FilePatch after a repo update', async function() { - const hunk1 = new Hunk(5, 5, 2, 1, '', [new HunkLine('line-1', 'added', -1, 5)]); - const hunk2 = new Hunk(8, 8, 1, 1, '', [new HunkLine('line-5', 'deleted', 8, -1)]); - const filePatch0 = createFilePatch(filePath, filePath, 'modified', [hunk1, hunk2]); - getFilePatchForPath.returns(filePatch0); - - const wrapper = shallow(component); - - let view0; - await until(() => { - view0 = wrapper.update().find('FilePatchView').shallow(); - return view0.find({hunk: hunk1}).exists(); - }); - assert.isTrue(view0.find({hunk: hunk2}).exists()); - - const hunk3 = new Hunk(8, 8, 1, 1, '', [new HunkLine('line-10', 'modified', 10, 10)]); - const filePatch1 = createFilePatch(filePath, filePath, 'modified', [hunk1, hunk3]); - getFilePatchForPath.returns(filePatch1); - - wrapper.instance().onRepoRefresh(repository); - let view1; - await until(() => { - view1 = wrapper.update().find('FilePatchView').shallow(); - return view1.find({hunk: hunk3}).exists(); - }); - assert.isTrue(view1.find({hunk: hunk1}).exists()); - assert.isFalse(view1.find({hunk: hunk2}).exists()); - }); - - it('invokes a didSurfaceFile callback with the current file path', async function() { - const filePatch = createFilePatch(filePath, filePath, 'modified', [new Hunk(1, 1, 1, 3, '', [])]); - getFilePatchForPath.returns(filePatch); - - const wrapper = mount(component); - - await assert.async.isTrue(wrapper.update().find('Commands').exists()); - commandRegistry.dispatch(wrapper.find('FilePatchView').getDOMNode(), 'core:move-right'); - assert.isTrue(didSurfaceFile.calledWith(filePath, 'unstaged')); - }); - - describe('openCurrentFile({lineNumber})', () => { - it('sets the cursor on the correct line of the opened text editor', async function() { - const editorSpy = { - relativePath: null, - scrollToBufferPosition: sinon.spy(), - setCursorBufferPosition: sinon.spy(), - }; - - const openFilesStub = relativePaths => { - assert.lengthOf(relativePaths, 1); - editorSpy.relativePath = relativePaths[0]; - return Promise.resolve([editorSpy]); - }; - - const hunk = new Hunk(5, 5, 2, 1, '', [new HunkLine('line-1', 'added', -1, 5)]); - const filePatch = createFilePatch(filePath, filePath, 'modified', [hunk]); - getFilePatchForPath.returns(filePatch); - - const wrapper = mount(React.cloneElement(component, {openFiles: openFilesStub})); - - await assert.async.isTrue(wrapper.update().find('HunkView').exists()); - - wrapper.find('LineView').simulate('mousedown', {button: 0, detail: 1}); - window.dispatchEvent(new MouseEvent('mouseup')); - commandRegistry.dispatch(wrapper.find('FilePatchView').getDOMNode(), 'github:open-file'); - wrapper.update(); - - await assert.async.isTrue(editorSpy.setCursorBufferPosition.called); - - assert.isTrue(editorSpy.relativePath === filePath); - - const scrollCall = editorSpy.scrollToBufferPosition.firstCall; - assert.isTrue(scrollCall.args[0].isEqual([4, 0])); - assert.deepEqual(scrollCall.args[1], {center: true}); - - const cursorCall = editorSpy.setCursorBufferPosition.firstCall; - assert.isTrue(cursorCall.args[0].isEqual([4, 0])); - }); - }); - }); - - describe('integration tests', function() { - describe('handling symlink files', function() { - async function indexModeAndOid(repository, filename) { - const output = await repository.git.exec(['ls-files', '-s', '--', filename]); - if (output) { - const parts = output.split(' '); - return {mode: parts[0], oid: parts[1]}; - } else { - return null; - } - } - - it('unstages added lines that don\'t require symlink change', async function() { - const workingDirPath = await cloneRepository('symlinks'); - const repository = await buildRepository(workingDirPath); - - // correctly handle symlinks on Windows - await repository.git.exec(['config', 'core.symlinks', 'true']); - - const deletedSymlinkAddedFilePath = 'symlink.txt'; - fs.unlinkSync(path.join(workingDirPath, deletedSymlinkAddedFilePath)); - fs.writeFileSync(path.join(workingDirPath, deletedSymlinkAddedFilePath), 'qux\nfoo\nbar\nbaz\nzoo\n', 'utf8'); - - // Stage whole file - await repository.stageFiles([deletedSymlinkAddedFilePath]); - - const component = createComponent(repository, deletedSymlinkAddedFilePath); - const wrapper = mount(React.cloneElement(component, {filePath: deletedSymlinkAddedFilePath, initialStagingStatus: 'staged'})); - - // index shows symlink deltion and added lines - assert.autocrlfEqual(await repository.readFileFromIndex(deletedSymlinkAddedFilePath), 'qux\nfoo\nbar\nbaz\nzoo\n'); - assert.equal((await indexModeAndOid(repository, deletedSymlinkAddedFilePath)).mode, '100644'); - - // Unstage a couple added lines, but not all - await assert.async.isTrue(wrapper.update().find('HunkView').exists()); - const opPromise0 = switchboard.getFinishStageOperationPromise(); - const hunkView0 = wrapper.find('HunkView').at(0); - hunkView0.find('LineView').at(1).find('.github-HunkView-line').simulate('mousedown', {button: 0, detail: 1}); - hunkView0.find('LineView').at(2).find('.github-HunkView-line').simulate('mousemove', {}); - window.dispatchEvent(new MouseEvent('mouseup')); - hunkView0.find('button.github-HunkView-stageButton').simulate('click'); - await opPromise0; - - await refreshRepository(wrapper); - - // index shows symlink deletions still staged, only a couple of lines have been unstaged - assert.equal((await indexModeAndOid(repository, deletedSymlinkAddedFilePath)).mode, '100644'); - assert.autocrlfEqual(await repository.readFileFromIndex(deletedSymlinkAddedFilePath), 'qux\nbaz\nzoo\n'); - }); - - it('stages deleted lines that don\'t require symlink change', async function() { - const workingDirPath = await cloneRepository('symlinks'); - const repository = await buildRepository(workingDirPath); - - const deletedFileAddedSymlinkPath = 'a.txt'; - fs.unlinkSync(path.join(workingDirPath, deletedFileAddedSymlinkPath)); - fs.symlinkSync(path.join(workingDirPath, 'regular-file.txt'), path.join(workingDirPath, deletedFileAddedSymlinkPath)); - - const component = createComponent(repository, deletedFileAddedSymlinkPath); - const wrapper = mount(React.cloneElement(component, {filePath: deletedFileAddedSymlinkPath, initialStagingStatus: 'unstaged'})); - - // index shows file is not a symlink, no deleted lines - assert.equal((await indexModeAndOid(repository, deletedFileAddedSymlinkPath)).mode, '100644'); - assert.autocrlfEqual(await repository.readFileFromIndex(deletedFileAddedSymlinkPath), 'foo\nbar\nbaz\n\n'); - - // stage a couple of lines, but not all - await assert.async.isTrue(wrapper.update().find('HunkView').exists()); - const opPromise0 = switchboard.getFinishStageOperationPromise(); - const hunkView0 = wrapper.find('HunkView').at(0); - hunkView0.find('LineView').at(1).find('.github-HunkView-line').simulate('mousedown', {button: 0, detail: 1}); - hunkView0.find('LineView').at(2).find('.github-HunkView-line').simulate('mousemove', {}); - window.dispatchEvent(new MouseEvent('mouseup')); - hunkView0.find('button.github-HunkView-stageButton').simulate('click'); - await opPromise0; - - await refreshRepository(wrapper); - - // index shows symlink change has not been staged, a couple of lines have been deleted - assert.equal((await indexModeAndOid(repository, deletedFileAddedSymlinkPath)).mode, '100644'); - assert.autocrlfEqual(await repository.readFileFromIndex(deletedFileAddedSymlinkPath), 'foo\n\n'); - }); - - it('stages symlink change when staging added lines that depend on change', async function() { - const workingDirPath = await cloneRepository('symlinks'); - const repository = await buildRepository(workingDirPath); - - // correctly handle symlinks on Windows - await repository.git.exec(['config', 'core.symlinks', 'true']); - - const deletedSymlinkAddedFilePath = 'symlink.txt'; - fs.unlinkSync(path.join(workingDirPath, deletedSymlinkAddedFilePath)); - fs.writeFileSync(path.join(workingDirPath, deletedSymlinkAddedFilePath), 'qux\nfoo\nbar\nbaz\nzoo\n', 'utf8'); - - const component = createComponent(repository, deletedSymlinkAddedFilePath); - const wrapper = mount(React.cloneElement(component, {filePath: deletedSymlinkAddedFilePath})); - - // index shows file is symlink - assert.equal((await indexModeAndOid(repository, deletedSymlinkAddedFilePath)).mode, '120000'); - - // Stage a couple added lines, but not all - await assert.async.isTrue(wrapper.update().find('HunkView').exists()); - const opPromise0 = switchboard.getFinishStageOperationPromise(); - const hunkView0 = wrapper.find('HunkView').at(0); - hunkView0.find('LineView').at(1).find('.github-HunkView-line').simulate('mousedown', {button: 0, detail: 1}); - hunkView0.find('LineView').at(2).find('.github-HunkView-line').simulate('mousemove', {}); - window.dispatchEvent(new MouseEvent('mouseup')); - hunkView0.find('button.github-HunkView-stageButton').simulate('click'); - await opPromise0; - - await refreshRepository(wrapper); - - // index no longer shows file is symlink (symlink has been deleted), now a regular file with contents - assert.equal((await indexModeAndOid(repository, deletedSymlinkAddedFilePath)).mode, '100644'); - assert.autocrlfEqual(await repository.readFileFromIndex(deletedSymlinkAddedFilePath), 'foo\nbar\n'); - }); - - it('unstages symlink change when unstaging deleted lines that depend on change', async function() { - const workingDirPath = await cloneRepository('symlinks'); - const repository = await buildRepository(workingDirPath); - - const deletedFileAddedSymlinkPath = 'a.txt'; - fs.unlinkSync(path.join(workingDirPath, deletedFileAddedSymlinkPath)); - fs.symlinkSync(path.join(workingDirPath, 'regular-file.txt'), path.join(workingDirPath, deletedFileAddedSymlinkPath)); - await repository.stageFiles([deletedFileAddedSymlinkPath]); - - const component = createComponent(repository, deletedFileAddedSymlinkPath); - const wrapper = mount(React.cloneElement(component, {filePath: deletedFileAddedSymlinkPath, initialStagingStatus: 'staged'})); - - // index shows file is symlink - assert.equal((await indexModeAndOid(repository, deletedFileAddedSymlinkPath)).mode, '120000'); - - // unstage a couple of lines, but not all - await assert.async.isTrue(wrapper.update().find('HunkView').exists()); - const opPromise0 = switchboard.getFinishStageOperationPromise(); - const hunkView0 = wrapper.find('HunkView').at(0); - hunkView0.find('LineView').at(1).find('.github-HunkView-line').simulate('mousedown', {button: 0, detail: 1}); - hunkView0.find('LineView').at(2).find('.github-HunkView-line').simulate('mousemove', {}); - window.dispatchEvent(new MouseEvent('mouseup')); - hunkView0.find('button.github-HunkView-stageButton').simulate('click'); - await opPromise0; - - await refreshRepository(wrapper); - - // index no longer shows file is symlink (symlink creation has been unstaged), shows contents of file that existed prior to symlink - assert.equal((await indexModeAndOid(repository, deletedFileAddedSymlinkPath)).mode, '100644'); - assert.autocrlfEqual(await repository.readFileFromIndex(deletedFileAddedSymlinkPath), 'bar\nbaz\n'); - }); - - it('stages file deletion when all deleted lines are staged', async function() { - const workingDirPath = await cloneRepository('symlinks'); - const repository = await buildRepository(workingDirPath); - await repository.getLoadPromise(); - - const deletedFileAddedSymlinkPath = 'a.txt'; - fs.unlinkSync(path.join(workingDirPath, deletedFileAddedSymlinkPath)); - fs.symlinkSync(path.join(workingDirPath, 'regular-file.txt'), path.join(workingDirPath, deletedFileAddedSymlinkPath)); - - const component = createComponent(repository, deletedFileAddedSymlinkPath); - const wrapper = mount(React.cloneElement(component, {filePath: deletedFileAddedSymlinkPath})); - - assert.equal((await indexModeAndOid(repository, deletedFileAddedSymlinkPath)).mode, '100644'); - - // stage all deleted lines - await assert.async.isTrue(wrapper.update().find('HunkView').exists()); - const opPromise0 = switchboard.getFinishStageOperationPromise(); - const hunkView0 = wrapper.find('HunkView').at(0); - hunkView0.find('.github-HunkView-title').simulate('click'); - hunkView0.find('button.github-HunkView-stageButton').simulate('click'); - await opPromise0; - - await refreshRepository(wrapper); - - // File is not on index, file deletion has been staged - assert.isNull(await indexModeAndOid(repository, deletedFileAddedSymlinkPath)); - const {stagedFiles, unstagedFiles} = await repository.getStatusesForChangedFiles(); - assert.equal(unstagedFiles[deletedFileAddedSymlinkPath], 'added'); - assert.equal(stagedFiles[deletedFileAddedSymlinkPath], 'deleted'); - }); - - it('unstages file creation when all added lines are unstaged', async function() { - const workingDirPath = await cloneRepository('symlinks'); - const repository = await buildRepository(workingDirPath); - - await repository.git.exec(['config', 'core.symlinks', 'true']); - - const deletedSymlinkAddedFilePath = 'symlink.txt'; - fs.unlinkSync(path.join(workingDirPath, deletedSymlinkAddedFilePath)); - fs.writeFileSync(path.join(workingDirPath, deletedSymlinkAddedFilePath), 'qux\nfoo\nbar\nbaz\nzoo\n', 'utf8'); - await repository.stageFiles([deletedSymlinkAddedFilePath]); - - const component = createComponent(repository, deletedSymlinkAddedFilePath); - const wrapper = mount(React.cloneElement(component, {filePath: deletedSymlinkAddedFilePath, initialStagingStatus: 'staged'})); - - assert.equal((await indexModeAndOid(repository, deletedSymlinkAddedFilePath)).mode, '100644'); - - // unstage all added lines - await assert.async.isTrue(wrapper.update().find('HunkView').exists()); - const opPromise0 = switchboard.getFinishStageOperationPromise(); - const hunkView0 = wrapper.find('HunkView').at(0); - hunkView0.find('.github-HunkView-title').simulate('click'); - hunkView0.find('button.github-HunkView-stageButton').simulate('click'); - await opPromise0; - - await refreshRepository(wrapper); - - // File is not on index, file creation has been unstaged - assert.isNull(await indexModeAndOid(repository, deletedSymlinkAddedFilePath)); - const {stagedFiles, unstagedFiles} = await repository.getStatusesForChangedFiles(); - assert.equal(unstagedFiles[deletedSymlinkAddedFilePath], 'added'); - assert.equal(stagedFiles[deletedSymlinkAddedFilePath], 'deleted'); - }); - }); - - describe('handling non-symlink changes', function() { - let workdirPath, repository, filePath, component; - beforeEach(async function() { - workdirPath = await cloneRepository('multi-line-file'); - repository = await buildRepository(workdirPath); - filePath = 'sample.js'; - component = createComponent(repository, filePath); - }); - - it('stages and unstages hunks when the stage button is clicked on hunk views with no individual lines selected', async function() { - const absFilePath = path.join(workdirPath, filePath); - const originalLines = fs.readFileSync(absFilePath, 'utf8').split('\n'); - const unstagedLines = originalLines.slice(); - unstagedLines.splice(1, 1, - 'this is a modified line', - 'this is a new line', - 'this is another new line', - ); - unstagedLines.splice(11, 2, 'this is a modified line'); - fs.writeFileSync(absFilePath, unstagedLines.join('\n')); - - const wrapper = mount(component); - - await assert.async.isTrue(wrapper.update().find('HunkView').exists()); - commandRegistry.dispatch(wrapper.find('FilePatchView').getDOMNode(), 'core:move-down'); - - await assert.async.isTrue(wrapper.update().find('HunkView').exists()); - const hunkView0 = wrapper.find('HunkView').at(0); - assert.isFalse(hunkView0.prop('isSelected')); - const opPromise0 = switchboard.getFinishStageOperationPromise(); - hunkView0.find('button.github-HunkView-stageButton').simulate('click'); - await opPromise0; - - const expectedStagedLines = originalLines.slice(); - expectedStagedLines.splice(1, 1, - 'this is a modified line', - 'this is a new line', - 'this is another new line', - ); - assert.autocrlfEqual(await repository.readFileFromIndex('sample.js'), expectedStagedLines.join('\n')); - const updatePromise0 = switchboard.getChangePatchPromise(); - const stagedFilePatch = await repository.getFilePatchForPath('sample.js', {staged: true}); - wrapper.setState({ - stagingStatus: 'staged', - filePatch: stagedFilePatch, - }); - await updatePromise0; - const hunkView1 = wrapper.find('HunkView').at(0); - const opPromise1 = switchboard.getFinishStageOperationPromise(); - hunkView1.find('button.github-HunkView-stageButton').simulate('click'); - await opPromise1; - assert.autocrlfEqual(await repository.readFileFromIndex('sample.js'), originalLines.join('\n')); - }); - - it('stages and unstages individual lines when the stage button is clicked on a hunk with selected lines', async function() { - const absFilePath = path.join(workdirPath, filePath); - const originalLines = fs.readFileSync(absFilePath, 'utf8').split('\n'); - - // write some unstaged changes - const unstagedLines = originalLines.slice(); - unstagedLines.splice(1, 1, - 'this is a modified line', - 'this is a new line', - 'this is another new line', - ); - unstagedLines.splice(11, 2, 'this is a modified line'); - fs.writeFileSync(absFilePath, unstagedLines.join('\n')); - - // stage a subset of lines from first hunk - const wrapper = mount(component); - - await assert.async.isTrue(wrapper.update().find('HunkView').exists()); - const opPromise0 = switchboard.getFinishStageOperationPromise(); - const hunkView0 = wrapper.find('HunkView').at(0); - hunkView0.find('LineView').at(1).find('.github-HunkView-line').simulate('mousedown', {button: 0, detail: 1}); - hunkView0.find('LineView').at(3).find('.github-HunkView-line').simulate('mousemove', {}); - window.dispatchEvent(new MouseEvent('mouseup')); - hunkView0.find('button.github-HunkView-stageButton').simulate('click'); - await opPromise0; - - await refreshRepository(wrapper); - - const expectedLines0 = originalLines.slice(); - expectedLines0.splice(1, 1, - 'this is a modified line', - 'this is a new line', - ); - assert.autocrlfEqual(await repository.readFileFromIndex('sample.js'), expectedLines0.join('\n')); - - // stage remaining lines in hunk - const opPromise1 = switchboard.getFinishStageOperationPromise(); - wrapper.find('HunkView').at(0).find('button.github-HunkView-stageButton').simulate('click'); - await opPromise1; - - await refreshRepository(wrapper); - - const expectedLines1 = originalLines.slice(); - expectedLines1.splice(1, 1, - 'this is a modified line', - 'this is a new line', - 'this is another new line', - ); - assert.autocrlfEqual(await repository.readFileFromIndex('sample.js'), expectedLines1.join('\n')); - - // unstage a subset of lines from the first hunk - wrapper.setState({stagingStatus: 'staged'}); - await refreshRepository(wrapper); - - const hunkView2 = wrapper.find('HunkView').at(0); - hunkView2.find('LineView').at(1).find('.github-HunkView-line') - .simulate('mousedown', {button: 0, detail: 1}); - window.dispatchEvent(new MouseEvent('mouseup')); - hunkView2.find('LineView').at(2).find('.github-HunkView-line') - .simulate('mousedown', {button: 0, detail: 1, metaKey: true}); - window.dispatchEvent(new MouseEvent('mouseup')); - - const opPromise2 = switchboard.getFinishStageOperationPromise(); - hunkView2.find('button.github-HunkView-stageButton').simulate('click'); - await opPromise2; - - await refreshRepository(wrapper); - - const expectedLines2 = originalLines.slice(); - expectedLines2.splice(2, 0, - 'this is a new line', - 'this is another new line', - ); - assert.autocrlfEqual(await repository.readFileFromIndex('sample.js'), expectedLines2.join('\n')); - - // unstage the rest of the hunk - commandRegistry.dispatch(wrapper.find('FilePatchView').getDOMNode(), 'github:toggle-patch-selection-mode'); - - const opPromise3 = switchboard.getFinishStageOperationPromise(); - wrapper.find('HunkView').at(0).find('button.github-HunkView-stageButton').simulate('click'); - await opPromise3; - - assert.autocrlfEqual(await repository.readFileFromIndex('sample.js'), originalLines.join('\n')); - }); - - // https://github.com/atom/github/issues/417 - describe('when unstaging the last lines/hunks from a file', function() { - it('removes added files from index when last hunk is unstaged', async function() { - const absFilePath = path.join(workdirPath, 'new-file.txt'); - - fs.writeFileSync(absFilePath, 'foo\n'); - await repository.stageFiles(['new-file.txt']); - - const wrapper = mount(React.cloneElement(component, { - filePath: 'new-file.txt', - initialStagingStatus: 'staged', - })); - - await assert.async.isTrue(wrapper.update().find('HunkView').exists()); - - const opPromise = switchboard.getFinishStageOperationPromise(); - wrapper.find('HunkView').at(0).find('button.github-HunkView-stageButton').simulate('click'); - await opPromise; - - const stagedChanges = await repository.getStagedChanges(); - assert.equal(stagedChanges.length, 0); - }); - - it('removes added files from index when last lines are unstaged', async function() { - const absFilePath = path.join(workdirPath, 'new-file.txt'); - - fs.writeFileSync(absFilePath, 'foo\n'); - await repository.stageFiles(['new-file.txt']); - - const wrapper = mount(React.cloneElement(component, { - filePath: 'new-file.txt', - initialStagingStatus: 'staged', - })); - - await assert.async.isTrue(wrapper.update().find('HunkView').exists()); - - const viewNode = wrapper.find('FilePatchView').getDOMNode(); - commandRegistry.dispatch(viewNode, 'github:toggle-patch-selection-mode'); - commandRegistry.dispatch(viewNode, 'core:select-all'); - - const opPromise = switchboard.getFinishStageOperationPromise(); - wrapper.find('HunkView').at(0).find('button.github-HunkView-stageButton').simulate('click'); - await opPromise; - - const stagedChanges = await repository.getStagedChanges(); - assert.lengthOf(stagedChanges, 0); - }); - }); - - // https://github.com/atom/github/issues/341 - describe('when duplicate staging occurs', function() { - it('avoids patch conflicts with pending line staging operations', async function() { - const absFilePath = path.join(workdirPath, filePath); - const originalLines = fs.readFileSync(absFilePath, 'utf8').split('\n'); - - // write some unstaged changes - const unstagedLines = originalLines.slice(); - unstagedLines.splice(1, 0, - 'this is a modified line', - 'this is a new line', - 'this is another new line', - ); - unstagedLines.splice(11, 2, 'this is a modified line'); - fs.writeFileSync(absFilePath, unstagedLines.join('\n')); - - const wrapper = mount(component); - - await assert.async.isTrue(wrapper.update().find('HunkView').exists()); - const hunkView0 = wrapper.find('HunkView').at(0); - hunkView0.find('LineView').at(1).find('.github-HunkView-line') - .simulate('mousedown', {button: 0, detail: 1}); - window.dispatchEvent(new MouseEvent('mouseup')); - - // stage lines in rapid succession - // second stage action is a no-op since the first staging operation is in flight - const line1StagingPromise = switchboard.getFinishStageOperationPromise(); - hunkView0.find('.github-HunkView-stageButton').simulate('click'); - hunkView0.find('.github-HunkView-stageButton').simulate('click'); - await line1StagingPromise; - - const changePatchPromise = switchboard.getChangePatchPromise(); - - // assert that only line 1 has been staged - await refreshRepository(wrapper); // clear the cached file patches - let expectedLines = originalLines.slice(); - expectedLines.splice(1, 0, - 'this is a modified line', - ); - let actualLines = await repository.readFileFromIndex(filePath); - assert.autocrlfEqual(actualLines, expectedLines.join('\n')); - await changePatchPromise; - wrapper.update(); - - const hunkView1 = wrapper.find('HunkView').at(0); - hunkView1.find('LineView').at(2).find('.github-HunkView-line') - .simulate('mousedown', {button: 0, detail: 1}); - window.dispatchEvent(new MouseEvent('mouseup')); - const line2StagingPromise = switchboard.getFinishStageOperationPromise(); - hunkView1.find('.github-HunkView-stageButton').simulate('click'); - await line2StagingPromise; - - // assert that line 2 has now been staged - expectedLines = originalLines.slice(); - expectedLines.splice(1, 0, - 'this is a modified line', - 'this is a new line', - ); - actualLines = await repository.readFileFromIndex(filePath); - assert.autocrlfEqual(actualLines, expectedLines.join('\n')); - }); - - it('avoids patch conflicts with pending hunk staging operations', async function() { - const absFilePath = path.join(workdirPath, filePath); - const originalLines = fs.readFileSync(absFilePath, 'utf8').split('\n'); - - // write some unstaged changes - const unstagedLines = originalLines.slice(); - unstagedLines.splice(1, 0, - 'this is a modified line', - 'this is a new line', - 'this is another new line', - ); - unstagedLines.splice(11, 2, 'this is a modified line'); - fs.writeFileSync(absFilePath, unstagedLines.join('\n')); - - const wrapper = mount(component); - - await assert.async.isTrue(wrapper.update().find('HunkView').exists()); - - // ensure staging the same hunk twice does not cause issues - // second stage action is a no-op since the first staging operation is in flight - const hunk1StagingPromise = switchboard.getFinishStageOperationPromise(); - wrapper.find('HunkView').at(0).find('.github-HunkView-stageButton').simulate('click'); - wrapper.find('HunkView').at(0).find('.github-HunkView-stageButton').simulate('click'); - await hunk1StagingPromise; - - const patchPromise0 = switchboard.getChangePatchPromise(); - await refreshRepository(wrapper); // clear the cached file patches - const modifiedFilePatch = await repository.getFilePatchForPath(filePath); - wrapper.setState({filePatch: modifiedFilePatch}); - await patchPromise0; - - let expectedLines = originalLines.slice(); - expectedLines.splice(1, 0, - 'this is a modified line', - 'this is a new line', - 'this is another new line', - ); - let actualLines = await repository.readFileFromIndex(filePath); - assert.autocrlfEqual(actualLines, expectedLines.join('\n')); - - const hunk2StagingPromise = switchboard.getFinishStageOperationPromise(); - wrapper.find('HunkView').at(0).find('.github-HunkView-stageButton').simulate('click'); - await hunk2StagingPromise; - - expectedLines = originalLines.slice(); - expectedLines.splice(1, 0, - 'this is a modified line', - 'this is a new line', - 'this is another new line', - ); - expectedLines.splice(11, 2, 'this is a modified line'); - actualLines = await repository.readFileFromIndex(filePath); - assert.autocrlfEqual(actualLines, expectedLines.join('\n')); - }); - }); - }); - }); -}); diff --git a/test/models/file-patch.test.old.js b/test/models/file-patch.test.old.js deleted file mode 100644 index 4c2dde5463..0000000000 --- a/test/models/file-patch.test.old.js +++ /dev/null @@ -1,507 +0,0 @@ -import {cloneRepository, buildRepository} from '../helpers'; -import {toGitPathSep} from '../../lib/helpers'; -import path from 'path'; -import fs from 'fs'; -import dedent from 'dedent-js'; - -import FilePatch from '../../lib/models/file-patch'; -import Hunk from '../../lib/models/hunk'; -import HunkLine from '../../lib/models/hunk-line'; - -function createFilePatch(oldFilePath, newFilePath, status, hunks) { - const oldFile = new FilePatch.File({path: oldFilePath}); - const newFile = new FilePatch.File({path: newFilePath}); - const patch = new FilePatch.Patch({status, hunks}); - - return new FilePatch(oldFile, newFile, patch); -} - -// oldStartRow, newStartRow, oldRowCount, newRowCount, sectionHeading, lines - -describe('FilePatch', function() { - it('detects executable mode changes', function() { - const of0 = new FilePatch.File({path: 'a.txt', mode: '100644'}); - const nf0 = new FilePatch.File({path: 'a.txt', mode: '100755'}); - const p0 = new FilePatch.Patch({status: 'modified', hunks: []}); - const fp0 = new FilePatch(of0, nf0, p0); - assert.isTrue(fp0.didChangeExecutableMode()); - - const of1 = new FilePatch.File({path: 'a.txt', mode: '100755'}); - const nf1 = new FilePatch.File({path: 'a.txt', mode: '100644'}); - const p1 = new FilePatch.Patch({status: 'modified', hunks: []}); - const fp1 = new FilePatch(of1, nf1, p1); - assert.isTrue(fp1.didChangeExecutableMode()); - - const of2 = new FilePatch.File({path: 'a.txt', mode: '100755'}); - const nf2 = new FilePatch.File({path: 'a.txt', mode: '100755'}); - const p2 = new FilePatch.Patch({status: 'modified', hunks: []}); - const fp2 = new FilePatch(of2, nf2, p2); - assert.isFalse(fp2.didChangeExecutableMode()); - - const of3 = FilePatch.File.empty(); - const nf3 = new FilePatch.File({path: 'a.txt', mode: '100755'}); - const p3 = new FilePatch.Patch({status: 'modified', hunks: []}); - const fp3 = new FilePatch(of3, nf3, p3); - assert.isFalse(fp3.didChangeExecutableMode()); - - const of4 = FilePatch.File.empty(); - const nf4 = new FilePatch.File({path: 'a.txt', mode: '100755'}); - const p4 = new FilePatch.Patch({status: 'modified', hunks: []}); - const fp4 = new FilePatch(of4, nf4, p4); - assert.isFalse(fp4.didChangeExecutableMode()); - }); - - describe('getStagePatchForLines()', function() { - it('returns a new FilePatch that applies only the specified lines', function() { - const filePatch = createFilePatch('a.txt', 'a.txt', 'modified', [ - new Hunk(1, 1, 1, 3, '', [ - new HunkLine('line-1', 'added', -1, 1), - new HunkLine('line-2', 'added', -1, 2), - new HunkLine('line-3', 'unchanged', 1, 3), - ]), - new Hunk(5, 7, 5, 4, '', [ - new HunkLine('line-4', 'unchanged', 5, 7), - new HunkLine('line-5', 'deleted', 6, -1), - new HunkLine('line-6', 'deleted', 7, -1), - new HunkLine('line-7', 'added', -1, 8), - new HunkLine('line-8', 'added', -1, 9), - new HunkLine('line-9', 'added', -1, 10), - new HunkLine('line-10', 'deleted', 8, -1), - new HunkLine('line-11', 'deleted', 9, -1), - ]), - new Hunk(20, 19, 2, 2, '', [ - new HunkLine('line-12', 'deleted', 20, -1), - new HunkLine('line-13', 'added', -1, 19), - new HunkLine('line-14', 'unchanged', 21, 20), - new HunkLine('No newline at end of file', 'nonewline', -1, -1), - ]), - ]); - const linesFromHunk2 = filePatch.getHunks()[1].getLines().slice(1, 4); - assert.deepEqual(filePatch.getStagePatchForLines(new Set(linesFromHunk2)), createFilePatch( - 'a.txt', 'a.txt', 'modified', [ - new Hunk(5, 5, 5, 4, '', [ - new HunkLine('line-4', 'unchanged', 5, 5), - new HunkLine('line-5', 'deleted', 6, -1), - new HunkLine('line-6', 'deleted', 7, -1), - new HunkLine('line-7', 'added', -1, 6), - new HunkLine('line-10', 'unchanged', 8, 7), - new HunkLine('line-11', 'unchanged', 9, 8), - ]), - ], - )); - - // add lines from other hunks - const linesFromHunk1 = filePatch.getHunks()[0].getLines().slice(0, 1); - const linesFromHunk3 = filePatch.getHunks()[2].getLines().slice(1, 2); - const selectedLines = linesFromHunk2.concat(linesFromHunk1, linesFromHunk3); - assert.deepEqual(filePatch.getStagePatchForLines(new Set(selectedLines)), createFilePatch( - 'a.txt', 'a.txt', 'modified', [ - new Hunk(1, 1, 1, 2, '', [ - new HunkLine('line-1', 'added', -1, 1), - new HunkLine('line-3', 'unchanged', 1, 2), - ]), - new Hunk(5, 6, 5, 4, '', [ - new HunkLine('line-4', 'unchanged', 5, 6), - new HunkLine('line-5', 'deleted', 6, -1), - new HunkLine('line-6', 'deleted', 7, -1), - new HunkLine('line-7', 'added', -1, 7), - new HunkLine('line-10', 'unchanged', 8, 8), - new HunkLine('line-11', 'unchanged', 9, 9), - ]), - new Hunk(20, 18, 2, 3, '', [ - new HunkLine('line-12', 'unchanged', 20, 18), - new HunkLine('line-13', 'added', -1, 19), - new HunkLine('line-14', 'unchanged', 21, 20), - new HunkLine('No newline at end of file', 'nonewline', -1, -1), - ]), - ], - )); - }); - - describe('staging lines from deleted files', function() { - it('handles staging part of the file', function() { - const filePatch = createFilePatch('a.txt', null, 'deleted', [ - new Hunk(1, 0, 3, 0, '', [ - new HunkLine('line-1', 'deleted', 1, -1), - new HunkLine('line-2', 'deleted', 2, -1), - new HunkLine('line-3', 'deleted', 3, -1), - ]), - ]); - const linesFromHunk = filePatch.getHunks()[0].getLines().slice(0, 2); - assert.deepEqual(filePatch.getStagePatchForLines(new Set(linesFromHunk)), createFilePatch( - 'a.txt', 'a.txt', 'modified', [ - new Hunk(1, 1, 3, 1, '', [ - new HunkLine('line-1', 'deleted', 1, -1), - new HunkLine('line-2', 'deleted', 2, -1), - new HunkLine('line-3', 'unchanged', 3, 1), - ]), - ], - )); - }); - - it('handles staging all lines, leaving nothing unstaged', function() { - const filePatch = createFilePatch('a.txt', null, 'deleted', [ - new Hunk(1, 0, 3, 0, '', [ - new HunkLine('line-1', 'deleted', 1, -1), - new HunkLine('line-2', 'deleted', 2, -1), - new HunkLine('line-3', 'deleted', 3, -1), - ]), - ]); - const linesFromHunk = filePatch.getHunks()[0].getLines(); - assert.deepEqual(filePatch.getStagePatchForLines(new Set(linesFromHunk)), createFilePatch( - 'a.txt', null, 'deleted', [ - new Hunk(1, 0, 3, 0, '', [ - new HunkLine('line-1', 'deleted', 1, -1), - new HunkLine('line-2', 'deleted', 2, -1), - new HunkLine('line-3', 'deleted', 3, -1), - ]), - ], - )); - }); - }); - }); - - describe('getUnstagePatchForLines()', function() { - it('returns a new FilePatch that applies only the specified lines', function() { - const filePatch = createFilePatch('a.txt', 'a.txt', 'modified', [ - new Hunk(1, 1, 1, 3, '', [ - new HunkLine('line-1', 'added', -1, 1), - new HunkLine('line-2', 'added', -1, 2), - new HunkLine('line-3', 'unchanged', 1, 3), - ]), - new Hunk(5, 7, 5, 4, '', [ - new HunkLine('line-4', 'unchanged', 5, 7), - new HunkLine('line-5', 'deleted', 6, -1), - new HunkLine('line-6', 'deleted', 7, -1), - new HunkLine('line-7', 'added', -1, 8), - new HunkLine('line-8', 'added', -1, 9), - new HunkLine('line-9', 'added', -1, 10), - new HunkLine('line-10', 'deleted', 8, -1), - new HunkLine('line-11', 'deleted', 9, -1), - ]), - new Hunk(20, 19, 2, 2, '', [ - new HunkLine('line-12', 'deleted', 20, -1), - new HunkLine('line-13', 'added', -1, 19), - new HunkLine('line-14', 'unchanged', 21, 20), - new HunkLine('No newline at end of file', 'nonewline', -1, -1), - ]), - ]); - const lines = new Set(filePatch.getHunks()[1].getLines().slice(1, 5)); - filePatch.getHunks()[2].getLines().forEach(line => lines.add(line)); - assert.deepEqual(filePatch.getUnstagePatchForLines(lines), createFilePatch( - 'a.txt', 'a.txt', 'modified', [ - new Hunk(7, 7, 4, 4, '', [ - new HunkLine('line-4', 'unchanged', 7, 7), - new HunkLine('line-7', 'deleted', 8, -1), - new HunkLine('line-8', 'deleted', 9, -1), - new HunkLine('line-5', 'added', -1, 8), - new HunkLine('line-6', 'added', -1, 9), - new HunkLine('line-9', 'unchanged', 10, 10), - ]), - new Hunk(19, 21, 2, 2, '', [ - new HunkLine('line-13', 'deleted', 19, -1), - new HunkLine('line-12', 'added', -1, 21), - new HunkLine('line-14', 'unchanged', 20, 22), - new HunkLine('No newline at end of file', 'nonewline', -1, -1), - ]), - ], - )); - }); - - describe('unstaging lines from an added file', function() { - it('handles unstaging part of the file', function() { - const filePatch = createFilePatch(null, 'a.txt', 'added', [ - new Hunk(0, 1, 0, 3, '', [ - new HunkLine('line-1', 'added', -1, 1), - new HunkLine('line-2', 'added', -1, 2), - new HunkLine('line-3', 'added', -1, 3), - ]), - ]); - const linesFromHunk = filePatch.getHunks()[0].getLines().slice(0, 2); - assert.deepEqual(filePatch.getUnstagePatchForLines(new Set(linesFromHunk)), createFilePatch( - 'a.txt', 'a.txt', 'modified', [ - new Hunk(1, 1, 3, 1, '', [ - new HunkLine('line-1', 'deleted', 1, -1), - new HunkLine('line-2', 'deleted', 2, -1), - new HunkLine('line-3', 'unchanged', 3, 1), - ]), - ], - )); - }); - - it('handles unstaging all lines, leaving nothign staged', function() { - const filePatch = createFilePatch(null, 'a.txt', 'added', [ - new Hunk(0, 1, 0, 3, '', [ - new HunkLine('line-1', 'added', -1, 1), - new HunkLine('line-2', 'added', -1, 2), - new HunkLine('line-3', 'added', -1, 3), - ]), - ]); - - const linesFromHunk = filePatch.getHunks()[0].getLines(); - assert.deepEqual(filePatch.getUnstagePatchForLines(new Set(linesFromHunk)), createFilePatch( - 'a.txt', null, 'deleted', [ - new Hunk(1, 0, 3, 0, '', [ - new HunkLine('line-1', 'deleted', 1, -1), - new HunkLine('line-2', 'deleted', 2, -1), - new HunkLine('line-3', 'deleted', 3, -1), - ]), - ], - )); - }); - }); - }); - - it('handles newly added files', function() { - const filePatch = createFilePatch(null, 'a.txt', 'added', [ - new Hunk(0, 1, 0, 3, '', [ - new HunkLine('line-1', 'added', -1, 1), - new HunkLine('line-2', 'added', -1, 2), - new HunkLine('line-3', 'added', -1, 3), - ]), - ]); - const linesFromHunk = filePatch.getHunks()[0].getLines().slice(0, 2); - assert.deepEqual(filePatch.getUnstagePatchForLines(new Set(linesFromHunk)), createFilePatch( - 'a.txt', 'a.txt', 'modified', [ - new Hunk(1, 1, 3, 1, '', [ - new HunkLine('line-1', 'deleted', 1, -1), - new HunkLine('line-2', 'deleted', 2, -1), - new HunkLine('line-3', 'unchanged', 3, 1), - ]), - ], - )); - }); - - describe('toString()', function() { - it('converts the patch to the standard textual format', async function() { - const workdirPath = await cloneRepository('multi-line-file'); - const repository = await buildRepository(workdirPath); - - const lines = fs.readFileSync(path.join(workdirPath, 'sample.js'), 'utf8').split('\n'); - lines[0] = 'this is a modified line'; - lines.splice(1, 0, 'this is a new line'); - lines[11] = 'this is a modified line'; - lines.splice(12, 1); - fs.writeFileSync(path.join(workdirPath, 'sample.js'), lines.join('\n')); - - const patch = await repository.getFilePatchForPath('sample.js'); - assert.equal(patch.toString(), dedent` - diff --git a/sample.js b/sample.js - --- a/sample.js - +++ b/sample.js - @@ -1,4 +1,5 @@ - -var quicksort = function () { - +this is a modified line - +this is a new line - var sort = function(items) { - if (items.length <= 1) return items; - var pivot = items.shift(), current, left = [], right = []; - @@ -8,6 +9,5 @@ - } - return sort(left).concat(pivot).concat(sort(right)); - }; - - - - return sort(Array.apply(this, arguments)); - +this is a modified line - }; - - `); - }); - - it('correctly formats new files with no newline at the end', async function() { - const workingDirPath = await cloneRepository('three-files'); - const repo = await buildRepository(workingDirPath); - fs.writeFileSync(path.join(workingDirPath, 'e.txt'), 'qux', 'utf8'); - const patch = await repo.getFilePatchForPath('e.txt'); - - assert.equal(patch.toString(), dedent` - diff --git a/e.txt b/e.txt - new file mode 100644 - --- /dev/null - +++ b/e.txt - @@ -0,0 +1,1 @@ - +qux - \\ No newline at end of file - - `); - }); - - describe('typechange file patches', function() { - it('handles typechange patches for a symlink replaced with a file', async function() { - const workdirPath = await cloneRepository('symlinks'); - const repository = await buildRepository(workdirPath); - - await repository.git.exec(['config', 'core.symlinks', 'true']); - - const deletedSymlinkAddedFilePath = 'symlink.txt'; - fs.unlinkSync(path.join(workdirPath, deletedSymlinkAddedFilePath)); - fs.writeFileSync(path.join(workdirPath, deletedSymlinkAddedFilePath), 'qux\nfoo\nbar\n', 'utf8'); - - const patch = await repository.getFilePatchForPath(deletedSymlinkAddedFilePath); - assert.equal(patch.toString(), dedent` - diff --git a/symlink.txt b/symlink.txt - deleted file mode 120000 - --- a/symlink.txt - +++ /dev/null - @@ -1 +0,0 @@ - -./regular-file.txt - \\ No newline at end of file - diff --git a/symlink.txt b/symlink.txt - new file mode 100644 - --- /dev/null - +++ b/symlink.txt - @@ -0,0 +1,3 @@ - +qux - +foo - +bar - - `); - }); - - it('handles typechange patches for a file replaced with a symlink', async function() { - const workdirPath = await cloneRepository('symlinks'); - const repository = await buildRepository(workdirPath); - - const deletedFileAddedSymlinkPath = 'a.txt'; - fs.unlinkSync(path.join(workdirPath, deletedFileAddedSymlinkPath)); - fs.symlinkSync(path.join(workdirPath, 'regular-file.txt'), path.join(workdirPath, deletedFileAddedSymlinkPath)); - - const patch = await repository.getFilePatchForPath(deletedFileAddedSymlinkPath); - assert.equal(patch.toString(), dedent` - diff --git a/a.txt b/a.txt - deleted file mode 100644 - --- a/a.txt - +++ /dev/null - @@ -1,4 +0,0 @@ - -foo - -bar - -baz - - - diff --git a/a.txt b/a.txt - new file mode 120000 - --- /dev/null - +++ b/a.txt - @@ -0,0 +1 @@ - +${toGitPathSep(path.join(workdirPath, 'regular-file.txt'))} - \\ No newline at end of file - - `); - }); - }); - }); - - describe('getHeaderString()', function() { - it('formats paths with git path separators', function() { - const oldPath = path.join('foo', 'bar', 'old.js'); - const newPath = path.join('baz', 'qux', 'new.js'); - - const patch = createFilePatch(oldPath, newPath, 'modified', []); - assert.equal(patch.getHeaderString(), dedent` - diff --git a/foo/bar/old.js b/baz/qux/new.js - --- a/foo/bar/old.js - +++ b/baz/qux/new.js - - `); - }); - }); - - it('returns the size in bytes from getByteSize()', function() { - const filePatch = createFilePatch('a.txt', 'a.txt', 'modified', [ - new Hunk(1, 1, 1, 3, '', [ - new HunkLine('line-1', 'added', -1, 1), - new HunkLine('line-2', 'added', -1, 2), - new HunkLine('line-3', 'unchanged', 1, 3), - ]), - new Hunk(5, 7, 5, 4, '', [ - new HunkLine('line-4', 'unchanged', 5, 7), - new HunkLine('line-5', 'deleted', 6, -1), - new HunkLine('line-6', 'deleted', 7, -1), - ]), - ]); - - assert.strictEqual(filePatch.getByteSize(), 36); - }); - - describe('present()', function() { - let presented; - - beforeEach(function() { - const patch = createFilePatch('a.txt', 'a.txt', 'modified', [ - new Hunk(1, 1, 1, 3, '@@ -1,1 +2,2', [ - new HunkLine('line-1', 'added', -1, 1), - new HunkLine('line-2', 'added', -1, 2), - new HunkLine('line-3', 'unchanged', 1, 3), - ]), - new Hunk(5, 7, 5, 4, '@@ -3,3 +4,4', [ - new HunkLine('line-4', 'unchanged', 5, 7), - new HunkLine('line-5', 'deleted', 6, -1), - new HunkLine('line-6', 'deleted', 7, -1), - new HunkLine('line-7', 'added', -1, 8), - new HunkLine('line-8', 'added', -1, 9), - new HunkLine('line-9', 'added', -1, 10), - new HunkLine('line-10', 'deleted', 8, -1), - new HunkLine('line-11', 'deleted', 9, -1), - new HunkLine('line-12', 'unchanged', 5, 7), - ]), - new Hunk(20, 19, 2, 2, '@@ -5,5 +6,6', [ - new HunkLine('line-13', 'deleted', 20, -1), - new HunkLine('line-14', 'added', -1, 19), - new HunkLine('line-15', 'unchanged', 21, 20), - new HunkLine('No newline at end of file', 'nonewline', -1, -1), - ]), - ]); - - presented = patch.present(); - }); - - function assertPositions(actualPositions, expectedPositions) { - assert.lengthOf(actualPositions, expectedPositions.length); - for (let i = 0; i < expectedPositions.length; i++) { - const actual = actualPositions[i]; - const expected = expectedPositions[i]; - - assert.isTrue(actual.isEqual(expected), - `range ${i}: ${actual.toString()} does not equal [${expected.map(e => e.toString()).join(', ')}]`); - } - } - - it('unifies hunks into a continuous, unadorned string of text', function() { - const actualText = presented.getText(); - const expectedText = - [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15].map(num => `line-${num}\n`).join('') + - 'No newline at end of file\n'; - - assert.strictEqual(actualText, expectedText); - }); - - it("returns the buffer positions corresponding to each hunk's beginning", function() { - assertPositions(presented.getHunkStartPositions(), [ - [0, 0], [3, 0], [12, 0], - ]); - }); - - it('returns the buffer positions corresponding to unchanged lines', function() { - assertPositions(presented.getUnchangedBufferPositions(), [ - [2, 0], [3, 0], [11, 0], [14, 0], - ]); - }); - - it('returns the buffer positions corresponding to added lines', function() { - assertPositions(presented.getAddedBufferPositions(), [ - [0, 0], [1, 0], [6, 0], [7, 0], [8, 0], [13, 0], - ]); - }); - - it('returns the buffer positions corresponding to deleted lines', function() { - assertPositions(presented.getDeletedBufferPositions(), [ - [4, 0], [5, 0], [9, 0], [10, 0], [12, 0], - ]); - }); - - it('returns the buffer position of a "no newline" trailer', function() { - assertPositions(presented.getNoNewlineBufferPositions(), [ - [15, 0], - ]); - }); - }); -}); diff --git a/test/views/file-patch-view.test.old.js b/test/views/file-patch-view.test.old.js deleted file mode 100644 index 59132587d3..0000000000 --- a/test/views/file-patch-view.test.old.js +++ /dev/null @@ -1,615 +0,0 @@ -import React from 'react'; -import {shallow, mount} from 'enzyme'; - -import FilePatchView from '../../lib/views/file-patch-view'; -import Hunk from '../../lib/models/hunk'; -import HunkLine from '../../lib/models/hunk-line'; - -import {assertEqualSets} from '../helpers'; - -describe('FilePatchView', function() { - let atomEnv, commandRegistry, tooltips, component; - let attemptLineStageOperation, attemptHunkStageOperation, attemptFileStageOperation, attemptSymlinkStageOperation; - let attemptModeStageOperation, discardLines, undoLastDiscard, openCurrentFile; - let didSurfaceFile, didDiveIntoCorrespondingFilePatch, handleShowDiffClick; - - beforeEach(function() { - atomEnv = global.buildAtomEnvironment(); - commandRegistry = atomEnv.commands; - tooltips = atomEnv.tooltips; - - attemptLineStageOperation = sinon.spy(); - attemptHunkStageOperation = sinon.spy(); - attemptModeStageOperation = sinon.spy(); - attemptFileStageOperation = sinon.spy(); - attemptSymlinkStageOperation = sinon.spy(); - discardLines = sinon.spy(); - undoLastDiscard = sinon.spy(); - openCurrentFile = sinon.spy(); - didSurfaceFile = sinon.spy(); - didDiveIntoCorrespondingFilePatch = sinon.spy(); - handleShowDiffClick = sinon.spy(); - - component = ( - - ); - }); - - afterEach(function() { - atomEnv.destroy(); - }); - - describe('mouse selection', () => { - it('allows lines and hunks to be selected via mouse drag', function() { - const hunks = [ - new Hunk(1, 1, 2, 4, '', [ - new HunkLine('line-1', 'unchanged', 1, 1), - new HunkLine('line-2', 'added', -1, 2), - new HunkLine('line-3', 'added', -1, 3), - new HunkLine('line-4', 'unchanged', 2, 4), - ]), - new Hunk(5, 7, 1, 4, '', [ - new HunkLine('line-5', 'unchanged', 5, 7), - new HunkLine('line-6', 'added', -1, 8), - new HunkLine('line-7', 'added', -1, 9), - new HunkLine('line-8', 'added', -1, 10), - ]), - ]; - - const wrapper = shallow(React.cloneElement(component, {hunks})); - const getHunkView = index => wrapper.find({hunk: hunks[index]}); - - // drag a selection - getHunkView(0).prop('mousedownOnLine')({button: 0, detail: 1}, hunks[0], hunks[0].lines[2]); - getHunkView(1).prop('mousemoveOnLine')({}, hunks[1], hunks[1].lines[1]); - wrapper.instance().mouseup(); - wrapper.update(); - - assert.isTrue(getHunkView(0).prop('isSelected')); - assert.isTrue(getHunkView(0).prop('selectedLines').has(hunks[0].lines[2])); - assert.isTrue(getHunkView(1).prop('isSelected')); - assert.isTrue(getHunkView(1).prop('selectedLines').has(hunks[1].lines[1])); - - // start a new selection, drag it across an existing selection - getHunkView(1).prop('mousedownOnLine')({button: 0, detail: 1, metaKey: true}, hunks[1], hunks[1].lines[3]); - getHunkView(0).prop('mousemoveOnLine')({}, hunks[0], hunks[0].lines[0]); - wrapper.update(); - - assert.isTrue(getHunkView(0).prop('isSelected')); - assert.isTrue(getHunkView(0).prop('selectedLines').has(hunks[0].lines[1])); - assert.isTrue(getHunkView(0).prop('selectedLines').has(hunks[0].lines[2])); - assert.isTrue(getHunkView(1).prop('isSelected')); - assert.isTrue(getHunkView(1).prop('selectedLines').has(hunks[1].lines[1])); - assert.isTrue(getHunkView(1).prop('selectedLines').has(hunks[1].lines[2])); - assert.isTrue(getHunkView(1).prop('selectedLines').has(hunks[1].lines[3])); - - // drag back down without releasing mouse; the other selection remains intact - getHunkView(1).prop('mousemoveOnLine')({}, hunks[1], hunks[1].lines[3]); - wrapper.update(); - - assert.isTrue(getHunkView(0).prop('isSelected')); - assert.isFalse(getHunkView(0).prop('selectedLines').has(hunks[0].lines[1])); - assert.isTrue(getHunkView(0).prop('selectedLines').has(hunks[0].lines[2])); - assert.isTrue(getHunkView(1).prop('isSelected')); - assert.isTrue(getHunkView(1).prop('selectedLines').has(hunks[1].lines[1])); - assert.isFalse(getHunkView(1).prop('selectedLines').has(hunks[1].lines[2])); - assert.isTrue(getHunkView(1).prop('selectedLines').has(hunks[1].lines[3])); - - // drag back up so selections are adjacent, then release the mouse. selections should merge. - getHunkView(1).prop('mousemoveOnLine')({}, hunks[1], hunks[1].lines[2]); - wrapper.instance().mouseup(); - wrapper.update(); - - assert.isTrue(getHunkView(0).prop('isSelected')); - assert.isTrue(getHunkView(0).prop('selectedLines').has(hunks[0].lines[2])); - assert.isTrue(getHunkView(1).prop('isSelected')); - assert.isTrue(getHunkView(1).prop('selectedLines').has(hunks[1].lines[1])); - assert.isTrue(getHunkView(1).prop('selectedLines').has(hunks[1].lines[2])); - assert.isTrue(getHunkView(1).prop('selectedLines').has(hunks[1].lines[3])); - - // we detect merged selections based on the head here - wrapper.instance().selectToNext(); - wrapper.update(); - - assert.isFalse(getHunkView(0).prop('isSelected')); - assert.isFalse(getHunkView(0).prop('selectedLines').has(hunks[0].lines[2])); - - // double-clicking clears the existing selection and starts hunk-wise selection - getHunkView(0).prop('mousedownOnLine')({button: 0, detail: 2}, hunks[0], hunks[0].lines[2]); - wrapper.update(); - - assert.isTrue(getHunkView(0).prop('isSelected')); - assert.isTrue(getHunkView(0).prop('selectedLines').has(hunks[0].lines[1])); - assert.isTrue(getHunkView(0).prop('selectedLines').has(hunks[0].lines[2])); - assert.isFalse(getHunkView(1).prop('isSelected')); - - getHunkView(1).prop('mousemoveOnLine')({}, hunks[1], hunks[1].lines[1]); - wrapper.update(); - - assert.isTrue(getHunkView(0).prop('isSelected')); - assert.isTrue(getHunkView(0).prop('selectedLines').has(hunks[0].lines[1])); - assert.isTrue(getHunkView(0).prop('selectedLines').has(hunks[0].lines[2])); - assert.isTrue(getHunkView(1).prop('isSelected')); - assert.isTrue(getHunkView(1).prop('selectedLines').has(hunks[1].lines[1])); - assert.isTrue(getHunkView(1).prop('selectedLines').has(hunks[1].lines[2])); - assert.isTrue(getHunkView(1).prop('selectedLines').has(hunks[1].lines[3])); - - // clicking the header clears the existing selection and starts hunk-wise selection - getHunkView(0).prop('mousedownOnHeader')({button: 0, detail: 1}, hunks[0], hunks[0].lines[2]); - wrapper.update(); - - assert.isTrue(getHunkView(0).prop('isSelected')); - assert.isTrue(getHunkView(0).prop('selectedLines').has(hunks[0].lines[1])); - assert.isTrue(getHunkView(0).prop('selectedLines').has(hunks[0].lines[2])); - assert.isFalse(getHunkView(1).prop('isSelected')); - - getHunkView(1).prop('mousemoveOnLine')({}, hunks[1], hunks[1].lines[1]); - wrapper.update(); - - assert.isTrue(getHunkView(0).prop('isSelected')); - assert.isTrue(getHunkView(0).prop('selectedLines').has(hunks[0].lines[1])); - assert.isTrue(getHunkView(0).prop('selectedLines').has(hunks[0].lines[2])); - assert.isTrue(getHunkView(1).prop('isSelected')); - assert.isTrue(getHunkView(1).prop('selectedLines').has(hunks[1].lines[1])); - assert.isTrue(getHunkView(1).prop('selectedLines').has(hunks[1].lines[2])); - assert.isTrue(getHunkView(1).prop('selectedLines').has(hunks[1].lines[3])); - }); - - it('allows lines and hunks to be selected via cmd-clicking', function() { - const hunk0 = new Hunk(1, 1, 2, 4, '', [ - new HunkLine('line-0', 'added', -1, 1), - new HunkLine('line-1', 'added', -1, 2), - new HunkLine('line-2', 'added', -1, 3), - ]); - const hunk1 = new Hunk(5, 7, 1, 4, '', [ - new HunkLine('line-3', 'added', -1, 7), - new HunkLine('line-4', 'added', -1, 8), - new HunkLine('line-5', 'added', -1, 9), - new HunkLine('line-6', 'added', -1, 10), - ]); - - const wrapper = shallow(React.cloneElement(component, { - hunks: [hunk0, hunk1], - })); - const getHunkView = index => wrapper.find({hunk: [hunk0, hunk1][index]}); - - // in line selection mode, cmd-click line - getHunkView(0).prop('mousedownOnLine')({button: 0, detail: 1}, hunk0, hunk0.lines[2]); - wrapper.instance().mouseup(); - - assert.equal(wrapper.instance().getPatchSelectionMode(), 'line'); - assertEqualSets(wrapper.instance().getSelectedHunks(), new Set([hunk0])); - assertEqualSets(wrapper.instance().getSelectedLines(), new Set([hunk0.lines[2]])); - - getHunkView(1).prop('mousedownOnLine')({button: 0, detail: 1, metaKey: true}, hunk1, hunk1.lines[2]); - wrapper.instance().mouseup(); - - assertEqualSets(wrapper.instance().getSelectedHunks(), new Set([hunk0, hunk1])); - assertEqualSets(wrapper.instance().getSelectedLines(), new Set([hunk0.lines[2], hunk1.lines[2]])); - - getHunkView(1).prop('mousedownOnLine')({button: 0, detail: 1, metaKey: true}, hunk1, hunk1.lines[2]); - wrapper.instance().mouseup(); - - assertEqualSets(wrapper.instance().getSelectedHunks(), new Set([hunk0])); - assertEqualSets(wrapper.instance().getSelectedLines(), new Set([hunk0.lines[2]])); - - // in line selection mode, cmd-click hunk header for separate hunk - getHunkView(0).prop('mousedownOnLine')({button: 0, detail: 1}, hunk0, hunk0.lines[2]); - wrapper.instance().mouseup(); - - assert.equal(wrapper.instance().getPatchSelectionMode(), 'line'); - assertEqualSets(wrapper.instance().getSelectedHunks(), new Set([hunk0])); - assertEqualSets(wrapper.instance().getSelectedLines(), new Set([hunk0.lines[2]])); - - getHunkView(1).prop('mousedownOnHeader')({button: 0, metaKey: true}); - wrapper.instance().mouseup(); - - assertEqualSets(wrapper.instance().getSelectedHunks(), new Set([hunk0, hunk1])); - assertEqualSets(wrapper.instance().getSelectedLines(), new Set([hunk0.lines[2], ...hunk1.lines])); - - getHunkView(1).prop('mousedownOnHeader')({button: 0, metaKey: true}); - wrapper.instance().mouseup(); - - assertEqualSets(wrapper.instance().getSelectedHunks(), new Set([hunk0])); - assertEqualSets(wrapper.instance().getSelectedLines(), new Set([hunk0.lines[2]])); - - // in hunk selection mode, cmd-click line for separate hunk - getHunkView(0).prop('mousedownOnLine')({button: 0, detail: 1}, hunk0, hunk0.lines[2]); - wrapper.instance().mouseup(); - wrapper.instance().togglePatchSelectionMode(); - - assert.equal(wrapper.instance().getPatchSelectionMode(), 'hunk'); - assertEqualSets(wrapper.instance().getSelectedHunks(), new Set([hunk0])); - assertEqualSets(wrapper.instance().getSelectedLines(), new Set(hunk0.lines)); - - // in hunk selection mode, cmd-click hunk header for separate hunk - getHunkView(0).prop('mousedownOnLine')({button: 0, detail: 1}, hunk0, hunk0.lines[2]); - wrapper.instance().mouseup(); - wrapper.instance().togglePatchSelectionMode(); - - assert.equal(wrapper.instance().getPatchSelectionMode(), 'hunk'); - assertEqualSets(wrapper.instance().getSelectedHunks(), new Set([hunk0])); - assertEqualSets(wrapper.instance().getSelectedLines(), new Set(hunk0.lines)); - }); - - it('allows lines and hunks to be selected via shift-clicking', () => { - const hunk0 = new Hunk(1, 1, 2, 4, '', [ - new HunkLine('line-1', 'unchanged', 1, 1, 0), - new HunkLine('line-2', 'added', -1, 2, 1), - new HunkLine('line-3', 'added', -1, 3, 2), - ]); - const hunk1 = new Hunk(5, 7, 1, 4, '', [ - new HunkLine('line-5', 'added', -1, 7, 3), - new HunkLine('line-6', 'added', -1, 8, 4), - new HunkLine('line-7', 'added', -1, 9, 5), - new HunkLine('line-8', 'added', -1, 10, 6), - ]); - const hunk2 = new Hunk(15, 17, 1, 4, '', [ - new HunkLine('line-15', 'added', -1, 15, 7), - new HunkLine('line-16', 'added', -1, 18, 8), - new HunkLine('line-17', 'added', -1, 19, 9), - new HunkLine('line-18', 'added', -1, 20, 10), - ]); - const hunks = [hunk0, hunk1, hunk2]; - - const wrapper = shallow(React.cloneElement(component, {hunks})); - const getHunkView = index => wrapper.find({hunk: hunks[index]}); - - // in line selection mode, shift-click line in separate hunk that comes after selected line - getHunkView(0).prop('mousedownOnLine')({button: 0, detail: 1}, hunk0, hunk0.lines[2]); - wrapper.instance().mouseup(); - - assertEqualSets(wrapper.instance().getSelectedHunks(), new Set([hunk0])); - assertEqualSets(wrapper.instance().getSelectedLines(), new Set([hunk0.lines[2]])); - - getHunkView(2).prop('mousedownOnLine')({button: 0, detail: 1, shiftKey: true}, hunk2, hunk2.lines[2]); - wrapper.instance().mouseup(); - - assertEqualSets(wrapper.instance().getSelectedHunks(), new Set([hunk0, hunk1, hunk2])); - assertEqualSets(wrapper.instance().getSelectedLines(), new Set([...hunk0.lines.slice(2), ...hunk1.lines, ...hunk2.lines.slice(0, 3)])); - - getHunkView(1).prop('mousedownOnLine')({button: 0, detail: 1, shiftKey: true}, hunk1, hunk1.lines[2]); - wrapper.instance().mouseup(); - - assertEqualSets(wrapper.instance().getSelectedHunks(), new Set([hunk0, hunk1])); - assertEqualSets(wrapper.instance().getSelectedLines(), new Set([...hunk0.lines.slice(2), ...hunk1.lines.slice(0, 3)])); - - // in line selection mode, shift-click hunk header for separate hunk that comes after selected line - getHunkView(0).prop('mousedownOnLine')({button: 0, detail: 1}, hunk0, hunk0.lines[2]); - wrapper.instance().mouseup(); - - assertEqualSets(wrapper.instance().getSelectedHunks(), new Set([hunk0])); - assertEqualSets(wrapper.instance().getSelectedLines(), new Set([hunk0.lines[2]])); - - getHunkView(2).prop('mousedownOnHeader')({button: 0, shiftKey: true}, hunk2); - wrapper.instance().mouseup(); - - assertEqualSets(wrapper.instance().getSelectedHunks(), new Set([hunk0, hunk1, hunk2])); - assertEqualSets(wrapper.instance().getSelectedLines(), new Set([...hunk0.lines.slice(2), ...hunk1.lines, ...hunk2.lines])); - - getHunkView(1).prop('mousedownOnHeader')({button: 0, shiftKey: true}, hunk1); - wrapper.instance().mouseup(); - - assertEqualSets(wrapper.instance().getSelectedHunks(), new Set([hunk0, hunk1])); - assertEqualSets(wrapper.instance().getSelectedLines(), new Set([...hunk0.lines.slice(2), ...hunk1.lines])); - - // in line selection mode, shift-click hunk header for separate hunk that comes before selected line - getHunkView(2).prop('mousedownOnLine')({button: 0, detail: 1}, hunk2, hunk2.lines[2]); - wrapper.instance().mouseup(); - - assertEqualSets(wrapper.instance().getSelectedHunks(), new Set([hunk2])); - assertEqualSets(wrapper.instance().getSelectedLines(), new Set([hunk2.lines[2]])); - - getHunkView(0).prop('mousedownOnHeader')({button: 0, shiftKey: true}, hunk0); - wrapper.instance().mouseup(); - - assertEqualSets(wrapper.instance().getSelectedHunks(), new Set([hunk0, hunk1, hunk2])); - assertEqualSets(wrapper.instance().getSelectedLines(), new Set([...hunk0.lines.slice(1), ...hunk1.lines, ...hunk2.lines.slice(0, 3)])); - - getHunkView(1).prop('mousedownOnHeader')({button: 0, shiftKey: true}, hunk1); - wrapper.instance().mouseup(); - - assertEqualSets(wrapper.instance().getSelectedHunks(), new Set([hunk1, hunk2])); - assertEqualSets(wrapper.instance().getSelectedLines(), new Set([...hunk1.lines, ...hunk2.lines.slice(0, 3)])); - - // in hunk selection mode, shift-click hunk header for separate hunk that comes after selected line - getHunkView(0).prop('mousedownOnHeader')({button: 0}, hunk0); - wrapper.instance().mouseup(); - - assertEqualSets(wrapper.instance().getSelectedHunks(), new Set([hunk0])); - assertEqualSets(wrapper.instance().getSelectedLines(), new Set(hunk0.lines.slice(1))); - - getHunkView(2).prop('mousedownOnHeader')({button: 0, shiftKey: true}, hunk2); - wrapper.instance().mouseup(); - - assertEqualSets(wrapper.instance().getSelectedHunks(), new Set([hunk0, hunk1, hunk2])); - assertEqualSets(wrapper.instance().getSelectedLines(), new Set([...hunk0.lines.slice(1), ...hunk1.lines, ...hunk2.lines])); - - getHunkView(1).prop('mousedownOnHeader')({button: 0, shiftKey: true}, hunk1); - wrapper.instance().mouseup(); - - assertEqualSets(wrapper.instance().getSelectedHunks(), new Set([hunk0, hunk1])); - assertEqualSets(wrapper.instance().getSelectedLines(), new Set([...hunk0.lines.slice(1), ...hunk1.lines])); - - // in hunk selection mode, shift-click hunk header for separate hunk that comes before selected line - getHunkView(2).prop('mousedownOnHeader')({button: 0}, hunk2); - wrapper.instance().mouseup(); - - assertEqualSets(wrapper.instance().getSelectedHunks(), new Set([hunk2])); - assertEqualSets(wrapper.instance().getSelectedLines(), new Set(hunk2.lines)); - - getHunkView(0).prop('mousedownOnHeader')({button: 0, shiftKey: true}, hunk0); - wrapper.instance().mouseup(); - - assertEqualSets(wrapper.instance().getSelectedHunks(), new Set([hunk0, hunk1, hunk2])); - assertEqualSets(wrapper.instance().getSelectedLines(), new Set([...hunk0.lines.slice(1), ...hunk1.lines, ...hunk2.lines])); - - getHunkView(1).prop('mousedownOnHeader')({button: 0, shiftKey: true}, hunk1); - wrapper.instance().mouseup(); - - assertEqualSets(wrapper.instance().getSelectedHunks(), new Set([hunk1, hunk2])); - assertEqualSets(wrapper.instance().getSelectedLines(), new Set([...hunk1.lines, ...hunk2.lines])); - }); - - if (process.platform !== 'win32') { - // https://github.com/atom/github/issues/514 - describe('mousedownOnLine', function() { - it('does not select line or set selection to be in progress if ctrl-key is pressed and not on windows', function() { - const hunk0 = new Hunk(1, 1, 2, 4, '', [ - new HunkLine('line-1', 'added', -1, 1), - new HunkLine('line-2', 'added', -1, 2), - new HunkLine('line-3', 'added', -1, 3), - ]); - - const wrapper = shallow(React.cloneElement(component, {hunks: [hunk0]})); - - wrapper.instance().togglePatchSelectionMode(); - assert.equal(wrapper.instance().getPatchSelectionMode(), 'line'); - - sinon.spy(wrapper.state('selection'), 'addOrSubtractLineSelection'); - sinon.spy(wrapper.state('selection'), 'selectLine'); - - wrapper.find('HunkView').prop('mousedownOnLine')({button: 0, detail: 1, ctrlKey: true}, hunk0, hunk0.lines[2]); - assert.isFalse(wrapper.state('selection').addOrSubtractLineSelection.called); - assert.isFalse(wrapper.state('selection').selectLine.called); - assert.isFalse(wrapper.instance().mouseSelectionInProgress); - }); - }); - - // https://github.com/atom/github/issues/514 - describe('mousedownOnHeader', function() { - it('does not select line or set selection to be in progress if ctrl-key is pressed and not on windows', function() { - const hunk0 = new Hunk(1, 1, 2, 4, '', [ - new HunkLine('line-1', 'added', -1, 1), - new HunkLine('line-2', 'added', -1, 2), - new HunkLine('line-3', 'added', -1, 3), - ]); - const hunk1 = new Hunk(5, 7, 1, 4, '', [ - new HunkLine('line-5', 'added', -1, 7), - new HunkLine('line-6', 'added', -1, 8), - new HunkLine('line-7', 'added', -1, 9), - new HunkLine('line-8', 'added', -1, 10), - ]); - - const wrapper = shallow(React.cloneElement(component, {hunks: [hunk0, hunk1]})); - - wrapper.instance().togglePatchSelectionMode(); - assert.equal(wrapper.instance().getPatchSelectionMode(), 'line'); - - sinon.spy(wrapper.state('selection'), 'addOrSubtractLineSelection'); - sinon.spy(wrapper.state('selection'), 'selectLine'); - - // ctrl-click hunk line - wrapper.find({hunk: hunk0}).prop('mousedownOnHeader')({button: 0, detail: 1, ctrlKey: true}, hunk0); - - assert.isFalse(wrapper.state('selection').addOrSubtractLineSelection.called); - assert.isFalse(wrapper.state('selection').selectLine.called); - assert.isFalse(wrapper.instance().mouseSelectionInProgress); - }); - }); - } - }); - - it('scrolls off-screen lines and hunks into view when they are selected', async function() { - const hunks = [ - new Hunk(1, 1, 2, 4, '', [ - new HunkLine('line-1', 'unchanged', 1, 1), - new HunkLine('line-2', 'added', -1, 2), - new HunkLine('line-3', 'added', -1, 3), - new HunkLine('line-4', 'unchanged', 2, 4), - ]), - new Hunk(5, 7, 1, 4, '', [ - new HunkLine('line-5', 'unchanged', 5, 7), - new HunkLine('line-6', 'added', -1, 8), - new HunkLine('line-7', 'added', -1, 9), - new HunkLine('line-8', 'added', -1, 10), - ]), - ]; - - const root = document.createElement('div'); - root.style.overflow = 'scroll'; - root.style.height = '100px'; - document.body.appendChild(root); - - const wrapper = mount(React.cloneElement(component, {hunks}), {attachTo: root}); - - wrapper.instance().togglePatchSelectionMode(); - wrapper.instance().selectNext(); - await new Promise(resolve => root.addEventListener('scroll', resolve)); - assert.isAbove(root.scrollTop, 0); - const initScrollTop = root.scrollTop; - - wrapper.instance().togglePatchSelectionMode(); - wrapper.instance().selectNext(); - await new Promise(resolve => root.addEventListener('scroll', resolve)); - assert.isAbove(root.scrollTop, initScrollTop); - - root.remove(); - }); - - it('assigns the appropriate stage button label on hunks based on the stagingStatus and selection mode', function() { - const hunk = new Hunk(1, 1, 1, 2, '', [new HunkLine('line-1', 'added', -1, 1)]); - - const wrapper = shallow(React.cloneElement(component, { - hunks: [hunk], - stagingStatus: 'unstaged', - })); - - assert.equal(wrapper.find('HunkView').prop('stageButtonLabel'), 'Stage Hunk'); - wrapper.setProps({stagingStatus: 'staged'}); - assert.equal(wrapper.find('HunkView').prop('stageButtonLabel'), 'Unstage Hunk'); - - wrapper.instance().togglePatchSelectionMode(); - wrapper.update(); - - assert.equal(wrapper.find('HunkView').prop('stageButtonLabel'), 'Unstage Selection'); - wrapper.setProps({stagingStatus: 'unstaged'}); - assert.equal(wrapper.find('HunkView').prop('stageButtonLabel'), 'Stage Selection'); - }); - - describe('didClickStageButtonForHunk', function() { - // ref: https://github.com/atom/github/issues/339 - it('selects the next hunk after staging', function() { - const hunks = [ - new Hunk(1, 1, 2, 4, '', [ - new HunkLine('line-1', 'unchanged', 1, 1), - new HunkLine('line-2', 'added', -1, 2), - new HunkLine('line-3', 'added', -1, 3), - new HunkLine('line-4', 'unchanged', 2, 4), - ]), - new Hunk(5, 7, 1, 4, '', [ - new HunkLine('line-5', 'unchanged', 5, 7), - new HunkLine('line-6', 'added', -1, 8), - new HunkLine('line-7', 'added', -1, 9), - new HunkLine('line-8', 'added', -1, 10), - ]), - new Hunk(15, 17, 1, 4, '', [ - new HunkLine('line-9', 'unchanged', 15, 17), - new HunkLine('line-10', 'added', -1, 18), - new HunkLine('line-11', 'added', -1, 19), - new HunkLine('line-12', 'added', -1, 20), - ]), - ]; - - const wrapper = shallow(React.cloneElement(component, { - hunks, - stagingStatus: 'unstaged', - })); - - wrapper.find({hunk: hunks[2]}).prop('didClickStageButton')(); - wrapper.setProps({hunks: hunks.filter(h => h !== hunks[2])}); - - assertEqualSets(wrapper.state('selection').getSelectedHunks(), new Set([hunks[1]])); - }); - }); - - describe('keyboard navigation', function() { - it('invokes the didSurfaceFile callback on core:move-right', function() { - const hunks = [ - new Hunk(1, 1, 2, 2, '', [ - new HunkLine('line-1', 'unchanged', 1, 1), - new HunkLine('line-2', 'added', -1, 2), - ]), - ]; - - const wrapper = mount(React.cloneElement(component, {hunks})); - commandRegistry.dispatch(wrapper.getDOMNode(), 'core:move-right'); - - assert.equal(didSurfaceFile.callCount, 1); - }); - }); - - describe('openFile', function() { - describe('when the selected line is an added line', function() { - it('calls this.props.openCurrentFile with the first selected line\'s new line number', function() { - const hunks = [ - new Hunk(1, 1, 2, 4, '', [ - new HunkLine('line-1', 'unchanged', 1, 1), - new HunkLine('line-2', 'added', -1, 2), - new HunkLine('line-3', 'added', -1, 3), - new HunkLine('line-4', 'added', -1, 4), - new HunkLine('line-5', 'unchanged', 2, 5), - ]), - ]; - - const wrapper = shallow(React.cloneElement(component, {hunks})); - - wrapper.instance().mousedownOnLine({button: 0, detail: 1}, hunks[0], hunks[0].lines[2]); - wrapper.instance().mousemoveOnLine({}, hunks[0], hunks[0].lines[3]); - wrapper.instance().mouseup(); - - wrapper.instance().openFile(); - assert.isTrue(openCurrentFile.calledWith({lineNumber: 3})); - }); - }); - - describe('when the selected line is a deleted line in a non-empty file', function() { - it('calls this.props.openCurrentFile with the new start row of the first selected hunk', function() { - const hunks = [ - new Hunk(1, 1, 2, 4, '', [ - new HunkLine('line-1', 'unchanged', 1, 1), - new HunkLine('line-2', 'added', -1, 2), - new HunkLine('line-3', 'added', -1, 3), - new HunkLine('line-4', 'added', -1, 4), - new HunkLine('line-5', 'unchanged', 2, 5), - ]), - new Hunk(15, 17, 4, 1, '', [ - new HunkLine('line-5', 'unchanged', 15, 17), - new HunkLine('line-6', 'deleted', 16, -1), - new HunkLine('line-7', 'deleted', 17, -1), - new HunkLine('line-8', 'deleted', 18, -1), - ]), - ]; - - const wrapper = shallow(React.cloneElement(component, {hunks})); - - wrapper.instance().mousedownOnLine({button: 0, detail: 1}, hunks[1], hunks[1].lines[2]); - wrapper.instance().mousemoveOnLine({}, hunks[1], hunks[1].lines[3]); - wrapper.instance().mouseup(); - - wrapper.instance().openFile(); - assert.isTrue(openCurrentFile.calledWith({lineNumber: 17})); - }); - }); - - describe('when the selected line is a deleted line in an empty file', function() { - it('calls this.props.openCurrentFile with a line number of 0', function() { - const hunks = [ - new Hunk(1, 0, 4, 0, '', [ - new HunkLine('line-5', 'deleted', 1, -1), - new HunkLine('line-6', 'deleted', 2, -1), - new HunkLine('line-7', 'deleted', 3, -1), - new HunkLine('line-8', 'deleted', 4, -1), - ]), - ]; - - const wrapper = shallow(React.cloneElement(component, {hunks})); - - wrapper.instance().mousedownOnLine({button: 0, detail: 1}, hunks[0], hunks[0].lines[2]); - wrapper.instance().mouseup(); - - wrapper.instance().openFile(); - assert.isTrue(openCurrentFile.calledWith({lineNumber: 0})); - }); - }); - }); -}); From 4a7c4f91b7270d4b78b4fbd039c40e2e1e2080ba Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 5 Sep 2018 07:45:04 -0400 Subject: [PATCH 0273/4252] Span IndexedRowRanges to the final column of the row --- lib/models/indexed-row-range.js | 2 +- test/models/indexed-row-range.test.js | 64 +++++++++++++-------------- 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/lib/models/indexed-row-range.js b/lib/models/indexed-row-range.js index 1dc48345de..38357f703f 100644 --- a/lib/models/indexed-row-range.js +++ b/lib/models/indexed-row-range.js @@ -58,7 +58,7 @@ export default class IndexedRowRange { intersections.push({ intersection: new IndexedRowRange({ - bufferRange: Range.fromObject([[nextStartRow, 0], [currentRow - 1, 0]]), + bufferRange: Range.fromObject([[nextStartRow, 0], [currentRow - 1, Infinity]]), startOffset: nextStartOffset, endOffset: currentOffset, }), diff --git a/test/models/indexed-row-range.test.js b/test/models/indexed-row-range.test.js index 8985db788b..a87ee5c1fc 100644 --- a/test/models/indexed-row-range.test.js +++ b/test/models/indexed-row-range.test.js @@ -3,7 +3,7 @@ import IndexedRowRange, {nullIndexedRowRange} from '../../lib/models/indexed-row describe('IndexedRowRange', function() { it('computes its row count', function() { const range = new IndexedRowRange({ - bufferRange: [[0, 0], [1, 0]], + bufferRange: [[0, 0], [1, Infinity]], startOffset: 0, endOffset: 10, }); @@ -13,7 +13,7 @@ describe('IndexedRowRange', function() { it('returns its starting buffer row', function() { const range = new IndexedRowRange({ - bufferRange: [[2, 0], [8, 0]], + bufferRange: [[2, 0], [8, Infinity]], startOffset: 0, endOffset: 10, }); @@ -22,7 +22,7 @@ describe('IndexedRowRange', function() { it('returns its ending buffer row', function() { const range = new IndexedRowRange({ - bufferRange: [[2, 0], [8, 0]], + bufferRange: [[2, 0], [8, Infinity]], startOffset: 0, endOffset: 10, }); @@ -31,7 +31,7 @@ describe('IndexedRowRange', function() { it('returns an array of the covered rows', function() { const range = new IndexedRowRange({ - bufferRange: [[2, 0], [8, 0]], + bufferRange: [[2, 0], [8, Infinity]], startOffset: 0, endOffset: 10, }); @@ -40,7 +40,7 @@ describe('IndexedRowRange', function() { it('has a buffer row inclusion predicate', function() { const range = new IndexedRowRange({ - bufferRange: [[2, 0], [4, 0]], + bufferRange: [[2, 0], [4, Infinity]], startOffset: 0, endOffset: 10, }); @@ -55,7 +55,7 @@ describe('IndexedRowRange', function() { it('extracts its offset range from buffer text with toStringIn()', function() { const buffer = '0000\n1111\n2222\n3333\n4444\n5555\n'; const range = new IndexedRowRange({ - bufferRange: [[1, 0], [2, 0]], + bufferRange: [[1, 0], [2, Infinity]], startOffset: 5, endOffset: 25, }); @@ -75,87 +75,87 @@ describe('IndexedRowRange', function() { it('returns an array containing all gaps with no intersection rows', function() { const range = new IndexedRowRange({ - bufferRange: [[1, 0], [3, 0]], + bufferRange: [[1, 0], [3, Infinity]], startOffset: 5, endOffset: 20, }); assertIntersections(range.intersectRowsIn(new Set([0, 5, 6]), buffer, false), []); assertIntersections(range.intersectRowsIn(new Set([0, 5, 6]), buffer, true), [ - {intersection: {bufferRange: [[1, 0], [3, 0]], startOffset: 5, endOffset: 20}, gap: true}, + {intersection: {bufferRange: [[1, 0], [3, Infinity]], startOffset: 5, endOffset: 20}, gap: true}, ]); }); it('detects an intersection at the beginning of the range', function() { const range = new IndexedRowRange({ - bufferRange: [[2, 0], [6, 0]], + bufferRange: [[2, 0], [6, Infinity]], startOffset: 10, endOffset: 35, }); const rowSet = new Set([0, 1, 2, 3]); assertIntersections(range.intersectRowsIn(rowSet, buffer, false), [ - {intersection: {bufferRange: [[2, 0], [3, 0]], startOffset: 10, endOffset: 20}, gap: false}, + {intersection: {bufferRange: [[2, 0], [3, Infinity]], startOffset: 10, endOffset: 20}, gap: false}, ]); assertIntersections(range.intersectRowsIn(rowSet, buffer, true), [ - {intersection: {bufferRange: [[2, 0], [3, 0]], startOffset: 10, endOffset: 20}, gap: false}, - {intersection: {bufferRange: [[4, 0], [6, 0]], startOffset: 20, endOffset: 35}, gap: true}, + {intersection: {bufferRange: [[2, 0], [3, Infinity]], startOffset: 10, endOffset: 20}, gap: false}, + {intersection: {bufferRange: [[4, 0], [6, Infinity]], startOffset: 20, endOffset: 35}, gap: true}, ]); }); it('detects an intersection in the middle of the range', function() { const range = new IndexedRowRange({ - bufferRange: [[2, 0], [6, 0]], + bufferRange: [[2, 0], [6, Infinity]], startOffset: 10, endOffset: 35, }); const rowSet = new Set([0, 3, 4, 8, 9]); assertIntersections(range.intersectRowsIn(rowSet, buffer, false), [ - {intersection: {bufferRange: [[3, 0], [4, 0]], startOffset: 15, endOffset: 25}, gap: false}, + {intersection: {bufferRange: [[3, 0], [4, Infinity]], startOffset: 15, endOffset: 25}, gap: false}, ]); assertIntersections(range.intersectRowsIn(rowSet, buffer, true), [ - {intersection: {bufferRange: [[2, 0], [2, 0]], startOffset: 10, endOffset: 15}, gap: true}, - {intersection: {bufferRange: [[3, 0], [4, 0]], startOffset: 15, endOffset: 25}, gap: false}, - {intersection: {bufferRange: [[5, 0], [6, 0]], startOffset: 25, endOffset: 35}, gap: true}, + {intersection: {bufferRange: [[2, 0], [2, Infinity]], startOffset: 10, endOffset: 15}, gap: true}, + {intersection: {bufferRange: [[3, 0], [4, Infinity]], startOffset: 15, endOffset: 25}, gap: false}, + {intersection: {bufferRange: [[5, 0], [6, Infinity]], startOffset: 25, endOffset: 35}, gap: true}, ]); }); it('detects an intersection at the end of the range', function() { const range = new IndexedRowRange({ - bufferRange: [[2, 0], [6, 0]], + bufferRange: [[2, 0], [6, Infinity]], startOffset: 10, endOffset: 35, }); const rowSet = new Set([4, 5, 6, 7, 10, 11]); assertIntersections(range.intersectRowsIn(rowSet, buffer, false), [ - {intersection: {bufferRange: [[4, 0], [6, 0]], startOffset: 20, endOffset: 35}, gap: false}, + {intersection: {bufferRange: [[4, 0], [6, Infinity]], startOffset: 20, endOffset: 35}, gap: false}, ]); assertIntersections(range.intersectRowsIn(rowSet, buffer, true), [ - {intersection: {bufferRange: [[2, 0], [3, 0]], startOffset: 10, endOffset: 20}, gap: true}, - {intersection: {bufferRange: [[4, 0], [6, 0]], startOffset: 20, endOffset: 35}, gap: false}, + {intersection: {bufferRange: [[2, 0], [3, Infinity]], startOffset: 10, endOffset: 20}, gap: true}, + {intersection: {bufferRange: [[4, 0], [6, Infinity]], startOffset: 20, endOffset: 35}, gap: false}, ]); }); it('detects multiple intersections', function() { const range = new IndexedRowRange({ - bufferRange: [[2, 0], [8, 0]], + bufferRange: [[2, 0], [8, Infinity]], startOffset: 10, endOffset: 45, }); const rowSet = new Set([0, 3, 4, 6, 7, 10]); assertIntersections(range.intersectRowsIn(rowSet, buffer, false), [ - {intersection: {bufferRange: [[3, 0], [4, 0]], startOffset: 15, endOffset: 25}, gap: false}, - {intersection: {bufferRange: [[6, 0], [7, 0]], startOffset: 30, endOffset: 40}, gap: false}, + {intersection: {bufferRange: [[3, 0], [4, Infinity]], startOffset: 15, endOffset: 25}, gap: false}, + {intersection: {bufferRange: [[6, 0], [7, Infinity]], startOffset: 30, endOffset: 40}, gap: false}, ]); assertIntersections(range.intersectRowsIn(rowSet, buffer, true), [ - {intersection: {bufferRange: [[2, 0], [2, 0]], startOffset: 10, endOffset: 15}, gap: true}, - {intersection: {bufferRange: [[3, 0], [4, 0]], startOffset: 15, endOffset: 25}, gap: false}, - {intersection: {bufferRange: [[5, 0], [5, 0]], startOffset: 25, endOffset: 30}, gap: true}, - {intersection: {bufferRange: [[6, 0], [7, 0]], startOffset: 30, endOffset: 40}, gap: false}, - {intersection: {bufferRange: [[8, 0], [8, 0]], startOffset: 40, endOffset: 45}, gap: true}, + {intersection: {bufferRange: [[2, 0], [2, Infinity]], startOffset: 10, endOffset: 15}, gap: true}, + {intersection: {bufferRange: [[3, 0], [4, Infinity]], startOffset: 15, endOffset: 25}, gap: false}, + {intersection: {bufferRange: [[5, 0], [5, Infinity]], startOffset: 25, endOffset: 30}, gap: true}, + {intersection: {bufferRange: [[6, 0], [7, Infinity]], startOffset: 30, endOffset: 40}, gap: false}, + {intersection: {bufferRange: [[8, 0], [8, Infinity]], startOffset: 40, endOffset: 45}, gap: true}, ]); }); @@ -170,7 +170,7 @@ describe('IndexedRowRange', function() { beforeEach(function() { original = new IndexedRowRange({ - bufferRange: [[3, 0], [5, 0]], + bufferRange: [[3, 0], [5, Infinity]], startOffset: 15, endOffset: 25, }); @@ -183,7 +183,7 @@ describe('IndexedRowRange', function() { it('modifies the buffer range and the buffer offset', function() { const changed = original.offsetBy(10, 3); assert.deepEqual(changed.serialize(), { - bufferRange: [[6, 0], [8, 0]], + bufferRange: [[6, 0], [8, Infinity]], startOffset: 25, endOffset: 35, }); @@ -192,7 +192,7 @@ describe('IndexedRowRange', function() { it('may specify separate start and end offsets', function() { const changed = original.offsetBy(10, 2, 30, 4); assert.deepEqual(changed.serialize(), { - bufferRange: [[5, 0], [9, 0]], + bufferRange: [[5, 0], [9, Infinity]], startOffset: 25, endOffset: 55, }); From 4f33f31b5f623336b344c990ff5c652e809e8fc7 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 5 Sep 2018 07:48:13 -0400 Subject: [PATCH 0274/4252] Region test coverage --- test/models/patch/region.test.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/models/patch/region.test.js b/test/models/patch/region.test.js index 5be6612b12..bc1ab8757c 100644 --- a/test/models/patch/region.test.js +++ b/test/models/patch/region.test.js @@ -6,7 +6,7 @@ describe('Regions', function() { beforeEach(function() { buffer = '0000\n1111\n2222\n3333\n4444\n5555\n'; - range = new IndexedRowRange({bufferRange: [[1, 0], [3, 0]], startOffset: 5, endOffset: 20}); + range = new IndexedRowRange({bufferRange: [[1, 0], [3, Infinity]], startOffset: 5, endOffset: 20}); }); describe('Addition', function() { @@ -19,6 +19,7 @@ describe('Regions', function() { it('has range accessors', function() { assert.strictEqual(addition.getRowRange(), range); assert.strictEqual(addition.getStartBufferRow(), 1); + assert.strictEqual(addition.getEndBufferRow(), 3); }); it('delegates some methods to its row range', function() { From a8857c0d2df232ae4e1b923dbfcb48e435d0ce7c Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 5 Sep 2018 07:55:41 -0400 Subject: [PATCH 0275/4252] Preserve end columns in Hunk methods --- lib/models/patch/hunk.js | 4 +- test/models/patch/hunk.test.js | 118 ++++++++++++++++----------------- 2 files changed, 61 insertions(+), 61 deletions(-) diff --git a/lib/models/patch/hunk.js b/lib/models/patch/hunk.js index 3f577e5643..7ca5697d86 100644 --- a/lib/models/patch/hunk.js +++ b/lib/models/patch/hunk.js @@ -60,7 +60,7 @@ export default class Hunk { if (currentRow !== startRow) { regions.push(new Unchanged(new IndexedRowRange({ - bufferRange: [[currentRow, 0], [startRow - 1, 0]], + bufferRange: [[currentRow, 0], [startRow - 1, Infinity]], startOffset: currentPosition, endOffset: startPosition, }))); @@ -77,7 +77,7 @@ export default class Hunk { if (currentRow <= endRow) { regions.push(new Unchanged(new IndexedRowRange({ - bufferRange: [[currentRow, 0], [endRow, 0]], + bufferRange: [[currentRow, 0], [endRow, this.rowRange.bufferRange.end.column]], startOffset: currentPosition, endOffset: endPosition, }))); diff --git a/test/models/patch/hunk.test.js b/test/models/patch/hunk.test.js index faf10cc104..5f4509c71a 100644 --- a/test/models/patch/hunk.test.js +++ b/test/models/patch/hunk.test.js @@ -10,14 +10,14 @@ describe('Hunk', function() { newRowCount: 0, sectionHeading: 'sectionHeading', rowRange: new IndexedRowRange({ - bufferRange: [[1, 0], [10, 0]], + bufferRange: [[1, 0], [10, Infinity]], startOffset: 5, endOffset: 100, }), changes: [ - new Addition(new IndexedRowRange({bufferRange: [[1, 0], [2, 0]], startOffset: 6, endOffset: 7})), - new Deletion(new IndexedRowRange({bufferRange: [[3, 0], [4, 0]], startOffset: 8, endOffset: 9})), - new Deletion(new IndexedRowRange({bufferRange: [[5, 0], [6, 0]], startOffset: 10, endOffset: 11})), + new Addition(new IndexedRowRange({bufferRange: [[1, 0], [2, Infinity]], startOffset: 6, endOffset: 7})), + new Deletion(new IndexedRowRange({bufferRange: [[3, 0], [4, Infinity]], startOffset: 8, endOffset: 9})), + new Deletion(new IndexedRowRange({bufferRange: [[5, 0], [6, Infinity]], startOffset: 10, endOffset: 11})), ], }; @@ -29,14 +29,14 @@ describe('Hunk', function() { newRowCount: 3, sectionHeading: 'sectionHeading', rowRange: new IndexedRowRange({ - bufferRange: [[0, 0], [10, 0]], + bufferRange: [[0, 0], [10, Infinity]], startOffset: 0, endOffset: 100, }), changes: [ - new Addition(new IndexedRowRange({bufferRange: [[1, 0], [2, 0]], startOffset: 6, endOffset: 7})), - new Deletion(new IndexedRowRange({bufferRange: [[3, 0], [4, 0]], startOffset: 8, endOffset: 9})), - new Deletion(new IndexedRowRange({bufferRange: [[5, 0], [6, 0]], startOffset: 10, endOffset: 11})), + new Addition(new IndexedRowRange({bufferRange: [[1, 0], [2, Infinity]], startOffset: 6, endOffset: 7})), + new Deletion(new IndexedRowRange({bufferRange: [[3, 0], [4, Infinity]], startOffset: 8, endOffset: 9})), + new Deletion(new IndexedRowRange({bufferRange: [[5, 0], [6, Infinity]], startOffset: 10, endOffset: 11})), ], }); @@ -46,7 +46,7 @@ describe('Hunk', function() { assert.strictEqual(h.getNewRowCount(), 3); assert.strictEqual(h.getSectionHeading(), 'sectionHeading'); assert.deepEqual(h.getRowRange().serialize(), { - bufferRange: [[0, 0], [10, 0]], + bufferRange: [[0, 0], [10, Infinity]], startOffset: 0, endOffset: 100, }); @@ -61,28 +61,28 @@ describe('Hunk', function() { const h = new Hunk({ ...attrs, changes: [ - new Addition(new IndexedRowRange({bufferRange: [[1, 0], [2, 0]], startOffset: 6, endOffset: 7})), - new Deletion(new IndexedRowRange({bufferRange: [[4, 0], [5, 0]], startOffset: 8, endOffset: 9})), - new NoNewline(new IndexedRowRange({bufferRange: [[10, 0], [10, 0]], startOffset: 100, endOffset: 120})), + new Addition(new IndexedRowRange({bufferRange: [[1, 0], [2, Infinity]], startOffset: 6, endOffset: 7})), + new Deletion(new IndexedRowRange({bufferRange: [[4, 0], [5, Infinity]], startOffset: 8, endOffset: 9})), + new NoNewline(new IndexedRowRange({bufferRange: [[10, 0], [10, Infinity]], startOffset: 100, endOffset: 120})), ], }); const nl = h.getNoNewlineRange(); assert.isNotNull(nl); - assert.deepEqual(nl.serialize(), [[10, 0], [10, 0]]); + assert.deepEqual(nl.serialize(), [[10, 0], [10, Infinity]]); }); it('creates its row range for decoration placement', function() { const h = new Hunk({ ...attrs, rowRange: new IndexedRowRange({ - bufferRange: [[3, 0], [6, 0]], + bufferRange: [[3, 0], [6, Infinity]], startOffset: 15, endOffset: 35, }), }); - assert.deepEqual(h.getBufferRange().serialize(), [[3, 0], [6, 0]]); + assert.deepEqual(h.getBufferRange().serialize(), [[3, 0], [6, Infinity]]); }); it('generates a patch section header', function() { @@ -101,14 +101,14 @@ describe('Hunk', function() { const h = new Hunk({ ...attrs, rowRange: new IndexedRowRange({ - bufferRange: [[0, 0], [11, 0]], + bufferRange: [[0, 0], [11, Infinity]], startOffset: 0, endOffset: 120, }), changes: [ - new Addition(new IndexedRowRange({bufferRange: [[1, 0], [3, 0]], startOffset: 10, endOffset: 40})), - new Deletion(new IndexedRowRange({bufferRange: [[5, 0], [6, 0]], startOffset: 50, endOffset: 70})), - new Deletion(new IndexedRowRange({bufferRange: [[7, 0], [9, 0]], startOffset: 70, endOffset: 100})), + new Addition(new IndexedRowRange({bufferRange: [[1, 0], [3, Infinity]], startOffset: 10, endOffset: 40})), + new Deletion(new IndexedRowRange({bufferRange: [[5, 0], [6, Infinity]], startOffset: 50, endOffset: 70})), + new Deletion(new IndexedRowRange({bufferRange: [[7, 0], [9, Infinity]], startOffset: 70, endOffset: 100})), ], }); @@ -116,36 +116,36 @@ describe('Hunk', function() { assert.lengthOf(regions, 6); assert.isTrue(regions[0].isUnchanged()); - assert.deepEqual(regions[0].range.serialize(), {bufferRange: [[0, 0], [0, 0]], startOffset: 0, endOffset: 10}); + assert.deepEqual(regions[0].range.serialize(), {bufferRange: [[0, 0], [0, Infinity]], startOffset: 0, endOffset: 10}); assert.isTrue(regions[1].isAddition()); - assert.deepEqual(regions[1].range.serialize(), {bufferRange: [[1, 0], [3, 0]], startOffset: 10, endOffset: 40}); + assert.deepEqual(regions[1].range.serialize(), {bufferRange: [[1, 0], [3, Infinity]], startOffset: 10, endOffset: 40}); assert.isTrue(regions[2].isUnchanged()); - assert.deepEqual(regions[2].range.serialize(), {bufferRange: [[4, 0], [4, 0]], startOffset: 40, endOffset: 50}); + assert.deepEqual(regions[2].range.serialize(), {bufferRange: [[4, 0], [4, Infinity]], startOffset: 40, endOffset: 50}); assert.isTrue(regions[3].isDeletion()); - assert.deepEqual(regions[3].range.serialize(), {bufferRange: [[5, 0], [6, 0]], startOffset: 50, endOffset: 70}); + assert.deepEqual(regions[3].range.serialize(), {bufferRange: [[5, 0], [6, Infinity]], startOffset: 50, endOffset: 70}); assert.isTrue(regions[4].isDeletion()); - assert.deepEqual(regions[4].range.serialize(), {bufferRange: [[7, 0], [9, 0]], startOffset: 70, endOffset: 100}); + assert.deepEqual(regions[4].range.serialize(), {bufferRange: [[7, 0], [9, Infinity]], startOffset: 70, endOffset: 100}); assert.isTrue(regions[5].isUnchanged()); - assert.deepEqual(regions[5].range.serialize(), {bufferRange: [[10, 0], [11, 0]], startOffset: 100, endOffset: 120}); + assert.deepEqual(regions[5].range.serialize(), {bufferRange: [[10, 0], [11, Infinity]], startOffset: 100, endOffset: 120}); }); it('omits empty regions at the hunk beginning and end', function() { const h = new Hunk({ ...attrs, rowRange: new IndexedRowRange({ - bufferRange: [[1, 0], [9, 0]], + bufferRange: [[1, 0], [9, 20]], startOffset: 10, endOffset: 100, }), changes: [ - new Addition(new IndexedRowRange({bufferRange: [[1, 0], [3, 0]], startOffset: 10, endOffset: 40})), - new Deletion(new IndexedRowRange({bufferRange: [[5, 0], [6, 0]], startOffset: 50, endOffset: 70})), - new Deletion(new IndexedRowRange({bufferRange: [[7, 0], [9, 0]], startOffset: 70, endOffset: 100})), + new Addition(new IndexedRowRange({bufferRange: [[1, 0], [3, Infinity]], startOffset: 10, endOffset: 40})), + new Deletion(new IndexedRowRange({bufferRange: [[5, 0], [6, Infinity]], startOffset: 50, endOffset: 70})), + new Deletion(new IndexedRowRange({bufferRange: [[7, 0], [9, 20]], startOffset: 70, endOffset: 100})), ], }); @@ -153,23 +153,23 @@ describe('Hunk', function() { assert.lengthOf(regions, 4); assert.isTrue(regions[0].isAddition()); - assert.deepEqual(regions[0].range.serialize(), {bufferRange: [[1, 0], [3, 0]], startOffset: 10, endOffset: 40}); + assert.deepEqual(regions[0].range.serialize(), {bufferRange: [[1, 0], [3, Infinity]], startOffset: 10, endOffset: 40}); assert.isTrue(regions[1].isUnchanged()); - assert.deepEqual(regions[1].range.serialize(), {bufferRange: [[4, 0], [4, 0]], startOffset: 40, endOffset: 50}); + assert.deepEqual(regions[1].range.serialize(), {bufferRange: [[4, 0], [4, Infinity]], startOffset: 40, endOffset: 50}); assert.isTrue(regions[2].isDeletion()); - assert.deepEqual(regions[2].range.serialize(), {bufferRange: [[5, 0], [6, 0]], startOffset: 50, endOffset: 70}); + assert.deepEqual(regions[2].range.serialize(), {bufferRange: [[5, 0], [6, Infinity]], startOffset: 50, endOffset: 70}); assert.isTrue(regions[3].isDeletion()); - assert.deepEqual(regions[3].range.serialize(), {bufferRange: [[7, 0], [9, 0]], startOffset: 70, endOffset: 100}); + assert.deepEqual(regions[3].range.serialize(), {bufferRange: [[7, 0], [9, 20]], startOffset: 70, endOffset: 100}); }); it('returns a set of covered buffer rows', function() { const h = new Hunk({ ...attrs, rowRange: new IndexedRowRange({ - bufferRange: [[6, 0], [10, 0]], + bufferRange: [[6, 0], [10, 60]], startOffset: 30, endOffset: 55, }), @@ -181,7 +181,7 @@ describe('Hunk', function() { const h = new Hunk({ ...attrs, rowRange: new IndexedRowRange({ - bufferRange: [[3, 0], [5, 0]], + bufferRange: [[3, 0], [5, Infinity]], startOffset: 30, endOffset: 55, }), @@ -201,12 +201,12 @@ describe('Hunk', function() { oldRowCount: 6, newStartRow: 20, newRowCount: 7, - rowRange: new IndexedRowRange({bufferRange: [[2, 0], [12, 0]], startOffset: 0, endOffset: 0}), + rowRange: new IndexedRowRange({bufferRange: [[2, 0], [12, 10]], startOffset: 0, endOffset: 0}), changes: [ - new Addition(new IndexedRowRange({bufferRange: [[3, 0], [5, 0]], startOffset: 0, endOffset: 0})), - new Deletion(new IndexedRowRange({bufferRange: [[7, 0], [9, 0]], startOffset: 0, endOffset: 0})), - new Addition(new IndexedRowRange({bufferRange: [[11, 0], [11, 0]], startOffset: 0, endOffset: 0})), - new NoNewline(new IndexedRowRange({bufferRange: [[12, 0], [12, 0]], startOffset: 0, endOffset: 0})), + new Addition(new IndexedRowRange({bufferRange: [[3, 0], [5, Infinity]], startOffset: 0, endOffset: 0})), + new Deletion(new IndexedRowRange({bufferRange: [[7, 0], [9, Infinity]], startOffset: 0, endOffset: 0})), + new Addition(new IndexedRowRange({bufferRange: [[11, 0], [11, Infinity]], startOffset: 0, endOffset: 0})), + new NoNewline(new IndexedRowRange({bufferRange: [[12, 0], [12, Infinity]], startOffset: 0, endOffset: 0})), ], }); @@ -231,12 +231,12 @@ describe('Hunk', function() { oldRowCount: 6, newStartRow: 20, newRowCount: 7, - rowRange: new IndexedRowRange({bufferRange: [[2, 0], [12, 0]], startOffset: 0, endOffset: 0}), + rowRange: new IndexedRowRange({bufferRange: [[2, 0], [12, Infinity]], startOffset: 0, endOffset: 0}), changes: [ - new Addition(new IndexedRowRange({bufferRange: [[3, 0], [5, 0]], startOffset: 0, endOffset: 0})), - new Deletion(new IndexedRowRange({bufferRange: [[7, 0], [9, 0]], startOffset: 0, endOffset: 0})), - new Addition(new IndexedRowRange({bufferRange: [[11, 0], [11, 0]], startOffset: 0, endOffset: 0})), - new NoNewline(new IndexedRowRange({bufferRange: [[12, 0], [12, 0]], startOffset: 0, endOffset: 0})), + new Addition(new IndexedRowRange({bufferRange: [[3, 0], [5, Infinity]], startOffset: 0, endOffset: 0})), + new Deletion(new IndexedRowRange({bufferRange: [[7, 0], [9, Infinity]], startOffset: 0, endOffset: 0})), + new Addition(new IndexedRowRange({bufferRange: [[11, 0], [11, Infinity]], startOffset: 0, endOffset: 0})), + new NoNewline(new IndexedRowRange({bufferRange: [[12, 0], [12, Infinity]], startOffset: 0, endOffset: 0})), ], }); @@ -258,10 +258,10 @@ describe('Hunk', function() { const h0 = new Hunk({ ...attrs, changes: [ - new Addition(new IndexedRowRange({bufferRange: [[2, 0], [4, 0]], startOffset: 0, endOffset: 0})), - new Addition(new IndexedRowRange({bufferRange: [[6, 0], [6, 0]], startOffset: 0, endOffset: 0})), - new Deletion(new IndexedRowRange({bufferRange: [[7, 0], [10, 0]], startOffset: 0, endOffset: 0})), - new NoNewline(new IndexedRowRange({bufferRange: [[12, 0], [12, 0]], startOffset: 0, endOffset: 0})), + new Addition(new IndexedRowRange({bufferRange: [[2, 0], [4, Infinity]], startOffset: 0, endOffset: 0})), + new Addition(new IndexedRowRange({bufferRange: [[6, 0], [6, Infinity]], startOffset: 0, endOffset: 0})), + new Deletion(new IndexedRowRange({bufferRange: [[7, 0], [10, Infinity]], startOffset: 0, endOffset: 0})), + new NoNewline(new IndexedRowRange({bufferRange: [[12, 0], [12, Infinity]], startOffset: 0, endOffset: 0})), ], }); assert.strictEqual(h0.changedLineCount(), 8); @@ -321,17 +321,17 @@ describe('Hunk', function() { oldRowCount: 6, newRowCount: 6, rowRange: new IndexedRowRange({ - bufferRange: [[1, 0], [13, 0]], + bufferRange: [[1, 0], [13, Infinity]], startOffset: 5, endOffset: 91, }), changes: [ - new Addition(new IndexedRowRange({bufferRange: [[2, 0], [3, 0]], startOffset: 10, endOffset: 20})), - new Deletion(new IndexedRowRange({bufferRange: [[5, 0], [5, 0]], startOffset: 25, endOffset: 30})), - new Addition(new IndexedRowRange({bufferRange: [[7, 0], [7, 0]], startOffset: 35, endOffset: 40})), - new Deletion(new IndexedRowRange({bufferRange: [[8, 0], [9, 0]], startOffset: 40, endOffset: 50})), - new Addition(new IndexedRowRange({bufferRange: [[10, 0], [10, 0]], startOffset: 50, endOffset: 55})), - new NoNewline(new IndexedRowRange({bufferRange: [[13, 0], [13, 0]], startOffset: 65, endOffset: 92})), + new Addition(new IndexedRowRange({bufferRange: [[2, 0], [3, Infinity]], startOffset: 10, endOffset: 20})), + new Deletion(new IndexedRowRange({bufferRange: [[5, 0], [5, Infinity]], startOffset: 25, endOffset: 30})), + new Addition(new IndexedRowRange({bufferRange: [[7, 0], [7, Infinity]], startOffset: 35, endOffset: 40})), + new Deletion(new IndexedRowRange({bufferRange: [[8, 0], [9, Infinity]], startOffset: 40, endOffset: 50})), + new Addition(new IndexedRowRange({bufferRange: [[10, 0], [10, Infinity]], startOffset: 50, endOffset: 55})), + new NoNewline(new IndexedRowRange({bufferRange: [[13, 0], [13, Infinity]], startOffset: 65, endOffset: 92})), ], }); @@ -362,10 +362,10 @@ describe('Hunk', function() { newStartRow: 1, oldRowCount: 1, newRowCount: 1, - rowRange: new IndexedRowRange({bufferRange: [[0, 0], [3, 0]], startOffset: 0, endOffset: 20}), + rowRange: new IndexedRowRange({bufferRange: [[0, 0], [3, Infinity]], startOffset: 0, endOffset: 20}), changes: [ - new Addition(new IndexedRowRange({bufferRange: [[1, 0], [1, 0]], startOffset: 5, endOffset: 10})), - new Deletion(new IndexedRowRange({bufferRange: [[2, 0], [2, 0]], startOffset: 10, endOffset: 15})), + new Addition(new IndexedRowRange({bufferRange: [[1, 0], [1, Infinity]], startOffset: 5, endOffset: 10})), + new Deletion(new IndexedRowRange({bufferRange: [[2, 0], [2, Infinity]], startOffset: 10, endOffset: 15})), ], }); From 2caafaa1f9ff080af70babc51d75991d1f0d0570 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 5 Sep 2018 09:05:53 -0400 Subject: [PATCH 0276/4252] Don't assert against hunk row range columns --- test/helpers.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/helpers.js b/test/helpers.js index 8f0caf2fba..31dcbc475c 100644 --- a/test/helpers.js +++ b/test/helpers.js @@ -163,7 +163,8 @@ class PatchBufferAssertions { const hunk = this.patch.getHunks()[hunkIndex]; assert.isDefined(hunk); - assert.deepEqual(hunk.getRowRange().serialize().bufferRange, [[startRow, 0], [endRow, 0]]); + assert.strictEqual(hunk.getRowRange().getStartBufferRow(), startRow); + assert.strictEqual(hunk.getRowRange().getEndBufferRow(), endRow); assert.strictEqual(hunk.getHeader(), header); assert.lengthOf(hunk.getChanges(), changes.length); From 79d86626b4d77fb79f58134d78eedfce3c237236 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 5 Sep 2018 09:06:37 -0400 Subject: [PATCH 0277/4252] Patch coverage --- lib/models/patch/patch.js | 20 +-- test/models/patch/patch.test.js | 291 +++++++++++++++++++------------- 2 files changed, 184 insertions(+), 127 deletions(-) diff --git a/lib/models/patch/patch.js b/lib/models/patch/patch.js index 394839afc1..fc5d915c1e 100644 --- a/lib/models/patch/patch.js +++ b/lib/models/patch/patch.js @@ -316,7 +316,7 @@ export default class Patch { } const firstRow = firstChange.getStartBufferRow(); - return [[firstRow, 0], [firstRow, 0]]; + return [[firstRow, 0], [firstRow, Infinity]]; } getNextSelectionRange(lastPatch, lastSelectedRows) { @@ -328,9 +328,8 @@ export default class Patch { let lastSelectionIndex = 0; for (const hunk of lastPatch.getHunks()) { - let includesUnselectedChange = false; let includesMax = false; - let hunkSelectionIndex = 0; + let hunkSelectionOffset = 0; changeLoop: for (const change of hunk.getChanges()) { const intersections = change.getRowRange().intersectRowsIn(lastSelectedRows, this.getBufferText(), true); @@ -340,11 +339,8 @@ export default class Patch { const delta = includesMax ? lastMax - intersection.getStartBufferRow() + 1 : intersection.bufferRowCount(); if (gap) { - // This hunk includes at least one change row that was *not* selected. - includesUnselectedChange = true; - // Range of unselected changes. - hunkSelectionIndex += delta; + hunkSelectionOffset += delta; } if (includesMax) { @@ -353,9 +349,7 @@ export default class Patch { } } - if (includesUnselectedChange) { - lastSelectionIndex += hunkSelectionIndex; - } + lastSelectionIndex += hunkSelectionOffset; if (includesMax) { break; @@ -374,7 +368,7 @@ export default class Patch { } } - return [[newSelectionRow, 0], [newSelectionRow, 0]]; + return [[newSelectionRow, 0], [newSelectionRow, Infinity]]; } toString() { @@ -434,6 +428,10 @@ export const nullPatch = { return this; }, + getFirstChangeRange() { + return [[0, 0], [0, 0]]; + }, + getNextSelectionRange() { return [[0, 0], [0, 0]]; }, diff --git a/test/models/patch/patch.test.js b/test/models/patch/patch.test.js index 320bef70bf..d17441d3a4 100644 --- a/test/models/patch/patch.test.js +++ b/test/models/patch/patch.test.js @@ -21,28 +21,22 @@ describe('Patch', function() { it('computes the total changed line count', function() { const hunks = [ new Hunk({ - oldStartRow: 0, - newStartRow: 0, - oldRowCount: 1, - newRowCount: 1, + oldStartRow: 0, newStartRow: 0, oldRowCount: 1, newRowCount: 1, sectionHeading: 'zero', - rowRange: new IndexedRowRange({bufferRange: [[0, 0], [5, 0]], startOffset: 0, endOffset: 30}), + rowRange: buildRange(0, 5), changes: [ - new Addition(new IndexedRowRange({bufferRange: [[1, 0], [1, 0]], startOffset: 5, endOffset: 10})), - new Deletion(new IndexedRowRange({bufferRange: [[3, 0], [4, 0]], startOffset: 15, endOffset: 25})), + new Addition(buildRange(1)), + new Deletion(buildRange(3, 4)), ], }), new Hunk({ - oldStartRow: 0, - newStartRow: 0, - oldRowCount: 1, - newRowCount: 1, + oldStartRow: 0, newStartRow: 0, oldRowCount: 1, newRowCount: 1, sectionHeading: 'one', - rowRange: new IndexedRowRange({bufferRange: [[6, 0], [15, 0]], startOffset: 30, endOffset: 80}), + rowRange: buildRange(6, 15), changes: [ - new Deletion(new IndexedRowRange({bufferRange: [[7, 0], [7, 0]], startOffset: 35, endOffset: 40})), - new Deletion(new IndexedRowRange({bufferRange: [[9, 0], [11, 0]], startOffset: 45, endOffset: 60})), - new Addition(new IndexedRowRange({bufferRange: [[12, 0], [14, 0]], startOffset: 60, endOffset: 75})), + new Deletion(buildRange(7)), + new Deletion(buildRange(9, 11)), + new Addition(buildRange(12, 14)), ], }), ]; @@ -54,12 +48,9 @@ describe('Patch', function() { it('computes the maximum number of digits needed to display a diff line number', function() { const hunks = [ new Hunk({ - oldStartRow: 0, - oldRowCount: 1, - newStartRow: 0, - newRowCount: 1, + oldStartRow: 0, oldRowCount: 1, newStartRow: 0, newRowCount: 1, sectionHeading: 'zero', - rowRange: new IndexedRowRange({bufferRange: [[0, 0], [5, 0]], startOffset: 0, endOffset: 30}), + rowRange: buildRange(0, 5), changes: [], }), new Hunk({ @@ -68,7 +59,7 @@ describe('Patch', function() { newStartRow: 95, newRowCount: 3, sectionHeading: 'one', - rowRange: new IndexedRowRange({bufferRange: [[6, 0], [15, 0]], startOffset: 30, endOffset: 80}), + rowRange: buildRange(6, 15), changes: [], }), ]; @@ -146,9 +137,9 @@ describe('Patch', function() { endRow: 9, header: '@@ -12,9 +12,7 @@', changes: [ - {kind: 'addition', string: '+0008\n', range: [[1, 0], [1, 0]]}, - {kind: 'deletion', string: '-0013\n-0014\n', range: [[5, 0], [6, 0]]}, - {kind: 'deletion', string: '-0016\n', range: [[8, 0], [8, 0]]}, + {kind: 'addition', string: '+0008\n', range: [[1, 0], [1, Infinity]]}, + {kind: 'deletion', string: '-0013\n-0014\n', range: [[5, 0], [6, Infinity]]}, + {kind: 'deletion', string: '-0016\n', range: [[8, 0], [8, Infinity]]}, ], }, ); @@ -172,8 +163,8 @@ describe('Patch', function() { endRow: 4, header: '@@ -3,4 +3,4 @@', changes: [ - {kind: 'deletion', string: '-0001\n', range: [[1, 0], [1, 0]]}, - {kind: 'addition', string: '+0005\n', range: [[3, 0], [3, 0]]}, + {kind: 'deletion', string: '-0001\n', range: [[1, 0], [1, Infinity]]}, + {kind: 'addition', string: '+0005\n', range: [[3, 0], [3, Infinity]]}, ], }, { @@ -181,8 +172,8 @@ describe('Patch', function() { endRow: 14, header: '@@ -12,9 +12,8 @@', changes: [ - {kind: 'deletion', string: '-0015\n-0016\n', range: [[11, 0], [12, 0]]}, - {kind: 'addition', string: '+0017\n', range: [[13, 0], [13, 0]]}, + {kind: 'deletion', string: '-0015\n-0016\n', range: [[11, 0], [12, Infinity]]}, + {kind: 'addition', string: '+0017\n', range: [[13, 0], [13, Infinity]]}, ], }, { @@ -190,8 +181,8 @@ describe('Patch', function() { endRow: 17, header: '@@ -32,1 +31,2 @@', changes: [ - {kind: 'addition', string: '+0025\n', range: [[16, 0], [16, 0]]}, - {kind: 'nonewline', string: '\\ No newline at end of file\n', range: [[17, 0], [17, 0]]}, + {kind: 'addition', string: '+0025\n', range: [[16, 0], [16, Infinity]]}, + {kind: 'nonewline', string: '\\ No newline at end of file\n', range: [[17, 0], [17, Infinity]]}, ], }, ); @@ -202,14 +193,11 @@ describe('Patch', function() { const hunks = [ new Hunk({ - oldStartRow: 1, - oldRowCount: 5, - newStartRow: 1, - newRowCount: 0, + oldStartRow: 1, oldRowCount: 5, newStartRow: 1, newRowCount: 0, sectionHeading: 'zero', - rowRange: new IndexedRowRange({bufferRange: [[0, 0], [5, 0]], startOffset: 0, endOffset: 43}), + rowRange: buildRange(0, 5, 6), changes: [ - new Deletion(new IndexedRowRange({bufferRange: [[0, 0], [5, 0]], startOffset: 0, endOffset: 43})), + new Deletion(buildRange(0, 5, 6)), ], }), ]; @@ -224,8 +212,8 @@ describe('Patch', function() { endRow: 5, header: '@@ -1,5 +1,3 @@', changes: [ - {kind: 'deletion', string: '-line-1\n', range: [[1, 0], [1, 0]]}, - {kind: 'deletion', string: '-line-3\n-line-4\n', range: [[3, 0], [4, 0]]}, + {kind: 'deletion', string: '-line-1\n', range: [[1, 0], [1, Infinity]]}, + {kind: 'deletion', string: '-line-3\n-line-4\n', range: [[3, 0], [4, Infinity]]}, ], }, ); @@ -235,13 +223,10 @@ describe('Patch', function() { const bufferText = '0000\n0001\n0002\n'; const hunks = [ new Hunk({ - oldStartRow: 1, - oldRowCount: 3, - newStartRow: 1, - newRowCount: 0, - rowRange: new IndexedRowRange({bufferRange: [[0, 0], [2, 0]], startOffset: 0, endOffset: 15}), + oldStartRow: 1, oldRowCount: 3, newStartRow: 1, newRowCount: 0, + rowRange: buildRange(0, 2), changes: [ - new Deletion(new IndexedRowRange({bufferRange: [[0, 0], [2, 0]], startOffset: 0, endOffset: 15})), + new Deletion(buildRange(0, 2)), ], }), ]; @@ -271,8 +256,8 @@ describe('Patch', function() { endRow: 8, header: '@@ -13,7 +13,8 @@', changes: [ - {kind: 'deletion', string: '-0008\n', range: [[1, 0], [1, 0]]}, - {kind: 'addition', string: '+0012\n+0013\n', range: [[5, 0], [6, 0]]}, + {kind: 'deletion', string: '-0008\n', range: [[1, 0], [1, Infinity]]}, + {kind: 'addition', string: '+0012\n+0013\n', range: [[5, 0], [6, Infinity]]}, ], }, ); @@ -298,8 +283,8 @@ describe('Patch', function() { endRow: 5, header: '@@ -3,5 +3,4 @@', changes: [ - {kind: 'addition', string: '+0001\n', range: [[1, 0], [1, 0]]}, - {kind: 'deletion', string: '-0004\n-0005\n', range: [[3, 0], [4, 0]]}, + {kind: 'addition', string: '+0001\n', range: [[1, 0], [1, Infinity]]}, + {kind: 'deletion', string: '-0004\n-0005\n', range: [[3, 0], [4, Infinity]]}, ], }, { @@ -307,8 +292,8 @@ describe('Patch', function() { endRow: 13, header: '@@ -13,7 +12,7 @@', changes: [ - {kind: 'addition', string: '+0016\n', range: [[11, 0], [11, 0]]}, - {kind: 'deletion', string: '-0017\n', range: [[12, 0], [12, 0]]}, + {kind: 'addition', string: '+0016\n', range: [[11, 0], [11, Infinity]]}, + {kind: 'deletion', string: '-0017\n', range: [[12, 0], [12, Infinity]]}, ], }, { @@ -316,7 +301,7 @@ describe('Patch', function() { endRow: 16, header: '@@ -25,3 +24,2 @@', changes: [ - {kind: 'deletion', string: '-0020\n', range: [[15, 0], [15, 0]]}, + {kind: 'deletion', string: '-0020\n', range: [[15, 0], [15, Infinity]]}, ], }, { @@ -324,8 +309,8 @@ describe('Patch', function() { endRow: 19, header: '@@ -30,2 +28,1 @@', changes: [ - {kind: 'deletion', string: '-0025\n', range: [[18, 0], [18, 0]]}, - {kind: 'nonewline', string: '\\ No newline at end of file\n', range: [[19, 0], [19, 0]]}, + {kind: 'deletion', string: '-0025\n', range: [[18, 0], [18, Infinity]]}, + {kind: 'nonewline', string: '\\ No newline at end of file\n', range: [[19, 0], [19, Infinity]]}, ], }, ); @@ -342,8 +327,8 @@ describe('Patch', function() { endRow: 6, header: '@@ -3,5 +3,4 @@', changes: [ - {kind: 'addition', string: '+0001\n+0002\n', range: [[1, 0], [2, 0]]}, - {kind: 'deletion', string: '-0003\n-0004\n-0005\n', range: [[3, 0], [5, 0]]}, + {kind: 'addition', string: '+0001\n+0002\n', range: [[1, 0], [2, 4]]}, + {kind: 'deletion', string: '-0003\n-0004\n-0005\n', range: [[3, 0], [5, 4]]}, ], }, { @@ -351,9 +336,9 @@ describe('Patch', function() { endRow: 18, header: '@@ -13,7 +12,9 @@', changes: [ - {kind: 'deletion', string: '-0008\n-0009\n', range: [[8, 0], [9, 0]]}, - {kind: 'addition', string: '+0012\n+0013\n+0014\n+0015\n+0016\n', range: [[12, 0], [16, 0]]}, - {kind: 'deletion', string: '-0017\n', range: [[17, 0], [17, 0]]}, + {kind: 'deletion', string: '-0008\n-0009\n', range: [[8, 0], [9, 4]]}, + {kind: 'addition', string: '+0012\n+0013\n+0014\n+0015\n+0016\n', range: [[12, 0], [16, 4]]}, + {kind: 'deletion', string: '-0017\n', range: [[17, 0], [17, 4]]}, ], }, { @@ -361,8 +346,8 @@ describe('Patch', function() { endRow: 23, header: '@@ -25,3 +26,4 @@', changes: [ - {kind: 'deletion', string: '-0020\n', range: [[20, 0], [20, 0]]}, - {kind: 'addition', string: '+0021\n+0022\n', range: [[21, 0], [22, 0]]}, + {kind: 'deletion', string: '-0020\n', range: [[20, 0], [20, 4]]}, + {kind: 'addition', string: '+0021\n+0022\n', range: [[21, 0], [22, 4]]}, ], }, { @@ -370,8 +355,8 @@ describe('Patch', function() { endRow: 26, header: '@@ -30,2 +32,1 @@', changes: [ - {kind: 'deletion', string: '-0025\n', range: [[25, 0], [25, 0]]}, - {kind: 'nonewline', string: '\\ No newline at end of file\n', range: [[26, 0], [26, 0]]}, + {kind: 'deletion', string: '-0025\n', range: [[25, 0], [25, 4]]}, + {kind: 'nonewline', string: '\\ No newline at end of file\n', range: [[26, 0], [26, 26]]}, ], }, ); @@ -381,13 +366,10 @@ describe('Patch', function() { const bufferText = '0000\n0001\n0002\n'; const hunks = [ new Hunk({ - oldStartRow: 1, - oldRowCount: 0, - newStartRow: 1, - newRowCount: 3, - rowRange: new IndexedRowRange({bufferRange: [[0, 0], [2, 0]], startOffset: 0, endOffset: 15}), + oldStartRow: 1, oldRowCount: 0, newStartRow: 1, newRowCount: 3, + rowRange: buildRange(0, 2), changes: [ - new Addition(new IndexedRowRange({bufferRange: [[0, 0], [2, 0]], startOffset: 0, endOffset: 15})), + new Addition(buildRange(0, 2)), ], }), ]; @@ -401,7 +383,7 @@ describe('Patch', function() { endRow: 2, header: '@@ -1,3 +1,1 @@', changes: [ - {kind: 'deletion', string: '-0001\n-0002\n', range: [[1, 0], [2, 0]]}, + {kind: 'deletion', string: '-0001\n-0002\n', range: [[1, 0], [2, Infinity]]}, ], }, ); @@ -415,9 +397,9 @@ describe('Patch', function() { oldRowCount: 0, newStartRow: 1, newRowCount: 3, - rowRange: new IndexedRowRange({bufferRange: [[0, 0], [2, 0]], startOffset: 0, endOffset: 15}), + rowRange: buildRange(0, 2), changes: [ - new Addition(new IndexedRowRange({bufferRange: [[0, 0], [2, 0]], startOffset: 0, endOffset: 15})), + new Addition(buildRange(0, 2)), ], }), ]; @@ -436,6 +418,30 @@ describe('Patch', function() { }); }); + describe('getFirstChangeRange', function() { + it('accesses the range of the first change from the first hunk', function() { + const patch = buildPatchFixture(); + assert.deepEqual(patch.getFirstChangeRange(), [[1, 0], [1, Infinity]]); + }); + + it('returns the origin if the first hunk is empty', function() { + const hunks = [ + new Hunk({ + oldStartRow: 1, oldRowCount: 0, newStartRow: 1, newRowCount: 0, + rowRange: buildRange(0), + changes: [], + }), + ]; + const patch = new Patch({status: 'modified', hunks, bufferText: ''}); + assert.deepEqual(patch.getFirstChangeRange(), [[0, 0], [0, 0]]); + }); + + it('returns the origin if the patch is empty', function() { + const patch = new Patch({status: 'modified', hunks: [], bufferText: ''}); + assert.deepEqual(patch.getFirstChangeRange(), [[0, 0], [0, 0]]); + }); + }); + describe('next selection range derivation', function() { it('selects the first change region after the highest buffer row', function() { const lastPatch = buildPatchFixture(); @@ -493,7 +499,76 @@ describe('Patch', function() { const nextRange = nextPatch.getNextSelectionRange(lastPatch, lastSelectedRows); // Original buffer row 14 = the next changed row = new buffer row 11 - assert.deepEqual(nextRange, [[11, 0], [11, 0]]); + assert.deepEqual(nextRange, [[11, 0], [11, Infinity]]); + }); + + it('offsets the chosen selection index by hunks that were completely selected', function() { + const lastPatch = new Patch({ + status: 'modified', + hunks: [ + new Hunk({ + oldStartRow: 1, oldRowCount: 3, newStartRow: 1, newRowCount: 3, + rowRange: buildRange(0, 5), + changes: [ + new Addition(buildRange(1, 2)), + new Deletion(buildRange(3, 4)), + ], + }), + new Hunk({ + oldStartRow: 5, oldRowCount: 4, newStartRow: 5, newRowCount: 4, + rowRange: buildRange(6, 11), + changes: [ + new Addition(buildRange(7, 8)), + new Deletion(buildRange(9, 10)), + ], + }), + ], + bufferText: '0000\n0001\n0002\n0003\n0004\n0005\n0006\n0007\n0008\n0009\n0010\n0011\n', + }); + // Select: + // * all changes from hunk 0 + // * partial addition (8 of 7-8) from hunk 1 + const lastSelectedRows = new Set([1, 2, 3, 4, 8]); + + const nextPatch = new Patch({ + status: 'modified', + hunks: [ + new Hunk({ + oldStartRow: 5, oldRowCount: 4, newStartRow: 5, newRowCount: 4, + rowRange: buildRange(0, 5), + changes: [ + new Addition(buildRange(1, 1)), + new Deletion(buildRange(3, 4)), + ], + }), + ], + bufferText: '0006\n0007\n0008\n0009\n0010\n0011\n', + }); + + const range = nextPatch.getNextSelectionRange(lastPatch, lastSelectedRows); + assert.deepEqual(range, [[3, 0], [3, Infinity]]); + }); + + it('selects the first row of the first change of the patch if no rows were selected before', function() { + const lastPatch = buildPatchFixture(); + const lastSelectedRows = new Set(); + + const nextPatch = new Patch({ + status: 'modified', + hunks: [ + new Hunk({ + oldStartRow: 1, oldRowCount: 3, newStartRow: 1, newRowCount: 4, + rowRange: buildRange(0, 4), + changes: [ + new Addition(buildRange(1, 2)), + new Deletion(buildRange(3, 3)), + ], + }), + ], + }); + + const range = nextPatch.getNextSelectionRange(lastPatch, lastSelectedRows); + assert.deepEqual(range, [[1, 0], [1, Infinity]]); }); }); @@ -504,26 +579,20 @@ describe('Patch', function() { // patch buffer: 0000.1111.2222.3333.4444.5555.6666.7777.8888.9999. const hunk0 = new Hunk({ - oldStartRow: 0, - newStartRow: 0, - oldRowCount: 2, - newRowCount: 3, + oldStartRow: 0, newStartRow: 0, oldRowCount: 2, newRowCount: 3, sectionHeading: 'zero', - rowRange: new IndexedRowRange({bufferRange: [[0, 0], [2, 0]], startOffset: 0, endOffset: 15}), + rowRange: buildRange(0, 2), changes: [ - new Addition(new IndexedRowRange({bufferRange: [[1, 0], [1, 0]], startOffset: 5, endOffset: 10})), + new Addition(buildRange(1)), ], }); const hunk1 = new Hunk({ - oldStartRow: 5, - newStartRow: 6, - oldRowCount: 4, - newRowCount: 2, + oldStartRow: 5, newStartRow: 6, oldRowCount: 4, newRowCount: 2, sectionHeading: 'one', - rowRange: new IndexedRowRange({bufferRange: [[6, 0], [9, 0]], startOffset: 30, endOffset: 50}), + rowRange: buildRange(6, 9), changes: [ - new Deletion(new IndexedRowRange({bufferRange: [[7, 0], [8, 0]], startOffset: 35, endOffset: 45})), + new Deletion(buildRange(7, 8)), ], }); @@ -551,14 +620,16 @@ describe('Patch', function() { assert.strictEqual(nullPatch.toString(), ''); assert.strictEqual(nullPatch.getChangedLineCount(), 0); assert.strictEqual(nullPatch.getMaxLineNumberWidth(), 0); + assert.deepEqual(nullPatch.getFirstChangeRange(), [[0, 0], [0, 0]]); + assert.deepEqual(nullPatch.getNextSelectionRange(), [[0, 0], [0, 0]]); }); }); -function buildRange(startRow, endRow = startRow, rowLength = 5) { +function buildRange(startRow, endRow = startRow, rowLength = 5, endRowLength = rowLength) { return new IndexedRowRange({ - bufferRange: [[startRow, 0], [endRow, 0]], + bufferRange: [[startRow, 0], [endRow, endRowLength - 1]], startOffset: startRow * rowLength, - endOffset: (endRow + 1) * rowLength, + endOffset: endRow * rowLength + endRowLength, }); } @@ -571,52 +642,40 @@ function buildPatchFixture() { const hunks = [ new Hunk({ - oldStartRow: 3, - oldRowCount: 4, - newStartRow: 3, - newRowCount: 5, + oldStartRow: 3, oldRowCount: 4, newStartRow: 3, newRowCount: 5, sectionHeading: 'zero', - rowRange: new IndexedRowRange({bufferRange: [[0, 0], [6, 0]], startOffset: 0, endOffset: 35}), + rowRange: buildRange(0, 6), changes: [ - new Deletion(new IndexedRowRange({bufferRange: [[1, 0], [2, 0]], startOffset: 5, endOffset: 15})), - new Addition(new IndexedRowRange({bufferRange: [[3, 0], [5, 0]], startOffset: 15, endOffset: 30})), + new Deletion(buildRange(1, 2)), + new Addition(buildRange(3, 5)), ], }), new Hunk({ - oldStartRow: 12, - oldRowCount: 9, - newStartRow: 13, - newRowCount: 7, + oldStartRow: 12, oldRowCount: 9, newStartRow: 13, newRowCount: 7, sectionHeading: 'one', - rowRange: new IndexedRowRange({bufferRange: [[7, 0], [18, 0]], startOffset: 35, endOffset: 95}), + rowRange: buildRange(7, 18), changes: [ - new Addition(new IndexedRowRange({bufferRange: [[8, 0], [9, 0]], startOffset: 40, endOffset: 50})), - new Deletion(new IndexedRowRange({bufferRange: [[12, 0], [16, 0]], startOffset: 60, endOffset: 85})), - new Addition(new IndexedRowRange({bufferRange: [[17, 0], [17, 0]], startOffset: 85, endOffset: 90})), + new Addition(buildRange(8, 9)), + new Deletion(buildRange(12, 16)), + new Addition(buildRange(17, 17)), ], }), new Hunk({ - oldStartRow: 26, - oldRowCount: 4, - newStartRow: 25, - newRowCount: 3, + oldStartRow: 26, oldRowCount: 4, newStartRow: 25, newRowCount: 3, sectionHeading: 'two', - rowRange: new IndexedRowRange({bufferRange: [[19, 0], [23, 0]], startOffset: 95, endOffset: 120}), + rowRange: buildRange(19, 23), changes: [ - new Addition(new IndexedRowRange({bufferRange: [[20, 0], [20, 0]], startOffset: 100, endOffset: 105})), - new Deletion(new IndexedRowRange({bufferRange: [[21, 0], [22, 0]], startOffset: 105, endOffset: 115})), + new Addition(buildRange(20)), + new Deletion(buildRange(21, 22)), ], }), new Hunk({ - oldStartRow: 32, - oldRowCount: 1, - newStartRow: 30, - newRowCount: 2, + oldStartRow: 32, oldRowCount: 1, newStartRow: 30, newRowCount: 2, sectionHeading: 'three', - rowRange: new IndexedRowRange({bufferRange: [[24, 0], [26, 0]], startOffset: 120, endOffset: 157}), + rowRange: buildRange(24, 26), changes: [ - new Addition(new IndexedRowRange({bufferRange: [[25, 0], [25, 0]], startOffset: 125, endOffset: 130})), - new NoNewline(new IndexedRowRange({bufferRange: [[26, 0], [26, 0]], startOffset: 130, endOffset: 157})), + new Addition(buildRange(25)), + new NoNewline(buildRange(26, 26, 5, 27)), ], }), ]; From c6b7eb82e4b688974a46d5491fdc39049fa1156a Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 5 Sep 2018 09:42:02 -0400 Subject: [PATCH 0278/4252] FilePatch coverage --- test/helpers.js | 11 ++ test/models/patch/file-patch.test.js | 231 ++++++++++++--------------- test/models/patch/patch.test.js | 11 +- 3 files changed, 112 insertions(+), 141 deletions(-) diff --git a/test/helpers.js b/test/helpers.js index 31dcbc475c..ea359743a4 100644 --- a/test/helpers.js +++ b/test/helpers.js @@ -16,6 +16,7 @@ import WorkerManager from '../lib/worker-manager'; import ContextMenuInterceptor from '../lib/context-menu-interceptor'; import getRepoPipelineManager from '../lib/get-repo-pipeline-manager'; import {clearRelayExpectations} from '../lib/relay-network-layer-manager'; +import IndexedRowRange from '../lib/models/indexed-row-range'; assert.autocrlfEqual = (actual, expected, ...args) => { const newActual = actual.replace(/\r\n/g, '\n'); @@ -154,6 +155,16 @@ export function assertEqualSortedArraysByKey(arr1, arr2, key) { // Helpers for test/models/patch classes +// Quickly construct an IndexedRowRange into a buffer that has uniform line lengths (except for possibly the final +// line.) The default parameters are chosen to match buffers of the form "0000\n0001\n0002\n....". +export function buildRange(startRow, endRow = startRow, rowLength = 5, endRowLength = rowLength) { + return new IndexedRowRange({ + bufferRange: [[startRow, 0], [endRow, endRowLength - 1]], + startOffset: startRow * rowLength, + endOffset: endRow * rowLength + endRowLength, + }); +} + class PatchBufferAssertions { constructor(patch) { this.patch = patch; diff --git a/test/models/patch/file-patch.test.js b/test/models/patch/file-patch.test.js index 81ad43e0a9..b621425466 100644 --- a/test/models/patch/file-patch.test.js +++ b/test/models/patch/file-patch.test.js @@ -3,21 +3,17 @@ import File, {nullFile} from '../../../lib/models/patch/file'; import Patch from '../../../lib/models/patch/patch'; import Hunk from '../../../lib/models/patch/hunk'; import {Addition, Deletion, NoNewline} from '../../../lib/models/patch/region'; -import IndexedRowRange from '../../../lib/models/indexed-row-range'; -import {assertInFilePatch} from '../../helpers'; +import {assertInFilePatch, buildRange} from '../../helpers'; describe('FilePatch', function() { it('delegates methods to its files and patch', function() { - const bufferText = '0000\n0001\n'; + const bufferText = '0000\n0001\n0002\n'; const hunks = [ new Hunk({ - oldStartRow: 2, - oldRowCount: 1, - newStartRow: 2, - newRowCount: 2, - rowRange: new IndexedRowRange({bufferRange: [[0, 0], [1, 0]], startOffset: 0, endOffset: 10}), + oldStartRow: 2, oldRowCount: 1, newStartRow: 2, newRowCount: 3, + rowRange: buildRange(0, 2), changes: [ - new Addition(new IndexedRowRange({bufferRange: [[1, 0], [1, 0]], startOffset: 5, endOffset: 10})), + new Addition(buildRange(1, 2)), ], }), ]; @@ -36,9 +32,27 @@ describe('FilePatch', function() { assert.strictEqual(filePatch.getNewMode(), '100755'); assert.isUndefined(filePatch.getNewSymlink()); - assert.strictEqual(filePatch.getByteSize(), 10); + assert.strictEqual(filePatch.getByteSize(), 15); assert.strictEqual(filePatch.getBufferText(), bufferText); assert.strictEqual(filePatch.getMaxLineNumberWidth(), 1); + + assert.deepEqual(filePatch.getFirstChangeRange(), [[1, 0], [1, Infinity]]); + + const nBufferText = '0001\n0002\n'; + const nHunks = [ + new Hunk({ + oldStartRow: 3, oldRowCount: 1, newStartRow: 3, newRowCount: 2, + rowRange: buildRange(0, 1), + changes: [ + new Addition(buildRange(1)), + ], + }), + ]; + const nPatch = new Patch({status: 'modified', hunks: nHunks, bufferText: nBufferText}); + const nFilePatch = new FilePatch(oldFile, newFile, nPatch); + + const range = nFilePatch.getNextSelectionRange(filePatch, new Set([1])); + assert.deepEqual(range, [[1, 0], [1, Infinity]]); }); it('accesses a file path from either side of the patch', function() { @@ -56,18 +70,15 @@ describe('FilePatch', function() { const bufferText = '0000\n0001\n0002\n0003\n0004\n0005\n0006\n0007\n0008\n0009\n'; const hunks = [ new Hunk({ - oldStartRow: 1, - oldRowCount: 0, - newStartRow: 1, - newRowCount: 0, - rowRange: new IndexedRowRange({bufferRange: [[0, 0], [9, 0]], startOffset: 0, endOffset: 50}), + oldStartRow: 1, oldRowCount: 0, newStartRow: 1, newRowCount: 0, + rowRange: buildRange(0, 9), changes: [ - new Addition(new IndexedRowRange({bufferRange: [[1, 0], [1, 0]], startOffset: 5, endOffset: 10})), - new Addition(new IndexedRowRange({bufferRange: [[3, 0], [3, 0]], startOffset: 15, endOffset: 20})), - new Deletion(new IndexedRowRange({bufferRange: [[4, 0], [4, 0]], startOffset: 20, endOffset: 25})), - new Addition(new IndexedRowRange({bufferRange: [[5, 0], [6, 0]], startOffset: 25, endOffset: 35})), - new Deletion(new IndexedRowRange({bufferRange: [[7, 0], [7, 0]], startOffset: 35, endOffset: 40})), - new Addition(new IndexedRowRange({bufferRange: [[8, 0], [8, 0]], startOffset: 40, endOffset: 45})), + new Addition(buildRange(1)), + new Addition(buildRange(3)), + new Deletion(buildRange(4)), + new Addition(buildRange(5, 6)), + new Deletion(buildRange(7)), + new Addition(buildRange(8)), ], }), ]; @@ -78,16 +89,16 @@ describe('FilePatch', function() { const additionRanges = filePatch.getAdditionRanges(); assert.deepEqual(additionRanges.map(range => range.serialize()), [ - [[1, 0], [1, 0]], - [[3, 0], [3, 0]], - [[5, 0], [6, 0]], - [[8, 0], [8, 0]], + [[1, 0], [1, 4]], + [[3, 0], [3, 4]], + [[5, 0], [6, 4]], + [[8, 0], [8, 4]], ]); const deletionRanges = filePatch.getDeletionRanges(); assert.deepEqual(deletionRanges.map(range => range.serialize()), [ - [[4, 0], [4, 0]], - [[7, 0], [7, 0]], + [[4, 0], [4, 4]], + [[7, 0], [7, 4]], ]); const noNewlineRanges = filePatch.getNoNewlineRanges(); @@ -107,14 +118,11 @@ describe('FilePatch', function() { const bufferText = '0000\n No newline at end of file\n'; const hunks = [ new Hunk({ - oldStartRow: 1, - oldRowCount: 0, - newStartRow: 1, - newRowCount: 0, - rowRange: new IndexedRowRange({bufferRange: [[0, 0], [1, 0]], startOffset: 0, endOffset: 32}), + oldStartRow: 1, oldRowCount: 0, newStartRow: 1, newRowCount: 0, + rowRange: buildRange(0, 1), changes: [ - new Addition(new IndexedRowRange({bufferRange: [[0, 0], [0, 0]], startOffset: 0, endOffset: 5})), - new NoNewline(new IndexedRowRange({bufferRange: [[1, 0], [1, 0]], startOffset: 5, endOffset: 32})), + new Addition(buildRange(0)), + new NoNewline(buildRange(1, 1, 5, 26)), ], }), ]; @@ -125,7 +133,7 @@ describe('FilePatch', function() { const noNewlineRanges = filePatch.getNoNewlineRanges(); assert.deepEqual(noNewlineRanges.map(range => range.serialize()), [ - [[1, 0], [1, 0]], + [[1, 0], [1, 25]], ]); }); @@ -213,14 +221,11 @@ describe('FilePatch', function() { const bufferText = '0000\n0001\n0002\n0003\n0004\n'; const hunks = [ new Hunk({ - oldStartRow: 5, - oldRowCount: 3, - newStartRow: 5, - newRowCount: 4, - rowRange: new IndexedRowRange({bufferRange: [[0, 0], [4, 0]], startOffset: 0, endOffset: 25}), + oldStartRow: 5, oldRowCount: 3, newStartRow: 5, newRowCount: 4, + rowRange: buildRange(0, 4), changes: [ - new Addition(new IndexedRowRange({bufferRange: [[1, 0], [2, 0]], startOffset: 5, endOffset: 15})), - new Deletion(new IndexedRowRange({bufferRange: [[3, 0], [3, 0]], startOffset: 15, endOffset: 20})), + new Addition(buildRange(1, 2)), + new Deletion(buildRange(3)), ], }), ]; @@ -242,8 +247,8 @@ describe('FilePatch', function() { endRow: 3, header: '@@ -5,3 +5,3 @@', changes: [ - {kind: 'addition', string: '+0001\n', range: [[1, 0], [1, 0]]}, - {kind: 'deletion', string: '-0003\n', range: [[2, 0], [2, 0]]}, + {kind: 'addition', string: '+0001\n', range: [[1, 0], [1, Infinity]]}, + {kind: 'deletion', string: '-0003\n', range: [[2, 0], [2, Infinity]]}, ], }, ); @@ -256,13 +261,10 @@ describe('FilePatch', function() { const bufferText = '0000\n0001\n0002\n'; const hunks = [ new Hunk({ - oldStartRow: 1, - oldRowCount: 3, - newStartRow: 1, - newRowCount: 0, - rowRange: new IndexedRowRange({bufferRange: [[0, 0], [2, 0]], startOffset: 0, endOffset: 15}), + oldStartRow: 1, oldRowCount: 3, newStartRow: 1, newRowCount: 0, + rowRange: buildRange(0, 2), changes: [ - new Deletion(new IndexedRowRange({bufferRange: [[0, 0], [2, 0]], startOffset: 0, endOffset: 15})), + new Deletion(buildRange(0, 2)), ], }), ]; @@ -286,7 +288,7 @@ describe('FilePatch', function() { endRow: 2, header: '@@ -1,3 +1,1 @@', changes: [ - {kind: 'deletion', string: '-0001\n-0002\n', range: [[1, 0], [2, 0]]}, + {kind: 'deletion', string: '-0001\n-0002\n', range: [[1, 0], [2, Infinity]]}, ], }, ); @@ -305,7 +307,7 @@ describe('FilePatch', function() { endRow: 2, header: '@@ -1,3 +1,0 @@', changes: [ - {kind: 'deletion', string: '-0000\n-0001\n-0002\n', range: [[0, 0], [2, 0]]}, + {kind: 'deletion', string: '-0000\n-0001\n-0002\n', range: [[0, 0], [2, 4]]}, ], }, ); @@ -315,13 +317,10 @@ describe('FilePatch', function() { const bufferText = '0000\n0001\n0002\n'; const hunks = [ new Hunk({ - oldStartRow: 1, - oldRowCount: 3, - newStartRow: 1, - newRowCount: 0, - rowRange: new IndexedRowRange({bufferRange: [[0, 0], [2, 0]], startOffset: 0, endOffset: 15}), + oldStartRow: 1, oldRowCount: 3, newStartRow: 1, newRowCount: 0, + rowRange: buildRange(0, 2), changes: [ - new Deletion(new IndexedRowRange({bufferRange: [[0, 0], [2, 0]], startOffset: 0, endOffset: 15})), + new Deletion(buildRange(0, 2)), ], }), ]; @@ -341,13 +340,10 @@ describe('FilePatch', function() { const bufferText = '0000\n0001\n0002\n0003\n0004\n0005\n'; const hunks = [ new Hunk({ - oldStartRow: 10, - oldRowCount: 2, - newStartRow: 10, - newRowCount: 3, - rowRange: new IndexedRowRange({bufferRange: [[0, 0], [2, 0]], startOffset: 0, endOffset: 15}), + oldStartRow: 10, oldRowCount: 2, newStartRow: 10, newRowCount: 3, + rowRange: buildRange(0, 2), changes: [ - new Addition(new IndexedRowRange({bufferRange: [[1, 0], [1, 0]], startOffset: 5, endOffset: 10})), + new Addition(buildRange(1)), ], }), new Hunk({ @@ -355,9 +351,9 @@ describe('FilePatch', function() { oldRowCount: 3, newStartRow: 19, newRowCount: 2, - rowRange: new IndexedRowRange({bufferRange: [[3, 0], [5, 0]], startOffset: 15, endOffset: 35}), + rowRange: buildRange(3, 5), changes: [ - new Deletion(new IndexedRowRange({bufferRange: [[4, 0], [4, 0]], startOffset: 20, endOffset: 25})), + new Deletion(buildRange(4)), ], }), ]; @@ -374,7 +370,7 @@ describe('FilePatch', function() { endRow: 2, header: '@@ -20,3 +18,2 @@', changes: [ - {kind: 'deletion', string: '-0004\n', range: [[1, 0], [1, 0]]}, + {kind: 'deletion', string: '-0004\n', range: [[1, 0], [1, Infinity]]}, ], }, ); @@ -385,14 +381,11 @@ describe('FilePatch', function() { const bufferText = '0000\n0001\n0002\n0003\n0004\n'; const hunks = [ new Hunk({ - oldStartRow: 5, - oldRowCount: 3, - newStartRow: 5, - newRowCount: 4, - rowRange: new IndexedRowRange({bufferRange: [[0, 0], [4, 0]], startOffset: 0, endOffset: 25}), + oldStartRow: 5, oldRowCount: 3, newStartRow: 5, newRowCount: 4, + rowRange: buildRange(0, 4), changes: [ - new Addition(new IndexedRowRange({bufferRange: [[1, 0], [2, 0]], startOffset: 5, endOffset: 15})), - new Deletion(new IndexedRowRange({bufferRange: [[3, 0], [3, 0]], startOffset: 15, endOffset: 20})), + new Addition(buildRange(1, 2)), + new Deletion(buildRange(3)), ], }), ]; @@ -414,8 +407,8 @@ describe('FilePatch', function() { endRow: 4, header: '@@ -5,4 +5,4 @@', changes: [ - {kind: 'deletion', string: '-0001\n', range: [[1, 0], [1, 0]]}, - {kind: 'addition', string: '+0003\n', range: [[3, 0], [3, 0]]}, + {kind: 'deletion', string: '-0001\n', range: [[1, 0], [1, Infinity]]}, + {kind: 'addition', string: '+0003\n', range: [[3, 0], [3, Infinity]]}, ], }, ); @@ -428,13 +421,10 @@ describe('FilePatch', function() { const bufferText = '0000\n0001\n0002\n'; const hunks = [ new Hunk({ - oldStartRow: 1, - oldRowCount: 0, - newStartRow: 1, - newRowCount: 3, - rowRange: new IndexedRowRange({bufferRange: [[0, 0], [2, 0]], startOffset: 0, endOffset: 15}), + oldStartRow: 1, oldRowCount: 0, newStartRow: 1, newRowCount: 3, + rowRange: buildRange(0, 2), changes: [ - new Addition(new IndexedRowRange({bufferRange: [[0, 0], [2, 0]], startOffset: 0, endOffset: 15})), + new Addition(buildRange(0, 2)), ], }), ]; @@ -452,7 +442,7 @@ describe('FilePatch', function() { endRow: 2, header: '@@ -1,3 +1,2 @@', changes: [ - {kind: 'deletion', string: '-0002\n', range: [[2, 0], [2, 0]]}, + {kind: 'deletion', string: '-0002\n', range: [[2, 0], [2, Infinity]]}, ], }, ); @@ -467,7 +457,7 @@ describe('FilePatch', function() { endRow: 2, header: '@@ -1,3 +1,0 @@', changes: [ - {kind: 'deletion', string: '-0000\n-0001\n-0002\n', range: [[0, 0], [2, 0]]}, + {kind: 'deletion', string: '-0000\n-0001\n-0002\n', range: [[0, 0], [2, 4]]}, ], }, ); @@ -484,7 +474,7 @@ describe('FilePatch', function() { endRow: 2, header: '@@ -1,3 +1,0 @@', changes: [ - {kind: 'deletion', string: '-0000\n-0001\n-0002\n', range: [[0, 0], [2, 0]]}, + {kind: 'deletion', string: '-0000\n-0001\n-0002\n', range: [[0, 0], [2, 4]]}, ], }, ); @@ -496,23 +486,17 @@ describe('FilePatch', function() { const bufferText = '0000\n0001\n0002\n0003\n0004\n0005\n'; const hunks = [ new Hunk({ - oldStartRow: 10, - oldRowCount: 2, - newStartRow: 10, - newRowCount: 3, - rowRange: new IndexedRowRange({bufferRange: [[0, 0], [2, 0]], startOffset: 0, endOffset: 15}), + oldStartRow: 10, oldRowCount: 2, newStartRow: 10, newRowCount: 3, + rowRange: buildRange(0, 2), changes: [ - new Addition(new IndexedRowRange({bufferRange: [[1, 0], [1, 0]], startOffset: 5, endOffset: 10})), + new Addition(buildRange(1)), ], }), new Hunk({ - oldStartRow: 20, - oldRowCount: 3, - newStartRow: 19, - newRowCount: 2, - rowRange: new IndexedRowRange({bufferRange: [[3, 0], [5, 0]], startOffset: 15, endOffset: 35}), + oldStartRow: 20, oldRowCount: 3, newStartRow: 19, newRowCount: 2, + rowRange: buildRange(3, 5), changes: [ - new Deletion(new IndexedRowRange({bufferRange: [[4, 0], [4, 0]], startOffset: 20, endOffset: 25})), + new Deletion(buildRange(4)), ], }), ]; @@ -529,7 +513,7 @@ describe('FilePatch', function() { endRow: 2, header: '@@ -10,3 +10,2 @@', changes: [ - {kind: 'deletion', string: '-0001\n', range: [[1, 0], [1, 0]]}, + {kind: 'deletion', string: '-0001\n', range: [[1, 0], [1, Infinity]]}, ], }, ); @@ -540,24 +524,18 @@ describe('FilePatch', function() { const bufferText = '0000\n0001\n0002\n0003\n0004\n0005\n0006\n0007\n'; const hunks = [ new Hunk({ - oldStartRow: 10, - oldRowCount: 4, - newStartRow: 10, - newRowCount: 3, - rowRange: new IndexedRowRange({bufferRange: [[0, 0], [4, 0]], startOffset: 0, endOffset: 25}), + oldStartRow: 10, oldRowCount: 4, newStartRow: 10, newRowCount: 3, + rowRange: buildRange(0, 4), changes: [ - new Addition(new IndexedRowRange({bufferRange: [[1, 0], [1, 0]], startOffset: 5, endOffset: 10})), - new Deletion(new IndexedRowRange({bufferRange: [[2, 0], [3, 0]], startOffset: 10, endOffset: 20})), + new Addition(buildRange(1)), + new Deletion(buildRange(2, 3)), ], }), new Hunk({ - oldStartRow: 20, - oldRowCount: 2, - newStartRow: 20, - newRowCount: 3, - rowRange: new IndexedRowRange({bufferRange: [[5, 0], [7, 0]], startOffset: 25, endOffset: 40}), + oldStartRow: 20, oldRowCount: 2, newStartRow: 20, newRowCount: 3, + rowRange: buildRange(5, 7), changes: [ - new Addition(new IndexedRowRange({bufferRange: [[6, 0], [6, 0]], startOffset: 30, endOffset: 35})), + new Addition(buildRange(6)), ], }), ]; @@ -587,14 +565,11 @@ describe('FilePatch', function() { const bufferText = '0000\n0001\n No newline at end of file\n'; const hunks = [ new Hunk({ - oldStartRow: 1, - oldRowCount: 1, - newStartRow: 1, - newRowCount: 2, - rowRange: new IndexedRowRange({bufferRange: [[0, 0], [2, 0]], startOffset: 0, endOffset: 37}), + oldStartRow: 1, oldRowCount: 1, newStartRow: 1, newRowCount: 2, + rowRange: buildRange(0, 2), changes: [ - new Addition(new IndexedRowRange({bufferRange: [[1, 0], [1, 0]], startOffset: 5, endOffset: 10})), - new NoNewline(new IndexedRowRange({bufferRange: [[2, 0], [2, 0]], startOffset: 10, endOffset: 37})), + new Addition(buildRange(1)), + new NoNewline(buildRange(2, 2, 5, 27)), ], }), ]; @@ -619,13 +594,10 @@ describe('FilePatch', function() { const bufferText = '0000\n0001\n'; const hunks = [ new Hunk({ - oldStartRow: 1, - oldRowCount: 0, - newStartRow: 1, - newRowCount: 2, - rowRange: new IndexedRowRange({bufferRange: [[0, 0], [2, 0]], startOffset: 0, endOffset: 10}), + oldStartRow: 1, oldRowCount: 0, newStartRow: 1, newRowCount: 2, + rowRange: buildRange(0, 2), changes: [ - new Addition(new IndexedRowRange({bufferRange: [[0, 0], [2, 0]], startOffset: 0, endOffset: 10})), + new Addition(buildRange(0, 2)), ], }), ]; @@ -656,13 +628,10 @@ describe('FilePatch', function() { const bufferText = '0000\n0001\n'; const hunks = [ new Hunk({ - oldStartRow: 1, - oldRowCount: 2, - newStartRow: 1, - newRowCount: 0, - rowRange: new IndexedRowRange({bufferRange: [[0, 0], [2, 0]], startOffset: 0, endOffset: 10}), + oldStartRow: 1, oldRowCount: 2, newStartRow: 1, newRowCount: 0, + rowRange: buildRange(0, 2), changes: [ - new Deletion(new IndexedRowRange({bufferRange: [[0, 0], [2, 0]], startOffset: 0, endOffset: 10})), + new Deletion(buildRange(0, 2)), ], }), ]; @@ -692,7 +661,7 @@ describe('FilePatch', function() { }); it('has a nullFilePatch that stubs all FilePatch methods', function() { - const rowRange = new IndexedRowRange({bufferRange: [[0, 0], [1, 0]], startOffset: 0, endOffset: 10}); + const rowRange = buildRange(0, 1); assert.isFalse(nullFilePatch.isPresent()); assert.isFalse(nullFilePatch.getOldFile().isPresent()); diff --git a/test/models/patch/patch.test.js b/test/models/patch/patch.test.js index d17441d3a4..00f115e09c 100644 --- a/test/models/patch/patch.test.js +++ b/test/models/patch/patch.test.js @@ -1,8 +1,7 @@ import Patch, {nullPatch} from '../../../lib/models/patch/patch'; import Hunk from '../../../lib/models/patch/hunk'; -import IndexedRowRange from '../../../lib/models/indexed-row-range'; import {Addition, Deletion, NoNewline} from '../../../lib/models/patch/region'; -import {assertInPatch} from '../../helpers'; +import {assertInPatch, buildRange} from '../../helpers'; describe('Patch', function() { it('has some standard accessors', function() { @@ -625,14 +624,6 @@ describe('Patch', function() { }); }); -function buildRange(startRow, endRow = startRow, rowLength = 5, endRowLength = rowLength) { - return new IndexedRowRange({ - bufferRange: [[startRow, 0], [endRow, endRowLength - 1]], - startOffset: startRow * rowLength, - endOffset: endRow * rowLength + endRowLength, - }); -} - function buildPatchFixture() { const bufferText = '0000\n0001\n0002\n0003\n0004\n0005\n0006\n0007\n0008\n0009\n' + From 2ca4b5359b6117ca17d9b5346eebb3d456030abf Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 5 Sep 2018 09:58:02 -0400 Subject: [PATCH 0279/4252] Build FilePatches with row ranges that end at the end of the final row --- lib/models/patch/builder.js | 8 ++++++-- test/models/patch/builder.test.js | 28 ++++++++++++++-------------- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/lib/models/patch/builder.js b/lib/models/patch/builder.js index 5270aeebe5..3bce9deb72 100644 --- a/lib/models/patch/builder.js +++ b/lib/models/patch/builder.js @@ -112,6 +112,8 @@ function buildHunks(diff) { let LastChangeKind = null; let currentRangeStart = bufferRow; + let lastLineLength = 0; + let nextLineLength = 0; const finishCurrentRange = () => { if (currentRangeStart === bufferRow) { @@ -122,7 +124,7 @@ function buildHunks(diff) { changes.push( new LastChangeKind( new IndexedRowRange({ - bufferRange: [[currentRangeStart, 0], [bufferRow - 1, 0]], + bufferRange: [[currentRangeStart, 0], [bufferRow - 1, lastLineLength - 1]], startOffset, endOffset: bufferOffset, }), @@ -135,6 +137,7 @@ function buildHunks(diff) { for (const lineText of hunkData.lines) { const bufferLine = lineText.slice(1) + '\n'; + nextLineLength = bufferLine.length - 1; bufferText += bufferLine; const ChangeKind = CHANGEKIND[lineText[0]]; @@ -149,6 +152,7 @@ function buildHunks(diff) { LastChangeKind = ChangeKind; bufferOffset += bufferLine.length; bufferRow++; + lastLineLength = nextLineLength; } finishCurrentRange(); @@ -159,7 +163,7 @@ function buildHunks(diff) { newRowCount: hunkData.newLineCount, sectionHeading: hunkData.heading, rowRange: new IndexedRowRange({ - bufferRange: [[bufferStartRow, 0], [bufferRow - 1, 0]], + bufferRange: [[bufferStartRow, 0], [bufferRow - 1, nextLineLength - 1]], startOffset: bufferStartOffset, endOffset: bufferOffset, }), diff --git a/test/models/patch/builder.test.js b/test/models/patch/builder.test.js index 6539a4e824..4794be5530 100644 --- a/test/models/patch/builder.test.js +++ b/test/models/patch/builder.test.js @@ -81,8 +81,8 @@ describe('buildFilePatch', function() { endRow: 8, header: '@@ -0,7 +0,6 @@', changes: [ - {kind: 'deletion', string: '-line-1\n-line-2\n-line-3\n', range: [[1, 0], [3, 0]]}, - {kind: 'addition', string: '+line-5\n+line-6\n', range: [[5, 0], [6, 0]]}, + {kind: 'deletion', string: '-line-1\n-line-2\n-line-3\n', range: [[1, 0], [3, 5]]}, + {kind: 'addition', string: '+line-5\n+line-6\n', range: [[5, 0], [6, 5]]}, ], }, { @@ -90,8 +90,8 @@ describe('buildFilePatch', function() { endRow: 12, header: '@@ -10,3 +11,3 @@', changes: [ - {kind: 'deletion', string: '-line-9\n', range: [[9, 0], [9, 0]]}, - {kind: 'addition', string: '+line-12\n', range: [[12, 0], [12, 0]]}, + {kind: 'deletion', string: '-line-9\n', range: [[9, 0], [9, 5]]}, + {kind: 'addition', string: '+line-12\n', range: [[12, 0], [12, 6]]}, ], }, { @@ -99,8 +99,8 @@ describe('buildFilePatch', function() { endRow: 18, header: '@@ -20,4 +21,4 @@', changes: [ - {kind: 'deletion', string: '-line-14\n-line-15\n', range: [[14, 0], [15, 0]]}, - {kind: 'addition', string: '+line-16\n+line-17\n', range: [[16, 0], [17, 0]]}, + {kind: 'deletion', string: '-line-14\n-line-15\n', range: [[14, 0], [15, 6]]}, + {kind: 'addition', string: '+line-16\n+line-17\n', range: [[16, 0], [17, 6]]}, ], }, ); @@ -214,7 +214,7 @@ describe('buildFilePatch', function() { endRow: 3, header: '@@ -1,4 +0,0 @@', changes: [ - {kind: 'deletion', string: '-line-0\n-line-1\n-line-2\n-line-3\n', range: [[0, 0], [3, 0]]}, + {kind: 'deletion', string: '-line-0\n-line-1\n-line-2\n-line-3\n', range: [[0, 0], [3, 5]]}, ], }, ); @@ -257,7 +257,7 @@ describe('buildFilePatch', function() { endRow: 2, header: '@@ -0,0 +1,3 @@', changes: [ - {kind: 'addition', string: '+line-0\n+line-1\n+line-2\n', range: [[0, 0], [2, 0]]}, + {kind: 'addition', string: '+line-0\n+line-1\n+line-2\n', range: [[0, 0], [2, 5]]}, ], }, ); @@ -295,9 +295,9 @@ describe('buildFilePatch', function() { endRow: 2, header: '@@ -0,1 +0,1 @@', changes: [ - {kind: 'addition', string: '+line-0\n', range: [[0, 0], [0, 0]]}, - {kind: 'deletion', string: '-line-1\n', range: [[1, 0], [1, 0]]}, - {kind: 'nonewline', string: '\\ No newline at end of file\n', range: [[2, 0], [2, 0]]}, + {kind: 'addition', string: '+line-0\n', range: [[0, 0], [0, 5]]}, + {kind: 'deletion', string: '-line-1\n', range: [[1, 0], [1, 5]]}, + {kind: 'nonewline', string: '\\ No newline at end of file\n', range: [[2, 0], [2, 25]]}, ], }); }); @@ -354,7 +354,7 @@ describe('buildFilePatch', function() { endRow: 1, header: '@@ -0,0 +0,2 @@', changes: [ - {kind: 'addition', string: '+line-0\n+line-1\n', range: [[0, 0], [1, 0]]}, + {kind: 'addition', string: '+line-0\n+line-1\n', range: [[0, 0], [1, 5]]}, ], }); }); @@ -409,7 +409,7 @@ describe('buildFilePatch', function() { endRow: 1, header: '@@ -0,2 +0,0 @@', changes: [ - {kind: 'deletion', string: '-line-0\n-line-1\n', range: [[0, 0], [1, 0]]}, + {kind: 'deletion', string: '-line-0\n-line-1\n', range: [[0, 0], [1, 5]]}, ], }); }); @@ -463,7 +463,7 @@ describe('buildFilePatch', function() { endRow: 1, header: '@@ -0,0 +0,2 @@', changes: [ - {kind: 'addition', string: '+line-0\n+line-1\n', range: [[0, 0], [1, 0]]}, + {kind: 'addition', string: '+line-0\n+line-1\n', range: [[0, 0], [1, 5]]}, ], }); }); From 95e9a35983551bc397e04a79edd2af329f5d7802 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 5 Sep 2018 10:19:56 -0400 Subject: [PATCH 0280/4252] FilePatchItem coverage --- lib/items/file-patch-item.js | 1 + test/items/file-patch-item.test.js | 55 ++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/lib/items/file-patch-item.js b/lib/items/file-patch-item.js index db087e81f0..c8ed878ce2 100644 --- a/lib/items/file-patch-item.js +++ b/lib/items/file-patch-item.js @@ -59,6 +59,7 @@ export default class FilePatchItem extends React.Component { } destroy() { + /* istanbul ignore else */ if (!this.isDestroyed) { this.emitter.emit('did-destroy'); this.isDestroyed = true; diff --git a/test/items/file-patch-item.test.js b/test/items/file-patch-item.test.js index 84cbe3429b..660ab60b4f 100644 --- a/test/items/file-patch-item.test.js +++ b/test/items/file-patch-item.test.js @@ -102,4 +102,59 @@ describe('FilePatchItem', function() { assert.strictEqual(item.getTitle(), 'Staged Changes: a.txt'); }); }); + + it('terminates pending state', async function() { + const wrapper = mount(buildPaneApp()); + + const item = await open(wrapper); + const callback = sinon.spy(); + const sub = item.onDidTerminatePendingState(callback); + + assert.strictEqual(callback.callCount, 0); + item.terminatePendingState(); + assert.strictEqual(callback.callCount, 1); + item.terminatePendingState(); + assert.strictEqual(callback.callCount, 1); + + sub.dispose(); + }); + + it('may be destroyed once', async function() { + const wrapper = mount(buildPaneApp()); + + const item = await open(wrapper); + const callback = sinon.spy(); + const sub = item.onDidDestroy(callback); + + assert.strictEqual(callback.callCount, 0); + item.destroy(); + assert.strictEqual(callback.callCount, 1); + + sub.dispose(); + }); + + it('serializes itself as a FilePatchControllerStub', async function() { + const wrapper = mount(buildPaneApp()); + const item0 = await open(wrapper, {relPath: 'a.txt', workingDirectory: '/dir0', stagingStatus: 'unstaged'}); + assert.deepEqual(item0.serialize(), { + deserializer: 'FilePatchControllerStub', + uri: 'atom-github://file-patch/a.txt?workdir=%2Fdir0&stagingStatus=unstaged', + }); + + const item1 = await open(wrapper, {relPath: 'b.txt', workingDirectory: '/dir1', stagingStatus: 'staged'}); + assert.deepEqual(item1.serialize(), { + deserializer: 'FilePatchControllerStub', + uri: 'atom-github://file-patch/b.txt?workdir=%2Fdir1&stagingStatus=staged', + }); + }); + + it('has some item-level accessors', async function() { + const wrapper = mount(buildPaneApp()); + const item = await open(wrapper, {relPath: 'a.txt', workingDirectory: '/dir', stagingStatus: 'unstaged'}); + + assert.strictEqual(item.getStagingStatus(), 'unstaged'); + assert.strictEqual(item.getFilePath(), 'a.txt'); + assert.strictEqual(item.getWorkingDirectory(), '/dir'); + assert.isTrue(item.isFilePatchItem()); + }); }); From f301562d7e1725ae1b6484a85f2b0243bd9389b9 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 5 Sep 2018 13:15:38 -0400 Subject: [PATCH 0281/4252] FilePatchController coverage --- lib/controllers/file-patch-controller.js | 6 +- .../controllers/file-patch-controller.test.js | 302 +++++++++++++++++- 2 files changed, 302 insertions(+), 6 deletions(-) diff --git a/lib/controllers/file-patch-controller.js b/lib/controllers/file-patch-controller.js index 905f6caaea..63c9642df2 100644 --- a/lib/controllers/file-patch-controller.js +++ b/lib/controllers/file-patch-controller.js @@ -79,13 +79,13 @@ export default class FilePatchController extends React.Component { return this.props.undoLastDiscard(this.props.relPath, this.props.repository); } - async diveIntoMirrorPatch() { + diveIntoMirrorPatch() { const mirrorStatus = this.withStagingStatus({staged: 'unstaged', unstaged: 'staged'}); const workingDirectory = this.props.repository.getWorkingDirectoryPath(); const uri = FilePatchItem.buildURI(this.props.relPath, workingDirectory, mirrorStatus); this.props.destroy(); - await this.props.workspace.open(uri); + return this.props.workspace.open(uri); } async openFile(positions) { @@ -98,6 +98,7 @@ export default class FilePatchController extends React.Component { } editor.scrollToBufferPosition(positions[positions.length - 1], {center: true}); } + return editor; } toggleFile() { @@ -167,6 +168,7 @@ export default class FilePatchController extends React.Component { withStagingStatus(callbacks) { const callback = callbacks[this.props.stagingStatus]; + /* istanbul ignore if */ if (!callback) { throw new Error(`Unknown staging status: ${this.props.stagingStatus}`); } diff --git a/test/controllers/file-patch-controller.test.js b/test/controllers/file-patch-controller.test.js index 0e5b358c45..c08c57bc3b 100644 --- a/test/controllers/file-patch-controller.test.js +++ b/test/controllers/file-patch-controller.test.js @@ -1,7 +1,7 @@ import path from 'path'; -import fs from 'fs'; +import fs from 'fs-extra'; import React from 'react'; -import {mount} from 'enzyme'; +import {shallow} from 'enzyme'; import FilePatchController from '../../lib/controllers/file-patch-controller'; import {cloneRepository, buildRepository} from '../helpers'; @@ -16,7 +16,7 @@ describe('FilePatchController', function() { repository = await buildRepository(workdirPath); // a.txt: unstaged changes - await fs.writeFile(path.join(workdirPath, 'a.txt'), 'changed\n'); + await fs.writeFile(path.join(workdirPath, 'a.txt'), '00\n01\n02\n03\n04\n05\n06'); filePatch = await repository.getFilePatchForPath('a.txt', {staged: false}); }); @@ -46,8 +46,302 @@ describe('FilePatchController', function() { it('passes extra props to the FilePatchView', function() { const extra = Symbol('extra'); - const wrapper = mount(buildApp({extra})); + const wrapper = shallow(buildApp({extra})); assert.strictEqual(wrapper.find('FilePatchView').prop('extra'), extra); }); + + it('calls undoLastDiscard through with set arguments', function() { + const undoLastDiscard = sinon.spy(); + const wrapper = shallow(buildApp({relPath: 'b.txt', undoLastDiscard})); + wrapper.find('FilePatchView').prop('undoLastDiscard')(); + + assert.isTrue(undoLastDiscard.calledWith('b.txt', repository)); + }); + + describe('diveIntoMirrorPatch()', function() { + it('destroys the current pane and opens the staged changes', async function() { + const destroy = sinon.spy(); + sinon.stub(atomEnv.workspace, 'open').resolves(); + const wrapper = shallow(buildApp({relPath: 'c.txt', stagingStatus: 'unstaged', destroy})); + + await wrapper.find('FilePatchView').prop('diveIntoMirrorPatch')(); + + assert.isTrue(destroy.called); + assert.isTrue(atomEnv.workspace.open.calledWith( + 'atom-github://file-patch/c.txt' + + `?workdir=${encodeURIComponent(repository.getWorkingDirectoryPath())}&stagingStatus=staged`, + )); + }); + + it('destroys the current pane and opens the unstaged changes', async function() { + const destroy = sinon.spy(); + sinon.stub(atomEnv.workspace, 'open').resolves(); + const wrapper = shallow(buildApp({relPath: 'd.txt', stagingStatus: 'staged', destroy})); + + await wrapper.find('FilePatchView').prop('diveIntoMirrorPatch')(); + + assert.isTrue(destroy.called); + assert.isTrue(atomEnv.workspace.open.calledWith( + 'atom-github://file-patch/d.txt' + + `?workdir=${encodeURIComponent(repository.getWorkingDirectoryPath())}&stagingStatus=unstaged`, + )); + }); + }); + + describe('openFile()', function() { + it('opens an editor on the current file', async function() { + const wrapper = shallow(buildApp({relPath: 'a.txt', stagingStatus: 'unstaged'})); + const editor = await wrapper.find('FilePatchView').prop('openFile')([]); + + assert.strictEqual(editor.getPath(), path.join(repository.getWorkingDirectoryPath(), 'a.txt')); + }); + + it('sets the cursor to a single position', async function() { + const wrapper = shallow(buildApp({relPath: 'a.txt', stagingStatus: 'unstaged'})); + const editor = await wrapper.find('FilePatchView').prop('openFile')([[1, 1]]); + + assert.deepEqual(editor.getCursorBufferPositions().map(p => p.serialize()), [[1, 1]]); + }); + + it('adds cursors at a set of positions', async function() { + const wrapper = shallow(buildApp({relPath: 'a.txt', stagingStatus: 'unstaged'})); + const editor = await wrapper.find('FilePatchView').prop('openFile')([[1, 1], [3, 1], [5, 0]]); + + assert.deepEqual(editor.getCursorBufferPositions().map(p => p.serialize()), [[1, 1], [3, 1], [5, 0]]); + }); + }); + + describe('toggleFile()', function() { + it('stages the current file if unstaged', async function() { + sinon.spy(repository, 'stageFiles'); + const wrapper = shallow(buildApp({relPath: 'a.txt', stagingStatus: 'unstaged'})); + + await wrapper.find('FilePatchView').prop('toggleFile')(); + + assert.isTrue(repository.stageFiles.calledWith(['a.txt'])); + }); + + it('unstages the current file if staged', async function() { + sinon.spy(repository, 'unstageFiles'); + const wrapper = shallow(buildApp({relPath: 'a.txt', stagingStatus: 'staged'})); + + await wrapper.find('FilePatchView').prop('toggleFile')(); + + assert.isTrue(repository.unstageFiles.calledWith(['a.txt'])); + }); + + it('is a no-op if a staging operation is already in progress', async function() { + sinon.stub(repository, 'stageFiles').resolves('staged'); + sinon.stub(repository, 'unstageFiles').resolves('unstaged'); + + const wrapper = shallow(buildApp({relPath: 'a.txt', stagingStatus: 'unstaged'})); + assert.strictEqual(await wrapper.find('FilePatchView').prop('toggleFile')(), 'staged'); + + wrapper.setProps({stagingStatus: 'staged'}); + assert.isNull(await wrapper.find('FilePatchView').prop('toggleFile')()); + + const promise = wrapper.instance().patchChangePromise; + wrapper.setProps({filePatch: filePatch.clone()}); + await promise; + + assert.strictEqual(await wrapper.find('FilePatchView').prop('toggleFile')(), 'unstaged'); + }); + }); + + describe('selected row tracking', function() { + it('captures the selected row set', function() { + const wrapper = shallow(buildApp()); + assert.sameMembers(Array.from(wrapper.find('FilePatchView').prop('selectedRows')), []); + + wrapper.find('FilePatchView').prop('selectedRowsChanged')(new Set([1, 2])); + assert.sameMembers(Array.from(wrapper.find('FilePatchView').prop('selectedRows')), [1, 2]); + }); + + it('does not re-render if the row set is unchanged', function() { + const wrapper = shallow(buildApp()); + assert.sameMembers(Array.from(wrapper.find('FilePatchView').prop('selectedRows')), []); + wrapper.find('FilePatchView').prop('selectedRowsChanged')(new Set([1, 2])); + + assert.sameMembers(Array.from(wrapper.find('FilePatchView').prop('selectedRows')), [1, 2]); + sinon.spy(wrapper.instance(), 'render'); + wrapper.find('FilePatchView').prop('selectedRowsChanged')(new Set([2, 1])); + assert.sameMembers(Array.from(wrapper.find('FilePatchView').prop('selectedRows')), [1, 2]); + assert.isFalse(wrapper.instance().render.called); + }); + }); + + describe('toggleRows()', function() { + it('is a no-op with no selected rows', async function() { + const wrapper = shallow(buildApp()); + assert.sameMembers(Array.from(wrapper.find('FilePatchView').prop('selectedRows')), []); + + sinon.spy(repository, 'applyPatchToIndex'); + + await wrapper.find('FilePatchView').prop('toggleRows')(); + assert.isFalse(repository.applyPatchToIndex.called); + }); + + it('applies a stage patch to the index', async function() { + const wrapper = shallow(buildApp()); + wrapper.find('FilePatchView').prop('selectedRowsChanged')(new Set([1])); + + sinon.spy(filePatch, 'getStagePatchForLines'); + sinon.spy(repository, 'applyPatchToIndex'); + + await wrapper.find('FilePatchView').prop('toggleRows')(); + + assert.isTrue(filePatch.getStagePatchForLines.called); + assert.isTrue(repository.applyPatchToIndex.calledWith(filePatch.getStagePatchForLines.returnValues[0])); + }); + + it('applies an unstage patch to the index', async function() { + await repository.stageFiles(['a.txt']); + const otherPatch = await repository.getFilePatchForPath('a.txt', {staged: true}); + const wrapper = shallow(buildApp({filePatch: otherPatch, stagingStatus: 'staged'})); + wrapper.find('FilePatchView').prop('selectedRowsChanged')(new Set([2])); + + sinon.spy(otherPatch, 'getUnstagePatchForLines'); + sinon.spy(repository, 'applyPatchToIndex'); + + await wrapper.find('FilePatchView').prop('toggleRows')(); + + assert.isTrue(otherPatch.getUnstagePatchForLines.called); + assert.isTrue(repository.applyPatchToIndex.calledWith(otherPatch.getUnstagePatchForLines.returnValues[0])); + }); + }); + + describe('toggleModeChange()', function() { + it("it stages an unstaged file's new mode", async function() { + const p = path.join(repository.getWorkingDirectoryPath(), 'a.txt'); + await fs.chmod(p, 0o755); + repository.refresh(); + const newFilePatch = await repository.getFilePatchForPath('a.txt', {staged: false}); + + const wrapper = shallow(buildApp({filePatch: newFilePatch, stagingStatus: 'unstaged'})); + + sinon.spy(repository, 'stageFileModeChange'); + await wrapper.find('FilePatchView').prop('toggleModeChange')(); + + assert.isTrue(repository.stageFileModeChange.calledWith('a.txt', '100755')); + }); + + it("it stages a staged file's old mode", async function() { + const p = path.join(repository.getWorkingDirectoryPath(), 'a.txt'); + await fs.chmod(p, 0o755); + await repository.stageFiles(['a.txt']); + repository.refresh(); + const newFilePatch = await repository.getFilePatchForPath('a.txt', {staged: true}); + + const wrapper = shallow(buildApp({filePatch: newFilePatch, stagingStatus: 'staged'})); + + sinon.spy(repository, 'stageFileModeChange'); + await wrapper.find('FilePatchView').prop('toggleModeChange')(); + + assert.isTrue(repository.stageFileModeChange.calledWith('a.txt', '100644')); + }); + }); + + describe('toggleSymlinkChange', function() { + it('handles an addition and typechange with a special repository method', async function() { + const p = path.join(repository.getWorkingDirectoryPath(), 'waslink.txt'); + const dest = path.join(repository.getWorkingDirectoryPath(), 'destination'); + await fs.writeFile(dest, 'asdf\n', 'utf8'); + await fs.symlink(dest, p); + + await repository.stageFiles(['waslink.txt', 'destination']); + await repository.commit('zero'); + + await fs.unlink(p); + await fs.writeFile(p, 'fdsa\n', 'utf8'); + + repository.refresh(); + const symlinkPatch = await repository.getFilePatchForPath('waslink.txt', {staged: false}); + const wrapper = shallow(buildApp({filePatch: symlinkPatch, relPath: 'waslink.txt', stagingStatus: 'unstaged'})); + + sinon.spy(repository, 'stageFileSymlinkChange'); + + await wrapper.find('FilePatchView').prop('toggleSymlinkChange')(); + + assert.isTrue(repository.stageFileSymlinkChange.calledWith('waslink.txt')); + }); + + it('stages non-addition typechanges normally', async function() { + const p = path.join(repository.getWorkingDirectoryPath(), 'waslink.txt'); + const dest = path.join(repository.getWorkingDirectoryPath(), 'destination'); + await fs.writeFile(dest, 'asdf\n', 'utf8'); + await fs.symlink(dest, p); + + await repository.stageFiles(['waslink.txt', 'destination']); + await repository.commit('zero'); + + await fs.unlink(p); + + repository.refresh(); + const symlinkPatch = await repository.getFilePatchForPath('waslink.txt', {staged: false}); + const wrapper = shallow(buildApp({filePatch: symlinkPatch, relPath: 'waslink.txt', stagingStatus: 'unstaged'})); + + sinon.spy(repository, 'stageFiles'); + + await wrapper.find('FilePatchView').prop('toggleSymlinkChange')(); + + assert.isTrue(repository.stageFiles.calledWith(['waslink.txt'])); + }); + + it('handles a deletion and typechange with a special repository method', async function() { + const p = path.join(repository.getWorkingDirectoryPath(), 'waslink.txt'); + const dest = path.join(repository.getWorkingDirectoryPath(), 'destination'); + await fs.writeFile(dest, 'asdf\n', 'utf8'); + await fs.writeFile(p, 'fdsa\n', 'utf8'); + + await repository.stageFiles(['waslink.txt', 'destination']); + await repository.commit('zero'); + + await fs.unlink(p); + await fs.symlink(dest, p); + await repository.stageFiles(['waslink.txt']); + + repository.refresh(); + const symlinkPatch = await repository.getFilePatchForPath('waslink.txt', {staged: true}); + const wrapper = shallow(buildApp({filePatch: symlinkPatch, relPath: 'waslink.txt', stagingStatus: 'staged'})); + + sinon.spy(repository, 'stageFileSymlinkChange'); + + await wrapper.find('FilePatchView').prop('toggleSymlinkChange')(); + + assert.isTrue(repository.stageFileSymlinkChange.calledWith('waslink.txt')); + }); + + it('unstages non-deletion typechanges normally', async function() { + const p = path.join(repository.getWorkingDirectoryPath(), 'waslink.txt'); + const dest = path.join(repository.getWorkingDirectoryPath(), 'destination'); + await fs.writeFile(dest, 'asdf\n', 'utf8'); + await fs.symlink(dest, p); + + await repository.stageFiles(['waslink.txt', 'destination']); + await repository.commit('zero'); + + await fs.unlink(p); + + repository.refresh(); + const symlinkPatch = await repository.getFilePatchForPath('waslink.txt', {staged: true}); + const wrapper = shallow(buildApp({filePatch: symlinkPatch, relPath: 'waslink.txt', stagingStatus: 'staged'})); + + sinon.spy(repository, 'unstageFiles'); + + await wrapper.find('FilePatchView').prop('toggleSymlinkChange')(); + + assert.isTrue(repository.unstageFiles.calledWith(['waslink.txt'])); + }); + }); + + it('calls discardLines with selected rows', function() { + const discardLines = sinon.spy(); + const wrapper = shallow(buildApp({discardLines})); + wrapper.find('FilePatchView').prop('selectedRowsChanged')(new Set([1, 2])); + + wrapper.find('FilePatchView').prop('discardRows')(); + + assert.isTrue(discardLines.calledWith(filePatch, new Set([1, 2]), repository)); + }); }); From 646ebf4eb51a8d38f0869e4d3638835380c5f58d Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 5 Sep 2018 14:08:32 -0400 Subject: [PATCH 0282/4252] Remove unused hunk navigation commands (for now) --- lib/views/file-patch-view.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/views/file-patch-view.js b/lib/views/file-patch-view.js index 7832b4b917..a2528b2288 100644 --- a/lib/views/file-patch-view.js +++ b/lib/views/file-patch-view.js @@ -136,8 +136,6 @@ export default class FilePatchView extends React.Component { return ( - - ); From 66bae4938b082930c27cba424e55b9980e34fb30 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 5 Sep 2018 14:08:59 -0400 Subject: [PATCH 0283/4252] Use the real FilePatch builder --- test/views/file-patch-view.test.js | 229 ++++++++++++----------------- 1 file changed, 93 insertions(+), 136 deletions(-) diff --git a/test/views/file-patch-view.test.js b/test/views/file-patch-view.test.js index dcfb81a89e..9f18096e1f 100644 --- a/test/views/file-patch-view.test.js +++ b/test/views/file-patch-view.test.js @@ -1,16 +1,11 @@ -import path from 'path'; -import fs from 'fs-extra'; import React from 'react'; import {shallow, mount} from 'enzyme'; import {cloneRepository, buildRepository} from '../helpers'; import FilePatchView from '../../lib/views/file-patch-view'; -import FilePatchSelection from '../../lib/models/file-patch-selection'; -import {nullFilePatch} from '../../lib/models/patch/file-patch'; +import {buildFilePatch} from '../../lib/models/patch'; import {nullFile} from '../../lib/models/patch/file'; -import Hunk from '../../lib/models/patch/hunk'; -import {Addition, Deletion, NoNewline} from '../../lib/models/patch/region'; -import IndexedRowRange from '../../lib/models/indexed-row-range'; +import {nullFilePatch} from '../../lib/models/patch/file-patch'; describe('FilePatchView', function() { let atomEnv, repository, filePatch; @@ -22,8 +17,20 @@ describe('FilePatchView', function() { repository = await buildRepository(workdirPath); // a.txt: unstaged changes - await fs.writeFile(path.join(workdirPath, 'a.txt'), 'zero\none\ntwo\nthree\nfour\n'); - filePatch = await repository.getFilePatchForPath('a.txt', {staged: false}); + filePatch = buildFilePatch([{ + oldPath: 'path.txt', + oldMode: '100644', + newPath: 'path.txt', + newMode: '100644', + status: 'modified', + hunks: [ + { + oldStartLine: 5, oldLineCount: 3, newStartLine: 5, newLineCount: 3, + heading: 'heading', + lines: [' 0000', '+0001', '-0002', ' 0003'], + }, + ], + }]); }); afterEach(function() { @@ -32,35 +39,26 @@ describe('FilePatchView', function() { function buildApp(overrideProps = {}) { const props = { - relPath: 'a.txt', + relPath: 'path.txt', stagingStatus: 'unstaged', isPartiallyStaged: false, filePatch, - selection: new FilePatchSelection(filePatch.getHunks()), + selectedRows: new Set(), repository, commands: atomEnv.commands, tooltips: atomEnv.tooltips, - mouseDownOnHeader: () => {}, - mouseDownOnLineNumber: () => {}, - mouseMoveOnLineNumber: () => {}, - mouseUp: () => {}, selectedRowsChanged: () => {}, diveIntoMirrorPatch: () => {}, openFile: () => {}, toggleFile: () => {}, - selectAndToggleHunk: () => {}, - toggleLines: () => {}, + toggleRows: () => {}, toggleModeChange: () => {}, toggleSymlinkChange: () => {}, undoLastDiscard: () => {}, - discardLines: () => {}, - selectAndDiscardHunk: () => {}, - selectNextHunk: () => {}, - selectPreviousHunk: () => {}, - togglePatchSelectionMode: () => {}, + discardRows: () => {}, ...overrideProps, }; @@ -203,28 +201,27 @@ describe('FilePatchView', function() { }); it('renders a header for each hunk', function() { - const bufferText = '0000\n0001\n0002\n0003\n0004\n0005\n'; - const hunks = [ - new Hunk({ - oldStartRow: 1, oldRowCount: 2, newStartRow: 1, newRowCount: 3, - sectionHeading: 'first hunk', - rowRange: new IndexedRowRange({bufferRange: [[0, 0], [2, 0]], startOffset: 0, endOffset: 15}), - changes: [ - new Addition(new IndexedRowRange({bufferRange: [[1, 0], [1, 0]], startOffset: 5, endOffset: 10})), - ], - }), - new Hunk({ - oldStartRow: 10, oldRowCount: 3, newStartRow: 11, newRowCount: 2, - sectionHeading: 'second hunk', - rowRange: new IndexedRowRange({bufferRange: [[3, 0], [5, 0]], startOffset: 15, endOffset: 30}), - changes: [ - new Deletion(new IndexedRowRange({bufferRange: [[4, 0], [4, 0]], startOffset: 5, endOffset: 10})), - ], - }), - ]; - const fp = filePatch.clone({ - patch: filePatch.getPatch().clone({hunks, bufferText}), - }); + const fp = buildFilePatch([{ + oldPath: 'path.txt', + oldMode: '100644', + newPath: 'path.txt', + newMode: '100644', + status: 'modified', + hunks: [ + { + oldStartLine: 1, oldLineCount: 2, newStartLine: 1, newLineCount: 3, + heading: 'first hunk', + lines: [' 0000', '+0001', ' 0002'], + }, + { + oldStartLine: 10, oldLineCount: 3, newStartLine: 11, newLineCount: 2, + heading: 'second hunk', + lines: [' 0003', '-0004', ' 0005'], + }, + ], + }]); + const hunks = fp.getHunks(); + const wrapper = mount(buildApp({filePatch: fp})); assert.isTrue(wrapper.find('HunkHeaderView').someWhere(h => h.prop('hunk') === hunks[0])); assert.isTrue(wrapper.find('HunkHeaderView').someWhere(h => h.prop('hunk') === hunks[1])); @@ -234,37 +231,28 @@ describe('FilePatchView', function() { let linesPatch; beforeEach(function() { - const bufferText = - '0000\n0001\n0002\n0003\n0004\n0005\n0006\n0007\n0008\n0009\n' + - '0010\n0011\n0012\n0013\n0014\n0015\n0016\n' + - ' No newline at end of file\n'; - const hunks = [ - new Hunk({ - oldStartRow: 1, oldRowCount: 3, newStartRow: 1, newRowCount: 6, - sectionHeading: 'first hunk', - rowRange: new IndexedRowRange({bufferRange: [[0, 0], [6, 0]], startOffset: 0, endOffset: 35}), - changes: [ - new Addition(new IndexedRowRange({bufferRange: [[1, 0], [2, 0]], startOffset: 5, endOffset: 15})), - new Deletion(new IndexedRowRange({bufferRange: [[3, 0], [3, 0]], startOffset: 15, endOffset: 20})), - new Addition(new IndexedRowRange({bufferRange: [[4, 0], [5, 0]], startOffset: 20, endOffset: 30})), - ], - }), - new Hunk({ - oldStartRow: 10, oldRowCount: 0, newStartRow: 13, newRowCount: 0, - sectionHeading: 'second hunk', - rowRange: new IndexedRowRange({bufferRange: [[7, 0], [17, 0]], startOffset: 35, endOffset: 112}), - changes: [ - new Deletion(new IndexedRowRange({bufferRange: [[8, 0], [10, 0]], startOffset: 40, endOffset: 55})), - new Addition(new IndexedRowRange({bufferRange: [[12, 0], [14, 0]], startOffset: 60, endOffset: 75})), - new Deletion(new IndexedRowRange({bufferRange: [[15, 0], [15, 0]], startOffset: 75, endOffset: 80})), - new NoNewline(new IndexedRowRange({bufferRange: [[17, 0], [17, 0]], startOffset: 85, endOffset: 112})), - ], - }), - ]; - - linesPatch = filePatch.clone({ - patch: filePatch.getPatch().clone({hunks, bufferText}), - }); + linesPatch = buildFilePatch([{ + oldPath: 'file.txt', + oldMode: '100644', + newPath: 'file.txt', + newMode: '100644', + status: 'modified', + hunks: [ + { + oldStartLine: 1, oldLineCount: 3, newStartLine: 1, newLineCount: 6, + heading: 'first hunk', + lines: [' 0000', '+0001', '+0002', '-0003', '+0004', '+0005', ' 0006'], + }, + { + oldStartLine: 10, oldLineCount: 0, newStartLine: 13, newLineCount: 0, + heading: 'second hunk', + lines: [ + ' 0007', '-0008', '-0009', '-0010', ' 0011', '+0012', '+0013', '+0014', '-0015', ' 0016', + '\\ No newline at end of file', + ], + }, + ], + }]); }); it('decorates added lines', function() { @@ -277,9 +265,9 @@ describe('FilePatchView', function() { const layer = wrapper.find('MarkerLayer').filterWhere(each => each.find(decorationSelector).exists()); const markers = layer.find('Marker').map(marker => marker.prop('bufferRange').serialize()); assert.deepEqual(markers, [ - [[1, 0], [2, 0]], - [[4, 0], [5, 0]], - [[12, 0], [14, 0]], + [[1, 0], [2, 3]], + [[4, 0], [5, 3]], + [[12, 0], [14, 3]], ]); }); @@ -293,9 +281,9 @@ describe('FilePatchView', function() { const layer = wrapper.find('MarkerLayer').filterWhere(each => each.find(decorationSelector).exists()); const markers = layer.find('Marker').map(marker => marker.prop('bufferRange').serialize()); assert.deepEqual(markers, [ - [[3, 0], [3, 0]], - [[8, 0], [10, 0]], - [[15, 0], [15, 0]], + [[3, 0], [3, 3]], + [[8, 0], [10, 3]], + [[15, 0], [15, 3]], ]); }); @@ -309,7 +297,7 @@ describe('FilePatchView', function() { const layer = wrapper.find('MarkerLayer').filterWhere(each => each.find(decorationSelector).exists()); const markers = layer.find('Marker').map(marker => marker.prop('bufferRange').serialize()); assert.deepEqual(markers, [ - [[17, 0], [17, 0]], + [[17, 0], [17, 25]], ]); }); }); @@ -319,7 +307,7 @@ describe('FilePatchView', function() { const wrapper = mount(buildApp({selectedRowsChanged})); const editor = wrapper.find('atom-text-editor').getDOMNode().getModel(); - assert.isFalse(selectedRowsChanged.called); + selectedRowsChanged.resetHistory(); editor.addSelectionForBufferRange([[3, 1], [4, 0]]); @@ -340,69 +328,38 @@ describe('FilePatchView', function() { }); describe('registers Atom commands', function() { - it('toggles the patch selection mode', function() { - const togglePatchSelectionMode = sinon.spy(); - const wrapper = mount(buildApp({togglePatchSelectionMode})); - - atomEnv.commands.dispatch(wrapper.getDOMNode(), 'github:toggle-patch-selection-mode'); - - assert.isTrue(togglePatchSelectionMode.called); - }); - it('toggles the current selection', function() { - const toggleLines = sinon.spy(); - const wrapper = mount(buildApp({toggleLines})); + const toggleRows = sinon.spy(); + const wrapper = mount(buildApp({toggleRows})); atomEnv.commands.dispatch(wrapper.getDOMNode(), 'core:confirm'); - assert.isTrue(toggleLines.called); - }); - - it('selects the next hunk', function() { - const selectNextHunk = sinon.spy(); - const wrapper = mount(buildApp({selectNextHunk})); - - atomEnv.commands.dispatch(wrapper.getDOMNode(), 'github:select-next-hunk'); - - assert.isTrue(selectNextHunk.called); - }); - - it('selects the previous hunk', function() { - const selectPreviousHunk = sinon.spy(); - const wrapper = mount(buildApp({selectPreviousHunk})); - - atomEnv.commands.dispatch(wrapper.getDOMNode(), 'github:select-previous-hunk'); - - assert.isTrue(selectPreviousHunk.called); + assert.isTrue(toggleRows.called); }); describe('opening the file', function() { let fp; beforeEach(function() { - const bufferText = '0000\n0001\n0002\n0003\n0004\n0005\n0006\n0007\n0008\n'; - const hunks = [ - new Hunk({ - oldStartRow: 2, oldRowCount: 2, newStartRow: 2, newRowCount: 3, - sectionHeading: 'first hunk', - rowRange: new IndexedRowRange({bufferRange: [[0, 0], [2, 0]], startOffset: 0, endOffset: 15}), - changes: [ - new Addition(new IndexedRowRange({bufferRange: [[1, 0], [1, 0]], startOffset: 5, endOffset: 10})), - ], - }), - new Hunk({ - oldStartRow: 10, oldRowCount: 6, newStartRow: 11, newRowCount: 3, - sectionHeading: 'second hunk', - rowRange: new IndexedRowRange({bufferRange: [[3, 0], [8, 0]], startOffset: 15, endOffset: 45}), - changes: [ - new Deletion(new IndexedRowRange({bufferRange: [[4, 0], [4, 0]], startOffset: 20, endOffset: 25})), - new Deletion(new IndexedRowRange({bufferRange: [[6, 0], [7, 0]], startOffset: 30, endOffset: 40})), - ], - }), - ]; - fp = filePatch.clone({ - patch: filePatch.getPatch().clone({hunks, bufferText}), - }); + fp = buildFilePatch([{ + oldPath: 'path.txt', + oldMode: '100644', + newPath: 'path.txt', + newMode: '100644', + status: 'modified', + hunks: [ + { + oldStartLine: 2, oldLineCount: 2, newStartLine: 2, newLineCount: 3, + heading: 'first hunk', + lines: [' 0000', '+0001', ' 0002'], + }, + { + oldStartLine: 10, oldLineCount: 6, newStartLine: 11, newLineCount: 3, + heading: 'second hunk', + lines: [' 0003', '-0004', ' 0005', '-0006', '-0007', ' 0008'], + }, + ], + }]); }); it('opens the file at the current unchanged row', function() { From 24f31edc08442486a108b8fa5f561c035925edaa Mon Sep 17 00:00:00 2001 From: Tilde Ann Thurium Date: Wed, 5 Sep 2018 15:17:07 -0700 Subject: [PATCH 0284/4252] Fix typo in selector Co-Authored-By: David Wilson --- lib/views/commit-view.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/views/commit-view.js b/lib/views/commit-view.js index d6e9e3c28b..f472b60cee 100644 --- a/lib/views/commit-view.js +++ b/lib/views/commit-view.js @@ -146,7 +146,7 @@ export default class CommitView extends React.Component { callback={this.toggleExpandedCommitMessageEditor} /> - + From 34d222d1cd702da5ef88498614b5b49a86b8194e Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 6 Sep 2018 13:22:52 -0400 Subject: [PATCH 0285/4252] Remove the unused hunk selection methods (for now) --- lib/views/file-patch-view.js | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/lib/views/file-patch-view.js b/lib/views/file-patch-view.js index a2528b2288..f2932ec6ce 100644 --- a/lib/views/file-patch-view.js +++ b/lib/views/file-patch-view.js @@ -53,7 +53,7 @@ export default class FilePatchView extends React.Component { this, 'didMouseDownOnHeader', 'didMouseDownOnLineNumber', 'didMouseMoveOnLineNumber', 'didMouseUp', 'didOpenFile', 'didAddSelection', 'didChangeSelectionRange', 'didDestroySelection', - 'oldLineNumberLabel', 'newLineNumberLabel', 'selectNextHunk', 'selectPreviousHunk', + 'oldLineNumberLabel', 'newLineNumberLabel', ); this.mouseSelectionInProgress = false; @@ -679,14 +679,6 @@ export default class FilePatchView extends React.Component { return this.pad(newRow); } - selectNextHunk() { - // - } - - selectPreviousHunk() { - // - } - getHunkAt(bufferRow) { const hunkFromMarker = this.hunkMarkerLayerHolder.map(layer => { const markers = layer.findMarkers({intersectsRow: bufferRow}); From ec4ab28628234743ec696e4521591d1a367a9add Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 6 Sep 2018 13:23:08 -0400 Subject: [PATCH 0286/4252] :hocho: that case we could never actually get to --- lib/views/file-patch-view.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/views/file-patch-view.js b/lib/views/file-patch-view.js index f2932ec6ce..9c61f44023 100644 --- a/lib/views/file-patch-view.js +++ b/lib/views/file-patch-view.js @@ -292,7 +292,7 @@ export default class FilePatchView extends React.Component { ); title = 'Symlink deleted'; - } else if (!oldSymlink && newSymlink) { + } else { detail = ( Symlink @@ -303,8 +303,6 @@ export default class FilePatchView extends React.Component { ); title = 'Symlink created'; - } else { - return null; } const attrs = this.props.stagingStatus === 'unstaged' From e0bd870459902378957847806eee556670a8acbd Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 6 Sep 2018 13:23:27 -0400 Subject: [PATCH 0287/4252] Turns out Range.fromObject() normalizes start and end already --- lib/views/file-patch-view.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/views/file-patch-view.js b/lib/views/file-patch-view.js index 9c61f44023..436de7b031 100644 --- a/lib/views/file-patch-view.js +++ b/lib/views/file-patch-view.js @@ -484,8 +484,7 @@ export default class FilePatchView extends React.Component { // Normalize the target selection range const converted = Range.fromObject(rangeLike); - const flipped = converted.start.isLessThanOrEqual(converted.end) ? converted : converted.negate(); - const range = this.refEditor.map(editor => editor.clipBufferRange(flipped)).getOr(flipped); + const range = this.refEditor.map(editor => editor.clipBufferRange(converted)).getOr(converted); if (event.metaKey || (event.ctrlKey && isWindows)) { this.refEditor.map(editor => { From 0fb0a5da10a57b844b3c4bc51f1c4808ca5fba5b Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 6 Sep 2018 13:23:50 -0400 Subject: [PATCH 0288/4252] Handle eliminating the last row of a selection with ctrl-click --- lib/views/file-patch-view.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/lib/views/file-patch-view.js b/lib/views/file-patch-view.js index 436de7b031..7a95149eec 100644 --- a/lib/views/file-patch-view.js +++ b/lib/views/file-patch-view.js @@ -489,6 +489,7 @@ export default class FilePatchView extends React.Component { if (event.metaKey || (event.ctrlKey && isWindows)) { this.refEditor.map(editor => { let intersects = false; + let without = null; for (const selection of editor.getSelections()) { if (selection.intersectsBufferRange(range)) { @@ -526,10 +527,21 @@ export default class FilePatchView extends React.Component { for (const newRange of newRanges.slice(1)) { editor.addSelectionForBufferRange(newRange, {reversed: selection.isReversed()}); } + } else { + without = selection; } } } + if (without !== null) { + const replacementRanges = editor.getSelections() + .filter(each => each !== without) + .map(each => each.getBufferRange()); + if (replacementRanges.length > 0) { + editor.setSelectedBufferRanges(replacementRanges); + } + } + if (!intersects) { // Add this range as a new, distinct selection. editor.addSelectionForBufferRange(range); From 46897eecba8a8cab4ba45381efea5341aee29adc Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 6 Sep 2018 13:24:18 -0400 Subject: [PATCH 0289/4252] lastSelectionRange is always start-before-end --- lib/views/file-patch-view.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/views/file-patch-view.js b/lib/views/file-patch-view.js index 7a95149eec..e2466f0568 100644 --- a/lib/views/file-patch-view.js +++ b/lib/views/file-patch-view.js @@ -556,10 +556,9 @@ export default class FilePatchView extends React.Component { const lastSelectionRange = lastSelection.getBufferRange(); // You are now entering the wall of ternery operators. This is your last exit before the tollbooth - const cursorHead = lastSelection.isReversed() ? lastSelectionRange.end : lastSelectionRange.start; - const isBefore = range.start.isLessThan(cursorHead); + const isBefore = range.start.isLessThan(lastSelectionRange.start); const farEdge = isBefore ? range.start : range.end; - const newRange = isBefore ? [farEdge, cursorHead] : [cursorHead, farEdge]; + const newRange = isBefore ? [farEdge, lastSelectionRange.end] : [lastSelectionRange.start, farEdge]; lastSelection.setBufferRange(newRange, {reversed: isBefore}); return null; From 42c8c6790fefd9f8f0d57b0ba6b85b5a80e50a84 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 6 Sep 2018 13:24:35 -0400 Subject: [PATCH 0290/4252] Pad newLine labels without a hunk --- lib/views/file-patch-view.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/views/file-patch-view.js b/lib/views/file-patch-view.js index e2466f0568..29f72cc99b 100644 --- a/lib/views/file-patch-view.js +++ b/lib/views/file-patch-view.js @@ -677,7 +677,7 @@ export default class FilePatchView extends React.Component { newLineNumberLabel({bufferRow, softWrapped}) { const hunk = this.getHunkAt(bufferRow); if (hunk === undefined) { - return ''; + return this.pad(''); } const newRow = hunk.getNewRowAt(bufferRow); From 8b47f45c598589c595c39e5dc504aaeb583bd458 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 6 Sep 2018 13:24:59 -0400 Subject: [PATCH 0291/4252] Coverage-ignore branches that are platform-specific --- lib/views/file-patch-view.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/views/file-patch-view.js b/lib/views/file-patch-view.js index 29f72cc99b..b5169d897e 100644 --- a/lib/views/file-patch-view.js +++ b/lib/views/file-patch-view.js @@ -486,7 +486,7 @@ export default class FilePatchView extends React.Component { const converted = Range.fromObject(rangeLike); const range = this.refEditor.map(editor => editor.clipBufferRange(converted)).getOr(converted); - if (event.metaKey || (event.ctrlKey && isWindows)) { + if (event.metaKey || /* istanbul ignore next */ (event.ctrlKey && isWindows)) { this.refEditor.map(editor => { let intersects = false; let without = null; From f5cb601610fde2eb06becfb02070843583e12ffd Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 6 Sep 2018 13:25:23 -0400 Subject: [PATCH 0292/4252] Coverate for FilePatchView --- test/views/file-patch-view.test.js | 418 ++++++++++++++++++++++++++--- 1 file changed, 377 insertions(+), 41 deletions(-) diff --git a/test/views/file-patch-view.test.js b/test/views/file-patch-view.test.js index 9f18096e1f..06d07d35a0 100644 --- a/test/views/file-patch-view.test.js +++ b/test/views/file-patch-view.test.js @@ -16,7 +16,7 @@ describe('FilePatchView', function() { const workdirPath = await cloneRepository(); repository = await buildRepository(workdirPath); - // a.txt: unstaged changes + // path.txt: unstaged changes filePatch = buildFilePatch([{ oldPath: 'path.txt', oldMode: '100644', @@ -25,9 +25,14 @@ describe('FilePatchView', function() { status: 'modified', hunks: [ { - oldStartLine: 5, oldLineCount: 3, newStartLine: 5, newLineCount: 3, - heading: 'heading', - lines: [' 0000', '+0001', '-0002', ' 0003'], + oldStartLine: 4, oldLineCount: 3, newStartLine: 4, newLineCount: 4, + heading: 'zero', + lines: [' 0000', '+0001', '+0002', '-0003', ' 0004'], + }, + { + oldStartLine: 8, oldLineCount: 3, newStartLine: 9, newLineCount: 3, + heading: 'one', + lines: [' 0005', '+0006', '-0007', ' 0008'], }, ], }]); @@ -78,6 +83,74 @@ describe('FilePatchView', function() { assert.strictEqual(editor.instance().getModel().getText(), filePatch.getBufferText()); }); + it('preserves the selection index when a new file patch arrives', function() { + let lastSelectedRows = new Set([2]); + const selectedRowsChanged = sinon.stub().callsFake(rows => { + lastSelectedRows = rows; + }); + const wrapper = mount(buildApp({selectedRows: new Set([2]), selectedRowsChanged})); + + const nextPatch = buildFilePatch([{ + oldPath: 'path.txt', + oldMode: '100644', + newPath: 'path.txt', + newMode: '100644', + status: 'modified', + hunks: [ + { + oldStartLine: 5, oldLineCount: 4, newStartLine: 5, newLineCount: 3, + heading: 'heading', + lines: [' 0000', '+0001', ' 0002', '-0003', ' 0004'], + }, + ], + }]); + + wrapper.setProps({filePatch: nextPatch}); + assert.sameMembers(Array.from(lastSelectedRows), [3]); + + selectedRowsChanged.resetHistory(); + wrapper.setProps({isPartiallyStaged: true}); + assert.isFalse(selectedRowsChanged.called); + }); + + it('unregisters the mouseup handler on unmount', function() { + sinon.spy(window, 'addEventListener'); + sinon.spy(window, 'removeEventListener'); + + const wrapper = shallow(buildApp()); + assert.strictEqual(window.addEventListener.callCount, 1); + const addCall = window.addEventListener.getCall(0); + assert.strictEqual(addCall.args[0], 'mouseup'); + const handler = window.addEventListener.getCall(0).args[1]; + + wrapper.unmount(); + + assert.isTrue(window.removeEventListener.calledWith('mouseup', handler)); + }); + + it('locates hunks for a buffer row with or without markers', function() { + const [hunk0, hunk1] = filePatch.getHunks(); + const instance = mount(buildApp()).instance(); + + for (let i = 0; i <= 4; i++) { + assert.strictEqual(instance.getHunkAt(i), hunk0, `buffer row ${i} should retrieve hunk 0 with markers`); + } + for (let j = 5; j <= 8; j++) { + assert.strictEqual(instance.getHunkAt(j), hunk1, `buffer row ${j} should retrieve hunk 1 with markers`); + } + assert.isUndefined(instance.getHunkAt(9)); + + instance.hunkMarkerLayerHolder.map(layer => layer.destroy()); + + for (let i = 0; i <= 4; i++) { + assert.strictEqual(instance.getHunkAt(i), hunk0, `buffer row ${i} should retrieve hunk 0 without markers`); + } + for (let j = 5; j <= 8; j++) { + assert.strictEqual(instance.getHunkAt(j), hunk1, `buffer row ${j} should retrieve hunk 1 without markers`); + } + assert.isUndefined(instance.getHunkAt(9)); + }); + describe('executable mode changes', function() { it('does not render if the mode has not changed', function() { const fp = filePatch.clone({ @@ -200,31 +273,292 @@ describe('FilePatchView', function() { }); }); - it('renders a header for each hunk', function() { - const fp = buildFilePatch([{ - oldPath: 'path.txt', - oldMode: '100644', - newPath: 'path.txt', - newMode: '100644', - status: 'modified', - hunks: [ - { - oldStartLine: 1, oldLineCount: 2, newStartLine: 1, newLineCount: 3, - heading: 'first hunk', - lines: [' 0000', '+0001', ' 0002'], - }, - { - oldStartLine: 10, oldLineCount: 3, newStartLine: 11, newLineCount: 2, - heading: 'second hunk', - lines: [' 0003', '-0004', ' 0005'], - }, - ], - }]); - const hunks = fp.getHunks(); + describe('hunk headers', function() { + it('renders one for each hunk', function() { + const fp = buildFilePatch([{ + oldPath: 'path.txt', + oldMode: '100644', + newPath: 'path.txt', + newMode: '100644', + status: 'modified', + hunks: [ + { + oldStartLine: 1, oldLineCount: 2, newStartLine: 1, newLineCount: 3, + heading: 'first hunk', + lines: [' 0000', '+0001', ' 0002'], + }, + { + oldStartLine: 10, oldLineCount: 3, newStartLine: 11, newLineCount: 2, + heading: 'second hunk', + lines: [' 0003', '-0004', ' 0005'], + }, + ], + }]); + const hunks = fp.getHunks(); + + const wrapper = mount(buildApp({filePatch: fp})); + assert.isTrue(wrapper.find('HunkHeaderView').someWhere(h => h.prop('hunk') === hunks[0])); + assert.isTrue(wrapper.find('HunkHeaderView').someWhere(h => h.prop('hunk') === hunks[1])); + }); + + it('pluralizes the toggle and discard button labels', function() { + const wrapper = shallow(buildApp({selectedRows: new Set([2])})); + assert.strictEqual(wrapper.find('HunkHeaderView').at(0).prop('toggleSelectionLabel'), 'Stage Selected Change'); + assert.strictEqual(wrapper.find('HunkHeaderView').at(0).prop('discardSelectionLabel'), 'Discard Selected Change'); + + wrapper.setProps({selectedRows: new Set([1, 2, 3])}); + assert.strictEqual(wrapper.find('HunkHeaderView').at(0).prop('toggleSelectionLabel'), 'Stage Selected Changes'); + assert.strictEqual(wrapper.find('HunkHeaderView').at(0).prop('discardSelectionLabel'), 'Discard Selected Changes'); + }); + + it('uses the appropriate staging action verb in hunk header button labels', function() { + const wrapper = shallow(buildApp({selectedRows: new Set([2]), stagingStatus: 'unstaged'})); + assert.strictEqual(wrapper.find('HunkHeaderView').at(0).prop('toggleSelectionLabel'), 'Stage Selected Change'); - const wrapper = mount(buildApp({filePatch: fp})); - assert.isTrue(wrapper.find('HunkHeaderView').someWhere(h => h.prop('hunk') === hunks[0])); - assert.isTrue(wrapper.find('HunkHeaderView').someWhere(h => h.prop('hunk') === hunks[1])); + wrapper.setProps({stagingStatus: 'staged'}); + assert.strictEqual(wrapper.find('HunkHeaderView').at(0).prop('toggleSelectionLabel'), 'Unstage Selected Change'); + }); + + it('handles mousedown as a selection event', function() { + const fp = buildFilePatch([{ + oldPath: 'path.txt', + oldMode: '100644', + newPath: 'path.txt', + newMode: '100644', + status: 'modified', + hunks: [ + { + oldStartLine: 1, oldLineCount: 2, newStartLine: 1, newLineCount: 3, + heading: 'first hunk', + lines: [' 0000', '+0001', ' 0002'], + }, + { + oldStartLine: 10, oldLineCount: 3, newStartLine: 11, newLineCount: 2, + heading: 'second hunk', + lines: [' 0003', '-0004', ' 0005'], + }, + ], + }]); + + const selectedRowsChanged = sinon.spy(); + const wrapper = mount(buildApp({filePatch: fp, selectedRowsChanged})); + + wrapper.find('HunkHeaderView').at(1).prop('mouseDown')({button: 0}, fp.getHunks()[1]); + + assert.isTrue(selectedRowsChanged.calledWith(new Set([4]))); + }); + }); + + describe('custom gutters', function() { + let wrapper, instance, editor; + + beforeEach(function() { + wrapper = mount(buildApp()); + instance = wrapper.instance(); + editor = wrapper.find('AtomTextEditor').getDOMNode().getModel(); + }); + + it('computes the old line number for a buffer row', function() { + assert.strictEqual(instance.oldLineNumberLabel({bufferRow: 5, softWrapped: false}), '\u00a08'); + assert.strictEqual(instance.oldLineNumberLabel({bufferRow: 6, softWrapped: false}), '\u00a0\u00a0'); + assert.strictEqual(instance.oldLineNumberLabel({bufferRow: 6, softWrapped: true}), '\u00a0\u00a0'); + assert.strictEqual(instance.oldLineNumberLabel({bufferRow: 7, softWrapped: false}), '\u00a09'); + assert.strictEqual(instance.oldLineNumberLabel({bufferRow: 8, softWrapped: false}), '10'); + assert.strictEqual(instance.oldLineNumberLabel({bufferRow: 8, softWrapped: true}), '\u00a0•'); + + assert.strictEqual(instance.oldLineNumberLabel({bufferRow: 999, softWrapped: false}), '\u00a0\u00a0'); + }); + + it('computes the new line number for a buffer row', function() { + assert.strictEqual(instance.newLineNumberLabel({bufferRow: 5, softWrapped: false}), '\u00a09'); + assert.strictEqual(instance.newLineNumberLabel({bufferRow: 6, softWrapped: false}), '10'); + assert.strictEqual(instance.newLineNumberLabel({bufferRow: 6, softWrapped: true}), '\u00a0•'); + assert.strictEqual(instance.newLineNumberLabel({bufferRow: 7, softWrapped: false}), '\u00a0\u00a0'); + assert.strictEqual(instance.newLineNumberLabel({bufferRow: 7, softWrapped: true}), '\u00a0\u00a0'); + assert.strictEqual(instance.newLineNumberLabel({bufferRow: 8, softWrapped: false}), '11'); + + assert.strictEqual(instance.newLineNumberLabel({bufferRow: 999, softWrapped: false}), '\u00a0\u00a0'); + }); + + it('selects a single line on click', function() { + instance.didMouseDownOnLineNumber({bufferRow: 2, domEvent: {button: 0}}); + assert.deepEqual(editor.getSelectedBufferRanges().map(r => r.serialize()), [ + [[2, 0], [2, 4]], + ]); + }); + + it('ignores right clicks', function() { + instance.didMouseDownOnLineNumber({bufferRow: 2, domEvent: {button: 1}}); + assert.deepEqual(editor.getSelectedBufferRanges().map(r => r.serialize()), [ + [[1, 0], [1, 4]], + ]); + }); + + if (process.platform !== 'win32') { + it('ignores ctrl-clicks on non-Windows platforms', function() { + instance.didMouseDownOnLineNumber({bufferRow: 2, domEvent: {button: 0, ctrlKey: true}}); + assert.deepEqual(editor.getSelectedBufferRanges().map(r => r.serialize()), [ + [[1, 0], [1, 4]], + ]); + }); + } + + it('selects a range of lines on click and drag', function() { + instance.didMouseDownOnLineNumber({bufferRow: 2, domEvent: {button: 0}}); + assert.deepEqual(editor.getSelectedBufferRanges().map(r => r.serialize()), [ + [[2, 0], [2, 4]], + ]); + + instance.didMouseMoveOnLineNumber({bufferRow: 2, domEvent: {button: 0}}); + assert.deepEqual(editor.getSelectedBufferRanges().map(r => r.serialize()), [ + [[2, 0], [2, 4]], + ]); + + instance.didMouseMoveOnLineNumber({bufferRow: 3, domEvent: {button: 0}}); + assert.deepEqual(editor.getSelectedBufferRanges().map(r => r.serialize()), [ + [[2, 0], [3, 4]], + ]); + + instance.didMouseMoveOnLineNumber({bufferRow: 3, domEvent: {button: 0}}); + assert.deepEqual(editor.getSelectedBufferRanges().map(r => r.serialize()), [ + [[2, 0], [3, 4]], + ]); + + instance.didMouseMoveOnLineNumber({bufferRow: 4, domEvent: {button: 0}}); + assert.deepEqual(editor.getSelectedBufferRanges().map(r => r.serialize()), [ + [[2, 0], [4, 4]], + ]); + + instance.didMouseUp(); + assert.deepEqual(editor.getSelectedBufferRanges().map(r => r.serialize()), [ + [[2, 0], [4, 4]], + ]); + + instance.didMouseMoveOnLineNumber({bufferRow: 5, domEvent: {button: 0}}); + // Unchanged after mouse up + assert.deepEqual(editor.getSelectedBufferRanges().map(r => r.serialize()), [ + [[2, 0], [4, 4]], + ]); + }); + + describe('shift-click', function() { + it('selects a range of lines', function() { + instance.didMouseDownOnLineNumber({bufferRow: 2, domEvent: {button: 0}}); + assert.deepEqual(editor.getSelectedBufferRanges().map(r => r.serialize()), [ + [[2, 0], [2, 4]], + ]); + instance.didMouseUp(); + + instance.didMouseDownOnLineNumber({bufferRow: 4, domEvent: {shiftKey: true, button: 0}}); + instance.didMouseUp(); + assert.deepEqual(editor.getSelectedBufferRanges().map(r => r.serialize()), [ + [[2, 0], [4, 4]], + ]); + }); + + it("extends to the range's beginning when the selection is reversed", function() { + editor.setSelectedBufferRange([[4, 4], [2, 0]], {reversed: true}); + + instance.didMouseDownOnLineNumber({bufferRow: 6, domEvent: {shiftKey: true, button: 0}}); + assert.isFalse(editor.getLastSelection().isReversed()); + assert.deepEqual(editor.getLastSelection().getBufferRange().serialize(), [[2, 0], [6, 4]]); + }); + + it('reverses the selection if the extension line is before the existing selection', function() { + editor.setSelectedBufferRange([[3, 0], [4, 4]]); + + instance.didMouseDownOnLineNumber({bufferRow: 1, domEvent: {shiftKey: true, button: 0}}); + assert.isTrue(editor.getLastSelection().isReversed()); + assert.deepEqual(editor.getLastSelection().getBufferRange().serialize(), [[1, 0], [4, 4]]); + }); + }); + + describe('ctrl- or meta-click', function() { + beforeEach(function() { + // Select an initial row range. + instance.didMouseDownOnLineNumber({bufferRow: 2, domEvent: {button: 0}}); + instance.didMouseDownOnLineNumber({bufferRow: 5, domEvent: {shiftKey: true, button: 0}}); + instance.didMouseUp(); + // [[2, 0], [5, 4]] + }); + + it('deselects a line at the beginning of an existing selection', function() { + instance.didMouseDownOnLineNumber({bufferRow: 2, domEvent: {metaKey: true, button: 0}}); + assert.deepEqual(editor.getSelectedBufferRanges().map(r => r.serialize()), [ + [[3, 0], [5, 4]], + ]); + }); + + it('deselects a line within an existing selection', function() { + instance.didMouseDownOnLineNumber({bufferRow: 3, domEvent: {metaKey: true, button: 0}}); + assert.deepEqual(editor.getSelectedBufferRanges().map(r => r.serialize()), [ + [[2, 0], [2, 4]], + [[4, 0], [5, 4]], + ]); + }); + + it('deselects a line at the end of an existing selection', function() { + instance.didMouseDownOnLineNumber({bufferRow: 5, domEvent: {metaKey: true, button: 0}}); + assert.deepEqual(editor.getSelectedBufferRanges().map(r => r.serialize()), [ + [[2, 0], [4, 4]], + ]); + }); + + it('selects a line outside of an existing selection', function() { + instance.didMouseDownOnLineNumber({bufferRow: 8, domEvent: {metaKey: true, button: 0}}); + assert.deepEqual(editor.getSelectedBufferRanges().map(r => r.serialize()), [ + [[2, 0], [5, 4]], + [[8, 0], [8, 4]], + ]); + }); + + it('deselects the only line within an existing selection', function() { + instance.didMouseDownOnLineNumber({bufferRow: 7, domEvent: {metaKey: true, button: 0}}); + instance.didMouseUp(); + assert.deepEqual(editor.getSelectedBufferRanges().map(r => r.serialize()), [ + [[2, 0], [5, 4]], + [[7, 0], [7, 4]], + ]); + + instance.didMouseDownOnLineNumber({bufferRow: 7, domEvent: {metaKey: true, button: 0}}); + assert.deepEqual(editor.getSelectedBufferRanges().map(r => r.serialize()), [ + [[2, 0], [5, 4]], + ]); + }); + + it('cannot deselect the only selection', function() { + instance.didMouseDownOnLineNumber({bufferRow: 7, domEvent: {button: 0}}); + instance.didMouseUp(); + assert.deepEqual(editor.getSelectedBufferRanges().map(r => r.serialize()), [ + [[7, 0], [7, 4]], + ]); + + instance.didMouseDownOnLineNumber({bufferRow: 7, domEvent: {metaKey: true, button: 0}}); + assert.deepEqual(editor.getSelectedBufferRanges().map(r => r.serialize()), [ + [[7, 0], [7, 4]], + ]); + }); + + it('bonus points: understands ranges that do not cleanly align with editor rows', function() { + instance.handleSelectionEvent({metaKey: true, button: 0}, [[3, 1], [5, 2]]); + assert.deepEqual(editor.getSelectedBufferRanges().map(r => r.serialize()), [ + [[2, 0], [3, 1]], + [[5, 2], [5, 4]], + ]); + }); + }); + + it('does nothing on a click without a buffer row', function() { + instance.didMouseDownOnLineNumber({bufferRow: NaN, domEvent: {button: 0}}); + assert.deepEqual(editor.getSelectedBufferRanges().map(r => r.serialize()), [ + [[1, 0], [1, 4]], + ]); + + instance.didMouseDownOnLineNumber({bufferRow: undefined, domEvent: {button: 0}}); + assert.deepEqual(editor.getSelectedBufferRanges().map(r => r.serialize()), [ + [[1, 0], [1, 4]], + ]); + }); }); describe('hunk lines', function() { @@ -351,12 +685,14 @@ describe('FilePatchView', function() { { oldStartLine: 2, oldLineCount: 2, newStartLine: 2, newLineCount: 3, heading: 'first hunk', + // 2 3 4 lines: [' 0000', '+0001', ' 0002'], }, { - oldStartLine: 10, oldLineCount: 6, newStartLine: 11, newLineCount: 3, + oldStartLine: 10, oldLineCount: 5, newStartLine: 11, newLineCount: 6, heading: 'second hunk', - lines: [' 0003', '-0004', ' 0005', '-0006', '-0007', ' 0008'], + // 11 12 13 14 15 16 + lines: [' 0003', '+0004', '+0005', '-0006', ' 0007', '+0008', '-0009', ' 0010'], }, ], }]); @@ -367,11 +703,11 @@ describe('FilePatchView', function() { const wrapper = mount(buildApp({filePatch: fp, openFile})); const editor = wrapper.find('atom-text-editor').getDOMNode().getModel(); - editor.setCursorBufferPosition([3, 2]); + editor.setCursorBufferPosition([7, 2]); atomEnv.commands.dispatch(wrapper.getDOMNode(), 'github:open-file'); - assert.isTrue(openFile.calledWith([[11, 2]])); + assert.isTrue(openFile.calledWith([[14, 2]])); }); it('opens the file at a current added row', function() { @@ -379,11 +715,11 @@ describe('FilePatchView', function() { const wrapper = mount(buildApp({filePatch: fp, openFile})); const editor = wrapper.find('atom-text-editor').getDOMNode().getModel(); - editor.setCursorBufferPosition([1, 3]); + editor.setCursorBufferPosition([8, 3]); atomEnv.commands.dispatch(wrapper.getDOMNode(), 'github:open-file'); - assert.isTrue(openFile.calledWith([[3, 3]])); + assert.isTrue(openFile.calledWith([[15, 3]])); }); it('opens the file at the beginning of the previous added or unchanged row', function() { @@ -391,11 +727,11 @@ describe('FilePatchView', function() { const wrapper = mount(buildApp({filePatch: fp, openFile})); const editor = wrapper.find('atom-text-editor').getDOMNode().getModel(); - editor.setCursorBufferPosition([4, 2]); + editor.setCursorBufferPosition([9, 2]); atomEnv.commands.dispatch(wrapper.getDOMNode(), 'github:open-file'); - assert.isTrue(openFile.calledWith([[11, 0]])); + assert.isTrue(openFile.calledWith([[15, 0]])); }); it('preserves multiple cursors', function() { @@ -406,18 +742,18 @@ describe('FilePatchView', function() { editor.setCursorBufferPosition([3, 2]); editor.addCursorAtBufferPosition([4, 2]); editor.addCursorAtBufferPosition([1, 3]); - editor.addCursorAtBufferPosition([6, 2]); - editor.addCursorAtBufferPosition([7, 1]); + editor.addCursorAtBufferPosition([9, 2]); + editor.addCursorAtBufferPosition([9, 3]); - // The cursors at [6, 2] and [7, 1] are both collapsed to a single one on the unchanged line. + // [9, 2] and [9, 3] should be collapsed into a single cursor at [15, 0] atomEnv.commands.dispatch(wrapper.getDOMNode(), 'github:open-file'); assert.isTrue(openFile.calledWith([ [11, 2], - [11, 0], + [12, 2], [3, 3], - [12, 0], + [15, 0], ])); }); }); From f4d569701bb031f893298773896bf71eff07ed67 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 6 Sep 2018 15:30:00 -0400 Subject: [PATCH 0293/4252] Band-aids to get the CommitView tests passing --- lib/atom/atom-text-editor.js | 1 - lib/views/commit-view.js | 43 ++++++++++++++-------------------- test/views/commit-view.test.js | 22 ++++++++++++----- 3 files changed, 34 insertions(+), 32 deletions(-) diff --git a/lib/atom/atom-text-editor.js b/lib/atom/atom-text-editor.js index e5c2edca5c..2c02030017 100644 --- a/lib/atom/atom-text-editor.js +++ b/lib/atom/atom-text-editor.js @@ -92,7 +92,6 @@ export default class AtomTextEditor extends React.PureComponent { this.refElement.map(element => { const editor = element.getModel(); editor.setText(this.props.text, {bypassReadOnly: true}); - this.getRefModel().setter(editor); this.subs.add( diff --git a/lib/views/commit-view.js b/lib/views/commit-view.js index d6e9e3c28b..dcea46823b 100644 --- a/lib/views/commit-view.js +++ b/lib/views/commit-view.js @@ -80,14 +80,8 @@ export default class CommitView extends React.Component { this.refCoAuthorToggle = new RefHolder(); this.refCoAuthorSelect = new RefHolder(); this.refCoAuthorForm = new RefHolder(); - this.refEditor = new RefHolder(); - this.editor = null; - - this.subscriptions.add( - this.refEditor.observe(e => { - this.editor = e.getModel(); - }), - ); + this.refEditorComponent = new RefHolder(); + this.refEditorModel = new RefHolder(); } proxyKeyCode(keyCode) { @@ -162,7 +156,8 @@ export default class CommitView extends React.Component {
{this.commitButtonText()} - {this.commitIsEnabled() && + disabled={!this.commitIsEnabled(false)}>{this.commitButtonText()} + {this.commitIsEnabled(false) && { + if (editor.getCursorBufferPosition().row === 0) { + return (this.props.maximumCharacterLimit - editor.lineTextForBufferRow(0).length).toString(); } else { return '∞'; } - } else { - return this.props.maximumCharacterLimit || ''; - } + }).getOr(this.props.maximumCharacterLimit || ''); } // We don't want the user to see the UI flicker in the case @@ -469,7 +462,7 @@ export default class CommitView extends React.Component { } commitIsEnabled(amend) { - const messageExists = this.editor && this.editor.getText().length !== 0; + const messageExists = this.props.message.length > 0; return !this.props.isCommitting && (amend || this.props.stagedChangesExist) && !this.props.mergeConflictsExist && @@ -490,7 +483,7 @@ export default class CommitView extends React.Component { } toggleExpandedCommitMessageEditor() { - return this.props.toggleExpandedCommitMessageEditor(this.editor && this.editor.getText()); + return this.props.toggleExpandedCommitMessageEditor(this.props.message); } matchAuthors(authors, filterText, selectedAuthors) { @@ -556,11 +549,11 @@ export default class CommitView extends React.Component { } hasFocusEditor() { - return this.refEditor.map(editor => editor.contains(document.activeElement)).getOr(false); + return this.refEditorComponent.map(editor => editor.contains(document.activeElement)).getOr(false); } rememberFocus(event) { - if (this.refEditor.map(editor => editor.contains(event.target)).getOr(false)) { + if (this.refEditorComponent.map(editor => editor.contains(event.target)).getOr(false)) { return CommitView.focus.EDITOR; } @@ -587,7 +580,7 @@ export default class CommitView extends React.Component { }; if (focus === CommitView.focus.EDITOR) { - if (this.refEditor.map(focusElement).getOr(false)) { + if (this.refEditorComponent.map(focusElement).getOr(false)) { return true; } } @@ -613,7 +606,7 @@ export default class CommitView extends React.Component { fallback = true; } - if (fallback && this.refEditor.map(focusElement).getOr(false)) { + if (fallback && this.refEditorComponent.map(focusElement).getOr(false)) { return true; } diff --git a/test/views/commit-view.test.js b/test/views/commit-view.test.js index fbac9e15ff..f88f15ea53 100644 --- a/test/views/commit-view.test.js +++ b/test/views/commit-view.test.js @@ -137,17 +137,22 @@ describe('CommitView', function() { const wrapper = mount(app); assert.strictEqual(wrapper.find('.github-CommitView-remaining-characters').text(), '72'); + // It takes two renders for the remaining characters field to update based on editor state. + // FIXME: make sure this doesn't regress in the actual component wrapper.setProps({message: 'abcde fghij'}); + wrapper.setProps({}); assert.strictEqual(wrapper.find('.github-CommitView-remaining-characters').text(), '61'); assert.isFalse(wrapper.find('.github-CommitView-remaining-characters').hasClass('is-error')); assert.isFalse(wrapper.find('.github-CommitView-remaining-characters').hasClass('is-warning')); wrapper.setProps({message: '\nklmno'}); + wrapper.setProps({}); assert.strictEqual(wrapper.find('.github-CommitView-remaining-characters').text(), '∞'); assert.isFalse(wrapper.find('.github-CommitView-remaining-characters').hasClass('is-error')); assert.isFalse(wrapper.find('.github-CommitView-remaining-characters').hasClass('is-warning')); wrapper.setProps({message: 'abcde\npqrst'}); + wrapper.setProps({}); assert.strictEqual(wrapper.find('.github-CommitView-remaining-characters').text(), '∞'); assert.isFalse(wrapper.find('.github-CommitView-remaining-characters').hasClass('is-error')); assert.isFalse(wrapper.find('.github-CommitView-remaining-characters').hasClass('is-warning')); @@ -159,16 +164,19 @@ describe('CommitView', function() { assert.isFalse(wrapper.find('.github-CommitView-remaining-characters').hasClass('is-warning')); wrapper.setProps({stagedChangesExist: true, maximumCharacterLimit: 50}); + wrapper.setProps({}); assert.strictEqual(wrapper.find('.github-CommitView-remaining-characters').text(), '45'); assert.isFalse(wrapper.find('.github-CommitView-remaining-characters').hasClass('is-error')); assert.isFalse(wrapper.find('.github-CommitView-remaining-characters').hasClass('is-warning')); - wrapper.setProps({message: 'a'.repeat(41)}).update(); + wrapper.setProps({message: 'a'.repeat(41)}); + wrapper.setProps({}); assert.strictEqual(wrapper.find('.github-CommitView-remaining-characters').text(), '9'); assert.isFalse(wrapper.find('.github-CommitView-remaining-characters').hasClass('is-error')); assert.isTrue(wrapper.find('.github-CommitView-remaining-characters').hasClass('is-warning')); wrapper.setProps({message: 'a'.repeat(58)}).update(); + wrapper.setProps({}); assert.strictEqual(wrapper.find('.github-CommitView-remaining-characters').text(), '-8'); assert.isTrue(wrapper.find('.github-CommitView-remaining-characters').hasClass('is-error')); assert.isFalse(wrapper.find('.github-CommitView-remaining-characters').hasClass('is-warning')); @@ -207,10 +215,12 @@ describe('CommitView', function() { }); it('is disabled when the commit message is empty', function() { - wrapper.setProps({message: ''}).update(); + wrapper.setProps({message: ''}); + wrapper.setProps({}); assert.isTrue(wrapper.find('.github-CommitView-commit').prop('disabled')); - wrapper.setProps({message: 'Not empty'}).update(); + wrapper.setProps({message: 'Not empty'}); + wrapper.setProps({}); assert.isFalse(wrapper.find('.github-CommitView-commit').prop('disabled')); }); @@ -347,7 +357,7 @@ describe('CommitView', function() { assert.isFalse(wrapper.instance().hasFocusEditor()); editorNode.contains.returns(true); - wrapper.instance().refEditor.setter(null); + wrapper.instance().refEditorComponent.setter(null); assert.isFalse(wrapper.instance().hasFocusEditor()); }); @@ -370,7 +380,7 @@ describe('CommitView', function() { assert.isNull(wrapper.instance().rememberFocus({target: document.body})); const holders = [ - 'refEditor', 'refAbortMergeButton', 'refCommitButton', 'refCoAuthorSelect', + 'refEditorComponent', 'refEditorModel', 'refAbortMergeButton', 'refCommitButton', 'refCoAuthorSelect', ].map(ivar => wrapper.instance()[ivar]); for (const holder of holders) { holder.setter(null); @@ -434,7 +444,7 @@ describe('CommitView', function() { // Simulate an unmounted component by clearing out RefHolders manually. const holders = [ - 'refEditor', 'refAbortMergeButton', 'refCommitButton', 'refCoAuthorSelect', + 'refEditorComponent', 'refEditorModel', 'refAbortMergeButton', 'refCommitButton', 'refCoAuthorSelect', ].map(ivar => wrapper.instance()[ivar]); for (const holder of holders) { holder.setter(null); From b43be3a7fd6586a17f041cfdbcb046c9452b3645 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 6 Sep 2018 15:34:04 -0400 Subject: [PATCH 0294/4252] :fire: more unused code --- styles/hunk-view.old.less | 168 -------------------------- test/views/hunk-view.test.pending.js | 170 --------------------------- 2 files changed, 338 deletions(-) delete mode 100644 styles/hunk-view.old.less delete mode 100644 test/views/hunk-view.test.pending.js diff --git a/styles/hunk-view.old.less b/styles/hunk-view.old.less deleted file mode 100644 index 85d5707eb3..0000000000 --- a/styles/hunk-view.old.less +++ /dev/null @@ -1,168 +0,0 @@ -@import "ui-variables"; - -@hunk-fg-color: @text-color-subtle; -@hunk-bg-color: @pane-item-background-color; - -.github-HunkView { - font-family: Menlo, Consolas, 'DejaVu Sans Mono', monospace; - border-bottom: 1px solid @pane-item-border-color; - background-color: @hunk-bg-color; - - &-header { - display: flex; - align-items: stretch; - font-size: .9em; - background-color: @panel-heading-background-color; - border-bottom: 1px solid @panel-heading-border-color; - position: sticky; - top: 0; - } - - &-title { - flex: 1; - line-height: 2.4; - padding: 0 @component-padding; - color: @text-color-subtle; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - -webkit-font-smoothing: antialiased; - } - - &-stageButton, - &-discardButton { - line-height: 1; - padding-left: @component-padding; - padding-right: @component-padding; - font-family: @font-family; - border: none; - border-left: 1px solid @panel-heading-border-color; - background-color: transparent; - cursor: default; - &:hover { background-color: @button-background-color-hover; } - &:active { background-color: @panel-heading-border-color; } - } - - // pixel fit the icon - &-discardButton:before { - text-align: left; - width: auto; - } - - &-line { - display: table-row; - line-height: 1.5em; - color: @hunk-fg-color; - &.is-unchanged { - -webkit-font-smoothing: antialiased; - } - } - - &-lineNumber { - display: table-cell; - min-width: 3.5em; // min 4 chars - overflow: hidden; - padding: 0 .5em; - text-align: right; - border-right: 1px solid @base-border-color; - -webkit-font-smoothing: antialiased; - } - - &-plusMinus { - margin-right: 1ch; - color: fade(@text-color, 50%); - vertical-align: top; - } - - &-lineContent { - display: table-cell; - padding: 0 .5em 0 3ch; // indent 3 characters - text-indent: -2ch; // remove indentation for the +/- - white-space: pre-wrap; - word-break: break-word; - width: 100%; - vertical-align: top; - } - - &-lineText { - display: inline-block; - text-indent: 0; - } -} - - -// -// States -// ------------------------------- - -.github-HunkView.is-selected.is-hunkMode .github-HunkView-header { - background-color: @background-color-selected; - .github-HunkView-title { - color: @text-color; - } - .github-HunkView-stageButton, .github-HunkView-discardButton { - border-color: mix(@text-color, @background-color-selected, 25%); - } -} - -.github-HunkView-title:hover { - color: @text-color-highlight; -} - -.github-HunkView-line { - - // mixin - .hunk-line-mixin(@fg; @bg) { - &:hover { - background-color: @background-color-highlight; - } - &.is-selected { - color: @text-color; - background-color: @background-color-selected; - } - .github-HunkView-lineContent { - color: saturate( mix(@fg, @text-color-highlight, 20%), 20%); - background-color: saturate( mix(@bg, @hunk-bg-color, 15%), 20%); - } - // hightlight when focused + selected - .github-FilePatchView:focus &.is-selected .github-HunkView-lineContent { - color: saturate( mix(@fg, @text-color-highlight, 10%), 10%); - background-color: saturate( mix(@bg, @hunk-bg-color, 25%), 10%); - } - } - - &.is-deleted { - .hunk-line-mixin(@text-color-error, @background-color-error); - } - - &.is-added { - .hunk-line-mixin(@text-color-success, @background-color-success); - } - - // divider line between added and deleted lines - &.is-deleted + .is-added .github-HunkView-lineContent { - box-shadow: 0 -1px 0 hsla(0,0%,50%,.1); - } - -} - -// focus colors -.github-FilePatchView:focus { - .github-HunkView.is-selected.is-hunkMode .github-HunkView-title, - .github-HunkView.is-selected.is-hunkMode .github-HunkView-header, - .github-HunkView-line.is-selected .github-HunkView-lineNumber { - color: contrast(@button-background-color-selected); - background: @button-background-color-selected; - } - .github-HunkView-line.is-selected .github-HunkView-lineNumber { - border-color: mix(@button-border-color, @button-background-color-selected, 25%); - } - .github-HunkView.is-selected.is-hunkMode .github-HunkView { - &-stageButton, - &-discardButton { - border-color: mix(@hunk-bg-color, @button-background-color-selected, 30%); - &:hover { background-color: mix(@hunk-bg-color, @button-background-color-selected, 10%); } - &:active { background-color: @button-background-color-selected; } - } - } -} diff --git a/test/views/hunk-view.test.pending.js b/test/views/hunk-view.test.pending.js deleted file mode 100644 index 52bf4ede7d..0000000000 --- a/test/views/hunk-view.test.pending.js +++ /dev/null @@ -1,170 +0,0 @@ -import React from 'react'; -import {shallow} from 'enzyme'; - -import Hunk from '../../lib/models/hunk'; -import HunkLine from '../../lib/models/hunk-line'; -import HunkView from '../../lib/views/hunk-view'; - -describe('HunkView', function() { - let component, mousedownOnHeader, mousedownOnLine, mousemoveOnLine, contextMenuOnItem, didClickStageButton, didClickDiscardButton; - - beforeEach(function() { - const onlyLine = new HunkLine('only', 'added', 1, 1); - const emptyHunk = new Hunk(1, 1, 1, 1, 'heading', [ - onlyLine, - ]); - - mousedownOnHeader = sinon.spy(); - mousedownOnLine = sinon.spy(); - mousemoveOnLine = sinon.spy(); - contextMenuOnItem = sinon.spy(); - didClickStageButton = sinon.spy(); - didClickDiscardButton = sinon.spy(); - - component = ( - - ); - }); - - it('renders the hunk header and its lines', function() { - const hunk0 = new Hunk(5, 5, 2, 1, 'function fn {', [ - new HunkLine('line-1', 'unchanged', 5, 5), - new HunkLine('line-2', 'deleted', 6, -1), - new HunkLine('line-3', 'deleted', 7, -1), - new HunkLine('line-4', 'added', -1, 6), - ]); - - const wrapper = shallow(React.cloneElement(component, {hunk: hunk0})); - - assert.equal( - wrapper.find('.github-HunkView-header').render().text().trim(), - `${hunk0.getHeader().trim()} ${hunk0.getSectionHeading().trim()}`, - ); - - const lines0 = wrapper.find('LineView'); - assertHunkLineElementEqual( - lines0.at(0), - {oldLineNumber: '5', newLineNumber: '5', origin: ' ', content: 'line-1', isSelected: false}, - ); - assertHunkLineElementEqual( - lines0.at(1), - {oldLineNumber: '6', newLineNumber: ' ', origin: '-', content: 'line-2', isSelected: false}, - ); - assertHunkLineElementEqual( - lines0.at(2), - {oldLineNumber: '7', newLineNumber: ' ', origin: '-', content: 'line-3', isSelected: false}, - ); - assertHunkLineElementEqual( - lines0.at(3), - {oldLineNumber: ' ', newLineNumber: '6', origin: '+', content: 'line-4', isSelected: false}, - ); - - const hunk1 = new Hunk(8, 8, 1, 1, 'function fn2 {', [ - new HunkLine('line-1', 'deleted', 8, -1), - new HunkLine('line-2', 'added', -1, 8), - ]); - wrapper.setProps({hunk: hunk1}); - - assert.equal( - wrapper.find('.github-HunkView-header').render().text().trim(), - `${hunk1.getHeader().trim()} ${hunk1.getSectionHeading().trim()}`, - ); - - const lines1 = wrapper.find('LineView'); - assertHunkLineElementEqual( - lines1.at(0), - {oldLineNumber: '8', newLineNumber: ' ', origin: '-', content: 'line-1', isSelected: false}, - ); - assertHunkLineElementEqual( - lines1.at(1), - {oldLineNumber: ' ', newLineNumber: '8', origin: '+', content: 'line-2', isSelected: false}, - ); - - wrapper.setProps({ - selectedLines: new Set([hunk1.getLines()[1]]), - }); - - const lines2 = wrapper.find('LineView'); - assertHunkLineElementEqual( - lines2.at(0), - {oldLineNumber: '8', newLineNumber: ' ', origin: '-', content: 'line-1', isSelected: false}, - ); - assertHunkLineElementEqual( - lines2.at(1), - {oldLineNumber: ' ', newLineNumber: '8', origin: '+', content: 'line-2', isSelected: true}, - ); - }); - - it('adds the is-selected class based on the isSelected property', function() { - const wrapper = shallow(React.cloneElement(component, {isSelected: true})); - assert.isTrue(wrapper.find('.github-HunkView').hasClass('is-selected')); - - wrapper.setProps({isSelected: false}); - - assert.isFalse(wrapper.find('.github-HunkView').hasClass('is-selected')); - }); - - it('calls the didClickStageButton handler when the staging button is clicked', function() { - const wrapper = shallow(component); - - wrapper.find('.github-HunkView-stageButton').simulate('click'); - assert.isTrue(didClickStageButton.called); - }); - - describe('line selection', function() { - it('calls the mousedownOnLine and mousemoveOnLine handlers on mousedown and mousemove events', function() { - const hunk = new Hunk(1234, 1234, 1234, 1234, '', [ - new HunkLine('line-1', 'added', 1234, 1234), - new HunkLine('line-2', 'added', 1234, 1234), - new HunkLine('line-3', 'added', 1234, 1234), - new HunkLine('line-4', 'unchanged', 1234, 1234), - new HunkLine('line-5', 'deleted', 1234, 1234), - ]); - - // selectLine callback not called when selectionEnabled = false - const wrapper = shallow(React.cloneElement(component, {hunk, selectionEnabled: false})); - const lineDivAt = index => wrapper.find('LineView').at(index).shallow().find('.github-HunkView-line'); - - const payload0 = {}; - lineDivAt(0).simulate('mousedown', payload0); - assert.isTrue(mousedownOnLine.calledWith(payload0, hunk, hunk.lines[0])); - - const payload1 = {}; - lineDivAt(1).simulate('mousemove', payload1); - assert.isTrue(mousemoveOnLine.calledWith(payload1, hunk, hunk.lines[1])); - - // we don't call handler with redundant events - assert.equal(mousemoveOnLine.callCount, 1); - lineDivAt(1).simulate('mousemove'); - assert.equal(mousemoveOnLine.callCount, 1); - lineDivAt(2).simulate('mousemove'); - assert.equal(mousemoveOnLine.callCount, 2); - }); - }); -}); - -function assertHunkLineElementEqual(lineWrapper, {oldLineNumber, newLineNumber, origin, content, isSelected}) { - const subWrapper = lineWrapper.shallow(); - - assert.equal(subWrapper.find('.github-HunkView-lineNumber.is-old').render().text(), oldLineNumber); - assert.equal(subWrapper.find('.github-HunkView-lineNumber.is-new').render().text(), newLineNumber); - assert.equal(subWrapper.find('.github-HunkView-lineContent').render().text(), origin + content); - assert.equal(subWrapper.find('.github-HunkView-line').hasClass('is-selected'), isSelected); -} From 6af10df606ff8076d8b35b2af0006283f7e3fcbc Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 6 Sep 2018 15:55:25 -0400 Subject: [PATCH 0295/4252] Coverage for HunkHeaderView --- ...st.pending.js => hunk-header-view.test.js} | 34 +++++++++++++++---- 1 file changed, 28 insertions(+), 6 deletions(-) rename test/views/{hunk-header-view.test.pending.js => hunk-header-view.test.js} (71%) diff --git a/test/views/hunk-header-view.test.pending.js b/test/views/hunk-header-view.test.js similarity index 71% rename from test/views/hunk-header-view.test.pending.js rename to test/views/hunk-header-view.test.js index 525aa9a10c..8d7625d10d 100644 --- a/test/views/hunk-header-view.test.pending.js +++ b/test/views/hunk-header-view.test.js @@ -2,14 +2,16 @@ import React from 'react'; import {shallow} from 'enzyme'; import HunkHeaderView from '../../lib/views/hunk-header-view'; -import Hunk from '../../lib/models/hunk'; +import Hunk from '../../lib/models/patch/hunk'; describe('HunkHeaderView', function() { let atomEnv, hunk; beforeEach(function() { atomEnv = global.buildAtomEnvironment(); - hunk = new Hunk(0, 1, 10, 11, 'section heading', []); + hunk = new Hunk({ + oldStartRow: 0, oldRowCount: 10, newStartRow: 1, newRowCount: 11, sectionHeading: 'section heading', changes: [], + }); }); afterEach(function() { @@ -38,18 +40,18 @@ describe('HunkHeaderView', function() { it('applies a CSS class when selected', function() { const wrapper = shallow(buildApp({isSelected: true})); - assert.isTrue(wrapper.find('.github-HunkHeaderView').hasClass('is-selected')); + assert.isTrue(wrapper.find('.github-HunkHeaderView').hasClass('github-HunkHeaderView--isSelected')); wrapper.setProps({isSelected: false}); - assert.isFalse(wrapper.find('.github-HunkHeaderView').hasClass('is-selected')); + assert.isFalse(wrapper.find('.github-HunkHeaderView').hasClass('github-HunkHeaderView--isSelected')); }); it('applies a CSS class in hunk selection mode', function() { const wrapper = shallow(buildApp({selectionMode: 'hunk'})); - assert.isTrue(wrapper.find('.github-HunkHeaderView').hasClass('is-hunkMode')); + assert.isTrue(wrapper.find('.github-HunkHeaderView').hasClass('github-HunkHeaderView--isHunkMode')); wrapper.setProps({selectionMode: 'line'}); - assert.isFalse(wrapper.find('.github-HunkHeaderView').hasClass('is-hunkMode')); + assert.isFalse(wrapper.find('.github-HunkHeaderView').hasClass('github-HunkHeaderView--isHunkMode')); }); it('renders the hunk header title', function() { @@ -75,4 +77,24 @@ describe('HunkHeaderView', function() { button.simulate('click'); assert.isTrue(discardSelection.called); }); + + it('triggers the mousedown handler', function() { + const mouseDown = sinon.spy(); + const wrapper = shallow(buildApp({mouseDown})); + + wrapper.find('.github-HunkHeaderView').simulate('mousedown'); + + assert.isTrue(mouseDown.called); + }); + + it('stops mousedown events on the toggle button from propagating', function() { + const mouseDown = sinon.spy(); + const wrapper = shallow(buildApp({mouseDown})); + + const evt = {stopPropagation: sinon.spy()}; + wrapper.find('.github-HunkHeaderView-stageButton').simulate('mousedown', evt); + + assert.isFalse(mouseDown.called); + assert.isTrue(evt.stopPropagation.called); + }); }); From bc1f0231e51ac1dd6b7c2042e530ee854e1020c9 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 6 Sep 2018 16:04:01 -0400 Subject: [PATCH 0296/4252] ETOOMANYREFS --- test/controllers/git-tab-controller.test.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/controllers/git-tab-controller.test.js b/test/controllers/git-tab-controller.test.js index 890b39dfaf..1afaa57952 100644 --- a/test/controllers/git-tab-controller.test.js +++ b/test/controllers/git-tab-controller.test.js @@ -290,7 +290,7 @@ describe('GitTabController', function() { focusElement = stagingView.element; const commitViewElements = []; - commitView.refEditor.map(c => c.refElement.map(e => commitViewElements.push(e))); + commitView.refEditorComponent.map(e => commitViewElements.push(e)); commitView.refAbortMergeButton.map(e => commitViewElements.push(e)); commitView.refCommitButton.map(e => commitViewElements.push(e)); @@ -647,7 +647,7 @@ describe('GitTabController', function() { // new commit message const newMessage = 'such new very message'; const commitView = wrapper.find('CommitView'); - commitView.instance().editor.setText(newMessage); + commitView.instance().refEditorModel.map(e => e.setText(newMessage)); // no staged changes assert.lengthOf(wrapper.find('GitTabView').prop('stagedChanges'), 0); @@ -701,7 +701,7 @@ describe('GitTabController', function() { commitView.setState({showCoAuthorInput: true}); commitView.onSelectedCoAuthorsChanged([author]); const newMessage = 'Star Wars: A New Message'; - commitView.editor.setText(newMessage); + commitView.refEditorModel.map(e => e.setText(newMessage)); commandRegistry.dispatch(workspaceElement, 'github:amend-last-commit'); // verify that coAuthor was passed @@ -735,7 +735,7 @@ describe('GitTabController', function() { // buh bye co author const commitView = wrapper.find('CommitView').instance(); - assert.strictEqual(commitView.editor.getText(), ''); + assert.strictEqual(commitView.refEditorModel.map(e => e.getText()).getOr(''), ''); commitView.onSelectedCoAuthorsChanged([]); // amend again From e8bea900bdecc5ff4b0ce326c0bcd33f345442c7 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 6 Sep 2018 16:26:27 -0400 Subject: [PATCH 0297/4252] PASS YOU COWARDS --- test/controllers/git-tab-controller.test.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/test/controllers/git-tab-controller.test.js b/test/controllers/git-tab-controller.test.js index 1afaa57952..ddbed7873e 100644 --- a/test/controllers/git-tab-controller.test.js +++ b/test/controllers/git-tab-controller.test.js @@ -365,12 +365,12 @@ describe('GitTabController', function() { commandRegistry.dispatch(rootElement, 'core:focus-next'); assertSelected(['staged-1.txt']); - await assert.async.strictEqual(focusElement, wrapper.find('atom-text-editor').getDOMNode()); + await assert.async.strictEqual(focusElement, wrapper.find('AtomTextEditor').instance()); // This should be a no-op. (Actually, it'll insert a tab in the CommitView editor.) commandRegistry.dispatch(rootElement, 'core:focus-next'); assertSelected(['staged-1.txt']); - assert.strictEqual(focusElement, wrapper.find('atom-text-editor').getDOMNode()); + assert.strictEqual(focusElement, wrapper.find('AtomTextEditor').instance()); }); it('retreats focus from the CommitView through StagingView groups, but does not cycle', async function() { @@ -415,18 +415,18 @@ describe('GitTabController', function() { }); it('focuses the CommitView on github:commit with an empty commit message', async function() { - commitView.editor.setText(''); + commitView.refEditorModel.map(e => e.setText('')); sinon.spy(wrapper.instance(), 'commit'); wrapper.update(); commandRegistry.dispatch(workspaceElement, 'github:commit'); - await assert.async.strictEqual(focusElement, wrapper.find('atom-text-editor').getDOMNode()); + await assert.async.strictEqual(focusElement, wrapper.find('AtomTextEditor').instance()); assert.isFalse(wrapper.instance().commit.called); }); it('creates a commit on github:commit with a nonempty commit message', async function() { - commitView.editor.setText('I fixed the things'); + commitView.refEditorModel.map(e => e.setText('I fixed the things')); sinon.spy(repository, 'commit'); commandRegistry.dispatch(workspaceElement, 'github:commit'); @@ -625,7 +625,10 @@ describe('GitTabController', function() { assert.lengthOf(wrapper.find('GitTabView').prop('stagedChanges'), 1); // ensure that the commit editor is empty - assert.strictEqual(wrapper.find('CommitView').instance().editor.getText(), ''); + assert.strictEqual( + wrapper.find('CommitView').instance().refEditorModel.map(e => e.getText()).getOr(undefined), + '', + ); commandRegistry.dispatch(workspaceElement, 'github:amend-last-commit'); await assert.async.deepEqual( From d993c3b9fcede933b010d6be7ba1c8fbda859046 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 6 Sep 2018 16:37:57 -0400 Subject: [PATCH 0298/4252] :shirt: --- lib/github-package.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/github-package.js b/lib/github-package.js index 11040a3381..86e7271182 100644 --- a/lib/github-package.js +++ b/lib/github-package.js @@ -294,7 +294,6 @@ export default class GithubPackage { startOpen={this.startOpen} startRevealed={this.startRevealed} removeFilePatchItem={this.removeFilePatchItem} - workdirContextPool={this.contextPool} />, this.element, callback, ); } From 89e693f13d0559d67a8225c9c8637e4769d5a28a Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 7 Sep 2018 08:00:13 -0400 Subject: [PATCH 0299/4252] Disable symlink tests on Windows --- .../controllers/file-patch-controller.test.js | 130 +++++++++--------- 1 file changed, 66 insertions(+), 64 deletions(-) diff --git a/test/controllers/file-patch-controller.test.js b/test/controllers/file-patch-controller.test.js index c08c57bc3b..f69923022a 100644 --- a/test/controllers/file-patch-controller.test.js +++ b/test/controllers/file-patch-controller.test.js @@ -242,98 +242,100 @@ describe('FilePatchController', function() { }); }); - describe('toggleSymlinkChange', function() { - it('handles an addition and typechange with a special repository method', async function() { - const p = path.join(repository.getWorkingDirectoryPath(), 'waslink.txt'); - const dest = path.join(repository.getWorkingDirectoryPath(), 'destination'); - await fs.writeFile(dest, 'asdf\n', 'utf8'); - await fs.symlink(dest, p); + if (process.platform !== 'win32') { + describe('toggleSymlinkChange', function() { + it('handles an addition and typechange with a special repository method', async function() { + const p = path.join(repository.getWorkingDirectoryPath(), 'waslink.txt'); + const dest = path.join(repository.getWorkingDirectoryPath(), 'destination'); + await fs.writeFile(dest, 'asdf\n', 'utf8'); + await fs.symlink(dest, p); - await repository.stageFiles(['waslink.txt', 'destination']); - await repository.commit('zero'); + await repository.stageFiles(['waslink.txt', 'destination']); + await repository.commit('zero'); - await fs.unlink(p); - await fs.writeFile(p, 'fdsa\n', 'utf8'); + await fs.unlink(p); + await fs.writeFile(p, 'fdsa\n', 'utf8'); - repository.refresh(); - const symlinkPatch = await repository.getFilePatchForPath('waslink.txt', {staged: false}); - const wrapper = shallow(buildApp({filePatch: symlinkPatch, relPath: 'waslink.txt', stagingStatus: 'unstaged'})); + repository.refresh(); + const symlinkPatch = await repository.getFilePatchForPath('waslink.txt', {staged: false}); + const wrapper = shallow(buildApp({filePatch: symlinkPatch, relPath: 'waslink.txt', stagingStatus: 'unstaged'})); - sinon.spy(repository, 'stageFileSymlinkChange'); + sinon.spy(repository, 'stageFileSymlinkChange'); - await wrapper.find('FilePatchView').prop('toggleSymlinkChange')(); + await wrapper.find('FilePatchView').prop('toggleSymlinkChange')(); - assert.isTrue(repository.stageFileSymlinkChange.calledWith('waslink.txt')); - }); + assert.isTrue(repository.stageFileSymlinkChange.calledWith('waslink.txt')); + }); - it('stages non-addition typechanges normally', async function() { - const p = path.join(repository.getWorkingDirectoryPath(), 'waslink.txt'); - const dest = path.join(repository.getWorkingDirectoryPath(), 'destination'); - await fs.writeFile(dest, 'asdf\n', 'utf8'); - await fs.symlink(dest, p); + it('stages non-addition typechanges normally', async function() { + const p = path.join(repository.getWorkingDirectoryPath(), 'waslink.txt'); + const dest = path.join(repository.getWorkingDirectoryPath(), 'destination'); + await fs.writeFile(dest, 'asdf\n', 'utf8'); + await fs.symlink(dest, p); - await repository.stageFiles(['waslink.txt', 'destination']); - await repository.commit('zero'); + await repository.stageFiles(['waslink.txt', 'destination']); + await repository.commit('zero'); - await fs.unlink(p); + await fs.unlink(p); - repository.refresh(); - const symlinkPatch = await repository.getFilePatchForPath('waslink.txt', {staged: false}); - const wrapper = shallow(buildApp({filePatch: symlinkPatch, relPath: 'waslink.txt', stagingStatus: 'unstaged'})); + repository.refresh(); + const symlinkPatch = await repository.getFilePatchForPath('waslink.txt', {staged: false}); + const wrapper = shallow(buildApp({filePatch: symlinkPatch, relPath: 'waslink.txt', stagingStatus: 'unstaged'})); - sinon.spy(repository, 'stageFiles'); + sinon.spy(repository, 'stageFiles'); - await wrapper.find('FilePatchView').prop('toggleSymlinkChange')(); + await wrapper.find('FilePatchView').prop('toggleSymlinkChange')(); - assert.isTrue(repository.stageFiles.calledWith(['waslink.txt'])); - }); + assert.isTrue(repository.stageFiles.calledWith(['waslink.txt'])); + }); - it('handles a deletion and typechange with a special repository method', async function() { - const p = path.join(repository.getWorkingDirectoryPath(), 'waslink.txt'); - const dest = path.join(repository.getWorkingDirectoryPath(), 'destination'); - await fs.writeFile(dest, 'asdf\n', 'utf8'); - await fs.writeFile(p, 'fdsa\n', 'utf8'); + it('handles a deletion and typechange with a special repository method', async function() { + const p = path.join(repository.getWorkingDirectoryPath(), 'waslink.txt'); + const dest = path.join(repository.getWorkingDirectoryPath(), 'destination'); + await fs.writeFile(dest, 'asdf\n', 'utf8'); + await fs.writeFile(p, 'fdsa\n', 'utf8'); - await repository.stageFiles(['waslink.txt', 'destination']); - await repository.commit('zero'); + await repository.stageFiles(['waslink.txt', 'destination']); + await repository.commit('zero'); - await fs.unlink(p); - await fs.symlink(dest, p); - await repository.stageFiles(['waslink.txt']); + await fs.unlink(p); + await fs.symlink(dest, p); + await repository.stageFiles(['waslink.txt']); - repository.refresh(); - const symlinkPatch = await repository.getFilePatchForPath('waslink.txt', {staged: true}); - const wrapper = shallow(buildApp({filePatch: symlinkPatch, relPath: 'waslink.txt', stagingStatus: 'staged'})); + repository.refresh(); + const symlinkPatch = await repository.getFilePatchForPath('waslink.txt', {staged: true}); + const wrapper = shallow(buildApp({filePatch: symlinkPatch, relPath: 'waslink.txt', stagingStatus: 'staged'})); - sinon.spy(repository, 'stageFileSymlinkChange'); + sinon.spy(repository, 'stageFileSymlinkChange'); - await wrapper.find('FilePatchView').prop('toggleSymlinkChange')(); + await wrapper.find('FilePatchView').prop('toggleSymlinkChange')(); - assert.isTrue(repository.stageFileSymlinkChange.calledWith('waslink.txt')); - }); + assert.isTrue(repository.stageFileSymlinkChange.calledWith('waslink.txt')); + }); - it('unstages non-deletion typechanges normally', async function() { - const p = path.join(repository.getWorkingDirectoryPath(), 'waslink.txt'); - const dest = path.join(repository.getWorkingDirectoryPath(), 'destination'); - await fs.writeFile(dest, 'asdf\n', 'utf8'); - await fs.symlink(dest, p); + it('unstages non-deletion typechanges normally', async function() { + const p = path.join(repository.getWorkingDirectoryPath(), 'waslink.txt'); + const dest = path.join(repository.getWorkingDirectoryPath(), 'destination'); + await fs.writeFile(dest, 'asdf\n', 'utf8'); + await fs.symlink(dest, p); - await repository.stageFiles(['waslink.txt', 'destination']); - await repository.commit('zero'); + await repository.stageFiles(['waslink.txt', 'destination']); + await repository.commit('zero'); - await fs.unlink(p); + await fs.unlink(p); - repository.refresh(); - const symlinkPatch = await repository.getFilePatchForPath('waslink.txt', {staged: true}); - const wrapper = shallow(buildApp({filePatch: symlinkPatch, relPath: 'waslink.txt', stagingStatus: 'staged'})); + repository.refresh(); + const symlinkPatch = await repository.getFilePatchForPath('waslink.txt', {staged: true}); + const wrapper = shallow(buildApp({filePatch: symlinkPatch, relPath: 'waslink.txt', stagingStatus: 'staged'})); - sinon.spy(repository, 'unstageFiles'); + sinon.spy(repository, 'unstageFiles'); - await wrapper.find('FilePatchView').prop('toggleSymlinkChange')(); + await wrapper.find('FilePatchView').prop('toggleSymlinkChange')(); - assert.isTrue(repository.unstageFiles.calledWith(['waslink.txt'])); + assert.isTrue(repository.unstageFiles.calledWith(['waslink.txt'])); + }); }); - }); + } it('calls discardLines with selected rows', function() { const discardLines = sinon.spy(); From a1a531e0d4294645743aee385b6006f70c0740b2 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 7 Sep 2018 08:17:04 -0400 Subject: [PATCH 0300/4252] Mode changes aren't a thing on Windows either --- .../controllers/file-patch-controller.test.js | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/test/controllers/file-patch-controller.test.js b/test/controllers/file-patch-controller.test.js index f69923022a..d95211a04e 100644 --- a/test/controllers/file-patch-controller.test.js +++ b/test/controllers/file-patch-controller.test.js @@ -211,38 +211,38 @@ describe('FilePatchController', function() { }); }); - describe('toggleModeChange()', function() { - it("it stages an unstaged file's new mode", async function() { - const p = path.join(repository.getWorkingDirectoryPath(), 'a.txt'); - await fs.chmod(p, 0o755); - repository.refresh(); - const newFilePatch = await repository.getFilePatchForPath('a.txt', {staged: false}); + if (process.platform !== 'win32') { + describe('toggleModeChange()', function() { + it("it stages an unstaged file's new mode", async function() { + const p = path.join(repository.getWorkingDirectoryPath(), 'a.txt'); + await fs.chmod(p, 0o755); + repository.refresh(); + const newFilePatch = await repository.getFilePatchForPath('a.txt', {staged: false}); - const wrapper = shallow(buildApp({filePatch: newFilePatch, stagingStatus: 'unstaged'})); + const wrapper = shallow(buildApp({filePatch: newFilePatch, stagingStatus: 'unstaged'})); - sinon.spy(repository, 'stageFileModeChange'); - await wrapper.find('FilePatchView').prop('toggleModeChange')(); + sinon.spy(repository, 'stageFileModeChange'); + await wrapper.find('FilePatchView').prop('toggleModeChange')(); - assert.isTrue(repository.stageFileModeChange.calledWith('a.txt', '100755')); - }); + assert.isTrue(repository.stageFileModeChange.calledWith('a.txt', '100755')); + }); - it("it stages a staged file's old mode", async function() { - const p = path.join(repository.getWorkingDirectoryPath(), 'a.txt'); - await fs.chmod(p, 0o755); - await repository.stageFiles(['a.txt']); - repository.refresh(); - const newFilePatch = await repository.getFilePatchForPath('a.txt', {staged: true}); + it("it stages a staged file's old mode", async function() { + const p = path.join(repository.getWorkingDirectoryPath(), 'a.txt'); + await fs.chmod(p, 0o755); + await repository.stageFiles(['a.txt']); + repository.refresh(); + const newFilePatch = await repository.getFilePatchForPath('a.txt', {staged: true}); - const wrapper = shallow(buildApp({filePatch: newFilePatch, stagingStatus: 'staged'})); + const wrapper = shallow(buildApp({filePatch: newFilePatch, stagingStatus: 'staged'})); - sinon.spy(repository, 'stageFileModeChange'); - await wrapper.find('FilePatchView').prop('toggleModeChange')(); + sinon.spy(repository, 'stageFileModeChange'); + await wrapper.find('FilePatchView').prop('toggleModeChange')(); - assert.isTrue(repository.stageFileModeChange.calledWith('a.txt', '100644')); + assert.isTrue(repository.stageFileModeChange.calledWith('a.txt', '100644')); + }); }); - }); - if (process.platform !== 'win32') { describe('toggleSymlinkChange', function() { it('handles an addition and typechange with a special repository method', async function() { const p = path.join(repository.getWorkingDirectoryPath(), 'waslink.txt'); From 51b921f45aa1d9fe7189bd6fed7dd7566d4a90bb Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 7 Sep 2018 10:53:29 -0400 Subject: [PATCH 0301/4252] Track selection mode in FilePatchController --- lib/controllers/file-patch-controller.js | 38 +++++++--- .../controllers/file-patch-controller.test.js | 76 ++++++++++++++++--- 2 files changed, 92 insertions(+), 22 deletions(-) diff --git a/lib/controllers/file-patch-controller.js b/lib/controllers/file-patch-controller.js index 63c9642df2..95423b2946 100644 --- a/lib/controllers/file-patch-controller.js +++ b/lib/controllers/file-patch-controller.js @@ -33,6 +33,7 @@ export default class FilePatchController extends React.Component { this.state = { lastFilePatch: this.props.filePatch, + selectionMode: 'line', selectedRows: new Set(), }; @@ -59,6 +60,7 @@ export default class FilePatchController extends React.Component { {...this.props} selectedRows={this.state.selectedRows} + selectionMode={this.state.selectionMode} selectedRowsChanged={this.selectedRowsChanged} diveIntoMirrorPatch={this.diveIntoMirrorPatch} @@ -108,15 +110,22 @@ export default class FilePatchController extends React.Component { }); } - toggleRows() { - if (this.state.selectedRows.size === 0) { + async toggleRows(rowSet, nextSelectionMode) { + let chosenRows = rowSet; + if (chosenRows) { + await this.selectedRowsChanged(chosenRows, nextSelectionMode); + } else { + chosenRows = this.state.selectedRows; + } + + if (chosenRows.size === 0) { return Promise.resolve(); } return this.stagingOperation(() => { const patch = this.withStagingStatus({ - staged: () => this.props.filePatch.getUnstagePatchForLines(this.state.selectedRows), - unstaged: () => this.props.filePatch.getStagePatchForLines(this.state.selectedRows), + staged: () => this.props.filePatch.getUnstagePatchForLines(chosenRows), + unstaged: () => this.props.filePatch.getStagePatchForLines(chosenRows), }); return this.props.repository.applyPatchToIndex(patch); }); @@ -154,16 +163,25 @@ export default class FilePatchController extends React.Component { }); } - discardRows() { - return this.props.discardLines(this.props.filePatch, this.state.selectedRows, this.props.repository); + async discardRows(rowSet, nextSelectionMode) { + let chosenRows = rowSet; + if (chosenRows) { + await this.selectedRowsChanged(chosenRows, nextSelectionMode); + } else { + chosenRows = this.state.selectedRows; + } + + return this.props.discardLines(this.props.filePatch, chosenRows, this.props.repository); } - selectedRowsChanged(rows) { - if (equalSets(this.state.selectedRows, rows)) { - return; + selectedRowsChanged(rows, nextSelectionMode) { + if (equalSets(this.state.selectedRows, rows) && this.state.selectionMode === nextSelectionMode) { + return Promise.resolve(); } - this.setState({selectedRows: rows}); + return new Promise(resolve => { + this.setState({selectedRows: rows, selectionMode: nextSelectionMode}, resolve); + }); } withStagingStatus(callbacks) { diff --git a/test/controllers/file-patch-controller.test.js b/test/controllers/file-patch-controller.test.js index d95211a04e..61681ecf43 100644 --- a/test/controllers/file-patch-controller.test.js +++ b/test/controllers/file-patch-controller.test.js @@ -149,32 +149,49 @@ describe('FilePatchController', function() { }); }); - describe('selected row tracking', function() { + describe('selected row and selection mode tracking', function() { it('captures the selected row set', function() { const wrapper = shallow(buildApp()); assert.sameMembers(Array.from(wrapper.find('FilePatchView').prop('selectedRows')), []); + assert.strictEqual(wrapper.find('FilePatchView').prop('selectionMode'), 'line'); - wrapper.find('FilePatchView').prop('selectedRowsChanged')(new Set([1, 2])); + wrapper.find('FilePatchView').prop('selectedRowsChanged')(new Set([1, 2]), 'hunk'); assert.sameMembers(Array.from(wrapper.find('FilePatchView').prop('selectedRows')), [1, 2]); + assert.strictEqual(wrapper.find('FilePatchView').prop('selectionMode'), 'hunk'); }); - it('does not re-render if the row set is unchanged', function() { + it('does not re-render if the row set and selection mode are unchanged', function() { const wrapper = shallow(buildApp()); assert.sameMembers(Array.from(wrapper.find('FilePatchView').prop('selectedRows')), []); - wrapper.find('FilePatchView').prop('selectedRowsChanged')(new Set([1, 2])); + assert.strictEqual(wrapper.find('FilePatchView').prop('selectionMode'), 'line'); - assert.sameMembers(Array.from(wrapper.find('FilePatchView').prop('selectedRows')), [1, 2]); sinon.spy(wrapper.instance(), 'render'); - wrapper.find('FilePatchView').prop('selectedRowsChanged')(new Set([2, 1])); + + wrapper.find('FilePatchView').prop('selectedRowsChanged')(new Set([1, 2]), 'line'); + + assert.isTrue(wrapper.instance().render.called); assert.sameMembers(Array.from(wrapper.find('FilePatchView').prop('selectedRows')), [1, 2]); + assert.strictEqual(wrapper.find('FilePatchView').prop('selectionMode'), 'line'); + + wrapper.instance().render.resetHistory(); + wrapper.find('FilePatchView').prop('selectedRowsChanged')(new Set([2, 1]), 'line'); + + assert.sameMembers(Array.from(wrapper.find('FilePatchView').prop('selectedRows')), [1, 2]); + assert.strictEqual(wrapper.find('FilePatchView').prop('selectionMode'), 'line'); assert.isFalse(wrapper.instance().render.called); + + wrapper.instance().render.resetHistory(); + wrapper.find('FilePatchView').prop('selectedRowsChanged')(new Set([1, 2]), 'hunk'); + + assert.sameMembers(Array.from(wrapper.find('FilePatchView').prop('selectedRows')), [1, 2]); + assert.strictEqual(wrapper.find('FilePatchView').prop('selectionMode'), 'hunk'); + assert.isTrue(wrapper.instance().render.called); }); }); describe('toggleRows()', function() { it('is a no-op with no selected rows', async function() { const wrapper = shallow(buildApp()); - assert.sameMembers(Array.from(wrapper.find('FilePatchView').prop('selectedRows')), []); sinon.spy(repository, 'applyPatchToIndex'); @@ -191,8 +208,24 @@ describe('FilePatchController', function() { await wrapper.find('FilePatchView').prop('toggleRows')(); - assert.isTrue(filePatch.getStagePatchForLines.called); + assert.sameMembers(Array.from(filePatch.getStagePatchForLines.lastCall.args[0]), [1]); + assert.isTrue(repository.applyPatchToIndex.calledWith(filePatch.getStagePatchForLines.returnValues[0])); + }); + + it('toggles a different row set if provided', async function() { + const wrapper = shallow(buildApp()); + wrapper.find('FilePatchView').prop('selectedRowsChanged')(new Set([1]), 'line'); + + sinon.spy(filePatch, 'getStagePatchForLines'); + sinon.spy(repository, 'applyPatchToIndex'); + + await wrapper.find('FilePatchView').prop('toggleRows')(new Set([2]), 'hunk'); + + assert.sameMembers(Array.from(filePatch.getStagePatchForLines.lastCall.args[0]), [2]); assert.isTrue(repository.applyPatchToIndex.calledWith(filePatch.getStagePatchForLines.returnValues[0])); + + assert.sameMembers(Array.from(wrapper.find('FilePatchView').prop('selectedRows')), [2]); + assert.strictEqual(wrapper.find('FilePatchView').prop('selectionMode'), 'hunk'); }); it('applies an unstage patch to the index', async function() { @@ -206,7 +239,7 @@ describe('FilePatchController', function() { await wrapper.find('FilePatchView').prop('toggleRows')(); - assert.isTrue(otherPatch.getUnstagePatchForLines.called); + assert.sameMembers(Array.from(otherPatch.getUnstagePatchForLines.lastCall.args[0]), [2]); assert.isTrue(repository.applyPatchToIndex.calledWith(otherPatch.getUnstagePatchForLines.returnValues[0])); }); }); @@ -337,13 +370,32 @@ describe('FilePatchController', function() { }); } - it('calls discardLines with selected rows', function() { + it('calls discardLines with selected rows', async function() { + const discardLines = sinon.spy(); + const wrapper = shallow(buildApp({discardLines})); + wrapper.find('FilePatchView').prop('selectedRowsChanged')(new Set([1, 2])); + + await wrapper.find('FilePatchView').prop('discardRows')(); + + const lastArgs = discardLines.lastCall.args; + assert.strictEqual(lastArgs[0], filePatch); + assert.sameMembers(Array.from(lastArgs[1]), [1, 2]); + assert.strictEqual(lastArgs[2], repository); + }); + + it('calls discardLines with explicitly provided rows', async function() { const discardLines = sinon.spy(); const wrapper = shallow(buildApp({discardLines})); wrapper.find('FilePatchView').prop('selectedRowsChanged')(new Set([1, 2])); - wrapper.find('FilePatchView').prop('discardRows')(); + await wrapper.find('FilePatchView').prop('discardRows')(new Set([4, 5]), 'hunk'); + + const lastArgs = discardLines.lastCall.args; + assert.strictEqual(lastArgs[0], filePatch); + assert.sameMembers(Array.from(lastArgs[1]), [4, 5]); + assert.strictEqual(lastArgs[2], repository); - assert.isTrue(discardLines.calledWith(filePatch, new Set([1, 2]), repository)); + assert.sameMembers(Array.from(wrapper.find('FilePatchView').prop('selectedRows')), [4, 5]); + assert.strictEqual(wrapper.find('FilePatchView').prop('selectionMode'), 'hunk'); }); }); From 370ce170f721edc6a799c8b6078b326a582bff29 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 7 Sep 2018 16:08:01 -0400 Subject: [PATCH 0302/4252] Let's see if bumping node helps? --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index c18ed6b030..e084059b99 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -22,7 +22,7 @@ environment: - ATOM_CHANNEL: dev install: - - ps: Install-Product node 6 + - ps: Install-Product node 8 build_script: - ps: iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/atom/ci/master/build-package.ps1')) From 3e3a9aeea3d816383721478fc631b5edd0c2b370 Mon Sep 17 00:00:00 2001 From: Tilde Ann Thurium Date: Fri, 7 Sep 2018 14:27:03 -0700 Subject: [PATCH 0303/4252] npm install react-tabs --- package-lock.json | 3063 +++++++++++++++++++++++---------------------- package.json | 1 + 2 files changed, 1537 insertions(+), 1527 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0f5e626432..e0ba6f103b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,10 +26,10 @@ "dev": true, "requires": { "@babel/types": "7.0.0-beta.49", - "jsesc": "^2.5.1", - "lodash": "^4.17.5", - "source-map": "^0.5.0", - "trim-right": "^1.0.1" + "jsesc": "2.5.1", + "lodash": "4.17.5", + "source-map": "0.5.7", + "trim-right": "1.0.1" }, "dependencies": { "jsesc": { @@ -75,9 +75,9 @@ "integrity": "sha1-lr3GtD4TSCASumaRsQGEktOWIsw=", "dev": true, "requires": { - "chalk": "^2.0.0", - "esutils": "^2.0.2", - "js-tokens": "^3.0.0" + "chalk": "2.4.1", + "esutils": "2.0.2", + "js-tokens": "3.0.2" }, "dependencies": { "ansi-styles": { @@ -86,7 +86,7 @@ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, "requires": { - "color-convert": "^1.9.0" + "color-convert": "1.9.1" } }, "chalk": { @@ -95,9 +95,9 @@ "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", "dev": true, "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "ansi-styles": "3.2.1", + "escape-string-regexp": "1.0.5", + "supports-color": "5.4.0" } }, "supports-color": { @@ -106,7 +106,7 @@ "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", "dev": true, "requires": { - "has-flag": "^3.0.0" + "has-flag": "3.0.0" } } } @@ -126,7 +126,7 @@ "@babel/code-frame": "7.0.0-beta.49", "@babel/parser": "7.0.0-beta.49", "@babel/types": "7.0.0-beta.49", - "lodash": "^4.17.5" + "lodash": "4.17.5" } }, "@babel/traverse": { @@ -141,10 +141,10 @@ "@babel/helper-split-export-declaration": "7.0.0-beta.49", "@babel/parser": "7.0.0-beta.49", "@babel/types": "7.0.0-beta.49", - "debug": "^3.1.0", - "globals": "^11.1.0", - "invariant": "^2.2.0", - "lodash": "^4.17.5" + "debug": "3.1.0", + "globals": "11.7.0", + "invariant": "2.2.4", + "lodash": "4.17.5" }, "dependencies": { "debug": { @@ -170,9 +170,9 @@ "integrity": "sha1-t+Oxw/TUz+Eb34yJ8e/V4WF7h6Y=", "dev": true, "requires": { - "esutils": "^2.0.2", - "lodash": "^4.17.5", - "to-fast-properties": "^2.0.0" + "esutils": "2.0.2", + "lodash": "4.17.5", + "to-fast-properties": "2.0.0" } }, "@mrmlnc/readdir-enhanced": { @@ -181,8 +181,8 @@ "integrity": "sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g==", "dev": true, "requires": { - "call-me-maybe": "^1.0.1", - "glob-to-regexp": "^0.3.0" + "call-me-maybe": "1.0.1", + "glob-to-regexp": "0.3.0" } }, "@nodelib/fs.stat": { @@ -194,7 +194,7 @@ "@sinonjs/formatio": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-2.0.0.tgz", - "integrity": "sha1-hNt+nrVTHfGKjF4L+25EnlXmVLI=", + "integrity": "sha512-ls6CAMA6/5gG+O/IdsBcblvnd8qcO/l1TYoNeAzp3wcISOxlPXQEus0mLcdwazEkWjaBdaJ3TaxmNgCLWwvWzg==", "dev": true, "requires": { "samsam": "1.3.0" @@ -203,41 +203,41 @@ "@smashwilson/atom-mocha-test-runner": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@smashwilson/atom-mocha-test-runner/-/atom-mocha-test-runner-1.4.0.tgz", - "integrity": "sha512-Zp50XTy2QZEk53PUxXQ1kLTAkSwEuM2X7JXtMGLRWuU68piFghkXGaopTrjXK3CwgzmmFi26m65sTCrXg3zqbg==", + "integrity": "sha1-AjOAreJPt5xrC7TlXdTuc7LFdfo=", "dev": true, "requires": { "diff": "3.5.0", - "etch": "^0.8.0", - "grim": "^2.0.1", - "less": "^3.7.1", - "mocha": "^5.2.0", + "etch": "0.8.0", + "grim": "2.0.2", + "less": "3.8.1", + "mocha": "5.2.0", "tmp": "0.0.31" } }, "@smashwilson/enzyme-adapter-react-16": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@smashwilson/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.0.2.tgz", - "integrity": "sha1-8NRWRn/qp5AwV8vF5PqsZKWoBsg=", + "integrity": "sha512-YGotyPAPOwi9vbRzvTutDgqYbBo5gX8mELNHPICcAXilV9XMa0yxqCQ6OY4ItIO5dPiRkc9RkjSYBr2M1fFOZw==", "dev": true, "requires": { - "@smashwilson/enzyme-adapter-utils": "^1.0.0", - "lodash": "^4.17.4", - "object.assign": "^4.1.0", - "object.values": "^1.0.4", - "prop-types": "^15.6.0", - "react-reconciler": "^0.7.0", - "react-test-renderer": "^16.0.0-0" + "@smashwilson/enzyme-adapter-utils": "1.0.0", + "lodash": "4.17.5", + "object.assign": "4.1.0", + "object.values": "1.0.4", + "prop-types": "15.6.2", + "react-reconciler": "0.7.0", + "react-test-renderer": "16.4.1" } }, "@smashwilson/enzyme-adapter-utils": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@smashwilson/enzyme-adapter-utils/-/enzyme-adapter-utils-1.0.0.tgz", - "integrity": "sha1-H5zSVcvNm1Fd8WkCGgLbEMX8Ydc=", + "integrity": "sha512-/kQoTFU5bdbZbh6C9Ohxz1BgoHW+n2BX/kGUcS3DucGZfVNl4Em9lJqzd3aelyZSJz+cRX/FuE6ccnO6MnZc0Q==", "dev": true, "requires": { - "lodash": "^4.17.4", - "object.assign": "^4.1.0", - "prop-types": "^15.6.0" + "lodash": "4.17.5", + "object.assign": "4.1.0", + "prop-types": "15.6.2" } }, "@types/node": { @@ -258,7 +258,7 @@ "integrity": "sha512-JY+iV6r+cO21KtntVvFkD+iqjtdpRUpGqKWgfkCdZq1R+kbreEl8EcdcJR4SmiIgsIQT33s6QzheQ9a275Q8xw==", "dev": true, "requires": { - "acorn": "^5.0.3" + "acorn": "5.7.1" } }, "ajv": { @@ -266,10 +266,10 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", "requires": { - "co": "^4.6.0", - "fast-deep-equal": "^1.0.0", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.3.0" + "co": "4.6.0", + "fast-deep-equal": "1.1.0", + "fast-json-stable-stringify": "2.0.0", + "json-schema-traverse": "0.3.1" } }, "ajv-keywords": { @@ -283,9 +283,9 @@ "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", "requires": { - "kind-of": "^3.0.2", - "longest": "^1.0.1", - "repeat-string": "^1.5.2" + "kind-of": "3.2.2", + "longest": "1.0.1", + "repeat-string": "1.6.1" }, "dependencies": { "kind-of": { @@ -293,21 +293,21 @@ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "requires": { - "is-buffer": "^1.1.5" + "is-buffer": "1.1.6" } } } }, "amdefine": { "version": "1.0.1", - "resolved": "", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", "dev": true }, "ansi-escapes": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.1.0.tgz", - "integrity": "sha1-9zIHu4EgfXX9bIPxJa8m7qN4yjA=", + "integrity": "sha512-UgAb8H9D41AQnu/PbWlCofQVcnV4Gs2bBJi9eZPxfU/hgglFh3SMDMENRIqdr7H6XFnXdoknctFByVsCOotTVw==", "dev": true }, "ansi-regex": { @@ -322,11 +322,11 @@ }, "append-transform": { "version": "1.0.0", - "resolved": "", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-1.0.0.tgz", "integrity": "sha512-P009oYkeHyU742iSZJzZZywj4QRJdnTWffaKuJQLablCZ1uz6/cW4yaRgcDaoQ+uwOxxnt0gRUcwfsNP2ri0gw==", "dev": true, "requires": { - "default-require-extensions": "^2.0.0" + "default-require-extensions": "2.0.0" } }, "aproba": { @@ -336,7 +336,7 @@ }, "archy": { "version": "1.0.0", - "resolved": "", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=", "dev": true }, @@ -345,17 +345,17 @@ "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz", "integrity": "sha1-u13KOCu5TwXhUZQ3PRb9O6HKEQ0=", "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" + "delegates": "1.0.0", + "readable-stream": "2.3.3" } }, "argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha1-vNZ5HqWuCXJeF+WtmIE0zUCz2RE=", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "dev": true, "requires": { - "sprintf-js": "~1.0.2" + "sprintf-js": "1.0.3" } }, "aria-query": { @@ -365,7 +365,7 @@ "dev": true, "requires": { "ast-types-flow": "0.0.7", - "commander": "^2.11.0" + "commander": "2.15.1" } }, "arr-diff": { @@ -392,8 +392,8 @@ "integrity": "sha1-GEtI9i2S10UrsxsyMWXH+L0CJm0=", "dev": true, "requires": { - "define-properties": "^1.1.2", - "es-abstract": "^1.7.0" + "define-properties": "1.1.2", + "es-abstract": "1.11.0" } }, "array-union": { @@ -402,7 +402,7 @@ "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", "dev": true, "requires": { - "array-uniq": "^1.0.1" + "array-uniq": "1.0.3" } }, "array-uniq": { @@ -423,8 +423,8 @@ "integrity": "sha1-VWpcU2LAhkgyPdrrnenRS8GGTJA=", "dev": true, "requires": { - "define-properties": "^1.1.2", - "es-abstract": "^1.7.0" + "define-properties": "1.1.2", + "es-abstract": "1.11.0" } }, "array.prototype.flat": { @@ -433,9 +433,9 @@ "integrity": "sha512-rVqIs330nLJvfC7JqYvEWwqVr5QjYF1ib02i3YJtR/fICO6527Tjpc/e4Mvmxh3GIePPreRXMdaGyC99YphWEw==", "dev": true, "requires": { - "define-properties": "^1.1.2", - "es-abstract": "^1.10.0", - "function-bind": "^1.1.1" + "define-properties": "1.1.2", + "es-abstract": "1.11.0", + "function-bind": "1.1.1" } }, "arrify": { @@ -479,7 +479,7 @@ }, "async": { "version": "1.5.2", - "resolved": "", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", "dev": true }, @@ -497,9 +497,9 @@ "atom-babel6-transpiler": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/atom-babel6-transpiler/-/atom-babel6-transpiler-1.2.0.tgz", - "integrity": "sha1-OcgHq8H9WqZDqvCut8DqE3j+Y1Y=", + "integrity": "sha512-lZucrjVyRtPAPPJxvICCEBsAC1qn48wUHaIlieriWCXTXLqtLC2PvkQU7vNvU2w1eZ7tw9m0lojZ8PbpVyWTvg==", "requires": { - "babel-core": "6.x" + "babel-core": "6.26.3" } }, "aws-sign2": { @@ -526,9 +526,9 @@ "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", "requires": { - "chalk": "^1.1.3", - "esutils": "^2.0.2", - "js-tokens": "^3.0.2" + "chalk": "1.1.3", + "esutils": "2.0.2", + "js-tokens": "3.0.2" } }, "babel-core": { @@ -536,25 +536,25 @@ "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.26.3.tgz", "integrity": "sha1-suLwnjQtDwyI4vAuBneUEl51wgc=", "requires": { - "babel-code-frame": "^6.26.0", - "babel-generator": "^6.26.0", - "babel-helpers": "^6.24.1", - "babel-messages": "^6.23.0", - "babel-register": "^6.26.0", - "babel-runtime": "^6.26.0", - "babel-template": "^6.26.0", - "babel-traverse": "^6.26.0", - "babel-types": "^6.26.0", - "babylon": "^6.18.0", - "convert-source-map": "^1.5.1", - "debug": "^2.6.9", - "json5": "^0.5.1", - "lodash": "^4.17.4", - "minimatch": "^3.0.4", - "path-is-absolute": "^1.0.1", - "private": "^0.1.8", - "slash": "^1.0.0", - "source-map": "^0.5.7" + "babel-code-frame": "6.26.0", + "babel-generator": "6.26.1", + "babel-helpers": "6.24.1", + "babel-messages": "6.23.0", + "babel-register": "6.26.0", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0", + "babylon": "6.18.0", + "convert-source-map": "1.5.1", + "debug": "2.6.9", + "json5": "0.5.1", + "lodash": "4.17.5", + "minimatch": "3.0.4", + "path-is-absolute": "1.0.1", + "private": "0.1.8", + "slash": "1.0.0", + "source-map": "0.5.7" }, "dependencies": { "babel-runtime": { @@ -562,8 +562,8 @@ "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", "requires": { - "core-js": "^2.4.0", - "regenerator-runtime": "^0.11.0" + "core-js": "2.5.0", + "regenerator-runtime": "0.11.1" } }, "babel-template": { @@ -571,11 +571,11 @@ "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz", "integrity": "sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=", "requires": { - "babel-runtime": "^6.26.0", - "babel-traverse": "^6.26.0", - "babel-types": "^6.26.0", - "babylon": "^6.18.0", - "lodash": "^4.17.4" + "babel-runtime": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0", + "babylon": "6.18.0", + "lodash": "4.17.5" } }, "babel-traverse": { @@ -583,15 +583,15 @@ "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=", "requires": { - "babel-code-frame": "^6.26.0", - "babel-messages": "^6.23.0", - "babel-runtime": "^6.26.0", - "babel-types": "^6.26.0", - "babylon": "^6.18.0", - "debug": "^2.6.8", - "globals": "^9.18.0", - "invariant": "^2.2.2", - "lodash": "^4.17.4" + "babel-code-frame": "6.26.0", + "babel-messages": "6.23.0", + "babel-runtime": "6.26.0", + "babel-types": "6.26.0", + "babylon": "6.18.0", + "debug": "2.6.9", + "globals": "9.18.0", + "invariant": "2.2.4", + "lodash": "4.17.5" } }, "babel-types": { @@ -599,10 +599,10 @@ "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", "requires": { - "babel-runtime": "^6.26.0", - "esutils": "^2.0.2", - "lodash": "^4.17.4", - "to-fast-properties": "^1.0.3" + "babel-runtime": "6.26.0", + "esutils": "2.0.2", + "lodash": "4.17.5", + "to-fast-properties": "1.0.3" } }, "babylon": { @@ -628,10 +628,10 @@ "integrity": "sha1-sv4tgBJkcPXBlELcdXJTqJdxCCc=", "dev": true, "requires": { - "babel-code-frame": "^6.22.0", - "babel-traverse": "^6.23.1", - "babel-types": "^6.23.0", - "babylon": "^6.17.0" + "babel-code-frame": "6.26.0", + "babel-traverse": "6.25.0", + "babel-types": "6.25.0", + "babylon": "6.17.4" } }, "babel-generator": { @@ -639,14 +639,14 @@ "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.1.tgz", "integrity": "sha1-GERAjTuPDTWkBOp6wYDwh6YBvZA=", "requires": { - "babel-messages": "^6.23.0", - "babel-runtime": "^6.26.0", - "babel-types": "^6.26.0", - "detect-indent": "^4.0.0", - "jsesc": "^1.3.0", - "lodash": "^4.17.4", - "source-map": "^0.5.7", - "trim-right": "^1.0.1" + "babel-messages": "6.23.0", + "babel-runtime": "6.26.0", + "babel-types": "6.26.0", + "detect-indent": "4.0.0", + "jsesc": "1.3.0", + "lodash": "4.17.5", + "source-map": "0.5.7", + "trim-right": "1.0.1" }, "dependencies": { "babel-runtime": { @@ -654,8 +654,8 @@ "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", "requires": { - "core-js": "^2.4.0", - "regenerator-runtime": "^0.11.0" + "core-js": "2.5.0", + "regenerator-runtime": "0.11.1" } }, "babel-types": { @@ -663,10 +663,10 @@ "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", "requires": { - "babel-runtime": "^6.26.0", - "esutils": "^2.0.2", - "lodash": "^4.17.4", - "to-fast-properties": "^1.0.3" + "babel-runtime": "6.26.0", + "esutils": "2.0.2", + "lodash": "4.17.5", + "to-fast-properties": "1.0.3" } }, "regenerator-runtime": { @@ -686,9 +686,9 @@ "resolved": "https://registry.npmjs.org/babel-helper-builder-react-jsx/-/babel-helper-builder-react-jsx-6.26.0.tgz", "integrity": "sha1-Of+DE7dci2Xc7/HzHTg+D/KkCKA=", "requires": { - "babel-runtime": "^6.26.0", - "babel-types": "^6.26.0", - "esutils": "^2.0.2" + "babel-runtime": "6.26.0", + "babel-types": "6.26.0", + "esutils": "2.0.2" }, "dependencies": { "babel-runtime": { @@ -696,8 +696,8 @@ "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", "requires": { - "core-js": "^2.4.0", - "regenerator-runtime": "^0.11.0" + "core-js": "2.5.0", + "regenerator-runtime": "0.11.1" } }, "babel-types": { @@ -705,10 +705,10 @@ "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", "requires": { - "babel-runtime": "^6.26.0", - "esutils": "^2.0.2", - "lodash": "^4.17.4", - "to-fast-properties": "^1.0.3" + "babel-runtime": "6.26.0", + "esutils": "2.0.2", + "lodash": "4.17.5", + "to-fast-properties": "1.0.3" } }, "regenerator-runtime": { @@ -729,10 +729,10 @@ "integrity": "sha1-7Oaqzdx25Bw0YfiL/Fdb0Nqi340=", "dev": true, "requires": { - "babel-helper-hoist-variables": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-traverse": "^6.24.1", - "babel-types": "^6.24.1" + "babel-helper-hoist-variables": "6.24.1", + "babel-runtime": "6.25.0", + "babel-traverse": "6.25.0", + "babel-types": "6.25.0" } }, "babel-helper-define-map": { @@ -741,10 +741,10 @@ "integrity": "sha1-pfVtq0GiX5fstJjH66ypgZ+Vvl8=", "dev": true, "requires": { - "babel-helper-function-name": "^6.24.1", - "babel-runtime": "^6.26.0", - "babel-types": "^6.26.0", - "lodash": "^4.17.4" + "babel-helper-function-name": "6.24.1", + "babel-runtime": "6.26.0", + "babel-types": "6.26.0", + "lodash": "4.17.5" }, "dependencies": { "babel-runtime": { @@ -753,8 +753,8 @@ "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", "dev": true, "requires": { - "core-js": "^2.4.0", - "regenerator-runtime": "^0.11.0" + "core-js": "2.5.0", + "regenerator-runtime": "0.11.1" } }, "babel-types": { @@ -763,10 +763,10 @@ "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", "dev": true, "requires": { - "babel-runtime": "^6.26.0", - "esutils": "^2.0.2", - "lodash": "^4.17.4", - "to-fast-properties": "^1.0.3" + "babel-runtime": "6.26.0", + "esutils": "2.0.2", + "lodash": "4.17.5", + "to-fast-properties": "1.0.3" } }, "regenerator-runtime": { @@ -788,11 +788,11 @@ "resolved": "https://registry.npmjs.org/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz", "integrity": "sha1-00dbjAPtmCQqJbSDUasYOZ01gKk=", "requires": { - "babel-helper-get-function-arity": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1", - "babel-traverse": "^6.24.1", - "babel-types": "^6.24.1" + "babel-helper-get-function-arity": "6.24.1", + "babel-runtime": "6.25.0", + "babel-template": "6.25.0", + "babel-traverse": "6.25.0", + "babel-types": "6.25.0" } }, "babel-helper-get-function-arity": { @@ -800,8 +800,8 @@ "resolved": "https://registry.npmjs.org/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz", "integrity": "sha1-j3eCqpNAfEHTqlCQj4mwMbG2hT0=", "requires": { - "babel-runtime": "^6.22.0", - "babel-types": "^6.24.1" + "babel-runtime": "6.25.0", + "babel-types": "6.25.0" } }, "babel-helper-hoist-variables": { @@ -810,8 +810,8 @@ "integrity": "sha1-HssnaJydJVE+rbyZFKc/VAi+enY=", "dev": true, "requires": { - "babel-runtime": "^6.22.0", - "babel-types": "^6.24.1" + "babel-runtime": "6.25.0", + "babel-types": "6.25.0" } }, "babel-helper-optimise-call-expression": { @@ -820,8 +820,8 @@ "integrity": "sha1-96E0J7qfc/j0+pk8VKl4gtEkQlc=", "dev": true, "requires": { - "babel-runtime": "^6.22.0", - "babel-types": "^6.24.1" + "babel-runtime": "6.25.0", + "babel-types": "6.25.0" } }, "babel-helper-replace-supers": { @@ -830,12 +830,12 @@ "integrity": "sha1-v22/5Dk40XNpohPKiov3S2qQqxo=", "dev": true, "requires": { - "babel-helper-optimise-call-expression": "^6.24.1", - "babel-messages": "^6.23.0", - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1", - "babel-traverse": "^6.24.1", - "babel-types": "^6.24.1" + "babel-helper-optimise-call-expression": "6.24.1", + "babel-messages": "6.23.0", + "babel-runtime": "6.25.0", + "babel-template": "6.25.0", + "babel-traverse": "6.25.0", + "babel-types": "6.25.0" } }, "babel-helpers": { @@ -843,8 +843,8 @@ "resolved": "https://registry.npmjs.org/babel-helpers/-/babel-helpers-6.24.1.tgz", "integrity": "sha1-NHHenK7DiOXIUOWX5Yom3fN2ArI=", "requires": { - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1" + "babel-runtime": "6.25.0", + "babel-template": "6.25.0" } }, "babel-messages": { @@ -852,7 +852,7 @@ "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=", "requires": { - "babel-runtime": "^6.22.0" + "babel-runtime": "6.25.0" } }, "babel-plugin-chai-assert-async": { @@ -866,7 +866,7 @@ "integrity": "sha1-NRV7EBQm/S/9PaP3XH0ekYNbv4o=", "dev": true, "requires": { - "babel-runtime": "^6.22.0" + "babel-runtime": "6.25.0" } }, "babel-plugin-istanbul": { @@ -875,10 +875,10 @@ "integrity": "sha1-NsWbIZLvzoHFs3gyG3QXWt0cmkU=", "dev": true, "requires": { - "babel-plugin-syntax-object-rest-spread": "^6.13.0", - "find-up": "^2.1.0", - "istanbul-lib-instrument": "^1.10.1", - "test-exclude": "^4.2.1" + "babel-plugin-syntax-object-rest-spread": "6.13.0", + "find-up": "2.1.0", + "istanbul-lib-instrument": "1.10.1", + "test-exclude": "4.2.1" }, "dependencies": { "babylon": { @@ -899,13 +899,13 @@ "integrity": "sha512-1dYuzkOCbuR5GRJqySuZdsmsNKPL3PTuyPevQfoCXJePT9C8y1ga75neU+Tuy9+yS3G/dgx8wgOmp2KLpgdoeQ==", "dev": true, "requires": { - "babel-generator": "^6.18.0", - "babel-template": "^6.16.0", - "babel-traverse": "^6.18.0", - "babel-types": "^6.18.0", - "babylon": "^6.18.0", - "istanbul-lib-coverage": "^1.2.0", - "semver": "^5.3.0" + "babel-generator": "6.26.1", + "babel-template": "6.25.0", + "babel-traverse": "6.25.0", + "babel-types": "6.25.0", + "babylon": "6.18.0", + "istanbul-lib-coverage": "1.2.0", + "semver": "5.5.1" } } } @@ -915,8 +915,8 @@ "resolved": "https://registry.npmjs.org/babel-plugin-relay/-/babel-plugin-relay-1.6.0.tgz", "integrity": "sha1-oiTaUkNi1pA6UkIUobhAUw/fvSg=", "requires": { - "babel-runtime": "^6.23.0", - "babel-types": "^6.24.1" + "babel-runtime": "6.25.0", + "babel-types": "6.25.0" } }, "babel-plugin-syntax-class-properties": { @@ -950,10 +950,10 @@ "resolved": "https://registry.npmjs.org/babel-plugin-transform-class-properties/-/babel-plugin-transform-class-properties-6.24.1.tgz", "integrity": "sha1-anl2PqYdM9NvN7YRqp3vgagbRqw=", "requires": { - "babel-helper-function-name": "^6.24.1", - "babel-plugin-syntax-class-properties": "^6.8.0", - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1" + "babel-helper-function-name": "6.24.1", + "babel-plugin-syntax-class-properties": "6.13.0", + "babel-runtime": "6.25.0", + "babel-template": "6.25.0" } }, "babel-plugin-transform-es2015-arrow-functions": { @@ -962,7 +962,7 @@ "integrity": "sha1-RSaSy3EdX3ncf4XkQM5BufJE0iE=", "dev": true, "requires": { - "babel-runtime": "^6.22.0" + "babel-runtime": "6.25.0" } }, "babel-plugin-transform-es2015-block-scoped-functions": { @@ -971,7 +971,7 @@ "integrity": "sha1-u8UbSflk1wy42OC5ToICRs46YUE=", "dev": true, "requires": { - "babel-runtime": "^6.22.0" + "babel-runtime": "6.25.0" } }, "babel-plugin-transform-es2015-block-scoping": { @@ -980,11 +980,11 @@ "integrity": "sha1-1w9SmcEwjQXBL0Y4E7CgnnOxiV8=", "dev": true, "requires": { - "babel-runtime": "^6.26.0", - "babel-template": "^6.26.0", - "babel-traverse": "^6.26.0", - "babel-types": "^6.26.0", - "lodash": "^4.17.4" + "babel-runtime": "6.26.0", + "babel-template": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0", + "lodash": "4.17.5" }, "dependencies": { "babel-runtime": { @@ -993,8 +993,8 @@ "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", "dev": true, "requires": { - "core-js": "^2.4.0", - "regenerator-runtime": "^0.11.0" + "core-js": "2.5.0", + "regenerator-runtime": "0.11.1" } }, "babel-template": { @@ -1003,11 +1003,11 @@ "integrity": "sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=", "dev": true, "requires": { - "babel-runtime": "^6.26.0", - "babel-traverse": "^6.26.0", - "babel-types": "^6.26.0", - "babylon": "^6.18.0", - "lodash": "^4.17.4" + "babel-runtime": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0", + "babylon": "6.18.0", + "lodash": "4.17.5" } }, "babel-traverse": { @@ -1016,15 +1016,15 @@ "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=", "dev": true, "requires": { - "babel-code-frame": "^6.26.0", - "babel-messages": "^6.23.0", - "babel-runtime": "^6.26.0", - "babel-types": "^6.26.0", - "babylon": "^6.18.0", - "debug": "^2.6.8", - "globals": "^9.18.0", - "invariant": "^2.2.2", - "lodash": "^4.17.4" + "babel-code-frame": "6.26.0", + "babel-messages": "6.23.0", + "babel-runtime": "6.26.0", + "babel-types": "6.26.0", + "babylon": "6.18.0", + "debug": "2.6.9", + "globals": "9.18.0", + "invariant": "2.2.4", + "lodash": "4.17.5" } }, "babel-types": { @@ -1033,10 +1033,10 @@ "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", "dev": true, "requires": { - "babel-runtime": "^6.26.0", - "esutils": "^2.0.2", - "lodash": "^4.17.4", - "to-fast-properties": "^1.0.3" + "babel-runtime": "6.26.0", + "esutils": "2.0.2", + "lodash": "4.17.5", + "to-fast-properties": "1.0.3" } }, "babylon": { @@ -1065,15 +1065,15 @@ "integrity": "sha1-WkxYpQyclGHlZLSyo7+ryXolhNs=", "dev": true, "requires": { - "babel-helper-define-map": "^6.24.1", - "babel-helper-function-name": "^6.24.1", - "babel-helper-optimise-call-expression": "^6.24.1", - "babel-helper-replace-supers": "^6.24.1", - "babel-messages": "^6.23.0", - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1", - "babel-traverse": "^6.24.1", - "babel-types": "^6.24.1" + "babel-helper-define-map": "6.26.0", + "babel-helper-function-name": "6.24.1", + "babel-helper-optimise-call-expression": "6.24.1", + "babel-helper-replace-supers": "6.24.1", + "babel-messages": "6.23.0", + "babel-runtime": "6.25.0", + "babel-template": "6.25.0", + "babel-traverse": "6.25.0", + "babel-types": "6.25.0" } }, "babel-plugin-transform-es2015-computed-properties": { @@ -1082,8 +1082,8 @@ "integrity": "sha1-b+Ko0WiV1WNPTNmZttNICjCBWbM=", "dev": true, "requires": { - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1" + "babel-runtime": "6.25.0", + "babel-template": "6.25.0" } }, "babel-plugin-transform-es2015-destructuring": { @@ -1092,7 +1092,7 @@ "integrity": "sha1-mXux8auWf2gtKwh2/jWNYOdlxW0=", "dev": true, "requires": { - "babel-runtime": "^6.22.0" + "babel-runtime": "6.25.0" } }, "babel-plugin-transform-es2015-for-of": { @@ -1101,7 +1101,7 @@ "integrity": "sha1-9HyVsrYT3x0+zC/bdXNiPHUkhpE=", "dev": true, "requires": { - "babel-runtime": "^6.22.0" + "babel-runtime": "6.25.0" } }, "babel-plugin-transform-es2015-function-name": { @@ -1110,9 +1110,9 @@ "integrity": "sha1-g0yJhTvDaxrw86TF26qU/Y6sqos=", "dev": true, "requires": { - "babel-helper-function-name": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-types": "^6.24.1" + "babel-helper-function-name": "6.24.1", + "babel-runtime": "6.25.0", + "babel-types": "6.25.0" } }, "babel-plugin-transform-es2015-literals": { @@ -1121,7 +1121,7 @@ "integrity": "sha1-T1SgLWzWbPkVKAAZox0xklN3yi4=", "dev": true, "requires": { - "babel-runtime": "^6.22.0" + "babel-runtime": "6.25.0" } }, "babel-plugin-transform-es2015-modules-commonjs": { @@ -1129,10 +1129,10 @@ "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.2.tgz", "integrity": "sha1-WKeThjqefKhwvcWogRF/+sJ9tvM=", "requires": { - "babel-plugin-transform-strict-mode": "^6.24.1", - "babel-runtime": "^6.26.0", - "babel-template": "^6.26.0", - "babel-types": "^6.26.0" + "babel-plugin-transform-strict-mode": "6.24.1", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0", + "babel-types": "6.26.0" }, "dependencies": { "babel-runtime": { @@ -1140,8 +1140,8 @@ "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", "requires": { - "core-js": "^2.4.0", - "regenerator-runtime": "^0.11.0" + "core-js": "2.5.0", + "regenerator-runtime": "0.11.1" } }, "babel-template": { @@ -1149,11 +1149,11 @@ "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz", "integrity": "sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=", "requires": { - "babel-runtime": "^6.26.0", - "babel-traverse": "^6.26.0", - "babel-types": "^6.26.0", - "babylon": "^6.18.0", - "lodash": "^4.17.4" + "babel-runtime": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0", + "babylon": "6.18.0", + "lodash": "4.17.5" } }, "babel-traverse": { @@ -1161,15 +1161,15 @@ "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=", "requires": { - "babel-code-frame": "^6.26.0", - "babel-messages": "^6.23.0", - "babel-runtime": "^6.26.0", - "babel-types": "^6.26.0", - "babylon": "^6.18.0", - "debug": "^2.6.8", - "globals": "^9.18.0", - "invariant": "^2.2.2", - "lodash": "^4.17.4" + "babel-code-frame": "6.26.0", + "babel-messages": "6.23.0", + "babel-runtime": "6.26.0", + "babel-types": "6.26.0", + "babylon": "6.18.0", + "debug": "2.6.9", + "globals": "9.18.0", + "invariant": "2.2.4", + "lodash": "4.17.5" } }, "babel-types": { @@ -1177,10 +1177,10 @@ "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", "requires": { - "babel-runtime": "^6.26.0", - "esutils": "^2.0.2", - "lodash": "^4.17.4", - "to-fast-properties": "^1.0.3" + "babel-runtime": "6.26.0", + "esutils": "2.0.2", + "lodash": "4.17.5", + "to-fast-properties": "1.0.3" } }, "babylon": { @@ -1206,8 +1206,8 @@ "integrity": "sha1-JM72muIcuDp/hgPa0CH1cusnj40=", "dev": true, "requires": { - "babel-helper-replace-supers": "^6.24.1", - "babel-runtime": "^6.22.0" + "babel-helper-replace-supers": "6.24.1", + "babel-runtime": "6.25.0" } }, "babel-plugin-transform-es2015-parameters": { @@ -1216,12 +1216,12 @@ "integrity": "sha1-V6w1GrScrxSpfNE7CfZv3wpiXys=", "dev": true, "requires": { - "babel-helper-call-delegate": "^6.24.1", - "babel-helper-get-function-arity": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1", - "babel-traverse": "^6.24.1", - "babel-types": "^6.24.1" + "babel-helper-call-delegate": "6.24.1", + "babel-helper-get-function-arity": "6.24.1", + "babel-runtime": "6.25.0", + "babel-template": "6.25.0", + "babel-traverse": "6.25.0", + "babel-types": "6.25.0" } }, "babel-plugin-transform-es2015-shorthand-properties": { @@ -1230,8 +1230,8 @@ "integrity": "sha1-JPh11nIch2YbvZmkYi5R8U3jiqA=", "dev": true, "requires": { - "babel-runtime": "^6.22.0", - "babel-types": "^6.24.1" + "babel-runtime": "6.25.0", + "babel-types": "6.25.0" } }, "babel-plugin-transform-es2015-spread": { @@ -1240,7 +1240,7 @@ "integrity": "sha1-1taKmfia7cRTbIGlQujdnxdG+NE=", "dev": true, "requires": { - "babel-runtime": "^6.22.0" + "babel-runtime": "6.25.0" } }, "babel-plugin-transform-es2015-template-literals": { @@ -1249,7 +1249,7 @@ "integrity": "sha1-qEs0UPfp+PH2g51taH2oS7EjbY0=", "dev": true, "requires": { - "babel-runtime": "^6.22.0" + "babel-runtime": "6.25.0" } }, "babel-plugin-transform-es3-member-expression-literals": { @@ -1258,7 +1258,7 @@ "integrity": "sha1-cz00RPPsxBvvjtGmpOCWV7iWnrs=", "dev": true, "requires": { - "babel-runtime": "^6.22.0" + "babel-runtime": "6.25.0" } }, "babel-plugin-transform-es3-property-literals": { @@ -1267,7 +1267,7 @@ "integrity": "sha1-sgeNWELiKr9A9z6M3pzTcRq9V1g=", "dev": true, "requires": { - "babel-runtime": "^6.22.0" + "babel-runtime": "6.25.0" } }, "babel-plugin-transform-flow-strip-types": { @@ -1275,8 +1275,8 @@ "resolved": "https://registry.npmjs.org/babel-plugin-transform-flow-strip-types/-/babel-plugin-transform-flow-strip-types-6.22.0.tgz", "integrity": "sha1-hMtnKTXUNxT9wyvOhFaNh0Qc988=", "requires": { - "babel-plugin-syntax-flow": "^6.18.0", - "babel-runtime": "^6.22.0" + "babel-plugin-syntax-flow": "6.18.0", + "babel-runtime": "6.25.0" } }, "babel-plugin-transform-object-rest-spread": { @@ -1284,8 +1284,8 @@ "resolved": "https://registry.npmjs.org/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.26.0.tgz", "integrity": "sha1-DzZpLVD+9rfi1LOsFHgTepY7ewY=", "requires": { - "babel-plugin-syntax-object-rest-spread": "^6.8.0", - "babel-runtime": "^6.26.0" + "babel-plugin-syntax-object-rest-spread": "6.13.0", + "babel-runtime": "6.26.0" }, "dependencies": { "babel-runtime": { @@ -1293,8 +1293,8 @@ "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", "requires": { - "core-js": "^2.4.0", - "regenerator-runtime": "^0.11.0" + "core-js": "2.5.0", + "regenerator-runtime": "0.11.1" } }, "regenerator-runtime": { @@ -1309,7 +1309,7 @@ "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-display-name/-/babel-plugin-transform-react-display-name-6.25.0.tgz", "integrity": "sha1-Z+K/Hx6ck6sI25Z5LgU5K/LMKNE=", "requires": { - "babel-runtime": "^6.22.0" + "babel-runtime": "6.25.0" } }, "babel-plugin-transform-react-jsx": { @@ -1317,9 +1317,9 @@ "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-jsx/-/babel-plugin-transform-react-jsx-6.24.1.tgz", "integrity": "sha1-hAoCjn30YN/DotKfDA2R9jduZqM=", "requires": { - "babel-helper-builder-react-jsx": "^6.24.1", - "babel-plugin-syntax-jsx": "^6.8.0", - "babel-runtime": "^6.22.0" + "babel-helper-builder-react-jsx": "6.26.0", + "babel-plugin-syntax-jsx": "6.18.0", + "babel-runtime": "6.25.0" } }, "babel-plugin-transform-react-jsx-self": { @@ -1327,8 +1327,8 @@ "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-jsx-self/-/babel-plugin-transform-react-jsx-self-6.22.0.tgz", "integrity": "sha1-322AqdomEqEh5t3XVYvL7PBuY24=", "requires": { - "babel-plugin-syntax-jsx": "^6.8.0", - "babel-runtime": "^6.22.0" + "babel-plugin-syntax-jsx": "6.18.0", + "babel-runtime": "6.25.0" } }, "babel-plugin-transform-react-jsx-source": { @@ -1336,8 +1336,8 @@ "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-jsx-source/-/babel-plugin-transform-react-jsx-source-6.22.0.tgz", "integrity": "sha1-ZqwSFT9c0tF7PBkmj0vwGX9E7NY=", "requires": { - "babel-plugin-syntax-jsx": "^6.8.0", - "babel-runtime": "^6.22.0" + "babel-plugin-syntax-jsx": "6.18.0", + "babel-runtime": "6.25.0" } }, "babel-plugin-transform-strict-mode": { @@ -1345,8 +1345,8 @@ "resolved": "https://registry.npmjs.org/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz", "integrity": "sha1-1fr3qleKZbvlkc9e2uBKDGcCB1g=", "requires": { - "babel-runtime": "^6.22.0", - "babel-types": "^6.24.1" + "babel-runtime": "6.25.0", + "babel-types": "6.25.0" } }, "babel-polyfill": { @@ -1355,9 +1355,9 @@ "integrity": "sha1-N5k3q8Z9eJWXCtxiHyhM2WbPIVM=", "dev": true, "requires": { - "babel-runtime": "^6.26.0", - "core-js": "^2.5.0", - "regenerator-runtime": "^0.10.5" + "babel-runtime": "6.26.0", + "core-js": "2.5.0", + "regenerator-runtime": "0.10.5" }, "dependencies": { "babel-runtime": { @@ -1366,8 +1366,8 @@ "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", "dev": true, "requires": { - "core-js": "^2.4.0", - "regenerator-runtime": "^0.11.0" + "core-js": "2.5.0", + "regenerator-runtime": "0.11.1" }, "dependencies": { "regenerator-runtime": { @@ -1386,34 +1386,34 @@ "integrity": "sha512-jj0KFJDioYZMtPtZf77dQuU+Ad/1BtN0UnAYlHDa8J8f4tGXr3YrPoJImD5MdueaOPeN/jUdrCgu330EfXr0XQ==", "dev": true, "requires": { - "babel-plugin-check-es2015-constants": "^6.8.0", - "babel-plugin-syntax-class-properties": "^6.8.0", - "babel-plugin-syntax-flow": "^6.8.0", - "babel-plugin-syntax-jsx": "^6.8.0", - "babel-plugin-syntax-object-rest-spread": "^6.8.0", - "babel-plugin-syntax-trailing-function-commas": "^6.8.0", - "babel-plugin-transform-class-properties": "^6.8.0", - "babel-plugin-transform-es2015-arrow-functions": "^6.8.0", - "babel-plugin-transform-es2015-block-scoped-functions": "^6.8.0", - "babel-plugin-transform-es2015-block-scoping": "^6.8.0", - "babel-plugin-transform-es2015-classes": "^6.8.0", - "babel-plugin-transform-es2015-computed-properties": "^6.8.0", - "babel-plugin-transform-es2015-destructuring": "^6.8.0", - "babel-plugin-transform-es2015-for-of": "^6.8.0", - "babel-plugin-transform-es2015-function-name": "^6.8.0", - "babel-plugin-transform-es2015-literals": "^6.8.0", - "babel-plugin-transform-es2015-modules-commonjs": "^6.8.0", - "babel-plugin-transform-es2015-object-super": "^6.8.0", - "babel-plugin-transform-es2015-parameters": "^6.8.0", - "babel-plugin-transform-es2015-shorthand-properties": "^6.8.0", - "babel-plugin-transform-es2015-spread": "^6.8.0", - "babel-plugin-transform-es2015-template-literals": "^6.8.0", - "babel-plugin-transform-es3-member-expression-literals": "^6.8.0", - "babel-plugin-transform-es3-property-literals": "^6.8.0", - "babel-plugin-transform-flow-strip-types": "^6.8.0", - "babel-plugin-transform-object-rest-spread": "^6.8.0", - "babel-plugin-transform-react-display-name": "^6.8.0", - "babel-plugin-transform-react-jsx": "^6.8.0" + "babel-plugin-check-es2015-constants": "6.22.0", + "babel-plugin-syntax-class-properties": "6.13.0", + "babel-plugin-syntax-flow": "6.18.0", + "babel-plugin-syntax-jsx": "6.18.0", + "babel-plugin-syntax-object-rest-spread": "6.13.0", + "babel-plugin-syntax-trailing-function-commas": "6.22.0", + "babel-plugin-transform-class-properties": "6.24.1", + "babel-plugin-transform-es2015-arrow-functions": "6.22.0", + "babel-plugin-transform-es2015-block-scoped-functions": "6.22.0", + "babel-plugin-transform-es2015-block-scoping": "6.26.0", + "babel-plugin-transform-es2015-classes": "6.24.1", + "babel-plugin-transform-es2015-computed-properties": "6.24.1", + "babel-plugin-transform-es2015-destructuring": "6.23.0", + "babel-plugin-transform-es2015-for-of": "6.23.0", + "babel-plugin-transform-es2015-function-name": "6.24.1", + "babel-plugin-transform-es2015-literals": "6.22.0", + "babel-plugin-transform-es2015-modules-commonjs": "6.26.2", + "babel-plugin-transform-es2015-object-super": "6.24.1", + "babel-plugin-transform-es2015-parameters": "6.24.1", + "babel-plugin-transform-es2015-shorthand-properties": "6.24.1", + "babel-plugin-transform-es2015-spread": "6.22.0", + "babel-plugin-transform-es2015-template-literals": "6.22.0", + "babel-plugin-transform-es3-member-expression-literals": "6.22.0", + "babel-plugin-transform-es3-property-literals": "6.22.0", + "babel-plugin-transform-flow-strip-types": "6.22.0", + "babel-plugin-transform-object-rest-spread": "6.26.0", + "babel-plugin-transform-react-display-name": "6.25.0", + "babel-plugin-transform-react-jsx": "6.24.1" } }, "babel-preset-flow": { @@ -1421,7 +1421,7 @@ "resolved": "https://registry.npmjs.org/babel-preset-flow/-/babel-preset-flow-6.23.0.tgz", "integrity": "sha1-5xIYiHCFrpoktb5Baa/7WZgWxJ0=", "requires": { - "babel-plugin-transform-flow-strip-types": "^6.22.0" + "babel-plugin-transform-flow-strip-types": "6.22.0" } }, "babel-preset-react": { @@ -1429,12 +1429,12 @@ "resolved": "https://registry.npmjs.org/babel-preset-react/-/babel-preset-react-6.24.1.tgz", "integrity": "sha1-umnfrqRfw+xjm2pOzqbhdwLJE4A=", "requires": { - "babel-plugin-syntax-jsx": "^6.3.13", - "babel-plugin-transform-react-display-name": "^6.23.0", - "babel-plugin-transform-react-jsx": "^6.24.1", - "babel-plugin-transform-react-jsx-self": "^6.22.0", - "babel-plugin-transform-react-jsx-source": "^6.22.0", - "babel-preset-flow": "^6.23.0" + "babel-plugin-syntax-jsx": "6.18.0", + "babel-plugin-transform-react-display-name": "6.25.0", + "babel-plugin-transform-react-jsx": "6.24.1", + "babel-plugin-transform-react-jsx-self": "6.22.0", + "babel-plugin-transform-react-jsx-source": "6.22.0", + "babel-preset-flow": "6.23.0" } }, "babel-register": { @@ -1442,13 +1442,13 @@ "resolved": "https://registry.npmjs.org/babel-register/-/babel-register-6.26.0.tgz", "integrity": "sha1-btAhFz4vy0htestFxgCahW9kcHE=", "requires": { - "babel-core": "^6.26.0", - "babel-runtime": "^6.26.0", - "core-js": "^2.5.0", - "home-or-tmp": "^2.0.0", - "lodash": "^4.17.4", - "mkdirp": "^0.5.1", - "source-map-support": "^0.4.15" + "babel-core": "6.26.3", + "babel-runtime": "6.26.0", + "core-js": "2.5.0", + "home-or-tmp": "2.0.0", + "lodash": "4.17.5", + "mkdirp": "0.5.1", + "source-map-support": "0.4.18" }, "dependencies": { "babel-runtime": { @@ -1456,8 +1456,8 @@ "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", "requires": { - "core-js": "^2.4.0", - "regenerator-runtime": "^0.11.0" + "core-js": "2.5.0", + "regenerator-runtime": "0.11.1" } }, "regenerator-runtime": { @@ -1472,8 +1472,8 @@ "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.25.0.tgz", "integrity": "sha1-M7mOql1IK7AajRqmtDetKwGuxBw=", "requires": { - "core-js": "^2.4.0", - "regenerator-runtime": "^0.10.0" + "core-js": "2.5.0", + "regenerator-runtime": "0.10.5" } }, "babel-template": { @@ -1481,11 +1481,11 @@ "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.25.0.tgz", "integrity": "sha1-ZlJBFmt8KqTGGdceGSlpVSsQwHE=", "requires": { - "babel-runtime": "^6.22.0", - "babel-traverse": "^6.25.0", - "babel-types": "^6.25.0", - "babylon": "^6.17.2", - "lodash": "^4.2.0" + "babel-runtime": "6.25.0", + "babel-traverse": "6.25.0", + "babel-types": "6.25.0", + "babylon": "6.17.4", + "lodash": "4.17.5" } }, "babel-traverse": { @@ -1493,15 +1493,15 @@ "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.25.0.tgz", "integrity": "sha1-IldJfi/NGbie3BPEyROB+VEklvE=", "requires": { - "babel-code-frame": "^6.22.0", - "babel-messages": "^6.23.0", - "babel-runtime": "^6.22.0", - "babel-types": "^6.25.0", - "babylon": "^6.17.2", - "debug": "^2.2.0", - "globals": "^9.0.0", - "invariant": "^2.2.0", - "lodash": "^4.2.0" + "babel-code-frame": "6.26.0", + "babel-messages": "6.23.0", + "babel-runtime": "6.25.0", + "babel-types": "6.25.0", + "babylon": "6.17.4", + "debug": "2.6.9", + "globals": "9.18.0", + "invariant": "2.2.4", + "lodash": "4.17.5" } }, "babel-types": { @@ -1509,10 +1509,10 @@ "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.25.0.tgz", "integrity": "sha1-cK+ySNVmDl0Y+BHZHIMDtUE0oY4=", "requires": { - "babel-runtime": "^6.22.0", - "esutils": "^2.0.2", - "lodash": "^4.2.0", - "to-fast-properties": "^1.0.1" + "babel-runtime": "6.25.0", + "esutils": "2.0.2", + "lodash": "4.17.5", + "to-fast-properties": "1.0.3" }, "dependencies": { "to-fast-properties": { @@ -1538,13 +1538,13 @@ "integrity": "sha1-e95c7RRbbVUakNuH+DxVi060io8=", "dev": true, "requires": { - "cache-base": "^1.0.1", - "class-utils": "^0.3.5", - "component-emitter": "^1.2.1", - "define-property": "^1.0.0", - "isobject": "^3.0.1", - "mixin-deep": "^1.2.0", - "pascalcase": "^0.1.1" + "cache-base": "1.0.1", + "class-utils": "0.3.6", + "component-emitter": "1.2.1", + "define-property": "1.0.0", + "isobject": "3.0.1", + "mixin-deep": "1.3.1", + "pascalcase": "0.1.1" }, "dependencies": { "define-property": { @@ -1553,36 +1553,36 @@ "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", "dev": true, "requires": { - "is-descriptor": "^1.0.0" + "is-descriptor": "1.0.2" } }, "is-accessor-descriptor": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha1-FpwvbT3x+ZJhgHI2XJsOofaHhlY=", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", "dev": true, "requires": { - "kind-of": "^6.0.0" + "kind-of": "6.0.2" } }, "is-data-descriptor": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha1-2Eh2Mh0Oet0DmQQGq7u9NrqSaMc=", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", "dev": true, "requires": { - "kind-of": "^6.0.0" + "kind-of": "6.0.2" } }, "is-descriptor": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha1-OxWXRqZmBLBPjIFSS6NlxfFNhuw=", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", "dev": true, "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" + "is-accessor-descriptor": "1.0.0", + "is-data-descriptor": "1.0.0", + "kind-of": "6.0.2" } } } @@ -1593,7 +1593,7 @@ "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", "optional": true, "requires": { - "tweetnacl": "^0.14.3" + "tweetnacl": "0.14.5" } }, "bl": { @@ -1601,8 +1601,8 @@ "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz", "integrity": "sha1-oWCRFxcQPAdBDO9j71Gzl8Alr5w=", "requires": { - "readable-stream": "^2.3.5", - "safe-buffer": "^5.1.1" + "readable-stream": "2.3.6", + "safe-buffer": "5.1.1" }, "dependencies": { "isarray": { @@ -1615,13 +1615,13 @@ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "2.0.0", + "safe-buffer": "5.1.1", + "string_decoder": "1.1.1", + "util-deprecate": "1.0.2" } }, "string_decoder": { @@ -1629,7 +1629,7 @@ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "requires": { - "safe-buffer": "~5.1.0" + "safe-buffer": "5.1.1" } } } @@ -1646,7 +1646,7 @@ "integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=", "dev": true, "requires": { - "hoek": "4.x.x" + "hoek": "4.2.1" } }, "brace-expansion": { @@ -1654,26 +1654,26 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", "requires": { - "balanced-match": "^1.0.0", + "balanced-match": "1.0.0", "concat-map": "0.0.1" } }, "braces": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha1-WXn9PxTNUxVl5fot8av/8d+u5yk=", - "dev": true, - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "1.1.0", + "array-unique": "0.3.2", + "extend-shallow": "2.0.1", + "fill-range": "4.0.0", + "isobject": "3.0.1", + "repeat-element": "1.1.2", + "snapdragon": "0.8.2", + "snapdragon-node": "2.1.1", + "split-string": "3.1.0", + "to-regex": "3.0.2" }, "dependencies": { "extend-shallow": { @@ -1682,7 +1682,7 @@ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "dev": true, "requires": { - "is-extendable": "^0.1.0" + "is-extendable": "0.1.1" } } } @@ -1696,7 +1696,7 @@ "browser-stdout": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", - "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "integrity": "sha1-uqVZ7hTO1zRSIputcyZGfGH6vWA=", "dev": true }, "bser": { @@ -1705,7 +1705,7 @@ "integrity": "sha1-mseNPtXZFYBP2HrLFYvHlxR6Fxk=", "dev": true, "requires": { - "node-int64": "^0.4.0" + "node-int64": "0.4.0" } }, "buffer-alloc": { @@ -1713,8 +1713,8 @@ "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.1.0.tgz", "integrity": "sha1-BVFNM78WVtNUDGhPZbEgLpDsowM=", "requires": { - "buffer-alloc-unsafe": "^0.1.0", - "buffer-fill": "^0.1.0" + "buffer-alloc-unsafe": "0.1.1", + "buffer-fill": "0.1.1" } }, "buffer-alloc-unsafe": { @@ -1744,26 +1744,26 @@ "integrity": "sha1-Cn9GQWgxyLZi7jb+TnxZ129marI=", "dev": true, "requires": { - "collection-visit": "^1.0.0", - "component-emitter": "^1.2.1", - "get-value": "^2.0.6", - "has-value": "^1.0.0", - "isobject": "^3.0.1", - "set-value": "^2.0.0", - "to-object-path": "^0.3.0", - "union-value": "^1.0.0", - "unset-value": "^1.0.0" + "collection-visit": "1.0.0", + "component-emitter": "1.2.1", + "get-value": "2.0.6", + "has-value": "1.0.0", + "isobject": "3.0.1", + "set-value": "2.0.0", + "to-object-path": "0.3.0", + "union-value": "1.0.0", + "unset-value": "1.0.0" } }, "caching-transform": { "version": "1.0.1", - "resolved": "", + "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-1.0.1.tgz", "integrity": "sha1-bb2y8g+Nj7znnz6U6dF0Lc31wKE=", "dev": true, "requires": { - "md5-hex": "^1.2.0", - "mkdirp": "^0.5.1", - "write-file-atomic": "^1.1.4" + "md5-hex": "1.3.0", + "mkdirp": "0.5.1", + "write-file-atomic": "1.3.4" } }, "call-me-maybe": { @@ -1778,7 +1778,7 @@ "integrity": "sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8=", "dev": true, "requires": { - "callsites": "^0.2.0" + "callsites": "0.2.0" } }, "callsites": { @@ -1809,8 +1809,8 @@ "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", "requires": { - "align-text": "^0.1.3", - "lazy-cache": "^1.0.3" + "align-text": "0.1.4", + "lazy-cache": "1.0.4" } }, "chai": { @@ -1819,12 +1819,12 @@ "integrity": "sha1-D2RYS6ZC8PKs4oBiefTwbKI61zw=", "dev": true, "requires": { - "assertion-error": "^1.0.1", - "check-error": "^1.0.1", - "deep-eql": "^3.0.0", - "get-func-name": "^2.0.0", - "pathval": "^1.0.0", - "type-detect": "^4.0.0" + "assertion-error": "1.1.0", + "check-error": "1.0.2", + "deep-eql": "3.0.1", + "get-func-name": "2.0.0", + "pathval": "1.1.0", + "type-detect": "4.0.8" } }, "chai-as-promised": { @@ -1833,7 +1833,7 @@ "integrity": "sha1-CGRdgl3rhpbuYXJdv1kMAS6wDKA=", "dev": true, "requires": { - "check-error": "^1.0.2" + "check-error": "1.0.2" } }, "chalk": { @@ -1841,11 +1841,11 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" } }, "chardet": { @@ -1865,7 +1865,7 @@ "resolved": "https://registry.npmjs.org/checksum/-/checksum-0.1.1.tgz", "integrity": "sha1-3GUn1MkL6FYNvR7Uzs8yl9Uo6ek=", "requires": { - "optimist": "~0.3.5" + "optimist": "0.3.7" } }, "cheerio": { @@ -1874,12 +1874,12 @@ "integrity": "sha1-S59TqBsn5NXawxwP/Qz6A8xoMNs=", "dev": true, "requires": { - "css-select": "~1.2.0", - "dom-serializer": "~0.1.0", - "entities": "~1.1.1", - "htmlparser2": "^3.9.1", - "lodash": "^4.15.0", - "parse5": "^3.0.1" + "css-select": "1.2.0", + "dom-serializer": "0.1.0", + "entities": "1.1.1", + "htmlparser2": "3.9.2", + "lodash": "4.17.5", + "parse5": "3.0.3" } }, "chownr": { @@ -1899,10 +1899,10 @@ "integrity": "sha1-+TNprouafOAv1B+q0MqDAzGQxGM=", "dev": true, "requires": { - "arr-union": "^3.1.0", - "define-property": "^0.2.5", - "isobject": "^3.0.0", - "static-extend": "^0.1.1" + "arr-union": "3.1.0", + "define-property": "0.2.5", + "isobject": "3.0.1", + "static-extend": "0.1.2" }, "dependencies": { "define-property": { @@ -1911,7 +1911,7 @@ "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", "dev": true, "requires": { - "is-descriptor": "^0.1.0" + "is-descriptor": "0.1.6" } } } @@ -1919,7 +1919,7 @@ "classnames": { "version": "2.2.6", "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz", - "integrity": "sha1-Q5Nb/90pHzJtrQogUwmzjQD2UM4=" + "integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==" }, "cli-cursor": { "version": "2.1.0", @@ -1927,7 +1927,7 @@ "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", "dev": true, "requires": { - "restore-cursor": "^2.0.0" + "restore-cursor": "2.0.0" } }, "cli-width": { @@ -1942,9 +1942,9 @@ "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", "dev": true, "requires": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wrap-ansi": "^2.0.0" + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "wrap-ansi": "2.1.0" }, "dependencies": { "string-width": { @@ -1953,9 +1953,9 @@ "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "dev": true, "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" } } } @@ -1982,8 +1982,8 @@ "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", "dev": true, "requires": { - "map-visit": "^1.0.0", - "object-visit": "^1.0.0" + "map-visit": "1.0.0", + "object-visit": "1.0.1" } }, "color-convert": { @@ -1991,7 +1991,7 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz", "integrity": "sha1-wSYRB66y8pTr/+ye2eytUppgl+0=", "requires": { - "color-name": "^1.1.1" + "color-name": "1.1.3" } }, "color-name": { @@ -2010,18 +2010,18 @@ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", "requires": { - "delayed-stream": "~1.0.0" + "delayed-stream": "1.0.0" } }, "commander": { "version": "2.15.1", "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", - "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", + "integrity": "sha1-30boZ9D8Kuxmo0ZitAapzK//Ww8=", "dev": true }, "commondir": { "version": "1.0.1", - "resolved": "", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", "dev": true }, @@ -2073,34 +2073,34 @@ "integrity": "sha1-EuFZFOqikgTlaGml7Oe54UktKuI=", "dev": true, "requires": { - "js-yaml": "^3.6.1", - "lcov-parse": "^0.0.10", - "log-driver": "^1.2.5", - "minimist": "^1.2.0", - "request": "^2.79.0" + "js-yaml": "3.11.0", + "lcov-parse": "0.0.10", + "log-driver": "1.2.7", + "minimist": "1.2.0", + "request": "2.87.0" } }, "cross-env": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-5.2.0.tgz", - "integrity": "sha512-jtdNFfFW1hB7sMhr/H6rW1Z45LFqyI431m3qU6bFXcQ3Eh7LtBuG3h74o7ohHZ3crrRkkqHlo4jYHFPcjroANg==", + "integrity": "sha1-bs1MAV1Xc+YUA57lKQdmabnRJvI=", "dev": true, "requires": { - "cross-spawn": "^6.0.5", - "is-windows": "^1.0.0" + "cross-spawn": "6.0.5", + "is-windows": "1.0.2" }, "dependencies": { "cross-spawn": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "integrity": "sha1-Sl7Hxk364iw6FBJNus3uhG2Ay8Q=", "dev": true, "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" + "nice-try": "1.0.4", + "path-key": "2.0.1", + "semver": "5.5.1", + "shebang-command": "1.2.0", + "which": "1.3.0" } } } @@ -2111,9 +2111,9 @@ "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", "dev": true, "requires": { - "lru-cache": "^4.0.1", - "shebang-command": "^1.2.0", - "which": "^1.2.9" + "lru-cache": "4.1.2", + "shebang-command": "1.2.0", + "which": "1.3.0" }, "dependencies": { "yallist": { @@ -2134,7 +2134,7 @@ "integrity": "sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4=", "dev": true, "requires": { - "boom": "5.x.x" + "boom": "5.2.0" }, "dependencies": { "boom": { @@ -2143,7 +2143,7 @@ "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==", "dev": true, "requires": { - "hoek": "4.x.x" + "hoek": "4.2.1" } } } @@ -2154,10 +2154,10 @@ "integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=", "dev": true, "requires": { - "boolbase": "~1.0.0", - "css-what": "2.1", + "boolbase": "1.0.0", + "css-what": "2.1.0", "domutils": "1.5.1", - "nth-check": "~1.0.1" + "nth-check": "1.0.1" } }, "css-what": { @@ -2177,7 +2177,7 @@ "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", "requires": { - "assert-plus": "^1.0.0" + "assert-plus": "1.0.0" } }, "debug": { @@ -2190,7 +2190,7 @@ }, "debug-log": { "version": "1.0.1", - "resolved": "", + "resolved": "https://registry.npmjs.org/debug-log/-/debug-log-1.0.1.tgz", "integrity": "sha1-IwdjLUwEOCuN+KMvcLiVBG1SdF8=", "dev": true }, @@ -2211,7 +2211,7 @@ "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", "requires": { - "mimic-response": "^1.0.0" + "mimic-response": "1.0.0" } }, "dedent-js": { @@ -2226,7 +2226,7 @@ "integrity": "sha1-38lARACtHI/gI+faHfHBR8S0RN8=", "dev": true, "requires": { - "type-detect": "^4.0.0" + "type-detect": "4.0.8" } }, "deep-equal": { @@ -2248,11 +2248,11 @@ }, "default-require-extensions": { "version": "2.0.0", - "resolved": "", + "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-2.0.0.tgz", "integrity": "sha1-9fj7sYp9bVCyH2QfZJ67Uiz+JPc=", "dev": true, "requires": { - "strip-bom": "^3.0.0" + "strip-bom": "3.0.0" } }, "define-properties": { @@ -2261,8 +2261,8 @@ "integrity": "sha1-g6c/L+pWmJj7c3GTyPhzyvbUXJQ=", "dev": true, "requires": { - "foreach": "^2.0.5", - "object-keys": "^1.0.8" + "foreach": "2.0.5", + "object-keys": "1.0.11" } }, "define-property": { @@ -2271,37 +2271,37 @@ "integrity": "sha1-1Flono1lS6d+AqgX+HENcCyxbp0=", "dev": true, "requires": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" + "is-descriptor": "1.0.2", + "isobject": "3.0.1" }, "dependencies": { "is-accessor-descriptor": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha1-FpwvbT3x+ZJhgHI2XJsOofaHhlY=", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", "dev": true, "requires": { - "kind-of": "^6.0.0" + "kind-of": "6.0.2" } }, "is-data-descriptor": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha1-2Eh2Mh0Oet0DmQQGq7u9NrqSaMc=", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", "dev": true, "requires": { - "kind-of": "^6.0.0" + "kind-of": "6.0.2" } }, "is-descriptor": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha1-OxWXRqZmBLBPjIFSS6NlxfFNhuw=", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", "dev": true, "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" + "is-accessor-descriptor": "1.0.0", + "is-data-descriptor": "1.0.0", + "kind-of": "6.0.2" } } } @@ -2312,13 +2312,13 @@ "integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=", "dev": true, "requires": { - "globby": "^5.0.0", - "is-path-cwd": "^1.0.0", - "is-path-in-cwd": "^1.0.0", - "object-assign": "^4.0.1", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0", - "rimraf": "^2.2.8" + "globby": "5.0.0", + "is-path-cwd": "1.0.0", + "is-path-in-cwd": "1.0.1", + "object-assign": "4.1.1", + "pify": "2.3.0", + "pinkie-promise": "2.0.1", + "rimraf": "2.6.2" } }, "delayed-stream": { @@ -2342,7 +2342,7 @@ "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz", "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=", "requires": { - "repeating": "^2.0.0" + "repeating": "2.0.1" } }, "detect-libc": { @@ -2368,8 +2368,8 @@ "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=", "dev": true, "requires": { - "esutils": "^2.0.2", - "isarray": "^1.0.0" + "esutils": "2.0.2", + "isarray": "1.0.0" }, "dependencies": { "isarray": { @@ -2386,8 +2386,8 @@ "integrity": "sha1-BzxpdUbOB4DOI75KKOKT5AvDDII=", "dev": true, "requires": { - "domelementtype": "~1.1.1", - "entities": "~1.1.1" + "domelementtype": "1.1.3", + "entities": "1.1.1" }, "dependencies": { "domelementtype": { @@ -2416,7 +2416,7 @@ "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==", "dev": true, "requires": { - "domelementtype": "1" + "domelementtype": "1.3.0" } }, "domutils": { @@ -2425,8 +2425,8 @@ "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", "dev": true, "requires": { - "dom-serializer": "0", - "domelementtype": "1" + "dom-serializer": "0.1.0", + "domelementtype": "1.3.0" } }, "dugite": { @@ -2434,12 +2434,12 @@ "resolved": "https://registry.npmjs.org/dugite/-/dugite-1.66.0.tgz", "integrity": "sha1-X9q2aDwLU4p5vb7Emenz0+pyEPk=", "requires": { - "checksum": "^0.1.1", - "mkdirp": "^0.5.1", - "progress": "^2.0.0", - "request": "^2.86.0", - "rimraf": "^2.5.4", - "tar": "^4.0.2" + "checksum": "0.1.1", + "mkdirp": "0.5.1", + "progress": "2.0.0", + "request": "2.87.0", + "rimraf": "2.6.2", + "tar": "4.4.4" } }, "ecc-jsbn": { @@ -2448,19 +2448,19 @@ "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", "optional": true, "requires": { - "jsbn": "~0.1.0" + "jsbn": "0.1.1" } }, "electron-devtools-installer": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/electron-devtools-installer/-/electron-devtools-installer-2.2.4.tgz", - "integrity": "sha1-JhpQM343Eh0zi5ZvB5IutJOah2M=", + "integrity": "sha512-b5kcM3hmUqn64+RUcHjjr8ZMpHS2WJ5YO0pnG9+P/RTdx46of/JrEjuciHWux6pE+On6ynWhHJF53j/EDJN0PA==", "dev": true, "requires": { "7zip": "0.0.6", "cross-unzip": "0.0.2", - "rimraf": "^2.5.2", - "semver": "^5.3.0" + "rimraf": "2.6.2", + "semver": "5.5.1" } }, "emoji-regex": { @@ -2474,7 +2474,7 @@ "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=", "requires": { - "iconv-lite": "~0.4.13" + "iconv-lite": "0.4.18" } }, "end-of-stream": { @@ -2482,7 +2482,7 @@ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", "integrity": "sha1-7SljTRm6ukY7bOa4CjchPqtx7EM=", "requires": { - "once": "^1.4.0" + "once": "1.4.0" } }, "entities": { @@ -2497,23 +2497,23 @@ "integrity": "sha512-XBZbyUy36WipNSBVZKIR1sg9iF6zXfkfDEzwTc10T9zhB61UPnMo+c3WE17T/jyhfmPJOz6X073NXXsR7G/1rA==", "dev": true, "requires": { - "array.prototype.flat": "^1.2.1", - "cheerio": "^1.0.0-rc.2", - "function.prototype.name": "^1.1.0", - "has": "^1.0.3", - "is-boolean-object": "^1.0.0", - "is-callable": "^1.1.4", - "is-number-object": "^1.0.3", - "is-string": "^1.0.4", - "is-subset": "^0.1.1", - "lodash": "^4.17.4", - "object-inspect": "^1.6.0", - "object-is": "^1.0.1", - "object.assign": "^4.1.0", - "object.entries": "^1.0.4", - "object.values": "^1.0.4", - "raf": "^3.4.0", - "rst-selector-parser": "^2.2.3" + "array.prototype.flat": "1.2.1", + "cheerio": "1.0.0-rc.2", + "function.prototype.name": "1.1.0", + "has": "1.0.3", + "is-boolean-object": "1.0.0", + "is-callable": "1.1.4", + "is-number-object": "1.0.3", + "is-string": "1.0.4", + "is-subset": "0.1.1", + "lodash": "4.17.5", + "object-inspect": "1.6.0", + "object-is": "1.0.1", + "object.assign": "4.1.0", + "object.entries": "1.0.4", + "object.values": "1.0.4", + "raf": "3.4.0", + "rst-selector-parser": "2.2.3" }, "dependencies": { "has": { @@ -2522,7 +2522,7 @@ "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", "dev": true, "requires": { - "function-bind": "^1.1.1" + "function-bind": "1.1.1" } }, "is-callable": { @@ -2536,11 +2536,11 @@ "errno": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz", - "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==", + "integrity": "sha1-RoTXF3mtOa8Xfj8AeZb3xnyFJhg=", "dev": true, "optional": true, "requires": { - "prr": "~1.0.1" + "prr": "1.0.1" } }, "error": { @@ -2549,9 +2549,9 @@ "integrity": "sha1-v2n/JR+0onnBmtzNqmth6Q2b8So=", "dev": true, "requires": { - "camelize": "^1.0.0", - "string-template": "~0.2.0", - "xtend": "~4.0.0" + "camelize": "1.0.0", + "string-template": "0.2.1", + "xtend": "4.0.1" } }, "error-ex": { @@ -2560,7 +2560,7 @@ "integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=", "dev": true, "requires": { - "is-arrayish": "^0.2.1" + "is-arrayish": "0.2.1" } }, "es-abstract": { @@ -2569,11 +2569,11 @@ "integrity": "sha1-zOh9UY8Elok7GjDNhGGDVTVIBoE=", "dev": true, "requires": { - "es-to-primitive": "^1.1.1", - "function-bind": "^1.1.1", - "has": "^1.0.1", - "is-callable": "^1.1.3", - "is-regex": "^1.0.4" + "es-to-primitive": "1.1.1", + "function-bind": "1.1.1", + "has": "1.0.1", + "is-callable": "1.1.3", + "is-regex": "1.0.4" } }, "es-to-primitive": { @@ -2582,9 +2582,9 @@ "integrity": "sha1-RTVSSKiJeQNLZ5Lhm7gfK3l13Q0=", "dev": true, "requires": { - "is-callable": "^1.1.1", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.1" + "is-callable": "1.1.3", + "is-date-object": "1.0.1", + "is-symbol": "1.0.1" } }, "escape-string-regexp": { @@ -2598,44 +2598,44 @@ "integrity": "sha512-D5nG2rErquLUstgUaxJlWB5+gu+U/3VDY0fk/Iuq8y9CUFy/7Y6oF4N2cR1tV8knzQvciIbfqfohd359xTLIKQ==", "dev": true, "requires": { - "ajv": "^6.5.0", - "babel-code-frame": "^6.26.0", - "chalk": "^2.1.0", - "cross-spawn": "^6.0.5", - "debug": "^3.1.0", - "doctrine": "^2.1.0", - "eslint-scope": "^4.0.0", - "eslint-visitor-keys": "^1.0.0", - "espree": "^4.0.0", - "esquery": "^1.0.1", - "esutils": "^2.0.2", - "file-entry-cache": "^2.0.0", - "functional-red-black-tree": "^1.0.1", - "glob": "^7.1.2", - "globals": "^11.5.0", - "ignore": "^3.3.3", - "imurmurhash": "^0.1.4", - "inquirer": "^5.2.0", - "is-resolvable": "^1.1.0", - "js-yaml": "^3.11.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.3.0", - "lodash": "^4.17.5", - "minimatch": "^3.0.4", - "mkdirp": "^0.5.1", - "natural-compare": "^1.4.0", - "optionator": "^0.8.2", - "path-is-inside": "^1.0.2", - "pluralize": "^7.0.0", - "progress": "^2.0.0", - "regexpp": "^1.1.0", - "require-uncached": "^1.0.3", - "semver": "^5.5.0", - "string.prototype.matchall": "^2.0.0", - "strip-ansi": "^4.0.0", - "strip-json-comments": "^2.0.1", - "table": "^4.0.3", - "text-table": "^0.2.0" + "ajv": "6.5.2", + "babel-code-frame": "6.26.0", + "chalk": "2.4.1", + "cross-spawn": "6.0.5", + "debug": "3.1.0", + "doctrine": "2.1.0", + "eslint-scope": "4.0.0", + "eslint-visitor-keys": "1.0.0", + "espree": "4.0.0", + "esquery": "1.0.1", + "esutils": "2.0.2", + "file-entry-cache": "2.0.0", + "functional-red-black-tree": "1.0.1", + "glob": "7.1.2", + "globals": "11.7.0", + "ignore": "3.3.10", + "imurmurhash": "0.1.4", + "inquirer": "5.2.0", + "is-resolvable": "1.1.0", + "js-yaml": "3.11.0", + "json-stable-stringify-without-jsonify": "1.0.1", + "levn": "0.3.0", + "lodash": "4.17.5", + "minimatch": "3.0.4", + "mkdirp": "0.5.1", + "natural-compare": "1.4.0", + "optionator": "0.8.2", + "path-is-inside": "1.0.2", + "pluralize": "7.0.0", + "progress": "2.0.0", + "regexpp": "1.1.0", + "require-uncached": "1.0.3", + "semver": "5.5.1", + "string.prototype.matchall": "2.0.0", + "strip-ansi": "4.0.0", + "strip-json-comments": "2.0.1", + "table": "4.0.3", + "text-table": "0.2.0" }, "dependencies": { "ajv": { @@ -2644,10 +2644,10 @@ "integrity": "sha512-hOs7GfvI6tUI1LfZddH82ky6mOMyTuY0mk7kE2pWpmhhUSkumzaTO5vbVwij39MdwPQWCV4Zv57Eo06NtL/GVA==", "dev": true, "requires": { - "fast-deep-equal": "^2.0.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.1" + "fast-deep-equal": "2.0.1", + "fast-json-stable-stringify": "2.0.0", + "json-schema-traverse": "0.4.1", + "uri-js": "4.2.2" } }, "ansi-regex": { @@ -2662,7 +2662,7 @@ "integrity": "sha1-QfuyAkPlCxK+DwS43tvwdSDOhB0=", "dev": true, "requires": { - "color-convert": "^1.9.0" + "color-convert": "1.9.1" } }, "chalk": { @@ -2671,9 +2671,9 @@ "integrity": "sha1-GMSasWoDe26wFSzIPjRxM4IVtm4=", "dev": true, "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "ansi-styles": "3.2.1", + "escape-string-regexp": "1.0.5", + "supports-color": "5.4.0" } }, "cross-spawn": { @@ -2682,11 +2682,11 @@ "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", "dev": true, "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" + "nice-try": "1.0.4", + "path-key": "2.0.1", + "semver": "5.5.1", + "shebang-command": "1.2.0", + "which": "1.3.0" } }, "debug": { @@ -2704,7 +2704,7 @@ "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", "dev": true, "requires": { - "esutils": "^2.0.2" + "esutils": "2.0.2" } }, "fast-deep-equal": { @@ -2731,7 +2731,7 @@ "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", "dev": true, "requires": { - "ansi-regex": "^3.0.0" + "ansi-regex": "3.0.0" } }, "supports-color": { @@ -2740,7 +2740,7 @@ "integrity": "sha1-HGszdALCE3YF7+GfEP7DkPb6q1Q=", "dev": true, "requires": { - "has-flag": "^3.0.0" + "has-flag": "3.0.0" } } } @@ -2751,13 +2751,13 @@ "integrity": "sha1-n2yIvtUXwGDuqcQ0BqBXMw2whJc=", "dev": true, "requires": { - "babel-eslint": "^7.1.1", - "eslint-plugin-babel": "^4.0.1", - "eslint-plugin-flowtype": "^2.30.0", - "eslint-plugin-jasmine": "^2.2.0", - "eslint-plugin-prefer-object-spread": "^1.1.0", - "eslint-plugin-react": "^6.9.0", - "fbjs-eslint-utils": "^1.0.0" + "babel-eslint": "7.2.3", + "eslint-plugin-babel": "4.1.2", + "eslint-plugin-flowtype": "2.46.3", + "eslint-plugin-jasmine": "2.9.3", + "eslint-plugin-prefer-object-spread": "1.2.1", + "eslint-plugin-react": "6.10.3", + "fbjs-eslint-utils": "1.0.0" } }, "eslint-plugin-babel": { @@ -2772,7 +2772,7 @@ "integrity": "sha1-foQTHYfvGLSWsYEESFkzdIYLTo4=", "dev": true, "requires": { - "lodash": "^4.15.0" + "lodash": "4.17.5" } }, "eslint-plugin-jasmine": { @@ -2787,14 +2787,14 @@ "integrity": "sha512-JsxNKqa3TwmPypeXNnI75FntkUktGzI1wSa1LgNZdSOMI+B4sxnr1lSF8m8lPiz4mKiC+14ysZQM4scewUrP7A==", "dev": true, "requires": { - "aria-query": "^3.0.0", - "array-includes": "^3.0.3", - "ast-types-flow": "^0.0.7", - "axobject-query": "^2.0.1", - "damerau-levenshtein": "^1.0.4", - "emoji-regex": "^6.5.1", - "has": "^1.0.3", - "jsx-ast-utils": "^2.0.1" + "aria-query": "3.0.0", + "array-includes": "3.0.3", + "ast-types-flow": "0.0.7", + "axobject-query": "2.0.1", + "damerau-levenshtein": "1.0.4", + "emoji-regex": "6.5.1", + "has": "1.0.3", + "jsx-ast-utils": "2.0.1" }, "dependencies": { "has": { @@ -2803,7 +2803,7 @@ "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", "dev": true, "requires": { - "function-bind": "^1.1.1" + "function-bind": "1.1.1" } }, "jsx-ast-utils": { @@ -2812,7 +2812,7 @@ "integrity": "sha1-6AGxs5mF4g//yHtA43SAgOLcrH8=", "dev": true, "requires": { - "array-includes": "^3.0.3" + "array-includes": "3.0.3" } } } @@ -2829,11 +2829,11 @@ "integrity": "sha1-xUNb6wZ3ThLH2y9qut3L+QDNP3g=", "dev": true, "requires": { - "array.prototype.find": "^2.0.1", - "doctrine": "^1.2.2", - "has": "^1.0.1", - "jsx-ast-utils": "^1.3.4", - "object.assign": "^4.0.4" + "array.prototype.find": "2.0.4", + "doctrine": "1.5.0", + "has": "1.0.1", + "jsx-ast-utils": "1.4.1", + "object.assign": "4.1.0" } }, "eslint-scope": { @@ -2842,8 +2842,8 @@ "integrity": "sha512-1G6UTDi7Jc1ELFwnR58HV4fK9OQK4S6N985f166xqXxpjU6plxFISJa2Ba9KCQuFa8RCnj/lSFJbHo7UFDBnUA==", "dev": true, "requires": { - "esrecurse": "^4.1.0", - "estraverse": "^4.1.1" + "esrecurse": "4.2.1", + "estraverse": "4.2.0" } }, "eslint-visitor-keys": { @@ -2858,8 +2858,8 @@ "integrity": "sha512-kapdTCt1bjmspxStVKX6huolXVV5ZfyZguY1lcfhVVZstce3bqxH9mcLzNn3/mlgW6wQ732+0fuG9v7h0ZQoKg==", "dev": true, "requires": { - "acorn": "^5.6.0", - "acorn-jsx": "^4.1.1" + "acorn": "5.7.1", + "acorn-jsx": "4.1.1" } }, "esprima": { @@ -2871,10 +2871,10 @@ "esquery": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.1.tgz", - "integrity": "sha1-QGxRZYsfWZGl+bYrHcJbAOPlxwg=", + "integrity": "sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==", "dev": true, "requires": { - "estraverse": "^4.0.0" + "estraverse": "4.2.0" } }, "esrecurse": { @@ -2883,7 +2883,7 @@ "integrity": "sha1-AHo7n9vCs7uH5IeeoZyS/b05Qs8=", "dev": true, "requires": { - "estraverse": "^4.1.0" + "estraverse": "4.2.0" } }, "estraverse": { @@ -2903,7 +2903,7 @@ "integrity": "sha1-VPYZV0NG+KPueXP1T7vQG1YnItY=", "dev": true, "requires": { - "virtual-dom": "^2.0.1" + "virtual-dom": "2.1.1" } }, "ev-store": { @@ -2912,13 +2912,13 @@ "integrity": "sha1-GrDH+CE2UF3XSzHRdwHLK+bSZVg=", "dev": true, "requires": { - "individual": "^3.0.0" + "individual": "3.0.0" } }, "event-kit": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/event-kit/-/event-kit-2.5.0.tgz", - "integrity": "sha1-L3KxHitfUzzByVA4cEBiSkoCX+g=" + "integrity": "sha512-tUDxeNC9JzN2Tw/f8mLtksY34v1hHmaR7lV7X4p04XSjaeUhFMfzjF6Nwov9e0EKGEx63BaKcgXKxjpQaPo0wg==" }, "execa": { "version": "0.7.0", @@ -2926,13 +2926,13 @@ "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", "dev": true, "requires": { - "cross-spawn": "^5.0.1", - "get-stream": "^3.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" + "cross-spawn": "5.1.0", + "get-stream": "3.0.0", + "is-stream": "1.1.0", + "npm-run-path": "2.0.2", + "p-finally": "1.0.0", + "signal-exit": "3.0.2", + "strip-eof": "1.0.0" } }, "expand-brackets": { @@ -2941,13 +2941,13 @@ "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", "dev": true, "requires": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" + "debug": "2.6.9", + "define-property": "0.2.5", + "extend-shallow": "2.0.1", + "posix-character-classes": "0.1.1", + "regex-not": "1.0.2", + "snapdragon": "0.8.2", + "to-regex": "3.0.2" }, "dependencies": { "define-property": { @@ -2956,7 +2956,7 @@ "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", "dev": true, "requires": { - "is-descriptor": "^0.1.0" + "is-descriptor": "0.1.6" } }, "extend-shallow": { @@ -2965,7 +2965,7 @@ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "dev": true, "requires": { - "is-extendable": "^0.1.0" + "is-extendable": "0.1.1" } } } @@ -2986,8 +2986,8 @@ "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", "dev": true, "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" + "assign-symbols": "1.0.0", + "is-extendable": "1.0.1" }, "dependencies": { "is-extendable": { @@ -2996,7 +2996,7 @@ "integrity": "sha1-p0cPnkJnM9gb2B4RVSZOOjUHyrQ=", "dev": true, "requires": { - "is-plain-object": "^2.0.4" + "is-plain-object": "2.0.4" } } } @@ -3004,21 +3004,21 @@ "external-editor": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.2.0.tgz", - "integrity": "sha1-BFURz9jRM/OEZnPRBHwVTiFK09U=", + "integrity": "sha512-bSn6gvGxKt+b7+6TKEv1ZycHleA7aHhRHyAqJyp5pbUFuYYNIzpZnQDk7AsYckyWdEnTeAnay0aCy2aV6iTk9A==", "dev": true, "requires": { - "chardet": "^0.4.0", - "iconv-lite": "^0.4.17", - "tmp": "^0.0.33" + "chardet": "0.4.2", + "iconv-lite": "0.4.18", + "tmp": "0.0.33" }, "dependencies": { "tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha1-bTQzWIl2jSGyvNoKonfO07G/rfk=", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", "dev": true, "requires": { - "os-tmpdir": "~1.0.2" + "os-tmpdir": "1.0.2" } } } @@ -3029,14 +3029,14 @@ "integrity": "sha1-rQD+TcYSqSMuhxhxHcXLWrAoVUM=", "dev": true, "requires": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" + "array-unique": "0.3.2", + "define-property": "1.0.0", + "expand-brackets": "2.1.4", + "extend-shallow": "2.0.1", + "fragment-cache": "0.2.1", + "regex-not": "1.0.2", + "snapdragon": "0.8.2", + "to-regex": "3.0.2" }, "dependencies": { "define-property": { @@ -3045,7 +3045,7 @@ "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", "dev": true, "requires": { - "is-descriptor": "^1.0.0" + "is-descriptor": "1.0.2" } }, "extend-shallow": { @@ -3054,36 +3054,36 @@ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "dev": true, "requires": { - "is-extendable": "^0.1.0" + "is-extendable": "0.1.1" } }, "is-accessor-descriptor": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha1-FpwvbT3x+ZJhgHI2XJsOofaHhlY=", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", "dev": true, "requires": { - "kind-of": "^6.0.0" + "kind-of": "6.0.2" } }, "is-data-descriptor": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha1-2Eh2Mh0Oet0DmQQGq7u9NrqSaMc=", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", "dev": true, "requires": { - "kind-of": "^6.0.0" + "kind-of": "6.0.2" } }, "is-descriptor": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha1-OxWXRqZmBLBPjIFSS6NlxfFNhuw=", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", "dev": true, "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" + "is-accessor-descriptor": "1.0.0", + "is-data-descriptor": "1.0.0", + "kind-of": "6.0.2" } } } @@ -3104,12 +3104,12 @@ "integrity": "sha512-TR6zxCKftDQnUAPvkrCWdBgDq/gbqx8A3ApnBrR5rMvpp6+KMJI0Igw7fkWPgeVK0uhRXTXdvO3O+YP0CaUX2g==", "dev": true, "requires": { - "@mrmlnc/readdir-enhanced": "^2.2.1", - "@nodelib/fs.stat": "^1.0.1", - "glob-parent": "^3.1.0", - "is-glob": "^4.0.0", - "merge2": "^1.2.1", - "micromatch": "^3.1.10" + "@mrmlnc/readdir-enhanced": "2.2.1", + "@nodelib/fs.stat": "1.1.1", + "glob-parent": "3.1.0", + "is-glob": "4.0.0", + "merge2": "1.2.2", + "micromatch": "3.1.10" } }, "fast-json-stable-stringify": { @@ -3129,7 +3129,7 @@ "integrity": "sha1-VOmr99+i8mzZsWNsWIwa/AXeXVg=", "dev": true, "requires": { - "bser": "^2.0.0" + "bser": "2.0.0" } }, "fbjs": { @@ -3137,13 +3137,13 @@ "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.16.tgz", "integrity": "sha1-XmdDL1UNxBtXK/VYR7ispk5TN9s=", "requires": { - "core-js": "^1.0.0", - "isomorphic-fetch": "^2.1.1", - "loose-envify": "^1.0.0", - "object-assign": "^4.1.0", - "promise": "^7.1.1", - "setimmediate": "^1.0.5", - "ua-parser-js": "^0.7.9" + "core-js": "1.2.7", + "isomorphic-fetch": "2.2.1", + "loose-envify": "1.3.1", + "object-assign": "4.1.1", + "promise": "7.3.1", + "setimmediate": "1.0.5", + "ua-parser-js": "0.7.14" }, "dependencies": { "core-js": { @@ -3156,8 +3156,8 @@ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz", "integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==", "requires": { - "encoding": "^0.1.11", - "is-stream": "^1.0.1" + "encoding": "0.1.12", + "is-stream": "1.1.0" } } } @@ -3174,7 +3174,7 @@ "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", "dev": true, "requires": { - "escape-string-regexp": "^1.0.5" + "escape-string-regexp": "1.0.5" } }, "file-entry-cache": { @@ -3183,8 +3183,8 @@ "integrity": "sha1-w5KZDD5oR4PYOLjISkXYoEhFg2E=", "dev": true, "requires": { - "flat-cache": "^1.2.1", - "object-assign": "^4.0.1" + "flat-cache": "1.3.0", + "object-assign": "4.1.1" } }, "fill-range": { @@ -3193,10 +3193,10 @@ "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", "dev": true, "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" + "extend-shallow": "2.0.1", + "is-number": "3.0.0", + "repeat-string": "1.6.1", + "to-regex-range": "2.1.1" }, "dependencies": { "extend-shallow": { @@ -3205,20 +3205,20 @@ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "dev": true, "requires": { - "is-extendable": "^0.1.0" + "is-extendable": "0.1.1" } } } }, "find-cache-dir": { "version": "1.0.0", - "resolved": "", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-1.0.0.tgz", "integrity": "sha1-kojj6ePMN0hxfTnq3hfPcfww7m8=", "dev": true, "requires": { - "commondir": "^1.0.1", - "make-dir": "^1.0.0", - "pkg-dir": "^2.0.0" + "commondir": "1.0.1", + "make-dir": "1.3.0", + "pkg-dir": "2.0.0" } }, "find-up": { @@ -3227,7 +3227,7 @@ "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", "dev": true, "requires": { - "locate-path": "^2.0.0" + "locate-path": "2.0.0" } }, "flat-cache": { @@ -3236,10 +3236,10 @@ "integrity": "sha1-0wMLMrOBVPTjt+nHCfSQ9++XxIE=", "dev": true, "requires": { - "circular-json": "^0.3.1", - "del": "^2.0.2", - "graceful-fs": "^4.1.2", - "write": "^0.2.1" + "circular-json": "0.3.3", + "del": "2.2.2", + "graceful-fs": "4.1.11", + "write": "0.2.1" } }, "for-in": { @@ -3256,12 +3256,12 @@ }, "foreground-child": { "version": "1.5.6", - "resolved": "", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-1.5.6.tgz", "integrity": "sha1-T9ca0t/elnibmApcCilZN8svXOk=", "dev": true, "requires": { - "cross-spawn": "^4", - "signal-exit": "^3.0.0" + "cross-spawn": "4.0.2", + "signal-exit": "3.0.2" }, "dependencies": { "cross-spawn": { @@ -3270,8 +3270,8 @@ "integrity": "sha1-e5JHYhwjrf3ThWAEqCPL45dCTUE=", "dev": true, "requires": { - "lru-cache": "^4.0.1", - "which": "^1.2.9" + "lru-cache": "4.1.2", + "which": "1.3.0" } }, "yallist": { @@ -3290,9 +3290,9 @@ "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz", "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", "requires": { - "asynckit": "^0.4.0", + "asynckit": "0.4.0", "combined-stream": "1.0.6", - "mime-types": "^2.1.12" + "mime-types": "2.1.16" } }, "fragment-cache": { @@ -3301,7 +3301,7 @@ "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", "dev": true, "requires": { - "map-cache": "^0.2.2" + "map-cache": "0.2.2" } }, "fs-constants": { @@ -3312,11 +3312,11 @@ "fs-extra": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.3.tgz", - "integrity": "sha1-DYUhIuW8W+tFP7Ao6cDJvzY0DJQ=", + "integrity": "sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==", "requires": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" + "graceful-fs": "4.1.11", + "jsonfile": "4.0.0", + "universalify": "0.1.1" } }, "fs-minipass": { @@ -3324,7 +3324,7 @@ "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.5.tgz", "integrity": "sha1-BsJ3IYRU7CiN93raVKA7hwKqy50=", "requires": { - "minipass": "^2.2.1" + "minipass": "2.3.3" } }, "fs.realpath": { @@ -3335,7 +3335,7 @@ "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha1-pWiZ0+o8m6uHS7l3O3xe3pL0iV0=", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", "dev": true }, "function.prototype.name": { @@ -3344,9 +3344,9 @@ "integrity": "sha512-Bs0VRrTz4ghD8pTmbJQD1mZ8A/mN0ur/jGz+A6FBxPDUPkm1tNfF6bhTYPA7i7aF4lZJVr+OXTNNrnnIl58Wfg==", "dev": true, "requires": { - "define-properties": "^1.1.2", - "function-bind": "^1.1.1", - "is-callable": "^1.1.3" + "define-properties": "1.1.2", + "function-bind": "1.1.1", + "is-callable": "1.1.3" } }, "functional-red-black-tree": { @@ -3360,14 +3360,14 @@ "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" + "aproba": "1.2.0", + "console-control-strings": "1.1.0", + "has-unicode": "2.0.1", + "object-assign": "4.1.1", + "signal-exit": "3.0.2", + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "wide-align": "1.1.2" }, "dependencies": { "string-width": { @@ -3375,9 +3375,9 @@ "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" } } } @@ -3411,7 +3411,7 @@ "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", "requires": { - "assert-plus": "^1.0.0" + "assert-plus": "1.0.0" } }, "github-from-package": { @@ -3424,12 +3424,12 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=", "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" } }, "glob-parent": { @@ -3438,8 +3438,8 @@ "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", "dev": true, "requires": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" + "is-glob": "3.1.0", + "path-dirname": "1.0.2" }, "dependencies": { "is-glob": { @@ -3448,7 +3448,7 @@ "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", "dev": true, "requires": { - "is-extglob": "^2.1.0" + "is-extglob": "2.1.1" } } } @@ -3465,8 +3465,8 @@ "integrity": "sha1-52mJJopsdMOJCLEwWxD8DjlOnQ8=", "dev": true, "requires": { - "min-document": "^2.19.0", - "process": "~0.5.1" + "min-document": "2.19.0", + "process": "0.5.2" } }, "globals": { @@ -3480,12 +3480,12 @@ "integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=", "dev": true, "requires": { - "array-union": "^1.0.1", - "arrify": "^1.0.0", - "glob": "^7.0.3", - "object-assign": "^4.0.1", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" + "array-union": "1.0.2", + "arrify": "1.0.1", + "glob": "7.1.2", + "object-assign": "4.1.1", + "pify": "2.3.0", + "pinkie-promise": "2.0.1" } }, "graceful-fs": { @@ -3498,7 +3498,7 @@ "resolved": "https://registry.npmjs.org/graphql/-/graphql-0.13.2.tgz", "integrity": "sha1-THQK48Iigj5wBAlvgy57k7IQgnA=", "requires": { - "iterall": "^1.2.1" + "iterall": "1.2.2" } }, "graphql-compiler": { @@ -3507,36 +3507,36 @@ "integrity": "sha512-ZpnZm6ijwfphsnJpUvWd/M4taRR0xy5wYf1JR9LC29IGobORjAhXtEng41hYL4hAiSIpqep8zcac1yGCknaVMg==", "dev": true, "requires": { - "chalk": "^1.1.1", - "fb-watchman": "^2.0.0", - "immutable": "~3.7.6" + "chalk": "1.1.3", + "fb-watchman": "2.0.0", + "immutable": "3.7.6" } }, "grim": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/grim/-/grim-2.0.2.tgz", - "integrity": "sha512-Qj7hTJRfd87E/gUgfvM0YIH/g2UA2SV6niv6BYXk1o6w4mhgv+QyYM1EjOJQljvzgEj4SqSsRWldXIeKHz3e3Q==", + "integrity": "sha1-52CinKe4NDsMH/r2ziDyGkbuiu0=", "dev": true, "requires": { - "event-kit": "^2.0.0" + "event-kit": "2.5.0" } }, "growl": { "version": "1.10.5", "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", - "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", + "integrity": "sha1-8nNdwig2dPpnR4sQGBBZNVw2nl4=", "dev": true }, "handlebars": { "version": "4.0.11", - "resolved": "", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.0.11.tgz", "integrity": "sha1-Ywo13+ApS8KB7a5v/F0yn8eYLcw=", "dev": true, "requires": { - "async": "^1.4.0", - "optimist": "^0.6.1", - "source-map": "^0.4.4", - "uglify-js": "^2.6" + "async": "1.5.2", + "optimist": "0.6.1", + "source-map": "0.4.4", + "uglify-js": "2.8.29" }, "dependencies": { "minimist": { @@ -3551,17 +3551,17 @@ "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", "dev": true, "requires": { - "minimist": "~0.0.1", - "wordwrap": "~0.0.2" + "minimist": "0.0.10", + "wordwrap": "0.0.3" } }, "source-map": { "version": "0.4.4", - "resolved": "", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", "dev": true, "requires": { - "amdefine": ">=0.0.4" + "amdefine": "1.0.1" } } } @@ -3576,8 +3576,8 @@ "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", "requires": { - "ajv": "^5.1.0", - "har-schema": "^2.0.0" + "ajv": "5.5.2", + "har-schema": "2.0.0" } }, "has": { @@ -3586,7 +3586,7 @@ "integrity": "sha1-hGFzP1OLCDfJNh45qauelwTcLyg=", "dev": true, "requires": { - "function-bind": "^1.0.2" + "function-bind": "1.1.1" } }, "has-ansi": { @@ -3594,7 +3594,7 @@ "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", "requires": { - "ansi-regex": "^2.0.0" + "ansi-regex": "2.1.1" } }, "has-flag": { @@ -3619,9 +3619,9 @@ "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", "dev": true, "requires": { - "get-value": "^2.0.6", - "has-values": "^1.0.0", - "isobject": "^3.0.0" + "get-value": "2.0.6", + "has-values": "1.0.0", + "isobject": "3.0.1" } }, "has-values": { @@ -3630,8 +3630,8 @@ "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", "dev": true, "requires": { - "is-number": "^3.0.0", - "kind-of": "^4.0.0" + "is-number": "3.0.0", + "kind-of": "4.0.0" }, "dependencies": { "kind-of": { @@ -3640,7 +3640,7 @@ "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", "dev": true, "requires": { - "is-buffer": "^1.1.5" + "is-buffer": "1.1.6" } } } @@ -3651,10 +3651,10 @@ "integrity": "sha512-miowhl2+U7Qle4vdLqDdPt9m09K6yZhkLDTWGoUiUzrQCn+mHHSmfJgAyGaLRZbPmTqfFFjRV1QWCW0VWUJBbQ==", "dev": true, "requires": { - "boom": "4.x.x", - "cryptiles": "3.x.x", - "hoek": "4.x.x", - "sntp": "2.x.x" + "boom": "4.3.1", + "cryptiles": "3.1.2", + "hoek": "4.2.1", + "sntp": "2.1.0" } }, "he": { @@ -3684,14 +3684,14 @@ "resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz", "integrity": "sha1-42w/LSyufXRqhX440Y1fMqeILbg=", "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.1" + "os-homedir": "1.0.2", + "os-tmpdir": "1.0.2" } }, "hosted-git-info": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.6.0.tgz", - "integrity": "sha1-IyNbKasjDFdqqw1PE/wEawsDgiI=", + "integrity": "sha512-lIbgIIQA3lz5XaB6vxakj6sDHADJiZadYEJB+FgA+C4nubM1NwcuvUr9EJPmnH1skZqpqUzWborWo8EIUi0Sdw==", "dev": true }, "htmlparser2": { @@ -3700,12 +3700,12 @@ "integrity": "sha1-G9+HrMoPP55T+k/M6w9LTLsAszg=", "dev": true, "requires": { - "domelementtype": "^1.3.0", - "domhandler": "^2.3.0", - "domutils": "^1.5.1", - "entities": "^1.1.1", - "inherits": "^2.0.1", - "readable-stream": "^2.0.2" + "domelementtype": "1.3.0", + "domhandler": "2.4.2", + "domutils": "1.5.1", + "entities": "1.1.1", + "inherits": "2.0.3", + "readable-stream": "2.3.3" } }, "http-signature": { @@ -3713,9 +3713,9 @@ "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", "requires": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" + "assert-plus": "1.0.0", + "jsprim": "1.4.1", + "sshpk": "1.14.1" } }, "iconv-lite": { @@ -3759,8 +3759,8 @@ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "requires": { - "once": "^1.3.0", - "wrappy": "1" + "once": "1.4.0", + "wrappy": "1.0.2" } }, "inherits": { @@ -3779,19 +3779,19 @@ "integrity": "sha512-E9BmnJbAKLPGonz0HeWHtbKf+EeSP93paWO3ZYoUpq/aowXvYGjjCSuashhXPpzbArIjBbji39THkxTz9ZeEUQ==", "dev": true, "requires": { - "ansi-escapes": "^3.0.0", - "chalk": "^2.0.0", - "cli-cursor": "^2.1.0", - "cli-width": "^2.0.0", - "external-editor": "^2.1.0", - "figures": "^2.0.0", - "lodash": "^4.3.0", + "ansi-escapes": "3.1.0", + "chalk": "2.4.1", + "cli-cursor": "2.1.0", + "cli-width": "2.2.0", + "external-editor": "2.2.0", + "figures": "2.0.0", + "lodash": "4.17.5", "mute-stream": "0.0.7", - "run-async": "^2.2.0", - "rxjs": "^5.5.2", - "string-width": "^2.1.0", - "strip-ansi": "^4.0.0", - "through": "^2.3.6" + "run-async": "2.3.0", + "rxjs": "5.5.11", + "string-width": "2.1.1", + "strip-ansi": "4.0.0", + "through": "2.3.8" }, "dependencies": { "ansi-regex": { @@ -3806,7 +3806,7 @@ "integrity": "sha1-QfuyAkPlCxK+DwS43tvwdSDOhB0=", "dev": true, "requires": { - "color-convert": "^1.9.0" + "color-convert": "1.9.1" } }, "chalk": { @@ -3815,9 +3815,9 @@ "integrity": "sha1-GMSasWoDe26wFSzIPjRxM4IVtm4=", "dev": true, "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "ansi-styles": "3.2.1", + "escape-string-regexp": "1.0.5", + "supports-color": "5.4.0" } }, "strip-ansi": { @@ -3826,7 +3826,7 @@ "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", "dev": true, "requires": { - "ansi-regex": "^3.0.0" + "ansi-regex": "3.0.0" } }, "supports-color": { @@ -3835,7 +3835,7 @@ "integrity": "sha1-HGszdALCE3YF7+GfEP7DkPb6q1Q=", "dev": true, "requires": { - "has-flag": "^3.0.0" + "has-flag": "3.0.0" } } } @@ -3845,7 +3845,7 @@ "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", "integrity": "sha1-YQ88ksk1nOHbYW5TgAjSP/NRWOY=", "requires": { - "loose-envify": "^1.0.0" + "loose-envify": "1.3.1" } }, "invert-kv": { @@ -3860,7 +3860,7 @@ "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", "dev": true, "requires": { - "kind-of": "^3.0.2" + "kind-of": "3.2.2" }, "dependencies": { "kind-of": { @@ -3869,7 +3869,7 @@ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "dev": true, "requires": { - "is-buffer": "^1.1.5" + "is-buffer": "1.1.6" } } } @@ -3897,7 +3897,7 @@ "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", "dev": true, "requires": { - "builtin-modules": "^1.0.0" + "builtin-modules": "1.1.1" } }, "is-callable": { @@ -3912,7 +3912,7 @@ "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", "dev": true, "requires": { - "kind-of": "^3.0.2" + "kind-of": "3.2.2" }, "dependencies": { "kind-of": { @@ -3921,7 +3921,7 @@ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "dev": true, "requires": { - "is-buffer": "^1.1.5" + "is-buffer": "1.1.6" } } } @@ -3935,18 +3935,18 @@ "is-descriptor": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha1-Nm2CQN3kh8pRgjsaufB6EKeCUco=", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", "dev": true, "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" + "is-accessor-descriptor": "0.1.6", + "is-data-descriptor": "0.1.4", + "kind-of": "5.1.0" }, "dependencies": { "kind-of": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha1-cpyR4thXt6QZofmqZWhcTDP1hF0=", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", "dev": true } } @@ -3968,7 +3968,7 @@ "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", "requires": { - "number-is-nan": "^1.0.0" + "number-is-nan": "1.0.1" } }, "is-fullwidth-code-point": { @@ -3976,7 +3976,7 @@ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "requires": { - "number-is-nan": "^1.0.0" + "number-is-nan": "1.0.1" } }, "is-glob": { @@ -3985,7 +3985,7 @@ "integrity": "sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=", "dev": true, "requires": { - "is-extglob": "^2.1.1" + "is-extglob": "2.1.1" } }, "is-number": { @@ -3994,7 +3994,7 @@ "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", "dev": true, "requires": { - "kind-of": "^3.0.2" + "kind-of": "3.2.2" }, "dependencies": { "kind-of": { @@ -4003,7 +4003,7 @@ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "dev": true, "requires": { - "is-buffer": "^1.1.5" + "is-buffer": "1.1.6" } } } @@ -4026,7 +4026,7 @@ "integrity": "sha1-dkZiRnH9fqVYzNmieVGC8pWPGyQ=", "dev": true, "requires": { - "is-number": "^4.0.0" + "is-number": "4.0.0" }, "dependencies": { "is-number": { @@ -4046,10 +4046,10 @@ "is-path-in-cwd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz", - "integrity": "sha1-WsSLNF72dTOb1sekipEhELJBz1I=", + "integrity": "sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ==", "dev": true, "requires": { - "is-path-inside": "^1.0.0" + "is-path-inside": "1.0.1" } }, "is-path-inside": { @@ -4058,7 +4058,7 @@ "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", "dev": true, "requires": { - "path-is-inside": "^1.0.1" + "path-is-inside": "1.0.2" } }, "is-plain-object": { @@ -4066,7 +4066,7 @@ "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", "integrity": "sha1-LBY7P6+xtgbZ0Xko8FwqHDjgdnc=", "requires": { - "isobject": "^3.0.1" + "isobject": "3.0.1" } }, "is-promise": { @@ -4081,7 +4081,7 @@ "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", "dev": true, "requires": { - "has": "^1.0.1" + "has": "1.0.1" } }, "is-resolvable": { @@ -4151,8 +4151,8 @@ "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz", "integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=", "requires": { - "node-fetch": "^1.0.1", - "whatwg-fetch": ">=0.10.0" + "node-fetch": "1.7.3", + "whatwg-fetch": "2.0.4" }, "dependencies": { "node-fetch": { @@ -4160,8 +4160,8 @@ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz", "integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==", "requires": { - "encoding": "^0.1.11", - "is-stream": "^1.0.1" + "encoding": "0.1.12", + "is-stream": "1.1.0" } } } @@ -4179,11 +4179,11 @@ }, "istanbul-lib-hook": { "version": "2.0.0", - "resolved": "", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-2.0.0.tgz", "integrity": "sha512-qm3dt628HKpCVtIjbdZLuQyXn0+LO8qz+YHQDfkeXuSk5D+p299SEV5DrnUUnPi2SXvdMmWapMYWiuE75o2rUQ==", "dev": true, "requires": { - "append-transform": "^1.0.0" + "append-transform": "1.0.0" } }, "istanbul-lib-instrument": { @@ -4197,8 +4197,8 @@ "@babel/template": "7.0.0-beta.49", "@babel/traverse": "7.0.0-beta.49", "@babel/types": "7.0.0-beta.49", - "istanbul-lib-coverage": "^2.0.0", - "semver": "^5.5.0" + "istanbul-lib-coverage": "2.0.0", + "semver": "5.5.1" }, "dependencies": { "ansi-styles": { @@ -4206,7 +4206,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "requires": { - "color-convert": "^1.9.0" + "color-convert": "1.9.1" } }, "chalk": { @@ -4214,9 +4214,9 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "ansi-styles": "3.2.1", + "escape-string-regexp": "1.0.5", + "supports-color": "5.4.0" } }, "debug": { @@ -4242,20 +4242,20 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", "requires": { - "has-flag": "^3.0.0" + "has-flag": "3.0.0" } } } }, "istanbul-lib-report": { "version": "2.0.0", - "resolved": "", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-2.0.0.tgz", "integrity": "sha512-RiELmy9oIRYUv36ITOAhVum9PUvuj6bjyXVEKEHNiD1me6qXtxfx7vSEJWnjOGk2QmYw/GRFjLXWJv3qHpLceQ==", "dev": true, "requires": { - "istanbul-lib-coverage": "^2.0.0", - "make-dir": "^1.3.0", - "supports-color": "^5.4.0" + "istanbul-lib-coverage": "2.0.0", + "make-dir": "1.3.0", + "supports-color": "5.4.0" }, "dependencies": { "supports-color": { @@ -4264,22 +4264,22 @@ "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", "dev": true, "requires": { - "has-flag": "^3.0.0" + "has-flag": "3.0.0" } } } }, "istanbul-lib-source-maps": { "version": "2.0.0", - "resolved": "", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-2.0.0.tgz", "integrity": "sha512-jenUeC0gMSSMGkvqD9xuNfs3nD7XWeXLhqaIkqHsNZ3DJBWPdlKEydE7Ya5aTgdWjrEQhrCYTv+J606cGC2vuQ==", "dev": true, "requires": { - "debug": "^3.1.0", - "istanbul-lib-coverage": "^2.0.0", - "make-dir": "^1.3.0", - "rimraf": "^2.6.2", - "source-map": "^0.6.1" + "debug": "3.1.0", + "istanbul-lib-coverage": "2.0.0", + "make-dir": "1.3.0", + "rimraf": "2.6.2", + "source-map": "0.6.1" }, "dependencies": { "debug": { @@ -4293,7 +4293,7 @@ }, "source-map": { "version": "0.6.1", - "resolved": "", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true } @@ -4301,11 +4301,11 @@ }, "istanbul-reports": { "version": "1.5.0", - "resolved": "", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-1.5.0.tgz", "integrity": "sha512-HeZG0WHretI9FXBni5wZ9DOgNziqDCEwetxnme5k1Vv5e81uTqcsy3fMH99gXGDGKr1ea87TyGseDMa2h4HEUA==", "dev": true, "requires": { - "handlebars": "^4.0.11" + "handlebars": "4.0.11" } }, "iterall": { @@ -4324,8 +4324,8 @@ "integrity": "sha1-WXwai9VxUvJtYizkEXhRpR9euu8=", "dev": true, "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" + "argparse": "1.0.10", + "esprima": "4.0.0" } }, "jsbn": { @@ -4370,7 +4370,7 @@ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", "requires": { - "graceful-fs": "^4.1.6" + "graceful-fs": "4.1.11" } }, "jsprim": { @@ -4393,7 +4393,7 @@ "just-extend": { "version": "1.1.27", "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-1.1.27.tgz", - "integrity": "sha1-7G55QQ/5FORyZSq/oOYDwD1g6QU=", + "integrity": "sha512-mJVp13Ix6gFo3SBAy9U/kL+oeZqzlYYYLQBwXVBlVzIsZwBqGREnOro24oC/8s8aox+rJhtZ2DiQof++IrkA+g==", "dev": true }, "keytar": { @@ -4402,7 +4402,7 @@ "integrity": "sha1-igamV3/fY3PgqmsRInfmPex3/RI=", "requires": { "nan": "2.8.0", - "prebuild-install": "^2.4.1" + "prebuild-install": "2.5.3" } }, "kind-of": { @@ -4422,7 +4422,7 @@ "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", "dev": true, "requires": { - "invert-kv": "^1.0.0" + "invert-kv": "1.0.0" } }, "lcov-parse": { @@ -4434,24 +4434,24 @@ "less": { "version": "3.8.1", "resolved": "https://registry.npmjs.org/less/-/less-3.8.1.tgz", - "integrity": "sha512-8HFGuWmL3FhQR0aH89escFNBQH/nEiYPP2ltDFdQw2chE28Yx2E3lhAIq9Y2saYwLSwa699s4dBVEfCY8Drf7Q==", - "dev": true, - "requires": { - "clone": "^2.1.2", - "errno": "^0.1.1", - "graceful-fs": "^4.1.2", - "image-size": "~0.5.0", - "mime": "^1.4.1", - "mkdirp": "^0.5.0", - "promise": "^7.1.1", - "request": "^2.83.0", - "source-map": "~0.6.0" + "integrity": "sha1-8xdYWY71oZMN1MrvqeQ0BkHnHh0=", + "dev": true, + "requires": { + "clone": "2.1.2", + "errno": "0.1.7", + "graceful-fs": "4.1.11", + "image-size": "0.5.5", + "mime": "1.6.0", + "mkdirp": "0.5.1", + "promise": "7.3.1", + "request": "2.87.0", + "source-map": "0.6.1" }, "dependencies": { "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "integrity": "sha1-dHIq8y6WFOnCh6jQu95IteLxomM=", "dev": true, "optional": true } @@ -4463,8 +4463,8 @@ "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", "dev": true, "requires": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" + "prelude-ls": "1.1.2", + "type-check": "0.3.2" } }, "load-json-file": { @@ -4473,10 +4473,10 @@ "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", "dev": true, "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^2.2.0", - "pify": "^2.0.0", - "strip-bom": "^3.0.0" + "graceful-fs": "4.1.11", + "parse-json": "2.2.0", + "pify": "2.3.0", + "strip-bom": "3.0.0" } }, "locate-path": { @@ -4485,8 +4485,8 @@ "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", "dev": true, "requires": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" + "p-locate": "2.0.0", + "path-exists": "3.0.0" } }, "lodash": { @@ -4544,7 +4544,7 @@ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz", "integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=", "requires": { - "js-tokens": "^3.0.0" + "js-tokens": "3.0.2" } }, "lru-cache": { @@ -4552,8 +4552,8 @@ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.2.tgz", "integrity": "sha512-wgeVXhrDwAWnIF/yZARsFnMBtdFXOg1b8RIrhilp+0iDYN4mdQcNZElDZ0e4B64BhaxeQ5zN7PMyvu7we1kPeQ==", "requires": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" + "pseudomap": "1.0.2", + "yallist": "2.1.2" }, "dependencies": { "yallist": { @@ -4565,16 +4565,16 @@ }, "make-dir": { "version": "1.3.0", - "resolved": "", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", "dev": true, "requires": { - "pify": "^3.0.0" + "pify": "3.0.0" }, "dependencies": { "pify": { "version": "3.0.0", - "resolved": "", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", "dev": true } @@ -4592,21 +4592,21 @@ "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", "dev": true, "requires": { - "object-visit": "^1.0.0" + "object-visit": "1.0.1" } }, "md5-hex": { "version": "1.3.0", - "resolved": "", + "resolved": "https://registry.npmjs.org/md5-hex/-/md5-hex-1.3.0.tgz", "integrity": "sha1-0sSv6YPENwZiF5uMrRRSGRNQRsQ=", "dev": true, "requires": { - "md5-o-matic": "^0.1.1" + "md5-o-matic": "0.1.1" } }, "md5-o-matic": { "version": "0.1.1", - "resolved": "", + "resolved": "https://registry.npmjs.org/md5-o-matic/-/md5-o-matic-0.1.1.tgz", "integrity": "sha1-givM1l4RfFFPqxdrJZRdVBAKA8M=", "dev": true }, @@ -4616,21 +4616,21 @@ "integrity": "sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y=", "dev": true, "requires": { - "mimic-fn": "^1.0.0" + "mimic-fn": "1.2.0" } }, "merge-source-map": { "version": "1.1.0", - "resolved": "", + "resolved": "https://registry.npmjs.org/merge-source-map/-/merge-source-map-1.1.0.tgz", "integrity": "sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw==", "dev": true, "requires": { - "source-map": "^0.6.1" + "source-map": "0.6.1" }, "dependencies": { "source-map": { "version": "0.6.1", - "resolved": "", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true } @@ -4645,28 +4645,28 @@ "micromatch": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha1-cIWbyVyYQJUvNZoGij/En57PrCM=", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", "dev": true, "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" + "arr-diff": "4.0.0", + "array-unique": "0.3.2", + "braces": "2.3.2", + "define-property": "2.0.2", + "extend-shallow": "3.0.2", + "extglob": "2.0.4", + "fragment-cache": "0.2.1", + "kind-of": "6.0.2", + "nanomatch": "1.2.9", + "object.pick": "1.3.0", + "regex-not": "1.0.2", + "snapdragon": "0.8.2", + "to-regex": "3.0.2" } }, "mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "integrity": "sha1-Ms2eXGRVO9WNGaVor0Uqz/BJgbE=", "dev": true, "optional": true }, @@ -4680,7 +4680,7 @@ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.16.tgz", "integrity": "sha1-K4WKUuXs1RbbiXrCvodIeDBpjiM=", "requires": { - "mime-db": "~1.29.0" + "mime-db": "1.29.0" } }, "mimic-fn": { @@ -4700,7 +4700,7 @@ "integrity": "sha1-e9KC4/WELtKVu3SM3Z8f+iyCRoU=", "dev": true, "requires": { - "dom-walk": "^0.1.0" + "dom-walk": "0.1.1" } }, "minimatch": { @@ -4708,7 +4708,7 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=", "requires": { - "brace-expansion": "^1.1.7" + "brace-expansion": "1.1.8" } }, "minimist": { @@ -4719,10 +4719,10 @@ "minipass": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.3.tgz", - "integrity": "sha1-p9zIt7gz9dNodZzOVE3MtV9Q8jM=", + "integrity": "sha512-/jAn9/tEX4gnpyRATxgHEOV6xbcyxgT7iUnxo9Y3+OB0zX00TgKIv/2FZCf5brBbICcwbLqVv2ImjvWWrQMSYw==", "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" + "safe-buffer": "5.1.2", + "yallist": "3.0.2" }, "dependencies": { "safe-buffer": { @@ -4737,7 +4737,7 @@ "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.1.0.tgz", "integrity": "sha1-EeE2WM5GvDpwomeqxYNZ0eDCnOs=", "requires": { - "minipass": "^2.2.1" + "minipass": "2.3.3" } }, "mixin-deep": { @@ -4746,8 +4746,8 @@ "integrity": "sha1-pJ5yaNzhoNlpjkUybFYm3zVD0P4=", "dev": true, "requires": { - "for-in": "^1.0.2", - "is-extendable": "^1.0.1" + "for-in": "1.0.2", + "is-extendable": "1.0.1" }, "dependencies": { "is-extendable": { @@ -4756,7 +4756,7 @@ "integrity": "sha1-p0cPnkJnM9gb2B4RVSZOOjUHyrQ=", "dev": true, "requires": { - "is-plain-object": "^2.0.4" + "is-plain-object": "2.0.4" } } } @@ -4779,7 +4779,7 @@ "mocha": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz", - "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==", + "integrity": "sha1-bYrlCPWRZ/lA8rWzxKYSrlDJCuY=", "dev": true, "requires": { "browser-stdout": "1.3.1", @@ -4798,7 +4798,7 @@ "debug": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "integrity": "sha1-W7WgZyYotkFJVmuhaBnmFRjGcmE=", "dev": true, "requires": { "ms": "2.0.0" @@ -4807,10 +4807,10 @@ "supports-color": { "version": "5.4.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", - "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "integrity": "sha1-HGszdALCE3YF7+GfEP7DkPb6q1Q=", "dev": true, "requires": { - "has-flag": "^3.0.0" + "has-flag": "3.0.0" } } } @@ -4821,7 +4821,7 @@ "integrity": "sha512-3FOlmBP3DemyvmboFK2vMoPRAJ/7Co8gyvcdxctYMynofouRQAVFwpgO/f/mRCLCMn7GLEq7/sOyuj2HXave/g==", "dev": true, "requires": { - "request-json": "^0.6.3" + "request-json": "0.6.3" } }, "mocha-multi-reporters": { @@ -4830,8 +4830,8 @@ "integrity": "sha1-zH8/TTL0eFIJQdhSq7ZNmYhYfYI=", "dev": true, "requires": { - "debug": "^3.1.0", - "lodash": "^4.16.4" + "debug": "3.1.0", + "lodash": "4.17.5" }, "dependencies": { "debug": { @@ -4884,18 +4884,18 @@ "integrity": "sha1-h59xUMstq3pHElkGbBBO7m4Pp8I=", "dev": true, "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "fragment-cache": "^0.2.1", - "is-odd": "^2.0.0", - "is-windows": "^1.0.2", - "kind-of": "^6.0.2", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" + "arr-diff": "4.0.0", + "array-unique": "0.3.2", + "define-property": "2.0.2", + "extend-shallow": "3.0.2", + "fragment-cache": "0.2.1", + "is-odd": "2.0.0", + "is-windows": "1.0.2", + "kind-of": "6.0.2", + "object.pick": "1.3.0", + "regex-not": "1.0.2", + "snapdragon": "0.8.2", + "to-regex": "3.0.2" }, "dependencies": { "is-extendable": { @@ -4903,7 +4903,7 @@ "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", "integrity": "sha1-p0cPnkJnM9gb2B4RVSZOOjUHyrQ=", "requires": { - "is-plain-object": "^2.0.4" + "is-plain-object": "2.0.4" } } } @@ -4920,11 +4920,11 @@ "integrity": "sha512-8IUY/rUrKz2mIynUGh8k+tul1awMKEjeHHC5G3FHvvyAW6oq4mQfNp2c0BMea+sYZJvYcrrM6GmZVIle/GRXGw==", "dev": true, "requires": { - "moo": "^0.4.3", - "nomnom": "~1.6.2", - "railroad-diagrams": "^1.0.0", + "moo": "0.4.3", + "nomnom": "1.6.2", + "railroad-diagrams": "1.0.0", "randexp": "0.4.6", - "semver": "^5.4.1" + "semver": "5.5.1" } }, "next-tick": { @@ -4942,14 +4942,14 @@ "nise": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/nise/-/nise-1.4.2.tgz", - "integrity": "sha1-qaOADjmUmUr55FIzPVSdYPcrjow=", + "integrity": "sha512-BxH/DxoQYYdhKgVAfqVy4pzXRZELHOIewzoesxpjYvpU+7YOalQhGNPf7wAx8pLrTNPrHRDlLOkAl8UI0ZpXjw==", "dev": true, "requires": { - "@sinonjs/formatio": "^2.0.0", - "just-extend": "^1.1.27", - "lolex": "^2.3.2", - "path-to-regexp": "^1.7.0", - "text-encoding": "^0.6.4" + "@sinonjs/formatio": "2.0.0", + "just-extend": "1.1.27", + "lolex": "2.7.1", + "path-to-regexp": "1.7.0", + "text-encoding": "0.6.4" } }, "node-abi": { @@ -4957,15 +4957,15 @@ "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.4.0.tgz", "integrity": "sha1-PCdRXLhC9bvBMqMSVPnx4cVce4M=", "requires": { - "semver": "^5.4.1" + "semver": "5.5.1" } }, "node-emoji": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.8.1.tgz", - "integrity": "sha1-buxr+wdCHiFIx1xrunJCH4UwqCY=", + "integrity": "sha512-+ktMAh1Jwas+TnGodfCfjUbJKoANqPaJFN0z0iqh41eqD8dvguNzcitVSBSVK1pidz0AqGbLKcoVuVLRVZ/aVg==", "requires": { - "lodash.toarray": "^4.4.0" + "lodash.toarray": "4.4.0" } }, "node-fetch": { @@ -4986,8 +4986,8 @@ "integrity": "sha1-hKZqJgF0QI/Ft3oY+IjszET7aXE=", "dev": true, "requires": { - "colors": "0.5.x", - "underscore": "~1.4.4" + "colors": "0.5.1", + "underscore": "1.4.4" } }, "noop-logger": { @@ -5001,10 +5001,10 @@ "integrity": "sha1-EvlaMH1YNSB1oEkHuErIvpisAS8=", "dev": true, "requires": { - "hosted-git-info": "^2.1.4", - "is-builtin-module": "^1.0.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" + "hosted-git-info": "2.6.0", + "is-builtin-module": "1.0.0", + "semver": "5.5.1", + "validate-npm-package-license": "3.0.3" } }, "npm-run-path": { @@ -5013,7 +5013,7 @@ "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", "dev": true, "requires": { - "path-key": "^2.0.0" + "path-key": "2.0.1" } }, "npmlog": { @@ -5021,10 +5021,10 @@ "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", "integrity": "sha1-CKfyqL9zRgR3mp76StXMcXq7lUs=", "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" + "are-we-there-yet": "1.1.4", + "console-control-strings": "1.1.0", + "gauge": "2.7.4", + "set-blocking": "2.0.0" } }, "nth-check": { @@ -5033,7 +5033,7 @@ "integrity": "sha1-mSms32KPwsQQmN6rgqxYDPFJquQ=", "dev": true, "requires": { - "boolbase": "~1.0.0" + "boolbase": "1.0.0" } }, "number-is-nan": { @@ -5047,36 +5047,36 @@ "integrity": "sha1-4Awm6b0zq16B7emSu+STCEhYmbY=", "dev": true, "requires": { - "archy": "^1.0.0", - "arrify": "^1.0.1", - "caching-transform": "^1.0.1", - "convert-source-map": "^1.5.1", - "debug-log": "^1.0.1", - "find-cache-dir": "^1.0.0", - "find-up": "^2.1.0", - "foreground-child": "^1.5.6", - "glob": "^7.1.2", - "istanbul-lib-coverage": "^2.0.0", - "istanbul-lib-hook": "^2.0.0", - "istanbul-lib-instrument": "^2.2.0", - "istanbul-lib-report": "^2.0.0", - "istanbul-lib-source-maps": "^2.0.0", - "istanbul-reports": "^1.5.0", - "make-dir": "^1.3.0", - "md5-hex": "^2.0.0", - "merge-source-map": "^1.1.0", - "resolve-from": "^4.0.0", - "rimraf": "^2.6.2", - "signal-exit": "^3.0.2", - "spawn-wrap": "^1.4.2", - "test-exclude": "^4.2.2", + "archy": "1.0.0", + "arrify": "1.0.1", + "caching-transform": "1.0.1", + "convert-source-map": "1.5.1", + "debug-log": "1.0.1", + "find-cache-dir": "1.0.0", + "find-up": "2.1.0", + "foreground-child": "1.5.6", + "glob": "7.1.2", + "istanbul-lib-coverage": "2.0.0", + "istanbul-lib-hook": "2.0.0", + "istanbul-lib-instrument": "2.2.0", + "istanbul-lib-report": "2.0.0", + "istanbul-lib-source-maps": "2.0.0", + "istanbul-reports": "1.5.0", + "make-dir": "1.3.0", + "md5-hex": "2.0.0", + "merge-source-map": "1.1.0", + "resolve-from": "4.0.0", + "rimraf": "2.6.2", + "signal-exit": "3.0.2", + "spawn-wrap": "1.4.2", + "test-exclude": "4.2.2", "yargs": "11.1.0", - "yargs-parser": "^9.0.2" + "yargs-parser": "9.0.2" }, "dependencies": { "ansi-regex": { "version": "3.0.0", - "resolved": "", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", "dev": true }, @@ -5090,8 +5090,8 @@ "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", "requires": { - "center-align": "^0.1.1", - "right-align": "^0.1.1", + "center-align": "0.1.3", + "right-align": "0.1.3", "wordwrap": "0.0.2" }, "dependencies": { @@ -5107,8 +5107,8 @@ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-4.0.2.tgz", "integrity": "sha1-e5JHYhwjrf3ThWAEqCPL45dCTUE=", "requires": { - "lru-cache": "^4.0.1", - "which": "^1.2.9" + "lru-cache": "4.1.2", + "which": "1.3.0" } }, "debug": { @@ -5129,16 +5129,16 @@ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "requires": { - "is-buffer": "^1.1.5" + "is-buffer": "1.1.6" } }, "md5-hex": { "version": "2.0.0", - "resolved": "", + "resolved": "https://registry.npmjs.org/md5-hex/-/md5-hex-2.0.0.tgz", "integrity": "sha1-0FiOnxx0lUSS7NJKwKxs6ZfZLjM=", "dev": true, "requires": { - "md5-o-matic": "^0.1.1" + "md5-o-matic": "0.1.1" } }, "minimist": { @@ -5151,28 +5151,28 @@ "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", "requires": { - "minimist": "~0.0.1", - "wordwrap": "~0.0.2" + "minimist": "0.0.8", + "wordwrap": "0.0.3" } }, "resolve-from": { "version": "4.0.0", - "resolved": "", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true }, "semver": { "version": "5.5.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==" }, "strip-ansi": { "version": "4.0.0", - "resolved": "", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", "dev": true, "requires": { - "ansi-regex": "^3.0.0" + "ansi-regex": "3.0.0" } }, "supports-color": { @@ -5180,41 +5180,41 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", "requires": { - "has-flag": "^3.0.0" + "has-flag": "3.0.0" } }, "test-exclude": { "version": "4.2.2", - "resolved": "", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-4.2.2.tgz", "integrity": "sha512-2kTGf+3tykCfrWVREgyTR0bmVO0afE6i7zVXi/m+bZZ8ujV89Aulxdcdv32yH+unVFg3Y5o6GA8IzsHnGQuFgQ==", "dev": true, "requires": { - "arrify": "^1.0.1", - "minimatch": "^3.0.4", - "read-pkg-up": "^3.0.0", - "require-main-filename": "^1.0.1" + "arrify": "1.0.1", + "minimatch": "3.0.4", + "read-pkg-up": "3.0.0", + "require-main-filename": "1.0.1" }, "dependencies": { "load-json-file": { "version": "4.0.0", - "resolved": "", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", "dev": true, "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^4.0.0", - "pify": "^3.0.0", - "strip-bom": "^3.0.0" + "graceful-fs": "4.1.11", + "parse-json": "4.0.0", + "pify": "3.0.0", + "strip-bom": "3.0.0" } }, "parse-json": { "version": "4.0.0", - "resolved": "", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", "dev": true, "requires": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" + "error-ex": "1.3.1", + "json-parse-better-errors": "1.0.2" }, "dependencies": { "json-parse-better-errors": { @@ -5227,43 +5227,43 @@ }, "path-type": { "version": "3.0.0", - "resolved": "", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", "dev": true, "requires": { - "pify": "^3.0.0" + "pify": "3.0.0" } }, "pify": { "version": "3.0.0", - "resolved": "", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", "dev": true }, "read-pkg": { "version": "3.0.0", - "resolved": "", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", "dev": true, "requires": { - "load-json-file": "^4.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^3.0.0" + "load-json-file": "4.0.0", + "normalize-package-data": "2.4.0", + "path-type": "3.0.0" } }, "read-pkg-up": { "version": "3.0.0", - "resolved": "", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz", "integrity": "sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc=", "dev": true, "requires": { - "find-up": "^2.0.0", - "read-pkg": "^3.0.0" + "find-up": "2.1.0", + "read-pkg": "3.0.0" } }, "strip-bom": { "version": "3.0.0", - "resolved": "", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", "dev": true } @@ -5276,64 +5276,64 @@ }, "yargs": { "version": "11.1.0", - "resolved": "", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-11.1.0.tgz", "integrity": "sha512-NwW69J42EsCSanF8kyn5upxvjp5ds+t3+udGBeTbFnERA+lF541DDpMawzo4z6W/QrzNM18D+BPMiOBibnFV5A==", "dev": true, "requires": { - "cliui": "^4.0.0", - "decamelize": "^1.1.1", - "find-up": "^2.1.0", - "get-caller-file": "^1.0.1", - "os-locale": "^2.0.0", - "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", - "set-blocking": "^2.0.0", - "string-width": "^2.0.0", - "which-module": "^2.0.0", - "y18n": "^3.2.1", - "yargs-parser": "^9.0.2" + "cliui": "4.1.0", + "decamelize": "1.2.0", + "find-up": "2.1.0", + "get-caller-file": "1.0.2", + "os-locale": "2.1.0", + "require-directory": "2.1.1", + "require-main-filename": "1.0.1", + "set-blocking": "2.0.0", + "string-width": "2.1.1", + "which-module": "2.0.0", + "y18n": "3.2.1", + "yargs-parser": "9.0.2" }, "dependencies": { "camelcase": { "version": "4.1.0", - "resolved": "", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", "dev": true }, "cliui": { "version": "4.1.0", - "resolved": "", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", "dev": true, "requires": { - "string-width": "^2.1.1", - "strip-ansi": "^4.0.0", - "wrap-ansi": "^2.0.0" + "string-width": "2.1.1", + "strip-ansi": "4.0.0", + "wrap-ansi": "2.1.0" } }, "yargs-parser": { "version": "9.0.2", - "resolved": "", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-9.0.2.tgz", "integrity": "sha1-nM9qQ0YP5O1Aqbto9I1DuKaMwHc=", "dev": true, "requires": { - "camelcase": "^4.1.0" + "camelcase": "4.1.0" } } } }, "yargs-parser": { "version": "9.0.2", - "resolved": "", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-9.0.2.tgz", "integrity": "sha1-nM9qQ0YP5O1Aqbto9I1DuKaMwHc=", "dev": true, "requires": { - "camelcase": "^4.1.0" + "camelcase": "4.1.0" }, "dependencies": { "camelcase": { "version": "4.1.0", - "resolved": "", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", "dev": true } @@ -5357,9 +5357,9 @@ "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", "dev": true, "requires": { - "copy-descriptor": "^0.1.0", - "define-property": "^0.2.5", - "kind-of": "^3.0.3" + "copy-descriptor": "0.1.1", + "define-property": "0.2.5", + "kind-of": "3.2.2" }, "dependencies": { "define-property": { @@ -5368,7 +5368,7 @@ "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", "dev": true, "requires": { - "is-descriptor": "^0.1.0" + "is-descriptor": "0.1.6" } }, "kind-of": { @@ -5377,7 +5377,7 @@ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "dev": true, "requires": { - "is-buffer": "^1.1.5" + "is-buffer": "1.1.6" } } } @@ -5406,7 +5406,7 @@ "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", "dev": true, "requires": { - "isobject": "^3.0.0" + "isobject": "3.0.1" } }, "object.assign": { @@ -5415,10 +5415,10 @@ "integrity": "sha1-lovxEA15Vrs8oIbwBvhGs7xACNo=", "dev": true, "requires": { - "define-properties": "^1.1.2", - "function-bind": "^1.1.1", - "has-symbols": "^1.0.0", - "object-keys": "^1.0.11" + "define-properties": "1.1.2", + "function-bind": "1.1.1", + "has-symbols": "1.0.0", + "object-keys": "1.0.11" } }, "object.entries": { @@ -5427,10 +5427,10 @@ "integrity": "sha1-G/mk3SKI9bM/Opk9JXZh8F0WGl8=", "dev": true, "requires": { - "define-properties": "^1.1.2", - "es-abstract": "^1.6.1", - "function-bind": "^1.1.0", - "has": "^1.0.1" + "define-properties": "1.1.2", + "es-abstract": "1.11.0", + "function-bind": "1.1.1", + "has": "1.0.1" } }, "object.pick": { @@ -5439,7 +5439,7 @@ "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", "dev": true, "requires": { - "isobject": "^3.0.1" + "isobject": "3.0.1" } }, "object.values": { @@ -5448,10 +5448,10 @@ "integrity": "sha1-5STaCbT2b/Bd9FdUbscqyZ8TBpo=", "dev": true, "requires": { - "define-properties": "^1.1.2", - "es-abstract": "^1.6.1", - "function-bind": "^1.1.0", - "has": "^1.0.1" + "define-properties": "1.1.2", + "es-abstract": "1.11.0", + "function-bind": "1.1.1", + "has": "1.0.1" } }, "once": { @@ -5459,7 +5459,7 @@ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "requires": { - "wrappy": "1" + "wrappy": "1.0.2" } }, "onetime": { @@ -5468,7 +5468,7 @@ "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", "dev": true, "requires": { - "mimic-fn": "^1.0.0" + "mimic-fn": "1.2.0" } }, "optimist": { @@ -5476,7 +5476,7 @@ "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.3.7.tgz", "integrity": "sha1-yQlBrVnkJzMokjB00s8ufLxuwNk=", "requires": { - "wordwrap": "~0.0.2" + "wordwrap": "0.0.3" } }, "optionator": { @@ -5485,12 +5485,12 @@ "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", "dev": true, "requires": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.4", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "wordwrap": "~1.0.0" + "deep-is": "0.1.3", + "fast-levenshtein": "2.0.6", + "levn": "0.3.0", + "prelude-ls": "1.1.2", + "type-check": "0.3.2", + "wordwrap": "1.0.0" }, "dependencies": { "wordwrap": { @@ -5512,9 +5512,9 @@ "integrity": "sha1-QrwpAKa1uL0XN2yOiCtlr8zyS/I=", "dev": true, "requires": { - "execa": "^0.7.0", - "lcid": "^1.0.0", - "mem": "^1.1.0" + "execa": "0.7.0", + "lcid": "1.0.0", + "mem": "1.1.0" } }, "os-tmpdir": { @@ -5534,7 +5534,7 @@ "integrity": "sha1-DpK2vty1nwIsE9DxlJ3ILRWQnxw=", "dev": true, "requires": { - "p-try": "^1.0.0" + "p-try": "1.0.0" } }, "p-locate": { @@ -5543,7 +5543,7 @@ "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", "dev": true, "requires": { - "p-limit": "^1.1.0" + "p-limit": "1.2.0" } }, "p-try": { @@ -5558,7 +5558,7 @@ "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", "dev": true, "requires": { - "error-ex": "^1.2.0" + "error-ex": "1.3.1" } }, "parse5": { @@ -5567,7 +5567,7 @@ "integrity": "sha512-rgO9Zg5LLLkfJF9E6CCmXlSE4UVceloys8JrFqCcHloC3usd/kJCyPDwH2SOlzix2j3xaP9sUX3e8+kvkuleAA==", "dev": true, "requires": { - "@types/node": "*" + "@types/node": "10.7.1" } }, "pascalcase": { @@ -5620,7 +5620,7 @@ "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", "dev": true, "requires": { - "pify": "^2.0.0" + "pify": "2.3.0" } }, "pathval": { @@ -5652,22 +5652,22 @@ "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", "dev": true, "requires": { - "pinkie": "^2.0.0" + "pinkie": "2.0.4" } }, "pkg-dir": { "version": "2.0.0", - "resolved": "", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", "dev": true, "requires": { - "find-up": "^2.1.0" + "find-up": "2.1.0" } }, "pluralize": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-7.0.0.tgz", - "integrity": "sha1-KYuJ34uTsCIdv0Ia0rGx6iP8Z3c=", + "integrity": "sha512-ARhBOdzS3e41FbkW/XWrTEtukqqLoK5+Z/4UeDaLuSW+39JPeFgs4gCGqsrJHVZX0fUrx//4OF0K1CUGwlIFow==", "dev": true }, "posix-character-classes": { @@ -5681,21 +5681,21 @@ "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-2.5.3.tgz", "integrity": "sha1-n2XyQngtNwKWNTcQ6byENJDBn2k=", "requires": { - "detect-libc": "^1.0.3", - "expand-template": "^1.0.2", + "detect-libc": "1.0.3", + "expand-template": "1.1.0", "github-from-package": "0.0.0", - "minimist": "^1.2.0", - "mkdirp": "^0.5.1", - "node-abi": "^2.2.0", - "noop-logger": "^0.1.1", - "npmlog": "^4.0.1", - "os-homedir": "^1.0.1", - "pump": "^2.0.1", - "rc": "^1.1.6", - "simple-get": "^2.7.0", - "tar-fs": "^1.13.0", - "tunnel-agent": "^0.6.0", - "which-pm-runs": "^1.0.0" + "minimist": "1.2.0", + "mkdirp": "0.5.1", + "node-abi": "2.4.0", + "noop-logger": "0.1.1", + "npmlog": "4.1.2", + "os-homedir": "1.0.2", + "pump": "2.0.1", + "rc": "1.2.7", + "simple-get": "2.8.1", + "tar-fs": "1.16.2", + "tunnel-agent": "0.6.0", + "which-pm-runs": "1.0.0" } }, "prelude-ls": { @@ -5730,16 +5730,16 @@ "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", "integrity": "sha1-BktyYCsY+Q8pGSuLG8QY/9Hr078=", "requires": { - "asap": "~2.0.3" + "asap": "2.0.6" } }, "prop-types": { "version": "15.6.2", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.2.tgz", - "integrity": "sha1-BdXKd7RFPphdYPx/+MhZCUpJcQI=", + "integrity": "sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ==", "requires": { - "loose-envify": "^1.3.1", - "object-assign": "^4.1.1" + "loose-envify": "1.3.1", + "object-assign": "4.1.1" } }, "prr": { @@ -5759,8 +5759,8 @@ "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", "integrity": "sha1-Ejma3W5M91Jtlzy8i1zi4pCLOQk=", "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" + "end-of-stream": "1.4.1", + "once": "1.4.0" } }, "punycode": { @@ -5779,7 +5779,7 @@ "integrity": "sha512-pDP/NMRAXoTfrhCfyfSEwJAKLaxBU9eApMeBPB1TkDouZmvPerIClV8lTAd+uF8ZiTaVl69e1FCxQrAd/VTjGw==", "dev": true, "requires": { - "performance-now": "^2.1.0" + "performance-now": "2.1.0" } }, "railroad-diagrams": { @@ -5795,7 +5795,7 @@ "dev": true, "requires": { "discontinuous-range": "1.0.0", - "ret": "~0.1.10" + "ret": "0.1.15" } }, "rc": { @@ -5803,10 +5803,10 @@ "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.7.tgz", "integrity": "sha1-ihDKMNWI0ARkNgNyuJDQbazQIpc=", "requires": { - "deep-extend": "^0.5.1", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" + "deep-extend": "0.5.1", + "ini": "1.3.5", + "minimist": "1.2.0", + "strip-json-comments": "2.0.1" } }, "react": { @@ -5814,10 +5814,10 @@ "resolved": "https://registry.npmjs.org/react/-/react-16.4.0.tgz", "integrity": "sha512-K0UrkLXSAekf5nJu89obKUM7o2vc6MMN9LYoKnCa+c+8MJRAT120xzPLENcWSRc7GYKIg0LlgJRDorrufdglQQ==", "requires": { - "fbjs": "^0.8.16", - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "prop-types": "^15.6.0" + "fbjs": "0.8.16", + "loose-envify": "1.3.1", + "object-assign": "4.1.1", + "prop-types": "15.6.2" } }, "react-dom": { @@ -5825,10 +5825,10 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.4.0.tgz", "integrity": "sha512-bbLd+HYpBEnYoNyxDe9XpSG2t9wypMohwQPvKw8Hov3nF7SJiJIgK56b46zHpBUpHb06a1iEuw7G3rbrsnNL6w==", "requires": { - "fbjs": "^0.8.16", - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "prop-types": "^15.6.0" + "fbjs": "0.8.16", + "loose-envify": "1.3.1", + "object-assign": "4.1.1", + "prop-types": "15.6.2" } }, "react-input-autosize": { @@ -5836,7 +5836,7 @@ "resolved": "https://registry.npmjs.org/react-input-autosize/-/react-input-autosize-2.2.1.tgz", "integrity": "sha1-7EKPoVsVkplPtfmqFbsetrr0IPg=", "requires": { - "prop-types": "^15.5.8" + "prop-types": "15.6.2" }, "dependencies": { "core-js": { @@ -5849,8 +5849,8 @@ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz", "integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==", "requires": { - "encoding": "^0.1.11", - "is-stream": "^1.0.1" + "encoding": "0.1.12", + "is-stream": "1.1.0" } } } @@ -5858,29 +5858,29 @@ "react-is": { "version": "16.4.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.4.1.tgz", - "integrity": "sha1-1iTEZQ0sZdvVLHJiK784lDXZd24=", + "integrity": "sha512-xpb0PpALlFWNw/q13A+1aHeyJyLYCg0/cCHPUA43zYluZuIPHaHL3k8OBsTgQtxqW0FhyDEMvi8fZ/+7+r4OSQ==", "dev": true }, "react-reconciler": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.7.0.tgz", - "integrity": "sha1-lhSJQQPl8Tje7rXquvPugOsdAm0=", + "integrity": "sha512-50JwZ3yNyMS8fchN+jjWEJOH3Oze7UmhxeoJLn2j6f3NjpfCRbcmih83XTWmzqtar/ivd5f7tvQhvvhism2fgg==", "dev": true, "requires": { - "fbjs": "^0.8.16", - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "prop-types": "^15.6.0" + "fbjs": "0.8.16", + "loose-envify": "1.3.1", + "object-assign": "4.1.1", + "prop-types": "15.6.2" } }, "react-relay": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/react-relay/-/react-relay-1.6.0.tgz", - "integrity": "sha1-eg7KQ1yBubAdiRfUvKZQfu+83+Q=", + "integrity": "sha512-8clmRHXNo96pcdkA8ZeiqF7xGjE+mjSbdX/INj5upRm2M8AprSrFk2Oz5nH084O+0hvXQhZtFyraXJWQO9ld3A==", "requires": { - "babel-runtime": "^6.23.0", - "fbjs": "^0.8.14", - "prop-types": "^15.5.8", + "babel-runtime": "6.25.0", + "fbjs": "0.8.16", + "prop-types": "15.6.2", "relay-runtime": "1.6.0" } }, @@ -5889,9 +5889,9 @@ "resolved": "https://registry.npmjs.org/react-select/-/react-select-1.2.1.tgz", "integrity": "sha1-ov5YpWnrFNyqZUOBYmC5flOBINE=", "requires": { - "classnames": "^2.2.4", - "prop-types": "^15.5.8", - "react-input-autosize": "^2.1.2" + "classnames": "2.2.6", + "prop-types": "15.6.2", + "react-input-autosize": "2.2.1" }, "dependencies": { "core-js": { @@ -5904,22 +5904,31 @@ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz", "integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==", "requires": { - "encoding": "^0.1.11", - "is-stream": "^1.0.1" + "encoding": "0.1.12", + "is-stream": "1.1.0" } } } }, + "react-tabs": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/react-tabs/-/react-tabs-2.3.0.tgz", + "integrity": "sha512-pYaefgVy76/36AMEP+B8YuVVzDHa3C5UFZ3REU78zolk0qMxEhKvUFofvDCXyLZwf0RZjxIfiwok1BEb18nHyA==", + "requires": { + "classnames": "2.2.6", + "prop-types": "15.6.2" + } + }, "react-test-renderer": { "version": "16.4.1", "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-16.4.1.tgz", - "integrity": "sha1-8vswwse1F9tuWxDtILtrCnzNjXA=", + "integrity": "sha512-wyyiPxRZOTpKnNIgUBOB6xPLTpIzwcQMIURhZvzUqZzezvHjaGNsDPBhMac5fIY3Jf5NuKxoGvV64zDSOECPPQ==", "dev": true, "requires": { - "fbjs": "^0.8.16", - "object-assign": "^4.1.1", - "prop-types": "^15.6.0", - "react-is": "^16.4.1" + "fbjs": "0.8.16", + "object-assign": "4.1.1", + "prop-types": "15.6.2", + "react-is": "16.4.1" } }, "read-pkg": { @@ -5928,9 +5937,9 @@ "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", "dev": true, "requires": { - "load-json-file": "^2.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^2.0.0" + "load-json-file": "2.0.0", + "normalize-package-data": "2.4.0", + "path-type": "2.0.0" } }, "read-pkg-up": { @@ -5939,8 +5948,8 @@ "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", "dev": true, "requires": { - "find-up": "^2.0.0", - "read-pkg": "^2.0.0" + "find-up": "2.1.0", + "read-pkg": "2.0.0" } }, "readable-stream": { @@ -5948,13 +5957,13 @@ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~1.0.6", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.0.3", - "util-deprecate": "~1.0.1" + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "1.0.7", + "safe-buffer": "5.1.1", + "string_decoder": "1.0.3", + "util-deprecate": "1.0.2" }, "dependencies": { "isarray": { @@ -5980,8 +5989,8 @@ "integrity": "sha1-H07OJ+ALC2XgJHpoEOaoXYOldSw=", "dev": true, "requires": { - "extend-shallow": "^3.0.2", - "safe-regex": "^1.1.0" + "extend-shallow": "3.0.2", + "safe-regex": "1.1.0" }, "dependencies": { "is-extendable": { @@ -5989,7 +5998,7 @@ "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", "integrity": "sha1-p0cPnkJnM9gb2B4RVSZOOjUHyrQ=", "requires": { - "is-plain-object": "^2.0.4" + "is-plain-object": "2.0.4" } } } @@ -6000,13 +6009,13 @@ "integrity": "sha512-ztaw4M1VqgMwl9HlPpOuiYgItcHlunW0He2fE6eNfT6E/CF2FtYi9ofOYe4mKntstYk0Fyh/rDRBdS3AnxjlrA==", "dev": true, "requires": { - "define-properties": "^1.1.2" + "define-properties": "1.1.2" } }, "regexpp": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-1.1.0.tgz", - "integrity": "sha1-DjUW3Qt5BPQT0tQZPc5GGMOmias=", + "integrity": "sha512-LOPw8FpgdQF9etWMaAfG/WRthIdXJGYp4mJ2Jgn/2lpkbod9jPn0t9UqN7AxBOKNfzRbYyVfgc7Vk4t/MpnXgw==", "dev": true }, "relay-compiler": { @@ -6018,19 +6027,19 @@ "@babel/generator": "7.0.0-beta.54", "@babel/parser": "7.0.0-beta.54", "@babel/types": "7.0.0-beta.54", - "babel-polyfill": "^6.20.0", + "babel-polyfill": "6.26.0", "babel-preset-fbjs": "2.2.0", - "babel-runtime": "^6.23.0", - "babel-traverse": "^6.26.0", - "chalk": "^1.1.1", - "fast-glob": "^2.2.2", - "fb-watchman": "^2.0.0", + "babel-runtime": "6.25.0", + "babel-traverse": "6.26.0", + "chalk": "1.1.3", + "fast-glob": "2.2.2", + "fb-watchman": "2.0.0", "fbjs": "0.8.17", "graphql-compiler": "1.6.2", - "immutable": "~3.7.6", + "immutable": "3.7.6", "relay-runtime": "1.6.2", - "signedsource": "^1.0.0", - "yargs": "^9.0.0" + "signedsource": "1.0.0", + "yargs": "9.0.1" }, "dependencies": { "@babel/generator": { @@ -6040,10 +6049,10 @@ "dev": true, "requires": { "@babel/types": "7.0.0-beta.54", - "jsesc": "^2.5.1", - "lodash": "^4.17.5", - "source-map": "^0.5.0", - "trim-right": "^1.0.1" + "jsesc": "2.5.1", + "lodash": "4.17.5", + "source-map": "0.5.7", + "trim-right": "1.0.1" } }, "@babel/parser": { @@ -6058,9 +6067,9 @@ "integrity": "sha1-AlrWhJL+1ULBPxTFeaRMhI5TEGM=", "dev": true, "requires": { - "esutils": "^2.0.2", - "lodash": "^4.17.5", - "to-fast-properties": "^2.0.0" + "esutils": "2.0.2", + "lodash": "4.17.5", + "to-fast-properties": "2.0.0" } }, "babel-traverse": { @@ -6069,15 +6078,15 @@ "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=", "dev": true, "requires": { - "babel-code-frame": "^6.26.0", - "babel-messages": "^6.23.0", - "babel-runtime": "^6.26.0", - "babel-types": "^6.26.0", - "babylon": "^6.18.0", - "debug": "^2.6.8", - "globals": "^9.18.0", - "invariant": "^2.2.2", - "lodash": "^4.17.4" + "babel-code-frame": "6.26.0", + "babel-messages": "6.23.0", + "babel-runtime": "6.26.0", + "babel-types": "6.26.0", + "babylon": "6.18.0", + "debug": "2.6.9", + "globals": "9.18.0", + "invariant": "2.2.4", + "lodash": "4.17.5" }, "dependencies": { "babel-runtime": { @@ -6086,8 +6095,8 @@ "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", "dev": true, "requires": { - "core-js": "^2.4.0", - "regenerator-runtime": "^0.11.0" + "core-js": "2.5.0", + "regenerator-runtime": "0.11.1" } } } @@ -6098,10 +6107,10 @@ "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", "dev": true, "requires": { - "babel-runtime": "^6.26.0", - "esutils": "^2.0.2", - "lodash": "^4.17.4", - "to-fast-properties": "^1.0.3" + "babel-runtime": "6.26.0", + "esutils": "2.0.2", + "lodash": "4.17.5", + "to-fast-properties": "1.0.3" }, "dependencies": { "babel-runtime": { @@ -6110,8 +6119,8 @@ "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", "dev": true, "requires": { - "core-js": "^2.4.0", - "regenerator-runtime": "^0.11.0" + "core-js": "2.5.0", + "regenerator-runtime": "0.11.1" } }, "to-fast-properties": { @@ -6134,13 +6143,13 @@ "integrity": "sha1-xNWY6taUkRJlPWWIsBpc3Nn5D90=", "dev": true, "requires": { - "core-js": "^1.0.0", - "isomorphic-fetch": "^2.1.1", - "loose-envify": "^1.0.0", - "object-assign": "^4.1.0", - "promise": "^7.1.1", - "setimmediate": "^1.0.5", - "ua-parser-js": "^0.7.18" + "core-js": "1.2.7", + "isomorphic-fetch": "2.2.1", + "loose-envify": "1.3.1", + "object-assign": "4.1.1", + "promise": "7.3.1", + "setimmediate": "1.0.5", + "ua-parser-js": "0.7.18" }, "dependencies": { "core-js": { @@ -6169,7 +6178,7 @@ "integrity": "sha512-eWjdlK+OvzW35fbYcFrO+S4dMyxb1LqrCU9HBh1OEEK465Te+l+l2Rmg9RCdcvaPuq7zGC6+Anec2/2aNXtiUA==", "dev": true, "requires": { - "babel-runtime": "^6.23.0", + "babel-runtime": "6.25.0", "fbjs": "0.8.17" } }, @@ -6184,10 +6193,10 @@ "relay-runtime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/relay-runtime/-/relay-runtime-1.6.0.tgz", - "integrity": "sha1-K3AFj7d6TJOhcXUs4Uf47o2KiLk=", + "integrity": "sha512-UJiEHp8CX2uFxXdM0nVLTCQ6yAT0GLmyMceXLISuW/l2a9jrS9a4MdZgdr/9UkkYno7Sj1hU/EUIQ0GaVkou8g==", "requires": { - "babel-runtime": "^6.23.0", - "fbjs": "^0.8.14" + "babel-runtime": "6.25.0", + "fbjs": "0.8.16" } }, "repeat-element": { @@ -6206,34 +6215,34 @@ "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", "requires": { - "is-finite": "^1.0.0" + "is-finite": "1.0.2" } }, "request": { "version": "2.87.0", "resolved": "https://registry.npmjs.org/request/-/request-2.87.0.tgz", - "integrity": "sha1-MvACNc0I1IK00NaNuTqCnA7VdW4=", - "requires": { - "aws-sign2": "~0.7.0", - "aws4": "^1.6.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.5", - "extend": "~3.0.1", - "forever-agent": "~0.6.1", - "form-data": "~2.3.1", - "har-validator": "~5.0.3", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.17", - "oauth-sign": "~0.8.2", - "performance-now": "^2.1.0", - "qs": "~6.5.1", - "safe-buffer": "^5.1.1", - "tough-cookie": "~2.3.3", - "tunnel-agent": "^0.6.0", - "uuid": "^3.1.0" + "integrity": "sha512-fcogkm7Az5bsS6Sl0sibkbhcKsnyon/jV1kF3ajGmF0c8HrttdKTPRT9hieOaQHA5HEq6r8OyWOo/o781C1tNw==", + "requires": { + "aws-sign2": "0.7.0", + "aws4": "1.7.0", + "caseless": "0.12.0", + "combined-stream": "1.0.6", + "extend": "3.0.1", + "forever-agent": "0.6.1", + "form-data": "2.3.2", + "har-validator": "5.0.3", + "http-signature": "1.2.0", + "is-typedarray": "1.0.0", + "isstream": "0.1.2", + "json-stringify-safe": "5.0.1", + "mime-types": "2.1.18", + "oauth-sign": "0.8.2", + "performance-now": "2.1.0", + "qs": "6.5.1", + "safe-buffer": "5.1.1", + "tough-cookie": "2.3.4", + "tunnel-agent": "0.6.0", + "uuid": "3.1.0" }, "dependencies": { "mime-db": { @@ -6246,7 +6255,7 @@ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", "integrity": "sha1-bzI/YKg9ERRvgx/xH9ZuL+VQO7g=", "requires": { - "mime-db": "~1.33.0" + "mime-db": "1.33.0" } } } @@ -6273,7 +6282,7 @@ "integrity": "sha512-P1tKYHVSZ6uFo26mtnve4HQFE3koh1UWVkp8YUC+ESBHe945xWSoXuHHiGarDqcEZ+whpCDnlNw5LON0kLo+sw==", "dev": true, "requires": { - "mime-db": "~1.35.0" + "mime-db": "1.35.0" } }, "request": { @@ -6282,28 +6291,28 @@ "integrity": "sha512-lR3gD69osqm6EYLk9wB/G1W/laGWjzH90t1vEa2xuxHD5KUrSzp9pUSfTm+YC5Nxt2T8nMPEvKlhbQayU7bgFw==", "dev": true, "requires": { - "aws-sign2": "~0.7.0", - "aws4": "^1.6.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.5", - "extend": "~3.0.1", - "forever-agent": "~0.6.1", - "form-data": "~2.3.1", - "har-validator": "~5.0.3", - "hawk": "~6.0.2", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.17", - "oauth-sign": "~0.8.2", - "performance-now": "^2.1.0", - "qs": "~6.5.1", - "safe-buffer": "^5.1.1", - "stringstream": "~0.0.5", - "tough-cookie": "~2.3.3", - "tunnel-agent": "^0.6.0", - "uuid": "^3.1.0" + "aws-sign2": "0.7.0", + "aws4": "1.7.0", + "caseless": "0.12.0", + "combined-stream": "1.0.6", + "extend": "3.0.1", + "forever-agent": "0.6.1", + "form-data": "2.3.2", + "har-validator": "5.0.3", + "hawk": "6.0.2", + "http-signature": "1.2.0", + "is-typedarray": "1.0.0", + "isstream": "0.1.2", + "json-stringify-safe": "5.0.1", + "mime-types": "2.1.19", + "oauth-sign": "0.8.2", + "performance-now": "2.1.0", + "qs": "6.5.1", + "safe-buffer": "5.1.1", + "stringstream": "0.0.6", + "tough-cookie": "2.3.4", + "tunnel-agent": "0.6.0", + "uuid": "3.1.0" } } } @@ -6326,8 +6335,8 @@ "integrity": "sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM=", "dev": true, "requires": { - "caller-path": "^0.1.0", - "resolve-from": "^1.0.0" + "caller-path": "0.1.0", + "resolve-from": "1.0.1" } }, "resolve-from": { @@ -6348,8 +6357,8 @@ "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", "dev": true, "requires": { - "onetime": "^2.0.0", - "signal-exit": "^3.0.2" + "onetime": "2.0.1", + "signal-exit": "3.0.2" } }, "ret": { @@ -6363,15 +6372,15 @@ "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", "requires": { - "align-text": "^0.1.1" + "align-text": "0.1.4" } }, "rimraf": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", - "integrity": "sha1-LtgVDSShbqhlHm1u8PR8QVjOejY=", + "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", "requires": { - "glob": "^7.0.5" + "glob": "7.1.2" } }, "rst-selector-parser": { @@ -6380,8 +6389,8 @@ "integrity": "sha1-gbIw6i/MYGbInjRy3nlChdmwPZE=", "dev": true, "requires": { - "lodash.flattendeep": "^4.4.0", - "nearley": "^2.7.10" + "lodash.flattendeep": "4.4.0", + "nearley": "2.15.1" } }, "run-async": { @@ -6390,7 +6399,7 @@ "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", "dev": true, "requires": { - "is-promise": "^2.1.0" + "is-promise": "2.1.0" } }, "rxjs": { @@ -6413,19 +6422,19 @@ "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", "dev": true, "requires": { - "ret": "~0.1.10" + "ret": "0.1.15" } }, "samsam": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/samsam/-/samsam-1.3.0.tgz", - "integrity": "sha1-jR2TUOJWItow3j5EumkrUiGrfFA=", + "integrity": "sha512-1HwIYD/8UlOtFS3QO3w7ey+SdSDFE4HRNLZoZRYVQefrOY3l17epswImeB1ijgJFQJodIaHcwkp3r/myBjFVbg==", "dev": true }, "semver": { "version": "5.5.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.1.tgz", - "integrity": "sha512-PqpAxfrEhlSUWge8dwIp4tZnQ25DIOthpiaHNIthsjEFQD6EvqUKUDM7L8O2rShkFccYo1VjJR0coWfNkCubRw==" + "integrity": "sha1-ff3YgUvbfKvHvg+x1zTPtmyUBHc=" }, "set-blocking": { "version": "2.0.0", @@ -6438,10 +6447,10 @@ "integrity": "sha1-ca5KiPD+77v1LR6mBPP7MV67YnQ=", "dev": true, "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.3", - "split-string": "^3.0.1" + "extend-shallow": "2.0.1", + "is-extendable": "0.1.1", + "is-plain-object": "2.0.4", + "split-string": "3.1.0" }, "dependencies": { "extend-shallow": { @@ -6450,7 +6459,7 @@ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "dev": true, "requires": { - "is-extendable": "^0.1.0" + "is-extendable": "0.1.1" } } } @@ -6466,7 +6475,7 @@ "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", "dev": true, "requires": { - "shebang-regex": "^1.0.0" + "shebang-regex": "1.0.0" } }, "shebang-regex": { @@ -6496,33 +6505,33 @@ "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-2.8.1.tgz", "integrity": "sha1-DiLpHUV12HYgYgvJEwjVenf0S10=", "requires": { - "decompress-response": "^3.3.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" + "decompress-response": "3.3.0", + "once": "1.4.0", + "simple-concat": "1.0.0" } }, "sinon": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/sinon/-/sinon-6.0.1.tgz", - "integrity": "sha1-Qke5fdj+y+Hhn4uBal5+7TmTvP4=", + "integrity": "sha512-rfszhNcfamK2+ofIPi9XqeH89pH7KGDcAtM+F9CsjHXOK3jzWG99vyhyD2V+r7s4IipmWcWUFYq4ftZ9/Eu2Wg==", "dev": true, "requires": { - "@sinonjs/formatio": "^2.0.0", - "diff": "^3.5.0", - "lodash.get": "^4.4.2", - "lolex": "^2.4.2", - "nise": "^1.3.3", - "supports-color": "^5.4.0", - "type-detect": "^4.0.8" + "@sinonjs/formatio": "2.0.0", + "diff": "3.5.0", + "lodash.get": "4.4.2", + "lolex": "2.7.1", + "nise": "1.4.2", + "supports-color": "5.4.0", + "type-detect": "4.0.8" }, "dependencies": { "supports-color": { "version": "5.4.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", - "integrity": "sha1-HGszdALCE3YF7+GfEP7DkPb6q1Q=", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", "dev": true, "requires": { - "has-flag": "^3.0.0" + "has-flag": "3.0.0" } } } @@ -6535,10 +6544,10 @@ "slice-ansi": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-1.0.0.tgz", - "integrity": "sha1-BE8aSdiEL/MHqta1Be0Xi9lQE00=", + "integrity": "sha512-POqxBK6Lb3q6s047D/XsDVNPnF9Dl8JSaqe9h9lURl0OdNqy/ujDrOiIHtsqXMGbWWTIomRzAMaTyawAU//Reg==", "dev": true, "requires": { - "is-fullwidth-code-point": "^2.0.0" + "is-fullwidth-code-point": "2.0.0" }, "dependencies": { "is-fullwidth-code-point": { @@ -6551,24 +6560,24 @@ }, "slide": { "version": "1.1.6", - "resolved": "", + "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz", "integrity": "sha1-VusCfWW00tzmyy4tMsTUr8nh1wc=", "dev": true }, "snapdragon": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", - "integrity": "sha1-ZJIufFZbDhQgS6GqfWlkJ40lGC0=", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", "dev": true, "requires": { - "base": "^0.11.1", - "debug": "^2.2.0", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "map-cache": "^0.2.2", - "source-map": "^0.5.6", - "source-map-resolve": "^0.5.0", - "use": "^3.1.0" + "base": "0.11.2", + "debug": "2.6.9", + "define-property": "0.2.5", + "extend-shallow": "2.0.1", + "map-cache": "0.2.2", + "source-map": "0.5.7", + "source-map-resolve": "0.5.1", + "use": "3.1.0" }, "dependencies": { "define-property": { @@ -6577,7 +6586,7 @@ "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", "dev": true, "requires": { - "is-descriptor": "^0.1.0" + "is-descriptor": "0.1.6" } }, "extend-shallow": { @@ -6586,7 +6595,7 @@ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "dev": true, "requires": { - "is-extendable": "^0.1.0" + "is-extendable": "0.1.1" } } } @@ -6597,9 +6606,9 @@ "integrity": "sha1-bBdfhv8UvbByRWPo88GwIaKGhTs=", "dev": true, "requires": { - "define-property": "^1.0.0", - "isobject": "^3.0.0", - "snapdragon-util": "^3.0.1" + "define-property": "1.0.0", + "isobject": "3.0.1", + "snapdragon-util": "3.0.1" }, "dependencies": { "define-property": { @@ -6608,36 +6617,36 @@ "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", "dev": true, "requires": { - "is-descriptor": "^1.0.0" + "is-descriptor": "1.0.2" } }, "is-accessor-descriptor": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha1-FpwvbT3x+ZJhgHI2XJsOofaHhlY=", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", "dev": true, "requires": { - "kind-of": "^6.0.0" + "kind-of": "6.0.2" } }, "is-data-descriptor": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha1-2Eh2Mh0Oet0DmQQGq7u9NrqSaMc=", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", "dev": true, "requires": { - "kind-of": "^6.0.0" + "kind-of": "6.0.2" } }, "is-descriptor": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha1-OxWXRqZmBLBPjIFSS6NlxfFNhuw=", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", "dev": true, "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" + "is-accessor-descriptor": "1.0.0", + "is-data-descriptor": "1.0.0", + "kind-of": "6.0.2" } } } @@ -6648,7 +6657,7 @@ "integrity": "sha1-+VZHlIbyrNeXAGk/b3uAXkWrVuI=", "dev": true, "requires": { - "kind-of": "^3.2.0" + "kind-of": "3.2.2" }, "dependencies": { "kind-of": { @@ -6657,7 +6666,7 @@ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "dev": true, "requires": { - "is-buffer": "^1.1.5" + "is-buffer": "1.1.6" } } } @@ -6668,7 +6677,7 @@ "integrity": "sha512-FL1b58BDrqS3A11lJ0zEdnJ3UOKqVxawAkF3k7F0CVN7VQ34aZrV+G8BZ1WC9ZL7NyrwsW0oviwsWDgRuVYtJg==", "dev": true, "requires": { - "hoek": "4.x.x" + "hoek": "4.2.1" } }, "source-map": { @@ -6682,11 +6691,11 @@ "integrity": "sha1-etD1k/IoFZjoVN+A8ZquS5LXoRo=", "dev": true, "requires": { - "atob": "^2.0.0", - "decode-uri-component": "^0.2.0", - "resolve-url": "^0.2.1", - "source-map-url": "^0.4.0", - "urix": "^0.1.0" + "atob": "2.1.1", + "decode-uri-component": "0.2.0", + "resolve-url": "0.2.1", + "source-map-url": "0.4.0", + "urix": "0.1.0" } }, "source-map-support": { @@ -6694,7 +6703,7 @@ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", "integrity": "sha1-Aoam3ovkJkEzhZTpfM6nXwosWF8=", "requires": { - "source-map": "^0.5.6" + "source-map": "0.5.7" } }, "source-map-url": { @@ -6705,48 +6714,48 @@ }, "spawn-wrap": { "version": "1.4.2", - "resolved": "", + "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-1.4.2.tgz", "integrity": "sha512-vMwR3OmmDhnxCVxM8M+xO/FtIp6Ju/mNaDfCMMW7FDcLRTPFWUswec4LXJHTJE2hwTI9O0YBfygu4DalFl7Ylg==", "dev": true, "requires": { - "foreground-child": "^1.5.6", - "mkdirp": "^0.5.0", - "os-homedir": "^1.0.1", - "rimraf": "^2.6.2", - "signal-exit": "^3.0.2", - "which": "^1.3.0" + "foreground-child": "1.5.6", + "mkdirp": "0.5.1", + "os-homedir": "1.0.2", + "rimraf": "2.6.2", + "signal-exit": "3.0.2", + "which": "1.3.0" } }, "spdx-correct": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.0.0.tgz", - "integrity": "sha1-BaW01xU6GVvJLDxCW2nzsqlSTII=", + "integrity": "sha512-N19o9z5cEyc8yQQPukRCZ9EUmb4HUpnrmaL/fxS2pBo2jbfcFRVuFZ/oFC+vZz0MNNk0h80iMn5/S6qGZOL5+g==", "dev": true, "requires": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" + "spdx-expression-parse": "3.0.0", + "spdx-license-ids": "3.0.0" } }, "spdx-exceptions": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.1.0.tgz", - "integrity": "sha1-LHrmEFbHFKW5ubKyr30xHvXHj+k=", + "integrity": "sha512-4K1NsmrlCU1JJgUrtgEeTVyfx8VaYea9J9LvARxhbHtVtohPs/gFGG5yy49beySjlIMhhXZ4QqujIZEfS4l6Cg==", "dev": true }, "spdx-expression-parse": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", - "integrity": "sha1-meEZt6XaAOBUkcn6M4t5BII7QdA=", + "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", "dev": true, "requires": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" + "spdx-exceptions": "2.1.0", + "spdx-license-ids": "3.0.0" } }, "spdx-license-ids": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.0.tgz", - "integrity": "sha1-enzShHDMbToc/m1miG9rxDDTrIc=", + "integrity": "sha512-2+EPwgbnmOIl8HjGBXXMd9NAu02vLjOO1nWw4kmeRDFyHn+M/ETfHxQUK0oXg8ctgVnl9t3rosNVsZ1jG61nDA==", "dev": true }, "split": { @@ -6754,7 +6763,7 @@ "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", "integrity": "sha1-YFvZvjA6pZ+zX5Ip++oN3snqB9k=", "requires": { - "through": "2" + "through": "2.3.8" } }, "split-string": { @@ -6763,7 +6772,7 @@ "integrity": "sha1-fLCd2jqGWFcFxks5pkZgOGguj+I=", "dev": true, "requires": { - "extend-shallow": "^3.0.0" + "extend-shallow": "3.0.2" }, "dependencies": { "is-extendable": { @@ -6771,7 +6780,7 @@ "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", "integrity": "sha1-p0cPnkJnM9gb2B4RVSZOOjUHyrQ=", "requires": { - "is-plain-object": "^2.0.4" + "is-plain-object": "2.0.4" } } } @@ -6787,14 +6796,14 @@ "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.1.tgz", "integrity": "sha1-Ew9Zde3a2WPx1W+SuaxsUfqfg+s=", "requires": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "tweetnacl": "~0.14.0" + "asn1": "0.2.3", + "assert-plus": "1.0.0", + "bcrypt-pbkdf": "1.0.1", + "dashdash": "1.14.1", + "ecc-jsbn": "0.1.1", + "getpass": "0.1.7", + "jsbn": "0.1.1", + "tweetnacl": "0.14.5" } }, "static-extend": { @@ -6803,8 +6812,8 @@ "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", "dev": true, "requires": { - "define-property": "^0.2.5", - "object-copy": "^0.1.0" + "define-property": "0.2.5", + "object-copy": "0.1.0" }, "dependencies": { "define-property": { @@ -6813,7 +6822,7 @@ "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", "dev": true, "requires": { - "is-descriptor": "^0.1.0" + "is-descriptor": "0.1.6" } } } @@ -6830,8 +6839,8 @@ "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", "dev": true, "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" + "is-fullwidth-code-point": "2.0.0", + "strip-ansi": "4.0.0" }, "dependencies": { "ansi-regex": { @@ -6852,7 +6861,7 @@ "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", "dev": true, "requires": { - "ansi-regex": "^3.0.0" + "ansi-regex": "3.0.0" } } } @@ -6863,11 +6872,11 @@ "integrity": "sha512-WoZ+B2ypng1dp4iFLF2kmZlwwlE19gmjgKuhL1FJfDgCREWb3ye3SDVHSzLH6bxfnvYmkCxbzkmWcQZHA4P//Q==", "dev": true, "requires": { - "define-properties": "^1.1.2", - "es-abstract": "^1.10.0", - "function-bind": "^1.1.1", - "has-symbols": "^1.0.0", - "regexp.prototype.flags": "^1.2.0" + "define-properties": "1.1.2", + "es-abstract": "1.11.0", + "function-bind": "1.1.1", + "has-symbols": "1.0.0", + "regexp.prototype.flags": "1.2.0" } }, "string_decoder": { @@ -6875,7 +6884,7 @@ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", "integrity": "sha1-D8Z9fBQYJd6UKC3VNr7GubzoYKs=", "requires": { - "safe-buffer": "~5.1.0" + "safe-buffer": "5.1.1" } }, "stringstream": { @@ -6889,7 +6898,7 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "requires": { - "ansi-regex": "^2.0.0" + "ansi-regex": "2.1.1" } }, "strip-bom": { @@ -6926,12 +6935,12 @@ "integrity": "sha512-S7rnFITmBH1EnyKcvxBh1LjYeQMmnZtCXSEbHcH6S0NoKit24ZuFO/T1vDcLdYsLQkM188PVVhQmzKIuThNkKg==", "dev": true, "requires": { - "ajv": "^6.0.1", - "ajv-keywords": "^3.0.0", - "chalk": "^2.1.0", - "lodash": "^4.17.4", + "ajv": "6.5.2", + "ajv-keywords": "3.2.0", + "chalk": "2.4.1", + "lodash": "4.17.5", "slice-ansi": "1.0.0", - "string-width": "^2.1.1" + "string-width": "2.1.1" }, "dependencies": { "ajv": { @@ -6940,10 +6949,10 @@ "integrity": "sha512-hOs7GfvI6tUI1LfZddH82ky6mOMyTuY0mk7kE2pWpmhhUSkumzaTO5vbVwij39MdwPQWCV4Zv57Eo06NtL/GVA==", "dev": true, "requires": { - "fast-deep-equal": "^2.0.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.1" + "fast-deep-equal": "2.0.1", + "fast-json-stable-stringify": "2.0.0", + "json-schema-traverse": "0.4.1", + "uri-js": "4.2.2" } }, "ansi-styles": { @@ -6952,7 +6961,7 @@ "integrity": "sha1-QfuyAkPlCxK+DwS43tvwdSDOhB0=", "dev": true, "requires": { - "color-convert": "^1.9.0" + "color-convert": "1.9.1" } }, "chalk": { @@ -6961,9 +6970,9 @@ "integrity": "sha1-GMSasWoDe26wFSzIPjRxM4IVtm4=", "dev": true, "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "ansi-styles": "3.2.1", + "escape-string-regexp": "1.0.5", + "supports-color": "5.4.0" } }, "fast-deep-equal": { @@ -6984,7 +6993,7 @@ "integrity": "sha1-HGszdALCE3YF7+GfEP7DkPb6q1Q=", "dev": true, "requires": { - "has-flag": "^3.0.0" + "has-flag": "3.0.0" } } } @@ -6994,13 +7003,13 @@ "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.4.tgz", "integrity": "sha1-7IQJ+un2ZaQ1XMO0CH0IICMruM0=", "requires": { - "chownr": "^1.0.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.3.3", - "minizlib": "^1.1.0", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.2" + "chownr": "1.0.1", + "fs-minipass": "1.2.5", + "minipass": "2.3.3", + "minizlib": "1.1.0", + "mkdirp": "0.5.1", + "safe-buffer": "5.1.2", + "yallist": "3.0.2" }, "dependencies": { "safe-buffer": { @@ -7015,10 +7024,10 @@ "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-1.16.2.tgz", "integrity": "sha1-F+Ujl0fjmffnc0T19TNl8Er1NXc=", "requires": { - "chownr": "^1.0.1", - "mkdirp": "^0.5.1", - "pump": "^1.0.0", - "tar-stream": "^1.1.2" + "chownr": "1.0.1", + "mkdirp": "0.5.1", + "pump": "1.0.3", + "tar-stream": "1.6.0" }, "dependencies": { "pump": { @@ -7026,8 +7035,8 @@ "resolved": "https://registry.npmjs.org/pump/-/pump-1.0.3.tgz", "integrity": "sha1-Xf6DEcM7v2/BgmH580cCxHwIqVQ=", "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" + "end-of-stream": "1.4.1", + "once": "1.4.0" } } } @@ -7037,13 +7046,13 @@ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.0.tgz", "integrity": "sha1-pQ76p7F3YLgsJ7PK5KMBqCVKVxU=", "requires": { - "bl": "^1.0.0", - "buffer-alloc": "^1.1.0", - "end-of-stream": "^1.0.0", - "fs-constants": "^1.0.0", - "readable-stream": "^2.0.0", - "to-buffer": "^1.1.0", - "xtend": "^4.0.0" + "bl": "1.2.2", + "buffer-alloc": "1.1.0", + "end-of-stream": "1.4.1", + "fs-constants": "1.0.0", + "readable-stream": "2.3.3", + "to-buffer": "1.1.1", + "xtend": "4.0.1" } }, "temp": { @@ -7051,8 +7060,8 @@ "resolved": "https://registry.npmjs.org/temp/-/temp-0.8.3.tgz", "integrity": "sha1-4Ma8TSa5AxJEEOT+2BEDAU38H1k=", "requires": { - "os-tmpdir": "^1.0.0", - "rimraf": "~2.2.6" + "os-tmpdir": "1.0.2", + "rimraf": "2.2.8" }, "dependencies": { "rimraf": { @@ -7068,11 +7077,11 @@ "integrity": "sha1-36Ii8DSAvKaSB8pyizfXS0X3JPo=", "dev": true, "requires": { - "arrify": "^1.0.1", - "micromatch": "^3.1.8", - "object-assign": "^4.1.0", - "read-pkg-up": "^1.0.1", - "require-main-filename": "^1.0.1" + "arrify": "1.0.1", + "micromatch": "3.1.10", + "object-assign": "4.1.1", + "read-pkg-up": "1.0.1", + "require-main-filename": "1.0.1" }, "dependencies": { "find-up": { @@ -7081,8 +7090,8 @@ "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", "dev": true, "requires": { - "path-exists": "^2.0.0", - "pinkie-promise": "^2.0.0" + "path-exists": "2.1.0", + "pinkie-promise": "2.0.1" } }, "load-json-file": { @@ -7091,11 +7100,11 @@ "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", "dev": true, "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^2.2.0", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0", - "strip-bom": "^2.0.0" + "graceful-fs": "4.1.11", + "parse-json": "2.2.0", + "pify": "2.3.0", + "pinkie-promise": "2.0.1", + "strip-bom": "2.0.0" } }, "path-exists": { @@ -7104,7 +7113,7 @@ "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", "dev": true, "requires": { - "pinkie-promise": "^2.0.0" + "pinkie-promise": "2.0.1" } }, "path-type": { @@ -7113,9 +7122,9 @@ "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", "dev": true, "requires": { - "graceful-fs": "^4.1.2", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" + "graceful-fs": "4.1.11", + "pify": "2.3.0", + "pinkie-promise": "2.0.1" } }, "read-pkg": { @@ -7124,9 +7133,9 @@ "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", "dev": true, "requires": { - "load-json-file": "^1.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^1.0.0" + "load-json-file": "1.1.0", + "normalize-package-data": "2.4.0", + "path-type": "1.1.0" } }, "read-pkg-up": { @@ -7135,8 +7144,8 @@ "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", "dev": true, "requires": { - "find-up": "^1.0.0", - "read-pkg": "^1.0.0" + "find-up": "1.1.2", + "read-pkg": "1.1.0" } }, "strip-bom": { @@ -7145,7 +7154,7 @@ "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", "dev": true, "requires": { - "is-utf8": "^0.2.0" + "is-utf8": "0.2.1" } } } @@ -7184,7 +7193,7 @@ "integrity": "sha1-jzirlDjhcxXl29izZX6L+yd65Kc=", "dev": true, "requires": { - "os-tmpdir": "~1.0.1" + "os-tmpdir": "1.0.2" } }, "to-buffer": { @@ -7204,7 +7213,7 @@ "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", "dev": true, "requires": { - "kind-of": "^3.0.2" + "kind-of": "3.2.2" }, "dependencies": { "kind-of": { @@ -7213,7 +7222,7 @@ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "dev": true, "requires": { - "is-buffer": "^1.1.5" + "is-buffer": "1.1.6" } } } @@ -7224,10 +7233,10 @@ "integrity": "sha1-E8/dmzNlUvMLUfM6iuG0Knp1mc4=", "dev": true, "requires": { - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "regex-not": "^1.0.2", - "safe-regex": "^1.1.0" + "define-property": "2.0.2", + "extend-shallow": "3.0.2", + "regex-not": "1.0.2", + "safe-regex": "1.1.0" }, "dependencies": { "is-extendable": { @@ -7235,7 +7244,7 @@ "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", "integrity": "sha1-p0cPnkJnM9gb2B4RVSZOOjUHyrQ=", "requires": { - "is-plain-object": "^2.0.4" + "is-plain-object": "2.0.4" } } } @@ -7246,8 +7255,8 @@ "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", "dev": true, "requires": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" + "is-number": "3.0.0", + "repeat-string": "1.6.1" } }, "tough-cookie": { @@ -7255,7 +7264,7 @@ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", "integrity": "sha1-7GDO44rGdQY//JelwYlwV47oNlU=", "requires": { - "punycode": "^1.4.1" + "punycode": "1.4.1" } }, "tree-kill": { @@ -7273,7 +7282,7 @@ "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", "requires": { - "safe-buffer": "^5.0.1" + "safe-buffer": "5.1.1" } }, "tweetnacl": { @@ -7288,7 +7297,7 @@ "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", "dev": true, "requires": { - "prelude-ls": "~1.1.2" + "prelude-ls": "1.1.2" } }, "type-detect": { @@ -7304,14 +7313,14 @@ }, "uglify-js": { "version": "2.8.29", - "resolved": "", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", "dev": true, "optional": true, "requires": { - "source-map": "~0.5.1", - "uglify-to-browserify": "~1.0.0", - "yargs": "~3.10.0" + "source-map": "0.5.7", + "uglify-to-browserify": "1.0.2", + "yargs": "3.10.0" }, "dependencies": { "camelcase": { @@ -7328,8 +7337,8 @@ "dev": true, "optional": true, "requires": { - "center-align": "^0.1.1", - "right-align": "^0.1.1", + "center-align": "0.1.3", + "right-align": "0.1.3", "wordwrap": "0.0.2" } }, @@ -7342,14 +7351,14 @@ }, "yargs": { "version": "3.10.0", - "resolved": "", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", "dev": true, "optional": true, "requires": { - "camelcase": "^1.0.2", - "cliui": "^2.1.0", - "decamelize": "^1.0.0", + "camelcase": "1.2.1", + "cliui": "2.1.0", + "decamelize": "1.2.0", "window-size": "0.1.0" } } @@ -7357,7 +7366,7 @@ }, "uglify-to-browserify": { "version": "1.0.2", - "resolved": "", + "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", "dev": true, "optional": true @@ -7374,10 +7383,10 @@ "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=", "dev": true, "requires": { - "arr-union": "^3.1.0", - "get-value": "^2.0.6", - "is-extendable": "^0.1.1", - "set-value": "^0.4.3" + "arr-union": "3.1.0", + "get-value": "2.0.6", + "is-extendable": "0.1.1", + "set-value": "0.4.3" }, "dependencies": { "extend-shallow": { @@ -7386,7 +7395,7 @@ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "dev": true, "requires": { - "is-extendable": "^0.1.0" + "is-extendable": "0.1.1" } }, "set-value": { @@ -7395,10 +7404,10 @@ "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=", "dev": true, "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.1", - "to-object-path": "^0.3.0" + "extend-shallow": "2.0.1", + "is-extendable": "0.1.1", + "is-plain-object": "2.0.4", + "to-object-path": "0.3.0" } } } @@ -7414,8 +7423,8 @@ "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", "dev": true, "requires": { - "has-value": "^0.3.1", - "isobject": "^3.0.0" + "has-value": "0.3.1", + "isobject": "3.0.1" }, "dependencies": { "has-value": { @@ -7424,9 +7433,9 @@ "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", "dev": true, "requires": { - "get-value": "^2.0.3", - "has-values": "^0.1.4", - "isobject": "^2.0.0" + "get-value": "2.0.6", + "has-values": "0.1.4", + "isobject": "2.1.0" }, "dependencies": { "isobject": { @@ -7460,7 +7469,7 @@ "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", "dev": true, "requires": { - "punycode": "^2.1.0" + "punycode": "2.1.1" }, "dependencies": { "punycode": { @@ -7483,7 +7492,7 @@ "integrity": "sha1-IjeVIL/gfSa1kIAEkIruEuhNBf8=", "dev": true, "requires": { - "deep-equal": "~1.0.1" + "deep-equal": "1.0.1" }, "dependencies": { "deep-equal": { @@ -7497,10 +7506,10 @@ "use": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/use/-/use-3.1.0.tgz", - "integrity": "sha1-FHFr8D/f79AwQK71jYtLhfOnxUQ=", + "integrity": "sha512-6UJEQM/L+mzC3ZJNM56Q4DFGLX/evKGRg15UJHGB9X5j5Z3AFbgZvjUh2yq/UJUY4U5dh7Fal++XbNg1uzpRAw==", "dev": true, "requires": { - "kind-of": "^6.0.2" + "kind-of": "6.0.2" } }, "util-deprecate": { @@ -7516,11 +7525,11 @@ "validate-npm-package-license": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.3.tgz", - "integrity": "sha1-gWQ7y+8b3+zUYjeT3EZIlIupgzg=", + "integrity": "sha512-63ZOUnL4SIXj4L0NixR3L1lcjO38crAbgrTpl28t8jjrfuiOBL5Iygm+60qPs/KsZGzPNg6Smnc/oY16QTjF0g==", "dev": true, "requires": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" + "spdx-correct": "3.0.0", + "spdx-expression-parse": "3.0.0" } }, "verror": { @@ -7528,9 +7537,9 @@ "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", "requires": { - "assert-plus": "^1.0.0", + "assert-plus": "1.0.0", "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" + "extsprintf": "1.3.0" } }, "virtual-dom": { @@ -7540,11 +7549,11 @@ "dev": true, "requires": { "browser-split": "0.0.1", - "error": "^4.3.0", - "ev-store": "^7.0.0", - "global": "^4.3.0", - "is-object": "^1.0.1", - "next-tick": "^0.2.2", + "error": "4.4.0", + "ev-store": "7.0.0", + "global": "4.3.2", + "is-object": "1.0.1", + "next-tick": "0.2.2", "x-is-array": "0.1.0", "x-is-string": "0.1.0" } @@ -7559,7 +7568,7 @@ "resolved": "https://registry.npmjs.org/what-the-status/-/what-the-status-1.0.3.tgz", "integrity": "sha1-lP3NAR/7U6Ijnnb6+NrL78mHdRA=", "requires": { - "split": "^1.0.0" + "split": "1.0.1" } }, "whatwg-fetch": { @@ -7572,7 +7581,7 @@ "resolved": "https://registry.npmjs.org/which/-/which-1.3.0.tgz", "integrity": "sha1-/wS9/AEO5UfXgL7DjhrBwnd9JTo=", "requires": { - "isexe": "^2.0.0" + "isexe": "2.0.0" } }, "which-module": { @@ -7591,7 +7600,7 @@ "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.2.tgz", "integrity": "sha1-Vx4PGwYEY268DfwhsDObvjE0FxA=", "requires": { - "string-width": "^1.0.2" + "string-width": "1.0.2" }, "dependencies": { "string-width": { @@ -7599,16 +7608,16 @@ "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" } } } }, "window-size": { "version": "0.1.0", - "resolved": "", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=", "dev": true, "optional": true @@ -7624,8 +7633,8 @@ "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", "dev": true, "requires": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1" + "string-width": "1.0.2", + "strip-ansi": "3.0.1" }, "dependencies": { "string-width": { @@ -7634,9 +7643,9 @@ "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "dev": true, "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" } } } @@ -7652,18 +7661,18 @@ "integrity": "sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c=", "dev": true, "requires": { - "mkdirp": "^0.5.1" + "mkdirp": "0.5.1" } }, "write-file-atomic": { "version": "1.3.4", - "resolved": "", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-1.3.4.tgz", "integrity": "sha1-+Aek8LHZ6ROuekgRLmzDrxmRtF8=", "dev": true, "requires": { - "graceful-fs": "^4.1.11", - "imurmurhash": "^0.1.4", - "slide": "^1.1.5" + "graceful-fs": "4.1.11", + "imurmurhash": "0.1.4", + "slide": "1.1.6" } }, "x-is-array": { @@ -7700,19 +7709,19 @@ "integrity": "sha1-UqzCP+7Kw0BCB47njAwAf1CF20w=", "dev": true, "requires": { - "camelcase": "^4.1.0", - "cliui": "^3.2.0", - "decamelize": "^1.1.1", - "get-caller-file": "^1.0.1", - "os-locale": "^2.0.0", - "read-pkg-up": "^2.0.0", - "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", - "set-blocking": "^2.0.0", - "string-width": "^2.0.0", - "which-module": "^2.0.0", - "y18n": "^3.2.1", - "yargs-parser": "^7.0.0" + "camelcase": "4.1.0", + "cliui": "3.2.0", + "decamelize": "1.2.0", + "get-caller-file": "1.0.2", + "os-locale": "2.1.0", + "read-pkg-up": "2.0.0", + "require-directory": "2.1.1", + "require-main-filename": "1.0.1", + "set-blocking": "2.0.0", + "string-width": "2.1.1", + "which-module": "2.0.0", + "y18n": "3.2.1", + "yargs-parser": "7.0.0" } }, "yargs-parser": { @@ -7721,7 +7730,7 @@ "integrity": "sha1-jQrELxbqVd69MyyvTEA4s+P139k=", "dev": true, "requires": { - "camelcase": "^4.1.0" + "camelcase": "4.1.0" } }, "yubikiri": { diff --git a/package.json b/package.json index a4966cfd98..88d28d7564 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "react-dom": "16.4.0", "react-relay": "1.6.0", "react-select": "1.2.1", + "react-tabs": "^2.3.0", "relay-runtime": "1.6.0", "temp": "0.8.3", "tinycolor2": "1.4.1", From b96d2460243da8495f578d996f1eb713b6d4cbbe Mon Sep 17 00:00:00 2001 From: Tilde Ann Thurium Date: Fri, 7 Sep 2018 15:30:28 -0700 Subject: [PATCH 0304/4252] Move some content into tabs You get a tab. And you get a tab. Everybody gets a tab! --- lib/views/issueish-detail-view.js | 74 ++++++++++++++++++++----------- styles/issueish-detail-view.less | 5 +++ 2 files changed, 54 insertions(+), 25 deletions(-) diff --git a/lib/views/issueish-detail-view.js b/lib/views/issueish-detail-view.js index 8e81f6ab31..b3cf986aec 100644 --- a/lib/views/issueish-detail-view.js +++ b/lib/views/issueish-detail-view.js @@ -2,6 +2,7 @@ import React from 'react'; import {graphql, createRefetchContainer} from 'react-relay'; import PropTypes from 'prop-types'; import cx from 'classnames'; +import { Tab, Tabs, TabList, TabPanel } from 'react-tabs'; import IssueTimelineController from '../controllers/issue-timeline-controller'; import PrTimelineContainer from '../controllers/pr-timeline-controller'; @@ -169,37 +170,60 @@ export class BareIssueishDetailView extends React.Component {
- {isPr &&
- -
} + {/* tabs start here! */} + + + Conversation + Commits + Checks + Files Changed + + + {/* conversation */} + +
+ {issueish.reactionGroups.map(group => ( + group.users.totalCount > 0 + ? + {reactionTypeToEmoji[group.content]}   {group.users.totalCount} + + : null + ))} +
+
+ + {/* commits */} + + {isPr ? + : + + } + + + {/* checks */} + + {isPr &&
+ +
} +
+ + {/* files changed */} + + +
No description provided.'} switchToIssueish={this.props.switchToIssueish} /> -
- {issueish.reactionGroups.map(group => ( - group.users.totalCount > 0 - ? - {reactionTypeToEmoji[group.content]}   {group.users.totalCount} - - : null - ))} -
- - {isPr ? - : - - } -
- - - {/* commits */} - + No description provided.'} + switchToIssueish={this.props.switchToIssueish} + /> {isPr ? + {/* commits */} + + + {/* checks */} {isPr &&
@@ -219,11 +223,6 @@ export class BareIssueishDetailView extends React.Component { - No description provided.'} - switchToIssueish={this.props.switchToIssueish} - /> -