From 4da74c344e7c7949dda4a3ea4e4ec27bd92e192f Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 10 Feb 2017 18:25:25 -0800 Subject: [PATCH] Revert "Merge pull request #487 from atom/ku-discard-lines" Due to bug in discard logic #509 This reverts commit b11e36b5b89d9ab76f8b3a811d4856bb90618c49, reversing changes made to 8e09a04a7a7c4ef29b5b90914b83686e4df0e686. --- lib/controllers/file-patch-controller.js | 26 ---- lib/controllers/git-controller.js | 108 +------------ lib/controllers/git-panel-controller.js | 5 - lib/discard-changes-in-buffer.js | 35 ----- lib/git-shell-out-strategy.js | 40 +---- lib/helpers.js | 8 - lib/models/file-discard-history.js | 65 -------- lib/models/repository.js | 63 +------- lib/views/file-patch-view.js | 114 ++++---------- lib/views/git-panel-view.js | 5 - lib/views/staging-view.js | 6 +- menus/git.cson | 18 +-- styles/file-patch-view.less | 25 ---- styles/pane-view.less | 1 + test/controllers/git-controller.test.js | 163 +------------------- test/discard-changes-in-buffer.test.js | 183 ----------------------- test/git-shell-out-strategy.test.js | 79 ---------- test/models/repository.test.js | 28 ---- test/views/staging-view.test.js | 9 +- 19 files changed, 49 insertions(+), 932 deletions(-) delete mode 100644 lib/discard-changes-in-buffer.js delete mode 100644 lib/models/file-discard-history.js delete mode 100644 test/discard-changes-in-buffer.test.js diff --git a/lib/controllers/file-patch-controller.js b/lib/controllers/file-patch-controller.js index beaeb96ce8..ec9c6c0dcb 100644 --- a/lib/controllers/file-patch-controller.js +++ b/lib/controllers/file-patch-controller.js @@ -40,8 +40,6 @@ export default class FilePatchController { ); } else { // NOTE: Outer div is required for etch to render elements correctly - const filePath = this.props.filePatch.getPath(); - const hasUndoHistory = this.props.repository ? this.hasUndoHistory() : false; return (
); @@ -172,14 +164,6 @@ export default class FilePatchController { } } - @autobind - diveIntoCorrespondingFilePatch() { - const filePath = this.props.filePatch.getPath(); - const stagingStatus = this.props.stagingStatus === 'staged' ? 'unstaged' : 'staged'; - this.props.quietlySelectItem(filePath, stagingStatus); - return this.props.didDiveIntoFilePath(filePath, stagingStatus, {amending: this.props.isAmending}); - } - didUpdateFilePatch() { // FilePatch was mutated so all we need to do is re-render return etch.update(this); @@ -203,14 +187,4 @@ export default class FilePatchController { textEditor.setCursorBufferPosition(position); return textEditor; } - - @autobind - undoLastDiscard() { - return this.props.undoLastDiscard(this.props.filePatch.getPath()); - } - - @autobind - hasUndoHistory() { - return this.props.repository.hasUndoHistory(this.props.filePatch.getPath()); - } } diff --git a/lib/controllers/git-controller.js b/lib/controllers/git-controller.js index 44f7a00445..415b8fc558 100644 --- a/lib/controllers/git-controller.js +++ b/lib/controllers/git-controller.js @@ -1,6 +1,6 @@ import path from 'path'; -import {CompositeDisposable, Disposable, File, TextBuffer} from 'atom'; +import {CompositeDisposable, Disposable, File} from 'atom'; import React from 'react'; import {autobind} from 'core-decorators'; @@ -18,14 +18,11 @@ import GitPanelController from './git-panel-controller'; import StatusBarTileController from './status-bar-tile-controller'; import ModelObserver from '../models/model-observer'; import ModelStateRegistry from '../models/model-state-registry'; -import discardChangesInBuffer from '../discard-changes-in-buffer'; -import {CannotRestoreError} from '../models/file-discard-history'; const nullFilePatchState = { filePath: null, filePatch: null, stagingStatus: null, - partiallyStaged: null, }; export default class GitController extends React.Component { @@ -185,15 +182,9 @@ export default class GitController extends React.Component { commandRegistry={this.props.commandRegistry} filePatch={this.state.filePatch} stagingStatus={this.state.stagingStatus} - isAmending={this.state.amending} - isPartiallyStaged={this.state.partiallyStaged} onRepoRefresh={this.onRepoRefresh} didSurfaceFile={this.surfaceFromFileAtPath} - didDiveIntoFilePath={this.diveIntoFilePatchForPath} - quietlySelectItem={this.quietlySelectItem} openFiles={this.openFiles} - discardLines={this.discardLines} - undoLastDiscard={this.undoLastDiscard} /> @@ -220,10 +211,9 @@ export default class GitController extends React.Component { const staged = stagingStatus === 'staged'; const filePatch = await repository.getFilePatchForPath(filePath, {staged, amending: staged && amending}); - const partiallyStaged = await repository.isPartiallyStaged(filePath); return new Promise(resolve => { if (filePatch) { - this.setState({filePath, filePatch, stagingStatus, partiallyStaged}, () => { + this.setState({filePath, filePatch, stagingStatus}, () => { // TODO: can be better done w/ a prop? if (activate && this.filePatchControllerPane) { this.filePatchControllerPane.activate(); @@ -346,98 +336,4 @@ export default class GitController extends React.Component { handleChangeTab(activeTab) { this.setState({activeTab}); } - - @autobind - async discardLines(lines) { - const relFilePath = this.state.filePatch.getPath(); - const absfilePath = path.join(this.props.repository.getWorkingDirectoryPath(), relFilePath); - let buffer, disposable; - const isSafe = async () => { - const editor = this.props.workspace.getTextEditors().find(e => e.getPath() === absfilePath); - if (editor) { - buffer = editor.getBuffer(); - if (buffer.isModified()) { - this.props.notificationManager.addError('Cannot discard lines.', {description: 'You have unsaved changes.'}); - return false; - } - } else { - buffer = new TextBuffer({filePath: absfilePath, load: true}); - await new Promise(resolve => { - disposable = buffer.onDidReload(() => { - disposable.dispose(); - resolve(); - }); - }); - } - return true; - }; - const snapshots = await this.props.repository.storeBeforeAndAfterBlobs(relFilePath, isSafe, () => { - this.discardChangesInBuffer(buffer, this.state.filePatch, lines); - }); - if (disposable) { disposable.dispose(); } - return snapshots; - } - - discardChangesInBuffer(buffer, filePatch, lines) { - discardChangesInBuffer(buffer, filePatch, lines); - } - - @autobind - async undoLastDiscard(filePath) { - const relFilePath = this.state.filePatch.getPath(); - const absfilePath = path.join(this.props.repository.getWorkingDirectoryPath(), relFilePath); - const isSafe = () => { - const editor = this.props.workspace.getTextEditors().find(e => e.getPath() === absfilePath); - if (editor && editor.getBuffer().isModified()) { - this.notifyInabilityToUndo(relFilePath, 'You have unsaved changes.'); - return false; - } - return true; - }; - try { - await this.props.repository.attemptToRestoreBlob(filePath, isSafe); - } catch (e) { - if (e instanceof CannotRestoreError) { - this.notifyInabilityToUndo(relFilePath, 'Contents have been modified since last discard.'); - } else if (e.stdErr.match(/fatal: Not a valid object name/)) { - this.notifyInabilityToUndo(relFilePath, 'Discard history has expired.'); - this.props.repository.clearDiscardHistoryForPath(filePath); - } else { - // eslint-disable-next-line no-console - console.error(e); - } - } - } - - notifyInabilityToUndo(filePath, description) { - const openPreDiscardVersion = () => this.openFileBeforeLastDiscard(filePath); - this.props.notificationManager.addError( - 'Cannot undo last discard.', - { - description: `${description} Would you like to open pre-discard version of "${filePath}" in new buffer?`, - buttons: [{ - text: 'Open in new buffer', - onDidClick: openPreDiscardVersion, - dismissable: true, - }], - }, - ); - } - - async openFileBeforeLastDiscard(filePath) { - const {beforeSha} = await this.props.repository.getLastHistorySnapshotsForPath(filePath); - const contents = await this.props.repository.getBlobContents(beforeSha); - const editor = await this.props.workspace.open(); - editor.setText(contents); - return editor; - } - - @autobind - quietlySelectItem(filePath, stagingStatus) { - if (this.gitPanelController) { - return this.gitPanelController.getWrappedComponent().quietlySelectItem(filePath, stagingStatus); - } else { - return null; - } - } } diff --git a/lib/controllers/git-panel-controller.js b/lib/controllers/git-panel-controller.js index 20d2177642..489aa6bde9 100644 --- a/lib/controllers/git-panel-controller.js +++ b/lib/controllers/git-panel-controller.js @@ -231,9 +231,4 @@ export default class GitPanelController { isFocused() { return this.refs.gitPanel.isFocused(); } - - @autobind - quietlySelectItem(filePath, stagingStatus) { - return this.refs.gitPanel.quitelySelectItem(filePath, stagingStatus); - } } diff --git a/lib/discard-changes-in-buffer.js b/lib/discard-changes-in-buffer.js deleted file mode 100644 index 9a78a3a0a0..0000000000 --- a/lib/discard-changes-in-buffer.js +++ /dev/null @@ -1,35 +0,0 @@ -export default function discardChangesInBuffer(buffer, filePatch, discardedLines) { - buffer.transact(() => { - let addedCount = 0; - let removedCount = 0; - let deletedCount = 0; - filePatch.getHunks().forEach(hunk => { - hunk.getLines().forEach(line => { - if (discardedLines.has(line)) { - if (line.status === 'deleted') { - const row = (line.oldLineNumber - deletedCount) + addedCount - removedCount - 1; - buffer.insert([row, 0], line.text + '\n'); - addedCount++; - } else if (line.status === 'added') { - const row = line.newLineNumber + addedCount - removedCount - 1; - if (buffer.lineForRow(row) === line.text) { - buffer.deleteRow(row); - removedCount++; - } else { - throw new Error(buffer.lineForRow(row) + ' does not match ' + line.text); - } - } else if (line.status === 'nonewline') { - // TODO: handle no new line case - } else { - throw new Error(`unrecognized status: ${line.status}. Must be 'added' or 'deleted'`); - } - } - if (line.getStatus() === 'deleted') { - deletedCount++; - } - }); - }); - }); - - buffer.save(); -} diff --git a/lib/git-shell-out-strategy.js b/lib/git-shell-out-strategy.js index 88f392a7c7..cd3cd275b3 100644 --- a/lib/git-shell-out-strategy.js +++ b/lib/git-shell-out-strategy.js @@ -7,7 +7,7 @@ import {parse as parseDiff} from 'what-the-diff'; import GitPromptServer from './git-prompt-server'; import AsyncQueue from './async-queue'; -import {readFile, writeFile, fsStat, deleteFileOrFolder} from './helpers'; +import {readFile, fsStat, deleteFileOrFolder} from './helpers'; const LINE_ENDING_REGEX = /\r?\n/; @@ -36,7 +36,7 @@ export default class GitShellOutStrategy { } // Execute a command and read the output using the embedded Git environment - exec(args, stdin = null, useGitPromptServer = false, redirectFilePath = null) { + exec(args, stdin = null, useGitPromptServer = false) { /* eslint-disable no-console */ const subscriptions = new CompositeDisposable(); return this.commandQueue.push(async () => { @@ -99,9 +99,6 @@ export default class GitShellOutStrategy { err.command = formattedArgs; return Promise.reject(err); } - if (redirectFilePath) { - return writeFile(redirectFilePath, stdout); - } return stdout; }); }); @@ -271,19 +268,6 @@ export default class GitShellOutStrategy { return rawDiffs[0]; } - async isPartiallyStaged(filePath) { - const args = ['status', '--short', '--', filePath]; - const output = await this.exec(args); - const results = output.trim().split(LINE_ENDING_REGEX); - if (results.length === 2) { - return true; - } else if (results.length === 1) { - return ['MM', 'AM', 'MD'].includes(results[0].slice(0, 2)); - } else { - throw new Error(`Unexpected output for ${args.join(' ')}: ${output}`); - } - } - /** * Miscellaneous getters */ @@ -461,26 +445,6 @@ export default class GitShellOutStrategy { return []; } } - - async createBlob({filePath, stdin} = {}) { - let output; - if (filePath) { - output = await this.exec(['hash-object', '-w', filePath]); - } else if (stdin) { - output = await this.exec(['hash-object', '-w', '--stdin'], stdin); - } else { - throw new Error('Must supply file path or stdin'); - } - return output.trim(); - } - - async restoreBlob(filePath, sha) { - await this.exec(['cat-file', '-p', sha], null, null, path.join(this.workingDir, filePath)); - } - - async getBlobContents(sha) { - return await this.exec(['cat-file', '-p', sha]); - } } function buildAddedFilePatch(filePath, contents, stats) { diff --git a/lib/helpers.js b/lib/helpers.js index f8c4ea4f43..af4936f008 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -9,14 +9,6 @@ export function readFile(absoluteFilePath, encoding = 'utf8') { }); } -export function writeFile(absoluteFilePath, contents) { - return new Promise((resolve, reject) => { - fs.writeFile(absoluteFilePath, contents, err => { - if (err) { return reject(err); } else { return resolve(); } - }); - }); -} - export function deleteFileOrFolder(path) { return new Promise((resolve, reject) => { fs.remove(path, err => { diff --git a/lib/models/file-discard-history.js b/lib/models/file-discard-history.js deleted file mode 100644 index 80101f0e89..0000000000 --- a/lib/models/file-discard-history.js +++ /dev/null @@ -1,65 +0,0 @@ -export class CannotRestoreError extends Error { } - -export default class FileDiscardHistory { - constructor(createBlob, restoreBlob, setConfig) { - this.createBlob = createBlob; - this.restoreBlob = restoreBlob; - this.setConfig = setConfig; - this.blobHistoryByFilePath = {}; - } - - async storeBlobs(filePath, isSafe, destructiveAction) { - const beforeSha = await this.createBlob({filePath}); - const isNotSafe = !(await isSafe()); - if (isNotSafe) { return null; } - destructiveAction(); - const afterSha = await this.createBlob({filePath}); - const snapshots = {beforeSha, afterSha}; - const history = this.blobHistoryByFilePath[filePath]; - if (history) { - history.push(snapshots); - if (history.length >= 60) { this.blobHistoryByFilePath[filePath] = history.slice(30); } - } else { - this.blobHistoryByFilePath[filePath] = [snapshots]; - } - await this.serializeHistory(); - return snapshots; - } - - async attemptToRestoreBlob(filePath, isSafe) { - const history = this.blobHistoryByFilePath[filePath]; - const {beforeSha, afterSha} = history[history.length - 1]; - const currentSha = await this.createBlob({filePath}); - if (currentSha === afterSha && isSafe()) { - await this.restoreBlob(filePath, beforeSha); - history.pop(); - await this.serializeHistory(); - } else { - throw new CannotRestoreError('Cannot restore file. Contents have been modified since last discard.'); - } - } - - hasUndoHistory(filePath) { - const history = this.blobHistoryByFilePath[filePath]; - return !!history && history.length > 0; - } - - async serializeHistory() { - const historySha = await this.createBlob({stdin: JSON.stringify(this.blobHistoryByFilePath)}); - await this.setConfig('atomGithub.historySha', historySha); - } - - updateHistory(history) { - this.blobHistoryByFilePath = history; - } - - async clearHistoryForPath(filePath) { - this.blobHistoryByFilePath[filePath] = []; - await this.serializeHistory(); - } - - getLastHistorySnapshotsForPath(filePath) { - const history = this.blobHistoryByFilePath[filePath]; - return history[history.length - 1]; - } -} diff --git a/lib/models/repository.js b/lib/models/repository.js index 5f41378ceb..e579ef0673 100644 --- a/lib/models/repository.js +++ b/lib/models/repository.js @@ -1,13 +1,11 @@ import path from 'path'; -import {Emitter, Disposable} from 'atom'; -import {autobind} from 'core-decorators'; +import {Emitter} from 'atom'; import GitShellOutStrategy from '../git-shell-out-strategy'; import FilePatch from './file-patch'; import Hunk from './hunk'; import HunkLine from './hunk-line'; -import FileDiscardHistory from './file-discard-history'; import {readFile} from '../helpers'; const MERGE_MARKER_REGEX = /^(>|<){7} \S+$/m; @@ -35,9 +33,7 @@ export default class Repository { static async open(workingDirectoryPath, gitStrategy) { const strategy = gitStrategy || new GitShellOutStrategy(workingDirectoryPath); if (await strategy.isGitRepository()) { - const historySha = await strategy.getConfig('atomGithub.historySha'); - const history = historySha ? JSON.parse(await strategy.getBlobContents(historySha)) : {}; - return new Repository(workingDirectoryPath, strategy, history); + return new Repository(workingDirectoryPath, strategy); } else { return null; } @@ -62,7 +58,7 @@ export default class Repository { } } - constructor(workingDirectoryPath, gitStrategy, history) { + constructor(workingDirectoryPath, gitStrategy) { this.workingDirectoryPath = workingDirectoryPath; this.emitter = new Emitter(); this.stagedFilePatchesByPath = new Map(); @@ -70,18 +66,12 @@ export default class Repository { this.unstagedFilePatchesByPath = new Map(); this.git = gitStrategy; this.destroyed = false; - this.fileDiscardHistory = new FileDiscardHistory(this.createBlob, this.restoreBlob, this.setConfig); - if (history) { this.fileDiscardHistory.updateHistory(history); } - const onWindowFocus = () => this.updateDiscardHistory(); - window.addEventListener('focus', onWindowFocus); - this.windowFocusDisposable = new Disposable(() => window.removeEventListener('focus', onWindowFocus)); } destroy() { this.destroyed = true; this.emitter.emit('did-destroy'); this.emitter.dispose(); - this.windowFocusDisposable.dispose(); } isDestroyed() { @@ -171,10 +161,6 @@ export default class Repository { this.unstagedFilePatchesByPath = new Map(); } - isPartiallyStaged(filePath) { - return this.git.isPartiallyStaged(filePath); - } - getCachedFilePatchForPath(filePath, {staged, amending} = {}) { if (staged && amending) { return this.stagedFilePatchesSinceParentCommitByPath.get(filePath); @@ -381,7 +367,6 @@ export default class Repository { return this.git.getConfig(configOption, options); } - @autobind setConfig(configOption, configValue, options) { return this.git.setConfig(configOption, configValue, options); } @@ -397,46 +382,4 @@ export default class Repository { getCommitState() { return this.commitState; } - - @autobind - createBlob(options = {}) { - return this.git.createBlob(options); - } - - @autobind - restoreBlob(filePath, sha) { - return this.git.restoreBlob(filePath, sha); - } - - @autobind - getBlobContents(sha) { - return this.git.getBlobContents(sha); - } - - storeBeforeAndAfterBlobs(filePath, isSafe, destructiveAction) { - return this.fileDiscardHistory.storeBlobs(filePath, isSafe, destructiveAction); - } - - attemptToRestoreBlob(filePath, isSafe) { - return this.fileDiscardHistory.attemptToRestoreBlob(filePath, isSafe); - } - - hasUndoHistory(filePath) { - return this.fileDiscardHistory.hasUndoHistory(filePath); - } - - async updateDiscardHistory() { - const historySha = await this.git.getConfig('atomGithub.historySha'); - const history = historySha ? JSON.parse(await this.git.getBlobContents(historySha)) : {}; - this.fileDiscardHistory.updateHistory(history); - } - - clearDiscardHistoryForPath(filePath) { - return this.fileDiscardHistory.clearHistoryForPath(filePath); - } - - @autobind - getLastHistorySnapshotsForPath(filePath) { - return this.fileDiscardHistory.getLastHistorySnapshotsForPath(filePath); - } } diff --git a/lib/views/file-patch-view.js b/lib/views/file-patch-view.js index dd63a393fc..ea22cb5a8d 100644 --- a/lib/views/file-patch-view.js +++ b/lib/views/file-patch-view.js @@ -43,12 +43,6 @@ export default class FilePatchView { 'github:select-next-hunk': () => this.selectNextHunk(), 'github:select-previous-hunk': () => this.selectPreviousHunk(), 'github:open-file': () => this.openFile(), - 'github:view-corresponding-diff': () => { - this.props.isPartiallyStaged && this.props.didDiveIntoCorrespondingFilePatch(); - }, - 'github:discard-selected-lines': () => this.discardSelection(), - 'github:undo-last-file-diff-discard': () => this.props.hasUndoHistory && this.props.undoLastDiscard(), - 'core:undo': () => this.props.hasUndoHistory && this.props.undoLastDiscard(), })); } @@ -75,79 +69,37 @@ export default class FilePatchView {
-
- - {unstaged ? 'Unstaged Changes for ' : 'Staged Changes for '} - {this.props.filePath} - - {this.renderButtonGroup()} -
- -
- {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; - - return ( - this.mousedownOnHeader(e, hunk)} - mousedownOnLine={this.mousedownOnLine} - mousemoveOnLine={this.mousemoveOnLine} - contextMenuOnItem={this.contextMenuOnItem} - didClickStageButton={() => this.didClickStageButtonForHunk(hunk)} - registerView={this.props.registerHunkView} - /> - ); - })} -
+ {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; + + return ( + this.mousedownOnHeader(e, hunk)} + mousedownOnLine={this.mousedownOnLine} + mousemoveOnLine={this.mousemoveOnLine} + contextMenuOnItem={this.contextMenuOnItem} + didClickStageButton={() => this.didClickStageButtonForHunk(hunk)} + registerView={this.props.registerHunkView} + /> + ); + })}
); } - @autobind - renderButtonGroup() { - const unstaged = this.props.stagingStatus === 'unstaged'; - - return ( - - {this.props.hasUndoHistory ? ( - - ) : null} - {this.props.isPartiallyStaged ? ( - - - ); - } - @autobind async contextMenuOnItem(event, hunk, line) { const mode = this.selection.getMode(); @@ -340,7 +292,6 @@ export default class FilePatchView { this.element.focus(); } - @autobind openFile() { let lineNumber = 0; const firstSelectedLine = Array.from(this.selection.getSelectedLines())[0]; @@ -352,15 +303,4 @@ export default class FilePatchView { } return this.props.openCurrentFile({lineNumber}); } - - @autobind - stageOrUnstageAll() { - this.selectAll(); - this.didConfirm(); - } - - discardSelection() { - const selectedLines = this.selection.getSelectedLines(); - return selectedLines.size ? this.props.discardLines(selectedLines) : null; - } } diff --git a/lib/views/git-panel-view.js b/lib/views/git-panel-view.js index 043067bbec..f331fdb45b 100644 --- a/lib/views/git-panel-view.js +++ b/lib/views/git-panel-view.js @@ -138,9 +138,4 @@ export default class GitPanelView { isFocused() { return this.element === document.activeElement || this.element.contains(document.activeElement); } - - @autobind - quitelySelectItem(filePath, stagingStatus) { - return this.refs.stagingView.quietlySelectItem(filePath, stagingStatus); - } } diff --git a/lib/views/staging-view.js b/lib/views/staging-view.js index 88c7c9798f..76936fa23f 100644 --- a/lib/views/staging-view.js +++ b/lib/views/staging-view.js @@ -68,7 +68,7 @@ export default class StagingView { 'github:unstage-all': () => this.unstageAll(), })); this.subscriptions.add(this.props.commandRegistry.add(this.refs.unstagedChanges, { - 'github:discard-changes-in-selected-file': () => this.discardChanges(), + 'github:discard-changes': () => this.discardChanges(), })); window.addEventListener('mouseup', this.mouseup); this.subscriptions.add(new Disposable(() => window.removeEventListener('mouseup', this.mouseup))); @@ -312,7 +312,7 @@ export default class StagingView { { this.props.unstagedChanges.length ? this.renderStageAllButton() : null } -
+
{ this.props.unstagedChanges.map(filePatch => ( 'path.txt'}, + filePatch: Symbol('filePatch'), stagingStatus: 'stagingStatus', }; wrapper.setState(state); @@ -403,165 +403,4 @@ describe('GitController', function() { }); }); - describe('discarding and restoring changed lines', () => { - describe('discardLines(lines)', () => { - it('only discards lines if buffer is unmodified, otherwise notifies user', async () => { - const workdirPath = await cloneRepository('three-files'); - const repository = await buildRepository(workdirPath); - - fs.writeFileSync(path.join(workdirPath, 'a.txt'), 'modification\n'); - const unstagedFilePatch = await repository.getFilePatchForPath('a.txt'); - - const editor = await workspace.open(path.join(workdirPath, 'a.txt')); - - app = React.cloneElement(app, {repository}); - const wrapper = shallow(app); - const state = { - filePath: 'a.txt', - filePatch: unstagedFilePatch, - stagingStatus: 'unstaged', - }; - wrapper.setState(state); - - // unmodified buffer - const discardChangesSpy = sinon.spy(wrapper.instance(), 'discardChangesInBuffer'); - const hunkLines = unstagedFilePatch.getHunks()[0].getLines(); - await wrapper.instance().discardLines(new Set([hunkLines[0]])); - assert.isTrue(discardChangesSpy.called); - - // modified buffer - discardChangesSpy.reset(); - sinon.stub(notificationManager, 'addError'); - const buffer = editor.getBuffer(); - buffer.append('new line'); - await wrapper.instance().discardLines(new Set(unstagedFilePatch.getHunks()[0].getLines())); - assert.isFalse(discardChangesSpy.called); - assert.deepEqual(notificationManager.addError.args[0], ['Cannot discard lines.', {description: 'You have unsaved changes.'}]); - }); - }); - - describe('undoLastDiscard(filePath)', () => { - let unstagedFilePatch, repository, absFilePath, wrapper; - beforeEach(async () => { - const workdirPath = await cloneRepository('multi-line-file'); - repository = await buildRepository(workdirPath); - - absFilePath = path.join(workdirPath, 'sample.js'); - fs.writeFileSync(absFilePath, 'foo\nbar\nbaz\n'); - unstagedFilePatch = await repository.getFilePatchForPath('sample.js'); - - app = React.cloneElement(app, {repository}); - wrapper = shallow(app); - wrapper.setState({ - filePath: 'sample.js', - filePatch: unstagedFilePatch, - stagingStatus: 'unstaged', - }); - }); - - it('reverses last discard for file path', async () => { - const contents1 = fs.readFileSync(absFilePath, 'utf8'); - await wrapper.instance().discardLines(new Set(unstagedFilePatch.getHunks()[0].getLines().slice(0, 2))); - const contents2 = fs.readFileSync(absFilePath, 'utf8'); - assert.notEqual(contents1, contents2); - unstagedFilePatch = await repository.getFilePatchForPath('sample.js'); - wrapper.setState({filePatch: unstagedFilePatch}); - await wrapper.instance().discardLines(new Set(unstagedFilePatch.getHunks()[0].getLines().slice(2, 4))); - const contents3 = fs.readFileSync(absFilePath, 'utf8'); - assert.notEqual(contents2, contents3); - - await wrapper.instance().undoLastDiscard('sample.js'); - assert.equal(fs.readFileSync(absFilePath, 'utf8'), contents2); - await wrapper.instance().undoLastDiscard('sample.js'); - assert.equal(fs.readFileSync(absFilePath, 'utf8'), contents1); - }); - - it('does not undo if buffer is modified', async () => { - const contents1 = fs.readFileSync(absFilePath, 'utf8'); - await wrapper.instance().discardLines(new Set(unstagedFilePatch.getHunks()[0].getLines().slice(0, 2))); - const contents2 = fs.readFileSync(absFilePath, 'utf8'); - assert.notEqual(contents1, contents2); - - // modify buffer - const editor = await workspace.open(absFilePath); - editor.getBuffer().append('new line'); - - const restoreBlob = sinon.spy(repository, 'restoreBlob'); - sinon.stub(notificationManager, 'addError'); - - await repository.refresh(); - unstagedFilePatch = await repository.getFilePatchForPath('sample.js'); - wrapper.setState({filePatch: unstagedFilePatch}); - await wrapper.instance().undoLastDiscard('sample.js'); - const notificationArgs = notificationManager.addError.args[0]; - assert.equal(notificationArgs[0], 'Cannot undo last discard.'); - assert.match(notificationArgs[1].description, /You have unsaved changes./); - assert.isFalse(restoreBlob.called); - }); - - it('does not undo if buffer contents differ from results of discard action', async () => { - const contents1 = fs.readFileSync(absFilePath, 'utf8'); - await wrapper.instance().discardLines(new Set(unstagedFilePatch.getHunks()[0].getLines().slice(0, 2))); - const contents2 = fs.readFileSync(absFilePath, 'utf8'); - assert.notEqual(contents1, contents2); - - // change file contents on disk - fs.writeFileSync(absFilePath, 'change file contents'); - - const restoreBlob = sinon.spy(repository, 'restoreBlob'); - sinon.stub(notificationManager, 'addError'); - - await repository.refresh(); - unstagedFilePatch = await repository.getFilePatchForPath('sample.js'); - wrapper.setState({filePatch: unstagedFilePatch}); - await wrapper.instance().undoLastDiscard('sample.js'); - const notificationArgs = notificationManager.addError.args[0]; - assert.equal(notificationArgs[0], 'Cannot undo last discard.'); - assert.match(notificationArgs[1].description, /Contents have been modified since last discard./); - assert.isFalse(restoreBlob.called); - }); - - 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(new Set(unstagedFilePatch.getHunks()[0].getLines().slice(0, 2))); - unstagedFilePatch = await repository.getFilePatchForPath('sample.js'); - wrapper.setState({filePatch: unstagedFilePatch}); - const {beforeSha} = await wrapper.instance().discardLines(new Set(unstagedFilePatch.getHunks()[0].getLines().slice(2, 4))); - - // remove blob from git object store - fs.unlinkSync(path.join(repository.getGitDirectoryPath(), 'objects', beforeSha.slice(0, 2), beforeSha.slice(2))); - - sinon.stub(notificationManager, 'addError'); - assert.isDefined(repository.getLastHistorySnapshotsForPath('sample.js')); - await wrapper.instance().undoLastDiscard('sample.js'); - const notificationArgs = notificationManager.addError.args[0]; - assert.equal(notificationArgs[0], 'Cannot undo last discard.'); - assert.match(notificationArgs[1].description, /Discard history has expired./); - assert.isUndefined(repository.getLastHistorySnapshotsForPath('sample.js')); - }); - }); - - describe('openFileBeforeLastDiscard(filePath)', () => { - it('opens the file in a new editor and loads the contents before the most recent discard', async () => { - const workdirPath = await cloneRepository('multi-line-file'); - const repository = await buildRepository(workdirPath); - - const absFilePath = path.join(workdirPath, 'sample.js'); - fs.writeFileSync(absFilePath, 'foo\nbar\nbaz\n'); - const unstagedFilePatch = await repository.getFilePatchForPath('sample.js'); - - app = React.cloneElement(app, {repository}); - const wrapper = shallow(app); - wrapper.setState({ - filePath: 'sample.js', - filePatch: unstagedFilePatch, - stagingStatus: 'unstaged', - }); - - await wrapper.instance().discardLines(new Set(unstagedFilePatch.getHunks()[0].getLines().slice(0, 2))); - await wrapper.instance().openFileBeforeLastDiscard('sample.js'); - assert.equal(workspace.getActiveTextEditor().getText(), 'foo\nbar\nbaz\n'); - }); - }); - }); }); diff --git a/test/discard-changes-in-buffer.test.js b/test/discard-changes-in-buffer.test.js deleted file mode 100644 index 1667394984..0000000000 --- a/test/discard-changes-in-buffer.test.js +++ /dev/null @@ -1,183 +0,0 @@ -import path from 'path'; -import fs from 'fs'; - -import {TextBuffer} from 'atom'; - -import until from 'test-until'; - -import {cloneRepository, buildRepository} from './helpers'; -import discardChangesInBuffer from '../lib/discard-changes-in-buffer'; - -const getHunkLinesForPatch = filePatch => { - return filePatch.getHunks().reduce((acc, hunk) => { - return acc.concat(hunk.getLines().filter(l => l.isChanged())); - }, []); -}; - -describe('discardChangesInBuffer', () => { - let buffer, repository, filePath; - beforeEach(async () => { - const workdirPath = await cloneRepository('multi-line-file'); - repository = await buildRepository(workdirPath); - filePath = path.join(workdirPath, 'sample.js'); - buffer = new TextBuffer({filePath, load: true}); - await new Promise(resolve => { - const disposable = buffer.onDidReload(() => { - disposable.dispose(); - resolve(); - }); - }); - }); - - it('discards added lines', async () => { - const originalLines = fs.readFileSync(filePath, 'utf8').split('\n'); - const unstagedLines = originalLines.slice(); - unstagedLines.splice(1, 0, 'a', 'b'); - fs.writeFileSync(filePath, unstagedLines.join('\n')); - - // discard non-first line - const unstagedFilePatch1 = await repository.getFilePatchForPath('sample.js'); - let addedLine = getHunkLinesForPatch(unstagedFilePatch1)[1]; - discardChangesInBuffer(buffer, unstagedFilePatch1, new Set([addedLine])); - let remainingLines = unstagedLines.filter(l => l !== addedLine.getText()); - await until(async () => { - await repository.refresh(); - return fs.readFileSync(filePath, 'utf8') === remainingLines.join('\n'); - }); - - // discard first line - const unstagedFilePatch2 = await repository.getFilePatchForPath('sample.js'); - addedLine = getHunkLinesForPatch(unstagedFilePatch2)[0]; - discardChangesInBuffer(buffer, unstagedFilePatch2, new Set([addedLine])); - remainingLines = remainingLines.filter(text => text !== addedLine.getText()); - await until(async () => { - await repository.refresh(); - return fs.readFileSync(filePath) === remainingLines.join('\n'); - }); - - remainingLines.splice(5, 0, 'c', 'd', 'e'); - fs.writeFileSync(filePath, remainingLines.join('\n')); - await assert.async.equal(buffer.getText(), remainingLines.join('\n')); - - // discard multiple lines - const unstagedFilePatch3 = await repository.getFilePatchForPath('sample.js'); - const addedLines = getHunkLinesForPatch(unstagedFilePatch3).slice(1, 3); - discardChangesInBuffer(buffer, unstagedFilePatch3, new Set(addedLines)); - remainingLines = remainingLines.filter(text => !addedLines.map(l => l.getText()).includes(text)); - await until(async () => { - await repository.refresh(); - return fs.readFileSync(filePath, 'utf8') === remainingLines.join('\n'); - }); - }); - - it('discards deleted lines', async () => { - const originalLines = fs.readFileSync(filePath, 'utf8').split('\n'); - const unstagedLines = originalLines.slice(); - let deletedTextLines = unstagedLines.splice(1, 2); - fs.writeFileSync(filePath, unstagedLines.join('\n')); - - // discard non-first line - const unstagedFilePatch1 = await repository.getFilePatchForPath('sample.js'); - let deletedLine = getHunkLinesForPatch(unstagedFilePatch1)[1]; - discardChangesInBuffer(buffer, unstagedFilePatch1, new Set([deletedLine])); - unstagedLines.splice(1, 0, deletedTextLines[1]); - await until(async () => { - await repository.refresh(); - return fs.readFileSync(filePath, 'utf8') === unstagedLines.join('\n'); - }); - - // discard first line - const unstagedFilePatch2 = await repository.getFilePatchForPath('sample.js'); - deletedLine = getHunkLinesForPatch(unstagedFilePatch2)[0]; - discardChangesInBuffer(buffer, unstagedFilePatch2, new Set([deletedLine])); - unstagedLines.splice(1, 0, deletedTextLines[0]); - await until(async () => { - await repository.refresh(); - return fs.readFileSync(filePath) === unstagedLines.join('\n'); - }); - - deletedTextLines = unstagedLines.splice(5, 3); - fs.writeFileSync(filePath, unstagedLines.join('\n')); - - // discard multiple lines - const unstagedFilePatch3 = await repository.getFilePatchForPath('sample.js'); - const deletedLines = getHunkLinesForPatch(unstagedFilePatch3).slice(1, 3); - discardChangesInBuffer(buffer, unstagedFilePatch3, new Set(deletedLines)); - unstagedLines.splice(5, 0, ...deletedTextLines.slice(1, 3)); - await until(async () => { - await repository.refresh(); - return fs.readFileSync(filePath, 'utf8') === unstagedLines.join('\n'); - }); - }); - - it('discards added and deleted lines together', async () => { - const originalLines = fs.readFileSync(filePath, 'utf8').split('\n'); - const unstagedLines = originalLines.slice(); - let deletedTextLines = unstagedLines.splice(1, 2, 'one modified line', 'another modified line'); - fs.writeFileSync(filePath, unstagedLines.join('\n')); - await assert.async.equal(buffer.getText(), unstagedLines.join('\n')); - - const unstagedFilePatch1 = await repository.getFilePatchForPath('sample.js'); - const hunkLines = getHunkLinesForPatch(unstagedFilePatch1); - let deletedLine = hunkLines[1]; - let addedLine = hunkLines[3]; - discardChangesInBuffer(buffer, unstagedFilePatch1, new Set([deletedLine, addedLine])); - unstagedLines.splice(1, 0, deletedTextLines[1]); - let remainingLines = unstagedLines.filter(l => l !== addedLine.getText()); - await until(async () => { - await repository.refresh(); - return fs.readFileSync(filePath, 'utf8') === remainingLines.join('\n'); - }); - - const unstagedFilePatch2 = await repository.getFilePatchForPath('sample.js'); - [deletedLine, addedLine] = getHunkLinesForPatch(unstagedFilePatch2); - discardChangesInBuffer(buffer, unstagedFilePatch2, new Set([deletedLine, addedLine])); - remainingLines.splice(1, 0, deletedTextLines[0]); - remainingLines = remainingLines.filter(l => l !== addedLine.getText()); - await until(async () => { - await repository.refresh(); - return fs.readFileSync(filePath, 'utf8') === remainingLines.join('\n'); - }); - - deletedTextLines = remainingLines.splice(5, 3, 'a', 'b', 'c'); - fs.writeFileSync(filePath, remainingLines.join('\n')); - await assert.async.equal(buffer.getText(), remainingLines.join('\n')); - - const unstagedFilePatch3 = await repository.getFilePatchForPath('sample.js'); - // discard last two deletions and first addition - const linesToDiscard = getHunkLinesForPatch(unstagedFilePatch3).slice(1, 4); - discardChangesInBuffer(buffer, unstagedFilePatch3, new Set(linesToDiscard)); - remainingLines.splice(5, 0, ...deletedTextLines.slice(1, 3)); - remainingLines = remainingLines.filter(l => l !== linesToDiscard[2].getText()); - await until(async () => { - await repository.refresh(); - return fs.readFileSync(filePath, 'utf8') === remainingLines.join('\n'); - }); - }); - - it('allows undoing the discard action', async () => { - const originalFileContents = fs.readFileSync(filePath, 'utf8'); - const originalLines = originalFileContents.split('\n'); - const unstagedLines = originalLines.slice(); - unstagedLines.splice(1, 2, 'one modified line', 'another modified line'); - fs.writeFileSync(filePath, unstagedLines.join('\n')); - await assert.async.equal(buffer.getText(), unstagedLines.join('\n')); - const contentSnapshot1 = buffer.getText(); - - const unstagedFilePatch1 = await repository.getFilePatchForPath('sample.js'); - const hunkLines = getHunkLinesForPatch(unstagedFilePatch1); - let deletedLine = hunkLines[1]; - let addedLine = hunkLines[3]; - discardChangesInBuffer(buffer, unstagedFilePatch1, new Set([deletedLine, addedLine])); - const contentSnapshot2 = buffer.getText(); - - const unstagedFilePatch2 = await repository.getFilePatchForPath('sample.js'); - [deletedLine, addedLine] = getHunkLinesForPatch(unstagedFilePatch2); - discardChangesInBuffer(buffer, unstagedFilePatch2, new Set([deletedLine, addedLine])); - - buffer.undo(); - assert.equal(buffer.getText(), contentSnapshot2); - buffer.undo(); - assert.equal(buffer.getText(), contentSnapshot1); - }); -}); diff --git a/test/git-shell-out-strategy.test.js b/test/git-shell-out-strategy.test.js index 28c04ac9b3..9481dec07b 100644 --- a/test/git-shell-out-strategy.test.js +++ b/test/git-shell-out-strategy.test.js @@ -409,42 +409,6 @@ describe('Git commands', function() { }); }); - describe('isPartiallyStaged(filePath)', () => { - it('returns true if specified file path is partially staged', async () => { - const workingDirPath = await cloneRepository('three-files'); - const git = new GitShellOutStrategy(workingDirPath); - fs.writeFileSync(path.join(workingDirPath, 'a.txt'), 'modified file', 'utf8'); - fs.writeFileSync(path.join(workingDirPath, 'new-file.txt'), 'foo\nbar\nbaz\n', 'utf8'); - fs.writeFileSync(path.join(workingDirPath, 'b.txt'), 'blah blah blah', 'utf8'); - fs.unlinkSync(path.join(workingDirPath, 'c.txt')); - - assert.isFalse(await git.isPartiallyStaged('a.txt')); - assert.isFalse(await git.isPartiallyStaged('b.txt')); - assert.isFalse(await git.isPartiallyStaged('c.txt')); - assert.isFalse(await git.isPartiallyStaged('new-file.txt')); - - await git.stageFiles(['a.txt', 'b.txt', 'c.txt', 'new-file.txt']); - assert.isFalse(await git.isPartiallyStaged('a.txt')); - assert.isFalse(await git.isPartiallyStaged('b.txt')); - assert.isFalse(await git.isPartiallyStaged('c.txt')); - assert.isFalse(await git.isPartiallyStaged('new-file.txt')); - - // modified on both - fs.writeFileSync(path.join(workingDirPath, 'a.txt'), 'more mods', 'utf8'); - // modified in working directory, added on index - fs.writeFileSync(path.join(workingDirPath, 'new-file.txt'), 'foo\nbar\nbaz\nqux\n', 'utf8'); - // deleted in working directory, modified on index - fs.unlinkSync(path.join(workingDirPath, 'b.txt')); - // untracked in working directory, deleted on index - fs.writeFileSync(path.join(workingDirPath, 'c.txt'), 'back baby', 'utf8'); - - assert.isTrue(await git.isPartiallyStaged('a.txt')); - assert.isTrue(await git.isPartiallyStaged('b.txt')); - assert.isTrue(await git.isPartiallyStaged('c.txt')); - assert.isTrue(await git.isPartiallyStaged('new-file.txt')); - }); - }); - describe('isMerging', function() { it('returns true if `.git/MERGE_HEAD` exists', async function() { const workingDirPath = await cloneRepository('merge-conflict'); @@ -684,47 +648,4 @@ describe('Git commands', function() { }); }); }); - - describe('createBlob({filePath})', () => { - it('creates a blob for the file path specified and returns its sha', async () => { - const workingDirPath = await cloneRepository('three-files'); - const git = new GitShellOutStrategy(workingDirPath); - fs.writeFileSync(path.join(workingDirPath, 'a.txt'), 'qux\nfoo\nbar\n', 'utf8'); - const sha = await git.createBlob({filePath: 'a.txt'}); - assert.equal(sha, 'c9f54222977c93ea17ba4a5a53c611fa7f1aaf56'); - const contents = await git.exec(['cat-file', '-p', sha]); - assert.equal(contents, 'qux\nfoo\nbar\n'); - }); - - it('creates a blob for the stdin specified and returns its sha', async () => { - const workingDirPath = await cloneRepository('three-files'); - const git = new GitShellOutStrategy(workingDirPath); - const sha = await git.createBlob({stdin: 'foo\n'}); - assert.equal(sha, '257cc5642cb1a054f08cc83f2d943e56fd3ebe99'); - const contents = await git.exec(['cat-file', '-p', sha]); - assert.equal(contents, 'foo\n'); - }); - }); - - describe('restoreBlob(filePath, sha)', () => { - it('restores blob contents for sha to specified file path', async () => { - const workingDirPath = await cloneRepository('three-files'); - const git = new GitShellOutStrategy(workingDirPath); - fs.writeFileSync(path.join(workingDirPath, 'a.txt'), 'qux\nfoo\nbar\n', 'utf8'); - const sha = await git.createBlob({filePath: 'a.txt'}); - fs.writeFileSync(path.join(workingDirPath, 'a.txt'), 'modifications', 'utf8'); - await git.restoreBlob('a.txt', sha); - assert.equal(fs.readFileSync(path.join(workingDirPath, 'a.txt')), 'qux\nfoo\nbar\n'); - }); - }); - - describe('getBlobContents(sha)', () => { - it('returns blob contents for sha', async () => { - const workingDirPath = await cloneRepository('three-files'); - const git = new GitShellOutStrategy(workingDirPath); - const sha = await git.createBlob({stdin: 'foo\nbar\nbaz\n'}); - const contents = await git.getBlobContents(sha); - assert.equal(contents, 'foo\nbar\nbaz\n'); - }); - }); }); diff --git a/test/models/repository.test.js b/test/models/repository.test.js index b3425dbf8c..2fbeea6372 100644 --- a/test/models/repository.test.js +++ b/test/models/repository.test.js @@ -696,32 +696,4 @@ describe('Repository', function() { assert.deepEqual(await repo.getUnstagedChanges(), []); }); }); - - describe('maintaining discard history across repository instances', () => { - it('restores the history', async () => { - const workingDirPath = await cloneRepository('three-files'); - const repo1 = await buildRepository(workingDirPath); - - fs.writeFileSync(path.join(workingDirPath, 'a.txt'), 'qux\nfoo\nbar\n', 'utf8'); - - const isSafe = () => true; - await repo1.storeBeforeAndAfterBlobs('a.txt', isSafe, () => { - fs.writeFileSync(path.join(workingDirPath, 'a.txt'), 'foo\nbar\n', 'utf8'); - }); - await repo1.storeBeforeAndAfterBlobs('a.txt', isSafe, () => { - fs.writeFileSync(path.join(workingDirPath, 'a.txt'), 'bar\n', 'utf8'); - }); - await repo1.attemptToRestoreBlob('a.txt', isSafe); - - const repo2 = await buildRepository(workingDirPath); - assert.isTrue(repo2.hasUndoHistory('a.txt')); - await repo2.attemptToRestoreBlob('a.txt', isSafe); - assert.equal(fs.readFileSync(path.join(workingDirPath, 'a.txt'), 'utf8'), 'qux\nfoo\nbar\n'); - assert.isFalse(repo2.hasUndoHistory('a.txt')); - - // upon re-focusing first window we update the discard history - await repo1.updateDiscardHistory(); - assert.isFalse(repo1.hasUndoHistory('a.txt')); - }); - }); }); diff --git a/test/views/staging-view.test.js b/test/views/staging-view.test.js index 330a981e91..e49fb9c7cb 100644 --- a/test/views/staging-view.test.js +++ b/test/views/staging-view.test.js @@ -156,17 +156,20 @@ describe('StagingView', function() { view.focus(); await view.selectNext(); assert.isFalse(didSelectFilePath.calledWith(filePatches[1].filePath)); - await assert.async.isTrue(didSelectFilePath.calledWith(filePatches[1].filePath)); + await new Promise(resolve => setTimeout(resolve, 100)); + assert.isTrue(didSelectFilePath.calledWith(filePatches[1].filePath)); await view.selectNext(); assert.isFalse(didSelectMergeConflictFile.calledWith(mergeConflicts[0].filePath)); - await assert.async.isTrue(didSelectMergeConflictFile.calledWith(mergeConflicts[0].filePath)); + await new Promise(resolve => setTimeout(resolve, 100)); + assert.isTrue(didSelectMergeConflictFile.calledWith(mergeConflicts[0].filePath)); document.body.focus(); assert.isFalse(view.isFocused()); didSelectFilePath.reset(); didSelectMergeConflictFile.reset(); await view.selectNext(); - await assert.async.isTrue(didSelectMergeConflictFile.callCount === 0); + await new Promise(resolve => setTimeout(resolve, 100)); + assert.equal(didSelectMergeConflictFile.callCount, 0); view.element.remove(); });