From cca6f09c9ed130c8b8442d23b1e8c91160236cf1 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 30 Oct 2018 09:34:51 -0400 Subject: [PATCH 001/284] CommitPreviewItem skeleton --- lib/items/commit-preview-item.js | 53 ++++++++++++++++++++++ test/items/commit-preview-item.test.js | 63 ++++++++++++++++++++++++++ 2 files changed, 116 insertions(+) create mode 100644 lib/items/commit-preview-item.js create mode 100644 test/items/commit-preview-item.test.js diff --git a/lib/items/commit-preview-item.js b/lib/items/commit-preview-item.js new file mode 100644 index 00000000000..1b6165203b2 --- /dev/null +++ b/lib/items/commit-preview-item.js @@ -0,0 +1,53 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import {Emitter} from 'event-kit'; + +import {WorkdirContextPoolPropType} from '../prop-types'; + +export default class CommitPreviewItem extends React.Component { + static propTypes = { + workdirContextPool: WorkdirContextPoolPropType.isRequired, + workingDirectory: PropTypes.string.isRequired, + } + + static uriPattern = 'atom-github://commit-preview?workdir={workingDirectory}' + + static buildURI(relPath, workingDirectory) { + return `atom-github://commit-preview?workdir=${encodeURIComponent(workingDirectory)}`; + } + + constructor(props) { + super(props); + + this.emitter = new Emitter(); + this.isDestroyed = false; + this.hasTerminatedPendingState = false; + } + + 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() { + /* istanbul ignore else */ + if (!this.isDestroyed) { + this.emitter.emit('did-destroy'); + this.isDestroyed = true; + } + } + + onDidDestroy(callback) { + return this.emitter.on('did-destroy', callback); + } + + render() { + return null; + } +} diff --git a/test/items/commit-preview-item.test.js b/test/items/commit-preview-item.test.js new file mode 100644 index 00000000000..28d2b42a815 --- /dev/null +++ b/test/items/commit-preview-item.test.js @@ -0,0 +1,63 @@ +import React from 'react'; +import {mount} from 'enzyme'; + +import CommitPreviewItem from '../../lib/items/commit-preview-item'; +import PaneItem from '../../lib/atom/pane-item'; +import WorkdirContextPool from '../../lib/models/workdir-context-pool'; +import {cloneRepository} from '../helpers'; + +describe('CommitPreviewItem', function() { + let atomEnv, repository, pool; + + beforeEach(async function() { + atomEnv = global.buildAtomEnvironment(); + const workdir = await cloneRepository(); + + pool = new WorkdirContextPool({ + workspace: atomEnv.workspace, + }); + + repository = pool.add(workdir).getRepository(); + }); + + afterEach(function() { + atomEnv.destroy(); + pool.clear(); + }); + + function buildPaneApp(override = {}) { + const props = { + workdirContextPool: pool, + ...override, + }; + + return ( + + {({itemHolder, params}) => { + return ( + + ); + }} + + ); + } + + function open(wrapper, options = {}) { + const opts = { + workingDirectory: repository.getWorkingDirectoryPath(), + ...options, + }; + const uri = CommitPreviewItem.buildURI(opts.workingDirectory); + return atomEnv.workspace.open(uri); + } + + it('constructs and opens the correct URI', async function() { + const wrapper = mount(buildPaneApp()); + await open(wrapper); + assert.isTrue(wrapper.update().find('CommitPreviewItem').exists()); + }); +}); From 9f91774eab351ad7cf8f0b04d1a8e2dbd2f6becb Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 30 Oct 2018 09:53:01 -0400 Subject: [PATCH 002/284] Stub out the CommitPreviewContainer --- lib/containers/commit-preview-container.js | 12 ++++++++ .../commit-preview-container.test.js | 28 +++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 lib/containers/commit-preview-container.js create mode 100644 test/containers/commit-preview-container.test.js diff --git a/lib/containers/commit-preview-container.js b/lib/containers/commit-preview-container.js new file mode 100644 index 00000000000..46022cca3f7 --- /dev/null +++ b/lib/containers/commit-preview-container.js @@ -0,0 +1,12 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +export default class CommitPreviewContainer extends React.Component { + static propTypes = { + repository: PropTypes.object.isRequired, + } + + render() { + return null; + } +} diff --git a/test/containers/commit-preview-container.test.js b/test/containers/commit-preview-container.test.js new file mode 100644 index 00000000000..49a91323e29 --- /dev/null +++ b/test/containers/commit-preview-container.test.js @@ -0,0 +1,28 @@ +import React from 'react'; +import {shallow} from 'enzyme'; + +import CommitPreviewContainer from '../lib/'; + +describe('CommitPreviewContainer', function() { + let atomEnv; + + beforeEach(function() { + atomEnv = global.buildAtomEnvironment(); + }); + + afterEach(function() { + atomEnv.destroy(); + }); + + function buildApp(override = {}) { + const props = { + ...override, + }; + + return ; + } + + it('renders a loading spinner while the repository is loading'); + + it('renders a loading spinner while the diff is being fetched'); +}); From 7d44ddd2d9d8b7a39d122ac8dff3839a0dc08109 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 30 Oct 2018 09:55:26 -0400 Subject: [PATCH 003/284] CommitPreviewController skeleton --- lib/controllers/commit-preview-controller.js | 12 +++++++++ .../commit-preview-controller.test.js | 26 +++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 lib/controllers/commit-preview-controller.js create mode 100644 test/controllers/commit-preview-controller.test.js diff --git a/lib/controllers/commit-preview-controller.js b/lib/controllers/commit-preview-controller.js new file mode 100644 index 00000000000..c3d34b6261c --- /dev/null +++ b/lib/controllers/commit-preview-controller.js @@ -0,0 +1,12 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +export default class CommitPreviewController extends React.Component { + static propTypes = { + multiFilePatch: PropTypes.object.isRequired, + } + + render() { + return null; + } +} diff --git a/test/controllers/commit-preview-controller.test.js b/test/controllers/commit-preview-controller.test.js new file mode 100644 index 00000000000..44a697831fb --- /dev/null +++ b/test/controllers/commit-preview-controller.test.js @@ -0,0 +1,26 @@ +import React from 'react'; +import {shallow} from 'enzyme'; + +import CommitPreviewController from '../lib/controllers/commit-preview-controller'; + +describe('CommitPreviewController', function() { + let atomEnv; + + beforeEach(function() { + atomEnv = global.buildAtomEnvironment(); + }); + + afterEach(function() { + atomEnv.destroy(); + }); + + function buildApp(override = {}) { + const props = { + ...override, + }; + + return ; + } + + it('renders the CommitPreviewView and passes extra props through'); +}); From 0ac071487092b938b7f80961a03332456d3f9d88 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 30 Oct 2018 09:55:54 -0400 Subject: [PATCH 004/284] It helps if you import the right path --- test/containers/commit-preview-container.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/containers/commit-preview-container.test.js b/test/containers/commit-preview-container.test.js index 49a91323e29..5b05ddb50c1 100644 --- a/test/containers/commit-preview-container.test.js +++ b/test/containers/commit-preview-container.test.js @@ -1,7 +1,7 @@ import React from 'react'; import {shallow} from 'enzyme'; -import CommitPreviewContainer from '../lib/'; +import CommitPreviewContainer from '../lib/containers/commit-preview-container'; describe('CommitPreviewContainer', function() { let atomEnv; From 315d3cf78244f7b4a82d8c452a6309c626c84f64 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 30 Oct 2018 10:05:14 -0400 Subject: [PATCH 005/284] Relative paths are hard okay --- test/containers/commit-preview-container.test.js | 2 +- test/controllers/commit-preview-controller.test.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/containers/commit-preview-container.test.js b/test/containers/commit-preview-container.test.js index 5b05ddb50c1..ffbc020239b 100644 --- a/test/containers/commit-preview-container.test.js +++ b/test/containers/commit-preview-container.test.js @@ -1,7 +1,7 @@ import React from 'react'; import {shallow} from 'enzyme'; -import CommitPreviewContainer from '../lib/containers/commit-preview-container'; +import CommitPreviewContainer from '../../lib/containers/commit-preview-container'; describe('CommitPreviewContainer', function() { let atomEnv; diff --git a/test/controllers/commit-preview-controller.test.js b/test/controllers/commit-preview-controller.test.js index 44a697831fb..e86834e3db6 100644 --- a/test/controllers/commit-preview-controller.test.js +++ b/test/controllers/commit-preview-controller.test.js @@ -1,7 +1,7 @@ import React from 'react'; import {shallow} from 'enzyme'; -import CommitPreviewController from '../lib/controllers/commit-preview-controller'; +import CommitPreviewController from '../../lib/controllers/commit-preview-controller'; describe('CommitPreviewController', function() { let atomEnv; From cdfe186b680f8cd25ec2c36f4e76132a03aba615 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 30 Oct 2018 10:19:07 -0400 Subject: [PATCH 006/284] Basic Item behavior and tests --- lib/items/commit-preview-item.js | 18 +++++++- test/items/commit-preview-item.test.js | 61 ++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 1 deletion(-) diff --git a/lib/items/commit-preview-item.js b/lib/items/commit-preview-item.js index 1b6165203b2..2574abac0cf 100644 --- a/lib/items/commit-preview-item.js +++ b/lib/items/commit-preview-item.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import {Emitter} from 'event-kit'; import {WorkdirContextPoolPropType} from '../prop-types'; +import CommitPreviewContainer from '../containers/commit-preview-container'; export default class CommitPreviewItem extends React.Component { static propTypes = { @@ -48,6 +49,21 @@ export default class CommitPreviewItem extends React.Component { } render() { - return null; + const repository = this.props.workdirContextPool.getContext(this.props.workingDirectory).getRepository(); + + return ( + + ); + } + + getTitle() { + return 'Commit preview'; + } + + getIconName() { + return 'git-commit'; } } diff --git a/test/items/commit-preview-item.test.js b/test/items/commit-preview-item.test.js index 28d2b42a815..ce5c7df0bfe 100644 --- a/test/items/commit-preview-item.test.js +++ b/test/items/commit-preview-item.test.js @@ -58,6 +58,67 @@ describe('CommitPreviewItem', function() { it('constructs and opens the correct URI', async function() { const wrapper = mount(buildPaneApp()); await open(wrapper); + assert.isTrue(wrapper.update().find('CommitPreviewItem').exists()); }); + + it('passes extra props to its container', async function() { + const extra = Symbol('extra'); + const wrapper = mount(buildPaneApp({extra})); + await open(wrapper); + + assert.strictEqual(wrapper.update().find('CommitPreviewContainer').prop('extra'), extra); + }); + + it('locates the repository from the context pool', async function() { + const wrapper = mount(buildPaneApp()); + await open(wrapper); + + assert.strictEqual(wrapper.update().find('CommitPreviewContainer').prop('repository'), repository); + }); + + it('passes an absent repository if the working directory is unrecognized', async function() { + const wrapper = mount(buildPaneApp()); + await open(wrapper, {workingDirectory: '/nah'}); + + assert.isTrue(wrapper.update().find('CommitPreviewContainer').prop('repository').isAbsent()); + }); + + it('returns a fixed title and icon', async function() { + const wrapper = mount(buildPaneApp()); + const item = await open(wrapper); + + assert.strictEqual(item.getTitle(), 'Commit preview'); + assert.strictEqual(item.getIconName(), 'git-commit'); + }); + + 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(); + }); }); From ea24bbecd53b5d7d415d06714cf318871824235c Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 30 Oct 2018 10:19:21 -0400 Subject: [PATCH 007/284] Correct copy/paste fail --- lib/items/commit-preview-item.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/items/commit-preview-item.js b/lib/items/commit-preview-item.js index 2574abac0cf..33d093a3f18 100644 --- a/lib/items/commit-preview-item.js +++ b/lib/items/commit-preview-item.js @@ -13,7 +13,7 @@ export default class CommitPreviewItem extends React.Component { static uriPattern = 'atom-github://commit-preview?workdir={workingDirectory}' - static buildURI(relPath, workingDirectory) { + static buildURI(workingDirectory) { return `atom-github://commit-preview?workdir=${encodeURIComponent(workingDirectory)}`; } From fa9143cd587c10e181850b9936c8e244967dbad0 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 30 Oct 2018 10:25:07 -0400 Subject: [PATCH 008/284] Serialize CommitPreview items --- lib/items/commit-preview-item.js | 7 +++++++ test/items/commit-preview-item.test.js | 15 +++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/lib/items/commit-preview-item.js b/lib/items/commit-preview-item.js index 33d093a3f18..32dd2c40195 100644 --- a/lib/items/commit-preview-item.js +++ b/lib/items/commit-preview-item.js @@ -66,4 +66,11 @@ export default class CommitPreviewItem extends React.Component { getIconName() { return 'git-commit'; } + + serialize() { + return { + deserializer: 'CommitPreviewStub', + uri: CommitPreviewItem.buildURI(this.props.workingDirectory), + }; + } } diff --git a/test/items/commit-preview-item.test.js b/test/items/commit-preview-item.test.js index ce5c7df0bfe..c6fb420a07c 100644 --- a/test/items/commit-preview-item.test.js +++ b/test/items/commit-preview-item.test.js @@ -121,4 +121,19 @@ describe('CommitPreviewItem', function() { sub.dispose(); }); + + it('serializes itself as a CommitPreviewStub', async function() { + const wrapper = mount(buildPaneApp()); + const item0 = await open(wrapper, {workingDirectory: '/dir0'}); + assert.deepEqual(item0.serialize(), { + deserializer: 'CommitPreviewStub', + uri: 'atom-github://commit-preview?workdir=%2Fdir0', + }); + + const item1 = await open(wrapper, {workingDirectory: '/dir1'}); + assert.deepEqual(item1.serialize(), { + deserializer: 'CommitPreviewStub', + uri: 'atom-github://commit-preview?workdir=%2Fdir1', + }); + }); }); From a2d37ce79dcb5a75c659660e4d5d497afc27a610 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 30 Oct 2018 10:25:32 -0400 Subject: [PATCH 009/284] Atom wiring for CommitPreview deserialization --- lib/github-package.js | 10 ++++++++++ package.json | 3 ++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/lib/github-package.js b/lib/github-package.js index b5b65be47ef..64b9b0bcf49 100644 --- a/lib/github-package.js +++ b/lib/github-package.js @@ -379,6 +379,16 @@ export default class GithubPackage { return item; } + createCommitPreviewStub({uri} = {}) { + const item = StubItem.create('git-commit-preview', { + title: 'Commit preview', + }, uri); + if (this.controller) { + this.rerender(); + } + return item; + } + destroyGitTabItem() { if (this.gitTabStubItem) { this.gitTabStubItem.destroy(); diff --git a/package.json b/package.json index 3504ad6e5f3..3617fd9f9d6 100644 --- a/package.json +++ b/package.json @@ -195,6 +195,7 @@ "IssueishPaneItem": "createIssueishPaneItemStub", "GitDockItem": "createDockItemStub", "GithubDockItem": "createDockItemStub", - "FilePatchControllerStub": "createFilePatchControllerStub" + "FilePatchControllerStub": "createFilePatchControllerStub", + "CommitPreviewStub": "createCommitPreviewStub" } } From 0e84b9b5eb3e7314e779c7c10a2766355db59cd1 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 30 Oct 2018 10:28:54 -0400 Subject: [PATCH 010/284] Play nicely with GithubPackage's working directory tracking --- lib/items/commit-preview-item.js | 4 ++++ test/items/commit-preview-item.test.js | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/lib/items/commit-preview-item.js b/lib/items/commit-preview-item.js index 32dd2c40195..b03aaa793df 100644 --- a/lib/items/commit-preview-item.js +++ b/lib/items/commit-preview-item.js @@ -67,6 +67,10 @@ export default class CommitPreviewItem extends React.Component { return 'git-commit'; } + getWorkingDirectory() { + return this.props.workingDirectory; + } + serialize() { return { deserializer: 'CommitPreviewStub', diff --git a/test/items/commit-preview-item.test.js b/test/items/commit-preview-item.test.js index c6fb420a07c..79bdce5a20c 100644 --- a/test/items/commit-preview-item.test.js +++ b/test/items/commit-preview-item.test.js @@ -136,4 +136,10 @@ describe('CommitPreviewItem', function() { uri: 'atom-github://commit-preview?workdir=%2Fdir1', }); }); + + it('has an item-level accessor for the current working directory', async function() { + const wrapper = mount(buildPaneApp()); + const item = await open(wrapper, {workingDirectory: '/dir7'}); + assert.strictEqual(item.getWorkingDirectory(), '/dir7'); + }); }); From 8b09ae5b4e53119768c27da1c6ca9f0950351a35 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 30 Oct 2018 10:50:53 -0400 Subject: [PATCH 011/284] Container loading behavior --- lib/containers/commit-preview-container.js | 30 ++++++++++++++++++- .../commit-preview-container.test.js | 18 +++++++---- 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/lib/containers/commit-preview-container.js b/lib/containers/commit-preview-container.js index 46022cca3f7..ef6fd11170f 100644 --- a/lib/containers/commit-preview-container.js +++ b/lib/containers/commit-preview-container.js @@ -1,12 +1,40 @@ import React from 'react'; import PropTypes from 'prop-types'; +import yubikiri from 'yubikiri'; + +import ObserveModel from '../views/observe-model'; +import LoadingView from '../views/loading-view'; +import CommitPreviewController from '../controllers/commit-preview-controller'; export default class CommitPreviewContainer extends React.Component { static propTypes = { repository: PropTypes.object.isRequired, } + fetchData = repository => { + return yubikiri({ + multiFilePatch: {}, + }); + } + render() { - return null; + return ( + + {this.renderResult} + + ); + } + + renderResult = data => { + if (this.props.repository.isLoading() || data === null) { + return ; + } + + return ( + + ); } } diff --git a/test/containers/commit-preview-container.test.js b/test/containers/commit-preview-container.test.js index ffbc020239b..5953d2d1abc 100644 --- a/test/containers/commit-preview-container.test.js +++ b/test/containers/commit-preview-container.test.js @@ -1,13 +1,17 @@ import React from 'react'; -import {shallow} from 'enzyme'; +import {mount} from 'enzyme'; import CommitPreviewContainer from '../../lib/containers/commit-preview-container'; +import {cloneRepository, buildRepository} from '../helpers'; describe('CommitPreviewContainer', function() { - let atomEnv; + let atomEnv, repository; - beforeEach(function() { + beforeEach(async function() { atomEnv = global.buildAtomEnvironment(); + + const workdir = await cloneRepository(); + repository = await buildRepository(workdir); }); afterEach(function() { @@ -16,13 +20,15 @@ describe('CommitPreviewContainer', function() { function buildApp(override = {}) { const props = { + repository, ...override, }; return ; } - it('renders a loading spinner while the repository is loading'); - - it('renders a loading spinner while the diff is being fetched'); + it('renders a loading spinner while the repository is loading', function() { + const wrapper = mount(buildApp()); + assert.isTrue(wrapper.find('LoadingView').exists()); + }); }); From 0f06d442e1a1ac7a0c31934123e4dedd680479b7 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 30 Oct 2018 11:10:12 -0400 Subject: [PATCH 012/284] A trivial kind of MultiFilePatch --- lib/models/patch/multi-file-patch.js | 9 +++ test/models/patch/multi-file-patch.test.js | 65 ++++++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 lib/models/patch/multi-file-patch.js create mode 100644 test/models/patch/multi-file-patch.test.js diff --git a/lib/models/patch/multi-file-patch.js b/lib/models/patch/multi-file-patch.js new file mode 100644 index 00000000000..b47b90f5450 --- /dev/null +++ b/lib/models/patch/multi-file-patch.js @@ -0,0 +1,9 @@ +export default class MultiFilePatch { + constructor(filePatches) { + this.filePatches = filePatches; + } + + getFilePatches() { + return this.filePatches; + } +} diff --git a/test/models/patch/multi-file-patch.test.js b/test/models/patch/multi-file-patch.test.js new file mode 100644 index 00000000000..3c0a38f5257 --- /dev/null +++ b/test/models/patch/multi-file-patch.test.js @@ -0,0 +1,65 @@ +import {TextBuffer} from 'atom'; + +import MultiFilePatch from '../../../lib/models/patch/multi-file-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 {Unchanged, Addition, Deletion} from '../../../lib/models/patch/region'; + +describe('MultiFilePatch', function() { + it('has an accessor for its file patches', function() { + const filePatches = [buildFilePatchFixture(0), buildFilePatchFixture(1)]; + const mp = new MultiFilePatch(filePatches); + assert.strictEqual(mp.getFilePatches(), filePatches); + }); +}); + +function buildFilePatchFixture(index) { + const buffer = new TextBuffer(); + for (let i = 0; i < 8; i++) { + buffer.append(`file-${index} line-${i}\n`); + } + + const layers = { + hunk: buffer.addMarkerLayer(), + unchanged: buffer.addMarkerLayer(), + addition: buffer.addMarkerLayer(), + deletion: buffer.addMarkerLayer(), + noNewline: buffer.addMarkerLayer(), + }; + + const mark = (layer, start, end = start) => layer.markRange([[start, 0], [end, Infinity]]); + + const hunks = [ + new Hunk({ + oldStartRow: 0, newStartRow: 0, oldRowCount: 3, newRowCount: 3, + sectionHeading: `file-${index} hunk-0`, + marker: mark(layers.hunk, 0, 3), + regions: [ + new Unchanged(mark(layers.unchanged, 0)), + new Addition(mark(layers.addition, 1)), + new Deletion(mark(layers.deletion, 2)), + new Unchanged(mark(layers.unchanged, 3)), + ], + }), + new Hunk({ + oldStartRow: 10, newStartRow: 10, oldRowCount: 3, newRowCount: 3, + sectionHeading: `file-${index} hunk-1`, + marker: mark(layers.hunk, 4, 7), + regions: [ + new Unchanged(mark(layers.unchanged, 4)), + new Addition(mark(layers.addition, 5)), + new Deletion(mark(layers.deletion, 6)), + new Unchanged(mark(layers.unchanged, 7)), + ], + }), + ]; + + const patch = new Patch({status: 'modified', hunks, buffer, layers}); + + const oldFile = new File({path: `file-${index}.txt`, mode: '100644'}); + const newFile = new File({path: `file-${index}.txt`, mode: '100644'}); + + return new FilePatch(oldFile, newFile, patch); +} From d6fc872553cb4dcbfdbde8ae84a51b3a04119f14 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 30 Oct 2018 11:14:36 -0400 Subject: [PATCH 013/284] Custom PropType for MultiFilePatches --- lib/containers/commit-preview-container.js | 3 ++- lib/controllers/commit-preview-controller.js | 5 +++-- lib/prop-types.js | 4 ++++ 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/containers/commit-preview-container.js b/lib/containers/commit-preview-container.js index ef6fd11170f..d8af3a424d3 100644 --- a/lib/containers/commit-preview-container.js +++ b/lib/containers/commit-preview-container.js @@ -5,6 +5,7 @@ import yubikiri from 'yubikiri'; import ObserveModel from '../views/observe-model'; import LoadingView from '../views/loading-view'; import CommitPreviewController from '../controllers/commit-preview-controller'; +import MultiFilePatch from '../models/patch/multi-file-patch'; export default class CommitPreviewContainer extends React.Component { static propTypes = { @@ -13,7 +14,7 @@ export default class CommitPreviewContainer extends React.Component { fetchData = repository => { return yubikiri({ - multiFilePatch: {}, + multiFilePatch: new MultiFilePatch([]), }); } diff --git a/lib/controllers/commit-preview-controller.js b/lib/controllers/commit-preview-controller.js index c3d34b6261c..8642647b262 100644 --- a/lib/controllers/commit-preview-controller.js +++ b/lib/controllers/commit-preview-controller.js @@ -1,9 +1,10 @@ import React from 'react'; -import PropTypes from 'prop-types'; + +import {MultiFilePatchPropType} from '../prop-types'; export default class CommitPreviewController extends React.Component { static propTypes = { - multiFilePatch: PropTypes.object.isRequired, + multiFilePatch: MultiFilePatchPropType.isRequired, } render() { diff --git a/lib/prop-types.js b/lib/prop-types.js index 13d0b4d394c..f00c709f8f3 100644 --- a/lib/prop-types.js +++ b/lib/prop-types.js @@ -130,6 +130,10 @@ export const FilePatchItemPropType = PropTypes.shape({ status: PropTypes.string.isRequired, }); +export const MultiFilePatchPropType = PropTypes.shape({ + getFilePatches: PropTypes.func.isRequired, +}); + const statusNames = [ 'added', 'deleted', From db731e1abd0925f1d816c26ddee376c9a9187e85 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 30 Oct 2018 12:59:36 -0400 Subject: [PATCH 014/284] Parse individual FilePatches in a set --- lib/models/patch/builder.js | 8 ++- lib/models/patch/index.js | 2 +- test/models/patch/builder.test.js | 102 +++++++++++++++++++++++++++++- 3 files changed, 108 insertions(+), 4 deletions(-) diff --git a/lib/models/patch/builder.js b/lib/models/patch/builder.js index a773e3d9768..0d44ee5d0db 100644 --- a/lib/models/patch/builder.js +++ b/lib/models/patch/builder.js @@ -5,8 +5,9 @@ import File, {nullFile} from './file'; import Patch from './patch'; import {Unchanged, Addition, Deletion, NoNewline} from './region'; import FilePatch from './file-patch'; +import MultiFilePatch from './multi-file-patch'; -export default function buildFilePatch(diffs) { +export function buildFilePatch(diffs) { if (diffs.length === 0) { return emptyDiffFilePatch(); } else if (diffs.length === 1) { @@ -18,6 +19,11 @@ export default function buildFilePatch(diffs) { } } +export function buildMultiFilePatch(diffs) { + // TODO: handle symlink/content pairs + return new MultiFilePatch(diffs.map(singleDiffFilePatch)); +} + function emptyDiffFilePatch() { return FilePatch.createNull(); } diff --git a/lib/models/patch/index.js b/lib/models/patch/index.js index 596fcfde501..525043dbc41 100644 --- a/lib/models/patch/index.js +++ b/lib/models/patch/index.js @@ -1 +1 @@ -export {default as buildFilePatch} from './builder'; +export {buildFilePatch, buildMultiFilePatch} from './builder'; diff --git a/test/models/patch/builder.test.js b/test/models/patch/builder.test.js index 6b2ce62401e..60eab52a71e 100644 --- a/test/models/patch/builder.test.js +++ b/test/models/patch/builder.test.js @@ -1,5 +1,5 @@ -import {buildFilePatch} from '../../../lib/models/patch'; -import {assertInPatch} from '../../helpers'; +import {buildFilePatch, buildMultiFilePatch} from '../../../lib/models/patch'; +import {assertInPatch, assertInFilePatch} from '../../helpers'; describe('buildFilePatch', function() { it('returns a null patch for an empty diff list', function() { @@ -501,6 +501,104 @@ describe('buildFilePatch', function() { }); }); + describe('with multiple diffs', function() { + it('creates a MultiFilePatch containing each', function() { + const mp = buildMultiFilePatch([ + { + oldPath: 'first', oldMode: '100644', newPath: 'first', newMode: '100755', status: 'modified', + hunks: [ + { + oldStartLine: 1, oldLineCount: 2, newStartLine: 1, newLineCount: 4, + lines: [ + ' line-0', + '+line-1', + '+line-2', + ' line-3', + ], + }, + { + oldStartLine: 10, oldLineCount: 3, newStartLine: 12, newLineCount: 2, + lines: [ + ' line-4', + '-line-5', + ' line-6', + ], + }, + ], + }, + { + oldPath: 'second', oldMode: '100644', newPath: 'second', newMode: '100644', status: 'modified', + hunks: [ + { + oldStartLine: 5, oldLineCount: 3, newStartLine: 5, newLineCount: 3, + lines: [ + ' line-5', + '+line-6', + '-line-7', + ' line-8', + ], + }, + ], + }, + { + oldPath: 'third', oldMode: '100755', newPath: 'third', newMode: '100755', status: 'added', + hunks: [ + { + oldStartLine: 1, oldLineCount: 0, newStartLine: 1, newLineCount: 3, + lines: [ + '+line-0', + '+line-1', + '+line-2', + ], + }, + ], + }, + ]); + + assert.lengthOf(mp.getFilePatches(), 3); + assert.strictEqual(mp.getFilePatches()[0].getOldPath(), 'first'); + assertInFilePatch(mp.getFilePatches()[0]).hunks( + { + startRow: 0, endRow: 3, header: '@@ -1,2 +1,4 @@', regions: [ + {kind: 'unchanged', string: ' line-0\n', range: [[0, 0], [0, 6]]}, + {kind: 'addition', string: '+line-1\n+line-2\n', range: [[1, 0], [2, 6]]}, + {kind: 'unchanged', string: ' line-3\n', range: [[3, 0], [3, 6]]}, + ], + }, + { + startRow: 4, endRow: 6, header: '@@ -10,3 +12,2 @@', regions: [ + {kind: 'unchanged', string: ' line-4\n', range: [[4, 0], [4, 6]]}, + {kind: 'deletion', string: '-line-5\n', range: [[5, 0], [5, 6]]}, + {kind: 'unchanged', string: ' line-6\n', range: [[6, 0], [6, 6]]}, + ], + }, + ); + assert.strictEqual(mp.getFilePatches()[1].getOldPath(), 'second'); + assertInFilePatch(mp.getFilePatches()[1]).hunks( + { + startRow: 0, endRow: 3, header: '@@ -5,3 +5,3 @@', regions: [ + {kind: 'unchanged', string: ' line-5\n', range: [[0, 0], [0, 6]]}, + {kind: 'addition', string: '+line-6\n', range: [[1, 0], [1, 6]]}, + {kind: 'deletion', string: '-line-7\n', range: [[2, 0], [2, 6]]}, + {kind: 'unchanged', string: ' line-8\n', range: [[3, 0], [3, 6]]}, + ], + }, + ); + assert.strictEqual(mp.getFilePatches()[2].getOldPath(), 'third'); + assertInFilePatch(mp.getFilePatches()[2]).hunks( + { + startRow: 0, endRow: 2, header: '@@ -1,0 +1,3 @@', regions: [ + {kind: 'addition', string: '+line-0\n+line-1\n+line-2\n', range: [[0, 0], [2, 6]]}, + ], + }, + ); + }); + + it('identifies a file that was deleted and replaced by a symlink'); + + it('identifies a symlink that was deleted and replaced by a file'); + }); + it('throws an error with an unexpected number of diffs', function() { assert.throws(() => buildFilePatch([1, 2, 3]), /Unexpected number of diffs: 3/); }); From 42c833fba730e2b78a294b17a364571711f27491 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 30 Oct 2018 13:49:30 -0400 Subject: [PATCH 015/284] Identify paired mode+content diffs in multi-diffs --- lib/models/patch/builder.js | 35 ++++++++++- test/models/patch/builder.test.js | 100 +++++++++++++++++++++++++++++- 2 files changed, 131 insertions(+), 4 deletions(-) diff --git a/lib/models/patch/builder.js b/lib/models/patch/builder.js index 0d44ee5d0db..c2bef5fe72e 100644 --- a/lib/models/patch/builder.js +++ b/lib/models/patch/builder.js @@ -20,8 +20,39 @@ export function buildFilePatch(diffs) { } export function buildMultiFilePatch(diffs) { - // TODO: handle symlink/content pairs - return new MultiFilePatch(diffs.map(singleDiffFilePatch)); + const byPath = new Map(); + const filePatches = []; + + let index = 0; + for (const diff of diffs) { + const thePath = diff.oldPath || diff.newPath; + + if (diff.status === 'added' || diff.status === 'deleted') { + // Potential paired diff. Either a symlink deletion + content addition or a symlink addition + + // content deletion. + const otherHalf = byPath.get(thePath); + if (otherHalf) { + // The second half. Complete the paired diff, or fail if they have unexpected statuses or modes. + const [otherDiff, otherIndex] = otherHalf; + filePatches[otherIndex] = dualDiffFilePatch(diff, otherDiff); + byPath.delete(thePath); + } else { + // The first half we've seen. + byPath.set(thePath, [diff, index]); + index++; + } + } else { + filePatches[index] = singleDiffFilePatch(diff); + index++; + } + } + + // Populate unpaired diffs that looked like they could be part of a pair, but weren't. + for (const [unpairedDiff, originalIndex] of byPath.values()) { + filePatches[originalIndex] = singleDiffFilePatch(unpairedDiff); + } + + return new MultiFilePatch(filePatches); } function emptyDiffFilePatch() { diff --git a/test/models/patch/builder.test.js b/test/models/patch/builder.test.js index 60eab52a71e..987194ef824 100644 --- a/test/models/patch/builder.test.js +++ b/test/models/patch/builder.test.js @@ -594,9 +594,105 @@ describe('buildFilePatch', function() { ); }); - it('identifies a file that was deleted and replaced by a symlink'); + it('identifies mode and content change pairs within the patch list', function() { + const mp = buildMultiFilePatch([ + { + oldPath: 'first', oldMode: '100644', newPath: 'first', newMode: '100755', status: 'modified', + hunks: [ + { + oldStartLine: 1, oldLineCount: 2, newStartLine: 1, newLineCount: 3, + lines: [ + ' line-0', + '+line-1', + ' line-2', + ], + }, + ], + }, + { + oldPath: 'was-non-symlink', oldMode: '100644', newPath: 'was-non-symlink', newMode: '000000', status: 'deleted', + hunks: [ + { + oldStartLine: 1, oldLineCount: 2, newStartLine: 1, newLineCount: 0, + lines: ['-line-0', '-line-1'], + }, + ], + }, + { + oldPath: 'was-symlink', oldMode: '000000', newPath: 'was-symlink', newMode: '100755', status: 'added', + hunks: [ + { + oldStartLine: 1, oldLineCount: 0, newStartLine: 1, newLineCount: 2, + lines: ['+line-0', '+line-1'], + }, + ], + }, + { + oldMode: '100644', newPath: 'third', newMode: '100644', status: 'deleted', + hunks: [ + { + oldStartLine: 1, oldLineCount: 3, newStartLine: 1, newLineCount: 0, + lines: ['-line-0', '-line-1', '-line-2'], + }, + ], + }, + { + oldPath: 'was-symlink', oldMode: '120000', newPath: 'was-non-symlink', newMode: '000000', status: 'deleted', + hunks: [ + { + oldStartLine: 1, oldLineCount: 0, newStartLine: 0, newLineCount: 0, + lines: ['-was-symlink-destination'], + }, + ], + }, + { + oldPath: 'was-non-symlink', oldMode: '000000', newPath: 'was-non-symlink', newMode: '120000', status: 'added', + hunks: [ + { + oldStartLine: 1, oldLineCount: 0, newStartLine: 1, newLineCount: 1, + lines: ['+was-non-symlink-destination'], + }, + ], + }, + ]); - it('identifies a symlink that was deleted and replaced by a file'); + assert.lengthOf(mp.getFilePatches(), 4); + const [fp0, fp1, fp2, fp3] = mp.getFilePatches(); + + assert.strictEqual(fp0.getOldPath(), 'first'); + assertInFilePatch(fp0).hunks({ + startRow: 0, endRow: 2, header: '@@ -1,2 +1,3 @@', regions: [ + {kind: 'unchanged', string: ' line-0\n', range: [[0, 0], [0, 6]]}, + {kind: 'addition', string: '+line-1\n', range: [[1, 0], [1, 6]]}, + {kind: 'unchanged', string: ' line-2\n', range: [[2, 0], [2, 6]]}, + ], + }); + + assert.strictEqual(fp1.getOldPath(), 'was-non-symlink'); + assert.isTrue(fp1.hasTypechange()); + assert.strictEqual(fp1.getNewSymlink(), 'was-non-symlink-destination'); + assertInFilePatch(fp1).hunks({ + startRow: 0, endRow: 1, header: '@@ -1,2 +1,0 @@', regions: [ + {kind: 'deletion', string: '-line-0\n-line-1\n', range: [[0, 0], [1, 6]]}, + ], + }); + + assert.strictEqual(fp2.getOldPath(), 'was-symlink'); + assert.isTrue(fp2.hasTypechange()); + assert.strictEqual(fp2.getOldSymlink(), 'was-symlink-destination'); + assertInFilePatch(fp2).hunks({ + startRow: 0, endRow: 1, header: '@@ -1,0 +1,2 @@', regions: [ + {kind: 'addition', string: '+line-0\n+line-1\n', range: [[0, 0], [1, 6]]}, + ], + }); + + assert.strictEqual(fp3.getNewPath(), 'third'); + assertInFilePatch(fp3).hunks({ + startRow: 0, endRow: 2, header: '@@ -1,3 +1,0 @@', regions: [ + {kind: 'deletion', string: '-line-0\n-line-1\n-line-2\n', range: [[0, 0], [2, 6]]}, + ], + }); + }); }); it('throws an error with an unexpected number of diffs', function() { From a231aa186efd0c3a35b38bcd736d03ff120dd3c9 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 30 Oct 2018 14:32:02 -0400 Subject: [PATCH 016/284] GitShellOutStrategy implementation of getStagedChangesPatch() --- lib/git-shell-out-strategy.js | 17 +++++++++++++++++ test/git-strategies.test.js | 26 ++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/lib/git-shell-out-strategy.js b/lib/git-shell-out-strategy.js index e6a4e6b6972..a2b5693dfb9 100644 --- a/lib/git-shell-out-strategy.js +++ b/lib/git-shell-out-strategy.js @@ -622,6 +622,23 @@ export default class GitShellOutStrategy { return rawDiffs; } + async getStagedChangesPatch() { + const output = await this.exec([ + 'diff', '--staged', '--no-prefix', '--no-ext-diff', '--no-renames', '--diff-filter=u', + ]); + + if (!output) { + return []; + } + + const diffs = parseDiff(output); + for (const diff of diffs) { + diff.oldPath = toNativePathSep(diff.oldPath); + diff.newPath = toNativePathSep(diff.newPath); + } + return diffs; + } + /** * Miscellaneous getters */ diff --git a/test/git-strategies.test.js b/test/git-strategies.test.js index 635b09dafd8..2c3bdcb7cc0 100644 --- a/test/git-strategies.test.js +++ b/test/git-strategies.test.js @@ -627,6 +627,32 @@ import * as reporterProxy from '../lib/reporter-proxy'; }); }); + describe('getStagedChangesPatch', function() { + it('returns an empty patch if there are no staged files', async function() { + const workdir = await cloneRepository('three-files'); + const git = createTestStrategy(workdir); + const mp = await git.getStagedChangesPatch(); + assert.lengthOf(mp, 0); + }); + + it('returns a combined diff of all staged files', async function() { + const workdir = await cloneRepository('each-staging-group'); + const git = createTestStrategy(workdir); + + await assert.isRejected(git.merge('origin/branch')); + await fs.writeFile(path.join(workdir, 'unstaged-1.txt'), 'Unstaged file'); + await fs.writeFile(path.join(workdir, 'unstaged-2.txt'), 'Unstaged file'); + + await fs.writeFile(path.join(workdir, 'staged-1.txt'), 'Staged file'); + await fs.writeFile(path.join(workdir, 'staged-2.txt'), 'Staged file'); + await fs.writeFile(path.join(workdir, 'staged-3.txt'), 'Staged file'); + await git.stageFiles(['staged-1.txt', 'staged-2.txt', 'staged-3.txt']); + + const diffs = await git.getStagedChangesPatch(); + assert.deepEqual(diffs.map(diff => diff.newPath), ['staged-1.txt', 'staged-2.txt', 'staged-3.txt']); + }); + }); + describe('isMerging', function() { it('returns true if `.git/MERGE_HEAD` exists', async function() { const workingDirPath = await cloneRepository('merge-conflict'); From fbc71352aea73fc91db51ed03c5a9e322d951c97 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 30 Oct 2018 14:55:29 -0400 Subject: [PATCH 017/284] Cached getStagedChangesPatch() method on Repository --- lib/models/repository-states/present.js | 12 +++++++++++- lib/models/repository-states/state.js | 5 +++++ lib/models/repository.js | 1 + test/models/repository.test.js | 19 +++++++++++++++++++ 4 files changed, 36 insertions(+), 1 deletion(-) diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js index ed25c56e497..6bef2053b05 100644 --- a/lib/models/repository-states/present.js +++ b/lib/models/repository-states/present.js @@ -6,7 +6,7 @@ import State from './state'; import {LargeRepoError} from '../../git-shell-out-strategy'; import {FOCUS} from '../workspace-change-observer'; -import {buildFilePatch} from '../patch'; +import {buildFilePatch, buildMultiFilePatch} from '../patch'; import DiscardHistory from '../discard-history'; import Branch, {nullBranch} from '../branch'; import Author from '../author'; @@ -92,6 +92,7 @@ export default class Present extends State { const includes = (...segments) => fullPath.includes(path.join(...segments)); if (endsWith('.git', 'index')) { + keys.add(Keys.staged); keys.add(Keys.stagedChangesSinceParentCommit); keys.add(Keys.filePatch.all); keys.add(Keys.index.all); @@ -626,6 +627,12 @@ export default class Present extends State { }); } + getStagedChangesPatch() { + return this.cache.getOrSet(Keys.stagedChanges, () => { + return this.git().getStagedChangesPatch().then(buildMultiFilePatch); + }); + } + readFileFromIndex(filePath) { return this.cache.getOrSet(Keys.index.oneWith(filePath), () => { return this.git().readFileFromIndex(filePath); @@ -950,6 +957,8 @@ class GroupKey { const Keys = { statusBundle: new CacheKey('status-bundle'), + stagedChanges: new CacheKey('staged-changes'), + stagedChangesSinceParentCommit: new CacheKey('staged-changes-since-parent-commit'), filePatch: { @@ -1029,6 +1038,7 @@ const Keys = { ...Keys.workdirOperationKeys(fileNames), ...Keys.filePatch.eachWithFileOpts(fileNames, [{staged: true}]), ...fileNames.map(Keys.index.oneWith), + Keys.stagedChanges, Keys.stagedChangesSinceParentCommit, ], diff --git a/lib/models/repository-states/state.js b/lib/models/repository-states/state.js index dc3b8e9bb4f..ccefa2cd1bf 100644 --- a/lib/models/repository-states/state.js +++ b/lib/models/repository-states/state.js @@ -3,6 +3,7 @@ import BranchSet from '../branch-set'; import RemoteSet from '../remote-set'; import {nullOperationStates} from '../operation-states'; import FilePatch from '../patch/file-patch'; +import MultiFilePatch from '../patch/multi-file-patch'; /** * Map of registered subclasses to allow states to transition to one another without circular dependencies. @@ -278,6 +279,10 @@ export default class State { return Promise.resolve(FilePatch.createNull()); } + getStagedChangesPatch() { + return Promise.resolve(new MultiFilePatch([])); + } + readFileFromIndex(filePath) { return Promise.reject(new Error(`fatal: Path ${filePath} does not exist (neither on disk nor in the index).`)); } diff --git a/lib/models/repository.js b/lib/models/repository.js index ab50db5f527..6801b952626 100644 --- a/lib/models/repository.js +++ b/lib/models/repository.js @@ -326,6 +326,7 @@ const delegates = [ 'getStatusBundle', 'getStatusesForChangedFiles', 'getFilePatchForPath', + 'getStagedChangesPatch', 'readFileFromIndex', 'getLastCommit', diff --git a/test/models/repository.test.js b/test/models/repository.test.js index d1ad3a84c4f..8fc548625d7 100644 --- a/test/models/repository.test.js +++ b/test/models/repository.test.js @@ -440,6 +440,25 @@ describe('Repository', function() { }); }); + describe('getStagedChangesPatch', function() { + it('computes a multi-file patch of the staged changes', async function() { + const workdir = await cloneRepository('each-staging-group'); + const repo = new Repository(workdir); + await repo.getLoadPromise(); + + await fs.writeFile(path.join(workdir, 'unstaged-1.txt'), 'Unstaged file'); + + await fs.writeFile(path.join(workdir, 'staged-1.txt'), 'Staged file'); + await fs.writeFile(path.join(workdir, 'staged-2.txt'), 'Staged file'); + await repo.stageFiles(['staged-1.txt', 'staged-2.txt']); + + const mp = await repo.getStagedChangesPatch(); + + assert.lengthOf(mp.getFilePatches(), 2); + assert.deepEqual(mp.getFilePatches().map(fp => fp.getPath()), ['staged-1.txt', 'staged-2.txt']); + }); + }); + describe('isPartiallyStaged(filePath)', function() { it('returns true if specified file path is partially staged', async function() { const workingDirPath = await cloneRepository('three-files'); From ad28ab8b0f1e53b36ceb7d847347df1a9a4c0e74 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 30 Oct 2018 15:07:41 -0400 Subject: [PATCH 018/284] Invalidate cached staged changes --- lib/models/repository-states/present.js | 7 ++++++- test/models/repository.test.js | 4 ++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js index 6bef2053b05..a226ef11e44 100644 --- a/lib/models/repository-states/present.js +++ b/lib/models/repository-states/present.js @@ -92,7 +92,7 @@ export default class Present extends State { const includes = (...segments) => fullPath.includes(path.join(...segments)); if (endsWith('.git', 'index')) { - keys.add(Keys.staged); + keys.add(Keys.stagedChanges); keys.add(Keys.stagedChangesSinceParentCommit); keys.add(Keys.filePatch.all); keys.add(Keys.index.all); @@ -232,6 +232,7 @@ export default class Present extends State { ...Keys.filePatch.eachWithOpts({staged: true}), Keys.headDescription, Keys.branches, + Keys.stagedChanges, ], // eslint-disable-next-line no-shadow () => this.executePipelineAction('COMMIT', async (message, options = {}) => { @@ -277,6 +278,7 @@ export default class Present extends State { return this.invalidate( () => [ Keys.statusBundle, + Keys.stagedChanges, Keys.stagedChangesSinceParentCommit, Keys.filePatch.all, Keys.index.all, @@ -298,6 +300,7 @@ export default class Present extends State { () => [ Keys.statusBundle, Keys.stagedChangesSinceParentCommit, + Keys.stagedChanges, ...Keys.filePatch.eachWithFileOpts([filePath], [{staged: false}, {staged: true}]), Keys.index.oneWith(filePath), ], @@ -310,6 +313,7 @@ export default class Present extends State { checkout(revision, options = {}) { return this.invalidate( () => [ + Keys.stagedChanges, Keys.stagedChangesSinceParentCommit, Keys.lastCommit, Keys.recentCommits, @@ -331,6 +335,7 @@ export default class Present extends State { return this.invalidate( () => [ Keys.statusBundle, + Keys.stagedChanges, Keys.stagedChangesSinceParentCommit, ...paths.map(fileName => Keys.index.oneWith(fileName)), ...Keys.filePatch.eachWithFileOpts(paths, [{staged: true}]), diff --git a/test/models/repository.test.js b/test/models/repository.test.js index 8fc548625d7..f0fb42c4e06 100644 --- a/test/models/repository.test.js +++ b/test/models/repository.test.js @@ -1492,6 +1492,10 @@ describe('Repository', function() { 'getRemotes', () => repository.getRemotes(), ); + calls.set( + 'getStagedChangesPatch', + () => repository.getStagedChangesPatch(), + ); const withFile = fileName => { calls.set( From c23bbd25fa99480837d2824ec70af65c33ff192a Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 30 Oct 2018 15:12:10 -0400 Subject: [PATCH 019/284] Load the real staged changes patch in CommitPreviewContainer --- lib/containers/commit-preview-container.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/containers/commit-preview-container.js b/lib/containers/commit-preview-container.js index d8af3a424d3..c7165e7e33d 100644 --- a/lib/containers/commit-preview-container.js +++ b/lib/containers/commit-preview-container.js @@ -5,7 +5,6 @@ import yubikiri from 'yubikiri'; import ObserveModel from '../views/observe-model'; import LoadingView from '../views/loading-view'; import CommitPreviewController from '../controllers/commit-preview-controller'; -import MultiFilePatch from '../models/patch/multi-file-patch'; export default class CommitPreviewContainer extends React.Component { static propTypes = { @@ -14,7 +13,7 @@ export default class CommitPreviewContainer extends React.Component { fetchData = repository => { return yubikiri({ - multiFilePatch: new MultiFilePatch([]), + multiFilePatch: repository.getStagedChangesPatch(), }); } From 0da683c65fcbd3f513ef695d18579e20c6caa0e6 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 30 Oct 2018 15:26:16 -0400 Subject: [PATCH 020/284] Register the CommitPreviewItem opener --- lib/controllers/root-controller.js | 11 +++++++++++ test/controllers/root-controller.test.js | 15 +++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/lib/controllers/root-controller.js b/lib/controllers/root-controller.js index 3b4a6ad9f0f..b88534d4c19 100644 --- a/lib/controllers/root-controller.js +++ b/lib/controllers/root-controller.js @@ -17,6 +17,7 @@ import Commands, {Command} from '../atom/commands'; import GitTimingsView from '../views/git-timings-view'; import FilePatchItem from '../items/file-patch-item'; import IssueishDetailItem from '../items/issueish-detail-item'; +import CommitPreviewItem from '../items/commit-preview-item'; import GitTabItem from '../items/git-tab-item'; import GitHubTabItem from '../items/github-tab-item'; import StatusBarTileController from './status-bar-tile-controller'; @@ -338,6 +339,16 @@ export default class RootController extends React.Component { /> )} + + {({itemHolder, params}) => ( + + )} + {({itemHolder, params}) => ( Date: Tue, 30 Oct 2018 15:32:57 -0400 Subject: [PATCH 021/284] Super secret dev-mode only command to open the preview item Don't tell anyone :speak_no_evil: --- lib/controllers/root-controller.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/controllers/root-controller.js b/lib/controllers/root-controller.js index b88534d4c19..b609d42bf25 100644 --- a/lib/controllers/root-controller.js +++ b/lib/controllers/root-controller.js @@ -129,6 +129,15 @@ export default class RootController extends React.Component { return ( {devMode && } + {devMode && ( + { + const workdir = this.props.repository.getWorkingDirectoryPath(); + this.props.workspace.toggle(CommitPreviewItem.buildURI(workdir)); + }} + /> + )} From 868d2ffca8bc07ffb0a61d0f21029e5bac44c309 Mon Sep 17 00:00:00 2001 From: Tilde Ann Thurium Date: Tue, 30 Oct 2018 15:19:58 -0700 Subject: [PATCH 022/284] render multiple `FilePatchContainer`s in a `CommitPreviewItem` Co-Authored-By: Katrina Uychaco --- lib/controllers/commit-preview-controller.js | 7 ++++++- lib/controllers/root-controller.js | 8 ++++++++ lib/items/commit-preview-item.js | 1 + lib/views/commit-preview-view.js | 20 ++++++++++++++++++++ styles/file-patch-view.less | 3 ++- 5 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 lib/views/commit-preview-view.js diff --git a/lib/controllers/commit-preview-controller.js b/lib/controllers/commit-preview-controller.js index 8642647b262..6016272d47a 100644 --- a/lib/controllers/commit-preview-controller.js +++ b/lib/controllers/commit-preview-controller.js @@ -1,6 +1,7 @@ import React from 'react'; import {MultiFilePatchPropType} from '../prop-types'; +import CommitPreviewView from '../views/commit-preview-view'; export default class CommitPreviewController extends React.Component { static propTypes = { @@ -8,6 +9,10 @@ export default class CommitPreviewController extends React.Component { } render() { - return null; + return ( + + ); } } diff --git a/lib/controllers/root-controller.js b/lib/controllers/root-controller.js index b609d42bf25..b7285655f9d 100644 --- a/lib/controllers/root-controller.js +++ b/lib/controllers/root-controller.js @@ -355,6 +355,14 @@ export default class RootController extends React.Component { workdirContextPool={this.props.workdirContextPool} workingDirectory={params.workingDirectory} + workspace={this.props.workspace} + commands={this.props.commandRegistry} + keymaps={this.props.keymaps} + tooltips={this.props.tooltips} + config={this.props.config} + discardLines={this.discardLines} + undoLastDiscard={this.undoLastDiscard} + surfaceFileAtPath={this.surfaceFromFileAtPath} /> )} diff --git a/lib/items/commit-preview-item.js b/lib/items/commit-preview-item.js index b03aaa793df..9634d3a4c58 100644 --- a/lib/items/commit-preview-item.js +++ b/lib/items/commit-preview-item.js @@ -55,6 +55,7 @@ export default class CommitPreviewItem extends React.Component { ); } diff --git a/lib/views/commit-preview-view.js b/lib/views/commit-preview-view.js new file mode 100644 index 00000000000..60f02c79ced --- /dev/null +++ b/lib/views/commit-preview-view.js @@ -0,0 +1,20 @@ +import React from 'react'; +import FilePatchContainer from '../containers/file-patch-container'; + +export default class CommitPreviewView extends React.Component { + render() { + + return this.props.multiFilePatch.getFilePatches().map(filePatch => { + const relPath = filePatch.getNewFile().getPath() + return ( + + ); + }); + } +} diff --git a/styles/file-patch-view.less b/styles/file-patch-view.less index c04a3445b02..418de093add 100644 --- a/styles/file-patch-view.less +++ b/styles/file-patch-view.less @@ -12,7 +12,8 @@ cursor: default; flex: 1; min-width: 0; - height: 100%; + // hack hack hack + height: 500px; &--blank &-container { flex: 1; From ba7645f6e45f2dfbbe80f71bf84f5ab1dcc8602b Mon Sep 17 00:00:00 2001 From: Tilde Ann Thurium Date: Tue, 30 Oct 2018 15:43:33 -0700 Subject: [PATCH 023/284] properly bind destroy function in `CommitPreviewItem` Co-Authored-By: Katrina Uychaco --- lib/items/commit-preview-item.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/items/commit-preview-item.js b/lib/items/commit-preview-item.js index 9634d3a4c58..ebd3a42c680 100644 --- a/lib/items/commit-preview-item.js +++ b/lib/items/commit-preview-item.js @@ -36,7 +36,7 @@ export default class CommitPreviewItem extends React.Component { return this.emitter.on('did-terminate-pending-state', callback); } - destroy() { + destroy = () => { /* istanbul ignore else */ if (!this.isDestroyed) { this.emitter.emit('did-destroy'); From 31e5935a6b46eb9fa80a896709b3b0488c3da000 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Tue, 30 Oct 2018 19:52:52 -0700 Subject: [PATCH 024/284] FilePatchItem --> ChangedFileItem Now that we will have other file patch related items we should specify that this item is specifically to display changed files. This also decouples what used to be the FilePatchItem from the FilePatchController and FilePatchView which we are hoping to generalize and use for all file patch item types (ChangedFileItem, CommitPreview, CommitView, CodeReviewDiff, etc) --- docs/react-component-classification.md | 2 +- lib/controllers/file-patch-controller.js | 4 ++-- lib/controllers/root-controller.js | 18 ++++++++--------- ...ile-patch-item.js => changed-file-item.js} | 4 ++-- lib/views/staging-view.js | 14 ++++++------- test/controllers/root-controller.test.js | 20 +++++++++---------- test/integration/file-patch.test.js | 6 +++--- ...item.test.js => changed-file-item.test.js} | 12 +++++------ test/views/staging-view.test.js | 16 +++++++-------- 9 files changed, 48 insertions(+), 48 deletions(-) rename lib/items/{file-patch-item.js => changed-file-item.js} (94%) rename test/items/{file-patch-item.test.js => changed-file-item.test.js} (92%) diff --git a/docs/react-component-classification.md b/docs/react-component-classification.md index 45805477045..3bf6cc76357 100644 --- a/docs/react-component-classification.md +++ b/docs/react-component-classification.md @@ -6,7 +6,7 @@ This is a high-level summary of the organization and implementation of our React **Items** are intended to be used as top-level components within subtrees that are rendered into some [Portal](https://reactjs.org/docs/portals.html) and passed to the Atom API, like pane items, dock items, or tooltips. They are mostly responsible for implementing the [Atom "item" contract](https://github.com/atom/atom/blob/a3631f0dafac146185289ac5e37eaff17b8b0209/src/workspace.js#L29-L174). -These live within [`lib/items/`](/lib/items), are tested within [`test/items/`](/test/items), and are named with an `Item` suffix. Examples: `PullRequestDetailItem`, `FilePatchItem`. +These live within [`lib/items/`](/lib/items), are tested within [`test/items/`](/test/items), and are named with an `Item` suffix. Examples: `PullRequestDetailItem`, `ChangedFileItem`. ## Containers diff --git a/lib/controllers/file-patch-controller.js b/lib/controllers/file-patch-controller.js index 14dc10db171..56491549da6 100644 --- a/lib/controllers/file-patch-controller.js +++ b/lib/controllers/file-patch-controller.js @@ -4,7 +4,7 @@ import path from 'path'; import {autobind, equalSets} from '../helpers'; import {addEvent} from '../reporter-proxy'; -import FilePatchItem from '../items/file-patch-item'; +import ChangedFileItem from '../items/changed-file-item'; import FilePatchView from '../views/file-patch-view'; export default class FilePatchController extends React.Component { @@ -96,7 +96,7 @@ export default class FilePatchController extends React.Component { diveIntoMirrorPatch() { const mirrorStatus = this.withStagingStatus({staged: 'unstaged', unstaged: 'staged'}); const workingDirectory = this.props.repository.getWorkingDirectoryPath(); - const uri = FilePatchItem.buildURI(this.props.relPath, workingDirectory, mirrorStatus); + const uri = ChangedFileItem.buildURI(this.props.relPath, workingDirectory, mirrorStatus); this.props.destroy(); return this.props.workspace.open(uri); diff --git a/lib/controllers/root-controller.js b/lib/controllers/root-controller.js index b7285655f9d..6ceef27e1ea 100644 --- a/lib/controllers/root-controller.js +++ b/lib/controllers/root-controller.js @@ -15,7 +15,7 @@ import InitDialog from '../views/init-dialog'; import CredentialDialog from '../views/credential-dialog'; import Commands, {Command} from '../atom/commands'; import GitTimingsView from '../views/git-timings-view'; -import FilePatchItem from '../items/file-patch-item'; +import ChangedFileItem from '../items/changed-file-item'; import IssueishDetailItem from '../items/issueish-detail-item'; import CommitPreviewItem from '../items/commit-preview-item'; import GitTabItem from '../items/git-tab-item'; @@ -326,9 +326,9 @@ export default class RootController extends React.Component { + uriPattern={ChangedFileItem.uriPattern}> {({itemHolder, params}) => ( - Promise.resolve(), getFilePatchLoadedPromise: () => Promise.resolve(), }; - sinon.stub(workspace, 'open').returns(filePatchItem); + sinon.stub(workspace, 'open').returns(changedFileItem); await wrapper.instance().viewUnstagedChangesForCurrentFile(); await assert.async.equal(workspace.open.callCount, 1); @@ -1004,9 +1004,9 @@ describe('RootController', function() { `atom-github://file-patch/a.txt?workdir=${encodeURIComponent(workdirPath)}&stagingStatus=unstaged`, {pending: true, activatePane: true, activateItem: true}, ]); - await assert.async.equal(filePatchItem.goToDiffLine.callCount, 1); - assert.deepEqual(filePatchItem.goToDiffLine.args[0], [8]); - assert.equal(filePatchItem.focus.callCount, 1); + await assert.async.equal(changedFileItem.goToDiffLine.callCount, 1); + assert.deepEqual(changedFileItem.goToDiffLine.args[0], [8]); + assert.equal(changedFileItem.focus.callCount, 1); }); it('does nothing on an untitled buffer', async function() { @@ -1035,13 +1035,13 @@ describe('RootController', function() { editor.setCursorBufferPosition([7, 0]); // TODO: too implementation-detail-y - const filePatchItem = { + const changedFileItem = { goToDiffLine: sinon.spy(), focus: sinon.spy(), getRealItemPromise: () => Promise.resolve(), getFilePatchLoadedPromise: () => Promise.resolve(), }; - sinon.stub(workspace, 'open').returns(filePatchItem); + sinon.stub(workspace, 'open').returns(changedFileItem); await wrapper.instance().viewStagedChangesForCurrentFile(); await assert.async.equal(workspace.open.callCount, 1); @@ -1049,9 +1049,9 @@ describe('RootController', function() { `atom-github://file-patch/a.txt?workdir=${encodeURIComponent(workdirPath)}&stagingStatus=staged`, {pending: true, activatePane: true, activateItem: true}, ]); - await assert.async.equal(filePatchItem.goToDiffLine.callCount, 1); - assert.deepEqual(filePatchItem.goToDiffLine.args[0], [8]); - assert.equal(filePatchItem.focus.callCount, 1); + await assert.async.equal(changedFileItem.goToDiffLine.callCount, 1); + assert.deepEqual(changedFileItem.goToDiffLine.args[0], [8]); + assert.equal(changedFileItem.focus.callCount, 1); }); it('does nothing on an untitled buffer', async function() { diff --git a/test/integration/file-patch.test.js b/test/integration/file-patch.test.js index 7341d3fcfbe..d91aaf82d4f 100644 --- a/test/integration/file-patch.test.js +++ b/test/integration/file-patch.test.js @@ -73,15 +73,15 @@ describe('integration: file patches', function() { listItem.simulate('mousedown', {button: 0, persist() {}}); window.dispatchEvent(new MouseEvent('mouseup')); - const itemSelector = `FilePatchItem[relPath="${relativePath}"][stagingStatus="${stagingStatus}"]`; + const itemSelector = `ChangedFileItem[relPath="${relativePath}"][stagingStatus="${stagingStatus}"]`; await until( () => wrapper.update().find(itemSelector).find('.github-FilePatchView').exists(), - `the FilePatchItem for ${relativePath} arrives and loads`, + `the ChangedFileItem for ${relativePath} arrives and loads`, ); } function getPatchItem(stagingStatus, relativePath) { - return wrapper.update().find(`FilePatchItem[relPath="${relativePath}"][stagingStatus="${stagingStatus}"]`); + return wrapper.update().find(`ChangedFileItem[relPath="${relativePath}"][stagingStatus="${stagingStatus}"]`); } function getPatchEditor(stagingStatus, relativePath) { diff --git a/test/items/file-patch-item.test.js b/test/items/changed-file-item.test.js similarity index 92% rename from test/items/file-patch-item.test.js rename to test/items/changed-file-item.test.js index 00467f1d2fc..466cce067db 100644 --- a/test/items/file-patch-item.test.js +++ b/test/items/changed-file-item.test.js @@ -3,11 +3,11 @@ import React from 'react'; import {mount} from 'enzyme'; import PaneItem from '../../lib/atom/pane-item'; -import FilePatchItem from '../../lib/items/file-patch-item'; +import ChangedFileItem from '../../lib/items/changed-file-item'; import WorkdirContextPool from '../../lib/models/workdir-context-pool'; import {cloneRepository} from '../helpers'; -describe('FilePatchItem', function() { +describe('ChangedFileItem', function() { let atomEnv, repository, pool; beforeEach(async function() { @@ -41,10 +41,10 @@ describe('FilePatchItem', function() { }; return ( - + {({itemHolder, params}) => { return ( - filePatchItem, - querySelector: () => filePatchItem, + const changedFileItem = { + getElement: () => changedFileItem, + querySelector: () => changedFileItem, focus: sinon.spy(), }; - workspace.open.returns(filePatchItem); + workspace.open.returns(changedFileItem); await wrapper.instance().showFilePatchItem('file.txt', 'staged', {activate: true}); @@ -238,15 +238,15 @@ describe('StagingView', function() { `atom-github://file-patch/file.txt?workdir=${encodeURIComponent(workingDirectoryPath)}&stagingStatus=staged`, {pending: true, activatePane: true, pane: undefined, activateItem: true}, ]); - assert.isTrue(filePatchItem.focus.called); + assert.isTrue(changedFileItem.focus.called); }); it('makes the item visible if activate is false', async function() { const wrapper = mount(app); const focus = sinon.spy(); - const filePatchItem = {focus}; - workspace.open.returns(filePatchItem); + const changedFileItem = {focus}; + workspace.open.returns(changedFileItem); const activateItem = sinon.spy(); workspace.paneForItem.returns({activateItem}); @@ -259,7 +259,7 @@ describe('StagingView', function() { ]); assert.isFalse(focus.called); assert.equal(activateItem.callCount, 1); - assert.equal(activateItem.args[0][0], filePatchItem); + assert.equal(activateItem.args[0][0], changedFileItem); }); }); }); From 1e86c511783c9a2d7b96b5f7c1364bc63aec23ff Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Tue, 30 Oct 2018 20:14:39 -0700 Subject: [PATCH 025/284] FilePatchContainer --> ChangedFileContainer --- .../{file-patch-container.js => changed-file-container.js} | 2 +- lib/items/changed-file-item.js | 4 ++-- lib/views/commit-preview-view.js | 4 ++-- ...tch-container.test.js => changed-file-container.test.js} | 6 +++--- test/items/changed-file-item.test.js | 6 +++--- 5 files changed, 11 insertions(+), 11 deletions(-) rename lib/containers/{file-patch-container.js => changed-file-container.js} (96%) rename test/containers/{file-patch-container.test.js => changed-file-container.test.js} (94%) diff --git a/lib/containers/file-patch-container.js b/lib/containers/changed-file-container.js similarity index 96% rename from lib/containers/file-patch-container.js rename to lib/containers/changed-file-container.js index 3b3865f08ce..b6de5d3b2a5 100644 --- a/lib/containers/file-patch-container.js +++ b/lib/containers/changed-file-container.js @@ -7,7 +7,7 @@ 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 { +export default class ChangedFileContainer extends React.Component { static propTypes = { repository: PropTypes.object.isRequired, stagingStatus: PropTypes.oneOf(['staged', 'unstaged']), diff --git a/lib/items/changed-file-item.js b/lib/items/changed-file-item.js index 8ba0359b831..57ddc3302b5 100644 --- a/lib/items/changed-file-item.js +++ b/lib/items/changed-file-item.js @@ -4,7 +4,7 @@ import {Emitter} from 'event-kit'; import {WorkdirContextPoolPropType} from '../prop-types'; import {autobind} from '../helpers'; -import FilePatchContainer from '../containers/file-patch-container'; +import ChangedFileContainer from '../containers/changed-file-container'; export default class ChangedFileItem extends React.Component { static propTypes = { @@ -77,7 +77,7 @@ export default class ChangedFileItem extends React.Component { const repository = this.props.workdirContextPool.getContext(this.props.workingDirectory).getRepository(); return ( - { const relPath = filePatch.getNewFile().getPath() return ( - ; + return ; } it('renders a loading spinner before file patch data arrives', function() { diff --git a/test/items/changed-file-item.test.js b/test/items/changed-file-item.test.js index 466cce067db..59f8d131011 100644 --- a/test/items/changed-file-item.test.js +++ b/test/items/changed-file-item.test.js @@ -72,14 +72,14 @@ describe('ChangedFileItem', function() { const wrapper = mount(buildPaneApp()); await open(wrapper); - assert.strictEqual(wrapper.update().find('FilePatchContainer').prop('repository'), repository); + assert.strictEqual(wrapper.update().find('ChangedFileContainer').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()); + assert.isTrue(wrapper.update().find('ChangedFileContainer').prop('repository').isAbsent()); }); it('passes other props to the container', async function() { @@ -87,7 +87,7 @@ describe('ChangedFileItem', function() { const wrapper = mount(buildPaneApp({other})); await open(wrapper); - assert.strictEqual(wrapper.update().find('FilePatchContainer').prop('other'), other); + assert.strictEqual(wrapper.update().find('ChangedFileContainer').prop('other'), other); }); describe('getTitle()', function() { From 35f824817b0d72ff94d716d6729ab19ef6116618 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Tue, 30 Oct 2018 20:11:15 -0700 Subject: [PATCH 026/284] CommitPreviewController --> MultiFilepatchController Generalize component. This will iterate over file patches passed in and create `FilePatchController`s for each --- lib/containers/commit-preview-container.js | 4 ++-- ...preview-controller.js => multi-file-patch-controller.js} | 2 +- ...ntroller.test.js => multi-file-patch-controller.test.js} | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) rename lib/controllers/{commit-preview-controller.js => multi-file-patch-controller.js} (81%) rename test/controllers/{commit-preview-controller.test.js => multi-file-patch-controller.test.js} (66%) diff --git a/lib/containers/commit-preview-container.js b/lib/containers/commit-preview-container.js index c7165e7e33d..c1c5d9a70f3 100644 --- a/lib/containers/commit-preview-container.js +++ b/lib/containers/commit-preview-container.js @@ -4,7 +4,7 @@ import yubikiri from 'yubikiri'; import ObserveModel from '../views/observe-model'; import LoadingView from '../views/loading-view'; -import CommitPreviewController from '../controllers/commit-preview-controller'; +import MultiFilePatchController from '../controllers/multi-file-patch-controller'; export default class CommitPreviewContainer extends React.Component { static propTypes = { @@ -31,7 +31,7 @@ export default class CommitPreviewContainer extends React.Component { } return ( - diff --git a/lib/controllers/commit-preview-controller.js b/lib/controllers/multi-file-patch-controller.js similarity index 81% rename from lib/controllers/commit-preview-controller.js rename to lib/controllers/multi-file-patch-controller.js index 6016272d47a..f10b7ddfaa7 100644 --- a/lib/controllers/commit-preview-controller.js +++ b/lib/controllers/multi-file-patch-controller.js @@ -3,7 +3,7 @@ import React from 'react'; import {MultiFilePatchPropType} from '../prop-types'; import CommitPreviewView from '../views/commit-preview-view'; -export default class CommitPreviewController extends React.Component { +export default class MultiFilePatchController extends React.Component { static propTypes = { multiFilePatch: MultiFilePatchPropType.isRequired, } diff --git a/test/controllers/commit-preview-controller.test.js b/test/controllers/multi-file-patch-controller.test.js similarity index 66% rename from test/controllers/commit-preview-controller.test.js rename to test/controllers/multi-file-patch-controller.test.js index e86834e3db6..78864d23ed1 100644 --- a/test/controllers/commit-preview-controller.test.js +++ b/test/controllers/multi-file-patch-controller.test.js @@ -1,9 +1,9 @@ import React from 'react'; import {shallow} from 'enzyme'; -import CommitPreviewController from '../../lib/controllers/commit-preview-controller'; +import MultiFilePatchController from '../../lib/controllers/multi-file-patch-controller'; -describe('CommitPreviewController', function() { +describe('MultiFilePatchController', function() { let atomEnv; beforeEach(function() { @@ -19,7 +19,7 @@ describe('CommitPreviewController', function() { ...override, }; - return ; + return ; } it('renders the CommitPreviewView and passes extra props through'); From cd95e4f68bf87107afd7e10790032b376a694b3b Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Tue, 30 Oct 2018 20:37:14 -0700 Subject: [PATCH 027/284] Use MultiFilePatchController in ChangedFileContainer Render FilePatchController in MultiFilePatchController and :fire: CommitPreviewView --- lib/containers/changed-file-container.js | 9 +++++---- .../multi-file-patch-controller.js | 19 ++++++++++++------ lib/models/repository-states/present.js | 6 ++++++ lib/models/repository-states/state.js | 4 ++++ lib/models/repository.js | 1 + lib/views/commit-preview-view.js | 20 ------------------- 6 files changed, 29 insertions(+), 30 deletions(-) delete mode 100644 lib/views/commit-preview-view.js diff --git a/lib/containers/changed-file-container.js b/lib/containers/changed-file-container.js index b6de5d3b2a5..22d73e70a2a 100644 --- a/lib/containers/changed-file-container.js +++ b/lib/containers/changed-file-container.js @@ -5,7 +5,8 @@ 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'; +import MultiFilePatchController from '../controllers/multi-file-patch-controller'; +import MultiFilePatch from '../models/patch/multi-file-patch'; export default class ChangedFileContainer extends React.Component { static propTypes = { @@ -31,7 +32,7 @@ export default class ChangedFileContainer extends React.Component { fetchData(repository) { return yubikiri({ - filePatch: repository.getFilePatchForPath(this.props.relPath, {staged: this.props.stagingStatus === 'staged'}), + multiFilePatch: repository.getChangedFilePatch(this.props.relPath, {staged: this.props.stagingStatus === 'staged'}), isPartiallyStaged: repository.isPartiallyStaged(this.props.relPath), hasUndoHistory: repository.hasDiscardHistory(this.props.relPath), }); @@ -51,8 +52,8 @@ export default class ChangedFileContainer extends React.Component { } return ( - - ); + return this.props.multiFilePatch.getFilePatches().map(filePatch => { + const relPath = filePatch.getNewFile().getPath(); + return ( + + ); + }); } } diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js index a226ef11e44..863c59a1e3d 100644 --- a/lib/models/repository-states/present.js +++ b/lib/models/repository-states/present.js @@ -14,6 +14,7 @@ import BranchSet from '../branch-set'; import Remote from '../remote'; import RemoteSet from '../remote-set'; import Commit from '../commit'; +import MultiFilePatch from '../patch/multi-file-patch'; import OperationStates from '../operation-states'; import {addEvent} from '../../reporter-proxy'; @@ -625,6 +626,11 @@ export default class Present extends State { return {stagedFiles, unstagedFiles, mergeConflictFiles}; } + // hack hack hack + async getChangedFilePatch(...args) { + return new MultiFilePatch([await this.getFilePatchForPath(...args)]); + } + getFilePatchForPath(filePath, {staged} = {staged: false}) { return this.cache.getOrSet(Keys.filePatch.oneWith(filePath, {staged}), async () => { const diffs = await this.git().getDiffsForFilePath(filePath, {staged}); diff --git a/lib/models/repository-states/state.js b/lib/models/repository-states/state.js index ccefa2cd1bf..791b52174d9 100644 --- a/lib/models/repository-states/state.js +++ b/lib/models/repository-states/state.js @@ -279,6 +279,10 @@ export default class State { return Promise.resolve(FilePatch.createNull()); } + getChangedFilePatch() { + return Promise.resolve(new MultiFilePatch([])); + } + getStagedChangesPatch() { return Promise.resolve(new MultiFilePatch([])); } diff --git a/lib/models/repository.js b/lib/models/repository.js index 6801b952626..08970a981d6 100644 --- a/lib/models/repository.js +++ b/lib/models/repository.js @@ -328,6 +328,7 @@ const delegates = [ 'getFilePatchForPath', 'getStagedChangesPatch', 'readFileFromIndex', + 'getChangedFilePatch', 'getLastCommit', 'getRecentCommits', diff --git a/lib/views/commit-preview-view.js b/lib/views/commit-preview-view.js deleted file mode 100644 index f14f59aa01e..00000000000 --- a/lib/views/commit-preview-view.js +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react'; -import ChangedFileContainer from '../containers/changed-file-container'; - -export default class CommitPreviewView extends React.Component { - render() { - - return this.props.multiFilePatch.getFilePatches().map(filePatch => { - const relPath = filePatch.getNewFile().getPath() - return ( - - ); - }); - } -} From 921df33769a5bb95e769e2167787839d46f69c5a Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Tue, 30 Oct 2018 20:38:06 -0700 Subject: [PATCH 028/284] Make some FilePatchController props optional --- lib/controllers/file-patch-controller.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/controllers/file-patch-controller.js b/lib/controllers/file-patch-controller.js index 56491549da6..6fffce65e19 100644 --- a/lib/controllers/file-patch-controller.js +++ b/lib/controllers/file-patch-controller.js @@ -22,9 +22,9 @@ export default class FilePatchController extends React.Component { config: PropTypes.object.isRequired, destroy: PropTypes.func.isRequired, - discardLines: PropTypes.func.isRequired, - undoLastDiscard: PropTypes.func.isRequired, - surfaceFileAtPath: PropTypes.func.isRequired, + discardLines: PropTypes.func, + undoLastDiscard: PropTypes.func, + surfaceFileAtPath: PropTypes.func, } constructor(props) { From 941bcea1b85f728d0103da392efb26a92bb6213b Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Tue, 30 Oct 2018 20:39:20 -0700 Subject: [PATCH 029/284] Make aheadCount prop optional --- lib/controllers/github-tab-controller.js | 2 +- lib/views/github-tab-view.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 f8eeb45b51b..153b723d280 100644 --- a/lib/controllers/github-tab-controller.js +++ b/lib/controllers/github-tab-controller.js @@ -19,7 +19,7 @@ export default class GitHubTabController extends React.Component { allRemotes: RemoteSetPropType.isRequired, branches: BranchSetPropType.isRequired, selectedRemoteName: PropTypes.string, - aheadCount: PropTypes.number.isRequired, + aheadCount: PropTypes.number, pushInProgress: PropTypes.bool.isRequired, isLoading: PropTypes.bool.isRequired, } diff --git a/lib/views/github-tab-view.js b/lib/views/github-tab-view.js index cac4e26b2ca..4c8bc73a4be 100644 --- a/lib/views/github-tab-view.js +++ b/lib/views/github-tab-view.js @@ -22,7 +22,7 @@ export default class GitHubTabView extends React.Component { remotes: RemoteSetPropType.isRequired, currentRemote: RemotePropType.isRequired, manyRemotesAvailable: PropTypes.bool.isRequired, - aheadCount: PropTypes.number.isRequired, + aheadCount: PropTypes.number, pushInProgress: PropTypes.bool.isRequired, isLoading: PropTypes.bool.isRequired, From 0a0358546b403d508010520c9b9ec4fed197cfe9 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Tue, 30 Oct 2018 20:40:53 -0700 Subject: [PATCH 030/284] Make hasUndoHistory prop optional --- lib/controllers/file-patch-controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/controllers/file-patch-controller.js b/lib/controllers/file-patch-controller.js index 6fffce65e19..f28277ec58b 100644 --- a/lib/controllers/file-patch-controller.js +++ b/lib/controllers/file-patch-controller.js @@ -13,7 +13,7 @@ export default class FilePatchController extends React.Component { stagingStatus: PropTypes.oneOf(['staged', 'unstaged']), relPath: PropTypes.string.isRequired, filePatch: PropTypes.object.isRequired, - hasUndoHistory: PropTypes.bool.isRequired, + hasUndoHistory: PropTypes.bool, workspace: PropTypes.object.isRequired, commands: PropTypes.object.isRequired, From 5c6781563c8608b307cd5d2f577dab3bb9d24377 Mon Sep 17 00:00:00 2001 From: simurai Date: Wed, 31 Oct 2018 17:29:09 +0900 Subject: [PATCH 031/284] Style commit-preview-view --- styles/commit-preview-view.less | 18 ++++++++++++++++++ styles/file-patch-view.less | 3 +-- 2 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 styles/commit-preview-view.less diff --git a/styles/commit-preview-view.less b/styles/commit-preview-view.less new file mode 100644 index 00000000000..36a66c3cbd6 --- /dev/null +++ b/styles/commit-preview-view.less @@ -0,0 +1,18 @@ +@import "variables"; + +.github-StubItem-git-commit-preview { // TODO Rename class + .github-FilePatchView { + border-bottom: 1px solid @base-border-color; + + & + .github-FilePatchView { + margin-top: @component-padding; + border-top: 1px solid @base-border-color; + } + } + + + // hack hack hack + .github-FilePatchView { + height: 500px; + } +} diff --git a/styles/file-patch-view.less b/styles/file-patch-view.less index 418de093add..c04a3445b02 100644 --- a/styles/file-patch-view.less +++ b/styles/file-patch-view.less @@ -12,8 +12,7 @@ cursor: default; flex: 1; min-width: 0; - // hack hack hack - height: 500px; + height: 100%; &--blank &-container { flex: 1; From e0fd4b3c2c37e0acea5c6be92cf7408ae0cb9c53 Mon Sep 17 00:00:00 2001 From: simurai Date: Wed, 31 Oct 2018 17:31:19 +0900 Subject: [PATCH 032/284] Enable scrolling of the whole pane --- styles/commit-preview-view.less | 2 ++ 1 file changed, 2 insertions(+) diff --git a/styles/commit-preview-view.less b/styles/commit-preview-view.less index 36a66c3cbd6..20e9b7c3689 100644 --- a/styles/commit-preview-view.less +++ b/styles/commit-preview-view.less @@ -1,6 +1,8 @@ @import "variables"; .github-StubItem-git-commit-preview { // TODO Rename class + overflow: auto; + .github-FilePatchView { border-bottom: 1px solid @base-border-color; From 1032a7477977378ae19fc3db5bd6c6ed56298b1e Mon Sep 17 00:00:00 2001 From: simurai Date: Wed, 31 Oct 2018 19:48:16 +0900 Subject: [PATCH 033/284] Remove last border --- styles/commit-preview-view.less | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/styles/commit-preview-view.less b/styles/commit-preview-view.less index 20e9b7c3689..6589a1ba009 100644 --- a/styles/commit-preview-view.less +++ b/styles/commit-preview-view.less @@ -6,6 +6,10 @@ .github-FilePatchView { border-bottom: 1px solid @base-border-color; + &:last-child { + border-bottom: none; + } + & + .github-FilePatchView { margin-top: @component-padding; border-top: 1px solid @base-border-color; From 2212d049551799c2e244411e85b237c247ba63ec Mon Sep 17 00:00:00 2001 From: simurai Date: Wed, 31 Oct 2018 19:49:35 +0900 Subject: [PATCH 034/284] Switch to auto height --- styles/commit-preview-view.less | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/styles/commit-preview-view.less b/styles/commit-preview-view.less index 6589a1ba009..087ef76832a 100644 --- a/styles/commit-preview-view.less +++ b/styles/commit-preview-view.less @@ -4,6 +4,7 @@ overflow: auto; .github-FilePatchView { + height: auto; border-bottom: 1px solid @base-border-color; &:last-child { @@ -16,9 +17,4 @@ } } - - // hack hack hack - .github-FilePatchView { - height: 500px; - } } From 1060c24e11ef081e36e60fa46890b8903a20da68 Mon Sep 17 00:00:00 2001 From: simurai Date: Wed, 31 Oct 2018 19:54:09 +0900 Subject: [PATCH 035/284] Temporarly enable autoHeight --- lib/views/file-patch-view.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/views/file-patch-view.js b/lib/views/file-patch-view.js index 8736496e217..071c14157df 100644 --- a/lib/views/file-patch-view.js +++ b/lib/views/file-patch-view.js @@ -228,7 +228,8 @@ export default class FilePatchView extends React.Component { buffer={this.props.filePatch.getBuffer()} lineNumberGutterVisible={false} autoWidth={false} - autoHeight={false} + // TODO only set to true for commit previews, but not for single FilePatchViews + autoHeight={true} readOnly={true} softWrapped={true} From 4d364c1d864d591f587543040ef7a8b98d2b56ea Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 31 Oct 2018 08:50:58 -0400 Subject: [PATCH 036/284] Use .getPath() to get the relPath from either old or new files --- lib/controllers/multi-file-patch-controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/controllers/multi-file-patch-controller.js b/lib/controllers/multi-file-patch-controller.js index 7119f2e893f..a35f673b458 100644 --- a/lib/controllers/multi-file-patch-controller.js +++ b/lib/controllers/multi-file-patch-controller.js @@ -10,7 +10,7 @@ export default class MultiFilePatchController extends React.Component { render() { return this.props.multiFilePatch.getFilePatches().map(filePatch => { - const relPath = filePatch.getNewFile().getPath(); + const relPath = filePatch.getPath(); return ( Date: Wed, 31 Oct 2018 09:02:50 -0400 Subject: [PATCH 037/284] Give the CommitPreviewItem root
a stable className --- lib/controllers/root-controller.js | 5 ++++- styles/commit-preview-view.less | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/controllers/root-controller.js b/lib/controllers/root-controller.js index 6ceef27e1ea..a0a3a0385ec 100644 --- a/lib/controllers/root-controller.js +++ b/lib/controllers/root-controller.js @@ -348,7 +348,10 @@ export default class RootController extends React.Component { /> )} - + {({itemHolder, params}) => ( Date: Wed, 31 Oct 2018 09:20:34 -0400 Subject: [PATCH 038/284] Conditionally enable autoHeight on the patch editor --- lib/views/file-patch-view.js | 8 ++++++-- test/views/file-patch-view.test.js | 9 +++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/lib/views/file-patch-view.js b/lib/views/file-patch-view.js index 071c14157df..8a0d1492b90 100644 --- a/lib/views/file-patch-view.js +++ b/lib/views/file-patch-view.js @@ -35,6 +35,7 @@ export default class FilePatchView extends React.Component { selectedRows: PropTypes.object.isRequired, repository: PropTypes.object.isRequired, hasUndoHistory: PropTypes.bool.isRequired, + useEditorAutoHeight: PropTypes.bool, workspace: PropTypes.object.isRequired, commands: PropTypes.object.isRequired, @@ -55,6 +56,10 @@ export default class FilePatchView extends React.Component { discardRows: PropTypes.func.isRequired, } + defaultProps = { + useEditorAutoHeight: false, + } + constructor(props) { super(props); autobind( @@ -228,8 +233,7 @@ export default class FilePatchView extends React.Component { buffer={this.props.filePatch.getBuffer()} lineNumberGutterVisible={false} autoWidth={false} - // TODO only set to true for commit previews, but not for single FilePatchViews - autoHeight={true} + autoHeight={this.props.useEditorAutoHeight} readOnly={true} softWrapped={true} diff --git a/test/views/file-patch-view.test.js b/test/views/file-patch-view.test.js index f0c70afd89d..8964aac0909 100644 --- a/test/views/file-patch-view.test.js +++ b/test/views/file-patch-view.test.js @@ -99,6 +99,15 @@ describe('FilePatchView', function() { assert.strictEqual(editor.instance().getModel().getText(), filePatch.getBuffer().getText()); }); + it('enables autoHeight on the editor when requested', function() { + const wrapper = mount(buildApp({useEditorAutoHeight: true})); + + assert.isTrue(wrapper.find('AtomTextEditor').prop('autoHeight')); + + wrapper.setProps({useEditorAutoHeight: false}); + assert.isFalse(wrapper.find('AtomTextEditor').prop('autoHeight')); + }); + it('sets the root class when in hunk selection mode', function() { const wrapper = shallow(buildApp({selectionMode: 'line'})); assert.isFalse(wrapper.find('.github-FilePatchView--hunkMode').exists()); From 8598f7c91f4bf2a137c5af93e3adbd17b938572b Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 31 Oct 2018 09:23:01 -0400 Subject: [PATCH 039/284] Set useEditorAutoHeight to true in CommitPreview, but not ChangedFile --- lib/containers/changed-file-container.js | 1 + lib/containers/commit-preview-container.js | 1 + 2 files changed, 2 insertions(+) diff --git a/lib/containers/changed-file-container.js b/lib/containers/changed-file-container.js index 22d73e70a2a..f2cc89db521 100644 --- a/lib/containers/changed-file-container.js +++ b/lib/containers/changed-file-container.js @@ -56,6 +56,7 @@ export default class ChangedFileContainer extends React.Component { multiFilePatch={data.multiFilePatch} isPartiallyStaged={data.isPartiallyStaged} hasUndoHistory={data.hasUndoHistory} + useEditorAutoHeight={false} {...this.props} /> ); diff --git a/lib/containers/commit-preview-container.js b/lib/containers/commit-preview-container.js index c1c5d9a70f3..877fa76cfbe 100644 --- a/lib/containers/commit-preview-container.js +++ b/lib/containers/commit-preview-container.js @@ -32,6 +32,7 @@ export default class CommitPreviewContainer extends React.Component { return ( From 96b21b9c745e19de1f62e12eda0b43f3624afa34 Mon Sep 17 00:00:00 2001 From: Vanessa Yuen Date: Wed, 31 Oct 2018 15:26:27 +0100 Subject: [PATCH 040/284] add commit preview button Co-Authored-By: Ash Wilson --- lib/views/commit-view.js | 7 +++++++ test/views/commit-view.test.js | 28 ++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/lib/views/commit-view.js b/lib/views/commit-view.js index 7690da29d95..6338ab489a7 100644 --- a/lib/views/commit-view.js +++ b/lib/views/commit-view.js @@ -51,6 +51,7 @@ export default class CommitView extends React.Component { abortMerge: PropTypes.func.isRequired, prepareToCommit: PropTypes.func.isRequired, toggleExpandedCommitMessageEditor: PropTypes.func.isRequired, + previewCommit: PropTypes.func.isRequired, }; constructor(props, context) { @@ -157,6 +158,12 @@ export default class CommitView extends React.Component { +
Date: Wed, 31 Oct 2018 15:50:43 +0100 Subject: [PATCH 041/284] logic to open commit preview Co-Authored-By: Ash Wilson --- lib/controllers/commit-controller.js | 9 ++++++++- test/controllers/commit-controller.test.js | 12 ++++++++++++ test/views/commit-view.test.js | 2 +- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/lib/controllers/commit-controller.js b/lib/controllers/commit-controller.js index c4d637c56b9..176138220a9 100644 --- a/lib/controllers/commit-controller.js +++ b/lib/controllers/commit-controller.js @@ -8,6 +8,7 @@ import fs from 'fs-extra'; import CommitView from '../views/commit-view'; import RefHolder from '../models/ref-holder'; +import CommitPreviewItem from '../items/commit-preview-item'; import {AuthorPropType, UserStorePropType} from '../prop-types'; import {autobind} from '../helpers'; import {addEvent} from '../reporter-proxy'; @@ -43,7 +44,8 @@ export default class CommitController extends React.Component { constructor(props, context) { super(props, context); - autobind(this, 'commit', 'handleMessageChange', 'toggleExpandedCommitMessageEditor', 'grammarAdded'); + autobind(this, 'commit', 'handleMessageChange', 'toggleExpandedCommitMessageEditor', 'grammarAdded', + 'previewCommit'); this.subscriptions = new CompositeDisposable(); this.refCommitView = new RefHolder(); @@ -110,6 +112,7 @@ export default class CommitController extends React.Component { userStore={this.props.userStore} selectedCoAuthors={this.props.selectedCoAuthors} updateSelectedCoAuthors={this.props.updateSelectedCoAuthors} + previewCommit={this.previewCommit} /> ); } @@ -256,6 +259,10 @@ export default class CommitController extends React.Component { hasFocusEditor() { return this.refCommitView.map(view => view.hasFocusEditor()).getOr(false); } + + previewCommit() { + return this.props.workspace.open(CommitPreviewItem.buildURI(this.props.repository.getWorkingDirectoryPath())); + } } function wrapCommitMessage(message) { diff --git a/test/controllers/commit-controller.test.js b/test/controllers/commit-controller.test.js index 3e716ea10da..24ccd2b7a57 100644 --- a/test/controllers/commit-controller.test.js +++ b/test/controllers/commit-controller.test.js @@ -8,6 +8,7 @@ import {nullBranch} from '../../lib/models/branch'; import UserStore from '../../lib/models/user-store'; import CommitController, {COMMIT_GRAMMAR_SCOPE} from '../../lib/controllers/commit-controller'; +import CommitPreviewItem from '../../lib/items/commit-preview-item'; import {cloneRepository, buildRepository, buildRepositoryWithPipeline} from '../helpers'; import * as reporterProxy from '../../lib/reporter-proxy'; @@ -410,4 +411,15 @@ describe('CommitController', function() { assert.isFalse(wrapper.instance().hasFocusEditor()); }); }); + + it('opens commit preview pane', async function() { + const workdir = await cloneRepository('three-files'); + const repository = await buildRepository(workdir); + + sinon.spy(workspace, 'open'); + + const wrapper = shallow(React.cloneElement(app, {repository})); + await wrapper.find('CommitView').prop('previewCommit')(); + assert.isTrue(workspace.open.calledWith(CommitPreviewItem.buildURI(workdir))); + }); }); diff --git a/test/views/commit-view.test.js b/test/views/commit-view.test.js index 6d8fb94b846..d2e8eb58a3a 100644 --- a/test/views/commit-view.test.js +++ b/test/views/commit-view.test.js @@ -485,5 +485,5 @@ describe('CommitView', function() { wrapper.find('.github-CommitView-commitPreview').simulate('click'); assert.isTrue(previewCommit.called); }); - }) + }); }); From f4b22bfc2ba6ca42b4b71664d8d92133572823e9 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 31 Oct 2018 11:14:25 -0400 Subject: [PATCH 042/284] defaultProps needs to be static --- 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 8a0d1492b90..93fab72a36a 100644 --- a/lib/views/file-patch-view.js +++ b/lib/views/file-patch-view.js @@ -56,7 +56,7 @@ export default class FilePatchView extends React.Component { discardRows: PropTypes.func.isRequired, } - defaultProps = { + static defaultProps = { useEditorAutoHeight: false, } From ba4022700c063bb3887861cf5a45680da01d99f7 Mon Sep 17 00:00:00 2001 From: Vanessa Yuen Date: Wed, 31 Oct 2018 16:28:48 +0100 Subject: [PATCH 043/284] focus management Co-Authored-By: Ash Wilson --- lib/controllers/commit-controller.js | 4 ++++ lib/views/commit-view.js | 17 +++++++++++++++++ lib/views/git-tab-view.js | 4 ++-- test/views/commit-view.test.js | 11 +++++++++++ test/views/git-tab-view.test.js | 8 ++++---- 5 files changed, 38 insertions(+), 6 deletions(-) diff --git a/lib/controllers/commit-controller.js b/lib/controllers/commit-controller.js index 176138220a9..ca162bd5875 100644 --- a/lib/controllers/commit-controller.js +++ b/lib/controllers/commit-controller.js @@ -260,6 +260,10 @@ export default class CommitController extends React.Component { return this.refCommitView.map(view => view.hasFocusEditor()).getOr(false); } + hasFocusPreviewButton() { + return this.refCommitView.map(view => view.hasFocusPreviewButton()).getOr(false); + } + previewCommit() { return this.props.workspace.open(CommitPreviewItem.buildURI(this.props.repository.getWorkingDirectoryPath())); } diff --git a/lib/views/commit-view.js b/lib/views/commit-view.js index 6338ab489a7..22e544d4045 100644 --- a/lib/views/commit-view.js +++ b/lib/views/commit-view.js @@ -23,6 +23,7 @@ let FakeKeyDownEvent; export default class CommitView extends React.Component { static focus = { + COMMIT_PREVIEW_BUTTON: Symbol('commit-preview-button'), EDITOR: Symbol('commit-editor'), COAUTHOR_INPUT: Symbol('coauthor-input'), ABORT_MERGE_BUTTON: Symbol('commit-abort-merge-button'), @@ -74,6 +75,7 @@ export default class CommitView extends React.Component { this.subscriptions = new CompositeDisposable(); this.refRoot = new RefHolder(); + this.refCommitPreviewButton = new RefHolder(); this.refExpandButton = new RefHolder(); this.refCommitButton = new RefHolder(); this.refHardWrapButton = new RefHolder(); @@ -159,6 +161,7 @@ export default class CommitView extends React.Component { +
+ +
Date: Wed, 31 Oct 2018 15:26:31 -0400 Subject: [PATCH 060/284] :fire: Etch-era "integration" tests --- test/controllers/git-tab-controller.test.js | 161 +------------------- 1 file changed, 1 insertion(+), 160 deletions(-) diff --git a/test/controllers/git-tab-controller.test.js b/test/controllers/git-tab-controller.test.js index 27838dd4274..2a0d62dd374 100644 --- a/test/controllers/git-tab-controller.test.js +++ b/test/controllers/git-tab-controller.test.js @@ -13,7 +13,7 @@ import Author from '../../lib/models/author'; import ResolutionProgress from '../../lib/models/conflicts/resolution-progress'; import {GitError} from '../../lib/git-shell-out-strategy'; -describe('GitTabController', function() { +describe.only('GitTabController', function() { let atomEnvironment, workspace, workspaceElement, commandRegistry, notificationManager; let resolutionProgress, refreshResolutionProgress; @@ -277,165 +277,6 @@ describe('GitTabController', function() { }); }); - describe('keyboard navigation commands', function() { - let wrapper, rootElement, gitTab, stagingView, commitView, commitController, focusElement; - const focuses = GitTabController.focus; - - const extractReferences = () => { - 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 = []; - commitView.refEditorComponent.map(e => commitViewElements.push(e)); - commitView.refAbortMergeButton.map(e => commitViewElements.push(e)); - commitView.refCommitButton.map(e => commitViewElements.push(e)); - - const stubFocus = element => { - sinon.stub(element, 'focus').callsFake(() => { - focusElement = element; - }); - }; - stubFocus(stagingView.refRoot.get()); - for (const e of commitViewElements) { - stubFocus(e); - } - - sinon.stub(commitController, 'hasFocus').callsFake(() => { - return commitViewElements.includes(focusElement); - }); - }; - - const assertSelected = paths => { - const selectionPaths = Array.from(stagingView.state.selection.getSelectedItems()).map(item => item.filePath); - assert.deepEqual(selectionPaths, paths); - }; - - const assertAsyncSelected = paths => { - return assert.async.deepEqual( - Array.from(stagingView.state.selection.getSelectedItems()).map(item => item.filePath), - paths, - ); - }; - - describe('with conflicts and staged files', function() { - beforeEach(async function() { - const workdirPath = await cloneRepository('each-staging-group'); - const repository = await buildRepository(workdirPath); - - // Merge with conflicts - assert.isRejected(repository.git.merge('origin/branch')); - - fs.writeFileSync(path.join(workdirPath, 'unstaged-1.txt'), 'This is an unstaged file.'); - fs.writeFileSync(path.join(workdirPath, 'unstaged-2.txt'), 'This is an unstaged file.'); - fs.writeFileSync(path.join(workdirPath, 'unstaged-3.txt'), 'This is an unstaged file.'); - - // Three staged files - fs.writeFileSync(path.join(workdirPath, 'staged-1.txt'), 'This is a file with some changes staged for commit.'); - fs.writeFileSync(path.join(workdirPath, 'staged-2.txt'), 'This is another file staged for commit.'); - fs.writeFileSync(path.join(workdirPath, 'staged-3.txt'), 'This is a third file staged for commit.'); - await repository.stageFiles(['staged-1.txt', 'staged-2.txt', 'staged-3.txt']); - repository.refresh(); - - wrapper = mount(await buildApp(repository)); - await assert.async.lengthOf(wrapper.update().find('GitTabView').prop('unstagedChanges'), 3); - - extractReferences(); - }); - - it('blurs on tool-panel:unfocus', function() { - sinon.spy(workspace.getActivePane(), 'activate'); - - commandRegistry.dispatch(wrapper.find('.github-Git').getDOMNode(), 'tool-panel:unfocus'); - - assert.isTrue(workspace.getActivePane().activate.called); - }); - - it('advances focus through StagingView groups and CommitView, but does not cycle', async function() { - assertSelected(['unstaged-1.txt']); - - commandRegistry.dispatch(rootElement, 'core:focus-next'); - assertSelected(['conflict-1.txt']); - - commandRegistry.dispatch(rootElement, 'core:focus-next'); - assertSelected(['staged-1.txt']); - - commandRegistry.dispatch(rootElement, 'core:focus-next'); - assertSelected(['staged-1.txt']); - 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('AtomTextEditor').instance()); - }); - - it('retreats focus from the CommitView through StagingView groups, but does not cycle', async function() { - gitTab.setFocus(focuses.EDITOR); - sinon.stub(commitView, 'hasFocusEditor').returns(true); - - commandRegistry.dispatch(rootElement, 'core:focus-previous'); - await assert.async.strictEqual(focusElement, stagingView.refRoot.get()); - assertSelected(['staged-1.txt']); - - commandRegistry.dispatch(rootElement, 'core:focus-previous'); - await assertAsyncSelected(['conflict-1.txt']); - - commandRegistry.dispatch(rootElement, 'core:focus-previous'); - await assertAsyncSelected(['unstaged-1.txt']); - - // This should be a no-op. - commandRegistry.dispatch(rootElement, 'core:focus-previous'); - await assertAsyncSelected(['unstaged-1.txt']); - }); - }); - - describe('with staged changes', function() { - let repository; - - beforeEach(async function() { - const workdirPath = await cloneRepository('each-staging-group'); - repository = await buildRepository(workdirPath); - - // A staged file - fs.writeFileSync(path.join(workdirPath, 'staged-1.txt'), 'This is a file with some changes staged for commit.'); - await repository.stageFiles(['staged-1.txt']); - repository.refresh(); - - const prepareToCommit = () => Promise.resolve(true); - const ensureGitTab = () => Promise.resolve(false); - - wrapper = mount(await buildApp(repository, {ensureGitTab, prepareToCommit})); - - extractReferences(); - await assert.async.isTrue(commitView.props.stagedChangesExist); - }); - - it('focuses the CommitView on github:commit with an empty commit message', async function() { - 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('AtomTextEditor').instance()); - assert.isFalse(wrapper.instance().commit.called); - }); - - it('creates a commit on github:commit with a nonempty commit message', async function() { - commitView.refEditorModel.map(e => e.setText('I fixed the things')); - sinon.spy(repository, 'commit'); - - commandRegistry.dispatch(workspaceElement, 'github:commit'); - - await until('Commit method called', () => repository.commit.calledWith('I fixed the things')); - }); - }); - }); - describe('integration tests', function() { it('can stage and unstage files and commit', async function() { const workdirPath = await cloneRepository('three-files'); From a2c201b1ff4258e2804520dfe2290bc22f80efac Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 31 Oct 2018 15:35:55 -0400 Subject: [PATCH 061/284] The tests assume -previewCommit is on the actual button :eyes: --- lib/views/commit-view.js | 4 ++-- styles/commit-view.less | 10 ++++------ 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/lib/views/commit-view.js b/lib/views/commit-view.js index 6531ea37142..0932fbf4e12 100644 --- a/lib/views/commit-view.js +++ b/lib/views/commit-view.js @@ -160,10 +160,10 @@ export default class CommitView extends React.Component { -
+
diff --git a/test/controllers/commit-controller.test.js b/test/controllers/commit-controller.test.js index ac9f6174d3b..1b705c5828e 100644 --- a/test/controllers/commit-controller.test.js +++ b/test/controllers/commit-controller.test.js @@ -422,7 +422,7 @@ describe('CommitController', function() { sinon.spy(workspace, 'open'); const wrapper = shallow(React.cloneElement(app, {repository})); - await wrapper.find('CommitView').prop('previewCommit')(); + await wrapper.find('CommitView').prop('toggleCommitPreview')(); assert.isTrue(workspace.open.calledWith(CommitPreviewItem.buildURI(workdir))); }); }); diff --git a/test/views/commit-view.test.js b/test/views/commit-view.test.js index 6b8c106e77f..7510cf84a55 100644 --- a/test/views/commit-view.test.js +++ b/test/views/commit-view.test.js @@ -641,15 +641,15 @@ describe('CommitView', function() { }); it('calls a callback when the button is clicked', function() { - const previewCommit = sinon.spy(); + const toggleCommitPreview = sinon.spy(); const wrapper = shallow(React.cloneElement(app, { - previewCommit, + toggleCommitPreview, stagedChangesExist: true, })); wrapper.find('.github-CommitView-commitPreview').simulate('click'); - assert.isTrue(previewCommit.called); + assert.isTrue(toggleCommitPreview.called); }); }); }); From 05b6b5047563ff96986dbf1a432325d1201375dd Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Wed, 31 Oct 2018 18:57:57 -0700 Subject: [PATCH 072/284] Add tests for toggling commit preview open Co-Authored-By: Tilde Ann Thurium --- test/controllers/commit-controller.test.js | 28 ++++++++++++++++------ test/views/commit-view.test.js | 14 +++++++++++ 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/test/controllers/commit-controller.test.js b/test/controllers/commit-controller.test.js index 1b705c5828e..516d1fa3883 100644 --- a/test/controllers/commit-controller.test.js +++ b/test/controllers/commit-controller.test.js @@ -415,14 +415,28 @@ describe('CommitController', function() { }); }); - it('opens commit preview pane', async function() { - const workdir = await cloneRepository('three-files'); - const repository = await buildRepository(workdir); + describe('toggleCommitPreview', function() { + it('opens and closes commit preview pane', async function() { + const workdir = await cloneRepository('three-files'); + const repository = await buildRepository(workdir); - sinon.spy(workspace, 'open'); + const wrapper = shallow(React.cloneElement(app, {repository})); - const wrapper = shallow(React.cloneElement(app, {repository})); - await wrapper.find('CommitView').prop('toggleCommitPreview')(); - assert.isTrue(workspace.open.calledWith(CommitPreviewItem.buildURI(workdir))); + sinon.spy(workspace, 'toggle'); + + assert.isFalse(wrapper.state('commitPreviewOpen')); + + await wrapper.find('CommitView').prop('toggleCommitPreview')(); + assert.isTrue(workspace.toggle.calledWith(CommitPreviewItem.buildURI(workdir))); + assert.isTrue(wrapper.state('commitPreviewOpen')); + + await wrapper.find('CommitView').prop('toggleCommitPreview')(); + assert.isTrue(workspace.toggle.calledTwice); + assert.isFalse(wrapper.state('commitPreviewOpen')); + + await wrapper.find('CommitView').prop('toggleCommitPreview')(); + assert.isTrue(workspace.toggle.calledThrice); + assert.isTrue(wrapper.state('commitPreviewOpen')); + }); }); }); diff --git a/test/views/commit-view.test.js b/test/views/commit-view.test.js index 7510cf84a55..f7567a4e629 100644 --- a/test/views/commit-view.test.js +++ b/test/views/commit-view.test.js @@ -651,5 +651,19 @@ describe('CommitView', function() { wrapper.find('.github-CommitView-commitPreview').simulate('click'); assert.isTrue(toggleCommitPreview.called); }); + + it('displays correct button text depending on prop value', function() { + const wrapper = shallow(React.cloneElement(app, { + stagedChangesExist: false, + })); + + assert.strictEqual(wrapper.find('.github-CommitView-commitPreview').text(), 'Preview Commit'); + + wrapper.setProps({commitPreviewOpen: true}); + assert.strictEqual(wrapper.find('.github-CommitView-commitPreview').text(), 'Close Commit Preview'); + + wrapper.setProps({commitPreviewOpen: false}); + assert.strictEqual(wrapper.find('.github-CommitView-commitPreview').text(), 'Preview Commit'); + }); }); }); From 25046778b87cd1ac9c6f7843081f7188f235948d Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Wed, 31 Oct 2018 19:08:32 -0700 Subject: [PATCH 073/284] Make `buildFilePatch` return a `MultiFilePatch` instance --- lib/containers/changed-file-container.js | 2 +- lib/models/patch/builder.js | 6 +++--- lib/models/repository-states/present.js | 5 ----- 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/lib/containers/changed-file-container.js b/lib/containers/changed-file-container.js index 6ee00e7df33..2c76b1349f1 100644 --- a/lib/containers/changed-file-container.js +++ b/lib/containers/changed-file-container.js @@ -33,7 +33,7 @@ export default class ChangedFileContainer extends React.Component { const staged = this.props.stagingStatus === 'staged'; return yubikiri({ - multiFilePatch: repository.getChangedFilePatch(this.props.relPath, {staged}), + multiFilePatch: repository.getFilePatchForPath(this.props.relPath, {staged}), isPartiallyStaged: repository.isPartiallyStaged(this.props.relPath), hasUndoHistory: repository.hasDiscardHistory(this.props.relPath), }); diff --git a/lib/models/patch/builder.js b/lib/models/patch/builder.js index c2bef5fe72e..4a8ba43fb1c 100644 --- a/lib/models/patch/builder.js +++ b/lib/models/patch/builder.js @@ -9,11 +9,11 @@ import MultiFilePatch from './multi-file-patch'; export function buildFilePatch(diffs) { if (diffs.length === 0) { - return emptyDiffFilePatch(); + return new MultiFilePatch(emptyDiffFilePatch()); } else if (diffs.length === 1) { - return singleDiffFilePatch(diffs[0]); + return new MultiFilePatch(singleDiffFilePatch(diffs[0])); } else if (diffs.length === 2) { - return dualDiffFilePatch(...diffs); + return new MultiFilePatch(dualDiffFilePatch(...diffs)); } else { throw new Error(`Unexpected number of diffs: ${diffs.length}`); } diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js index 863c59a1e3d..97ebccd24e2 100644 --- a/lib/models/repository-states/present.js +++ b/lib/models/repository-states/present.js @@ -626,11 +626,6 @@ export default class Present extends State { return {stagedFiles, unstagedFiles, mergeConflictFiles}; } - // hack hack hack - async getChangedFilePatch(...args) { - return new MultiFilePatch([await this.getFilePatchForPath(...args)]); - } - getFilePatchForPath(filePath, {staged} = {staged: false}) { return this.cache.getOrSet(Keys.filePatch.oneWith(filePath, {staged}), async () => { const diffs = await this.git().getDiffsForFilePath(filePath, {staged}); From 89c965e626322abfe39ffdb2f21f2932dad87d2f Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Wed, 31 Oct 2018 19:18:22 -0700 Subject: [PATCH 074/284] Revert "Make `buildFilePatch` return a `MultiFilePatch` instance" This reverts commit 25046778b87cd1ac9c6f7843081f7188f235948d. --- lib/containers/changed-file-container.js | 2 +- lib/models/patch/builder.js | 6 +++--- lib/models/repository-states/present.js | 5 +++++ 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/containers/changed-file-container.js b/lib/containers/changed-file-container.js index 2c76b1349f1..6ee00e7df33 100644 --- a/lib/containers/changed-file-container.js +++ b/lib/containers/changed-file-container.js @@ -33,7 +33,7 @@ export default class ChangedFileContainer extends React.Component { const staged = this.props.stagingStatus === 'staged'; return yubikiri({ - multiFilePatch: repository.getFilePatchForPath(this.props.relPath, {staged}), + multiFilePatch: repository.getChangedFilePatch(this.props.relPath, {staged}), isPartiallyStaged: repository.isPartiallyStaged(this.props.relPath), hasUndoHistory: repository.hasDiscardHistory(this.props.relPath), }); diff --git a/lib/models/patch/builder.js b/lib/models/patch/builder.js index 4a8ba43fb1c..c2bef5fe72e 100644 --- a/lib/models/patch/builder.js +++ b/lib/models/patch/builder.js @@ -9,11 +9,11 @@ import MultiFilePatch from './multi-file-patch'; export function buildFilePatch(diffs) { if (diffs.length === 0) { - return new MultiFilePatch(emptyDiffFilePatch()); + return emptyDiffFilePatch(); } else if (diffs.length === 1) { - return new MultiFilePatch(singleDiffFilePatch(diffs[0])); + return singleDiffFilePatch(diffs[0]); } else if (diffs.length === 2) { - return new MultiFilePatch(dualDiffFilePatch(...diffs)); + return dualDiffFilePatch(...diffs); } else { throw new Error(`Unexpected number of diffs: ${diffs.length}`); } diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js index 97ebccd24e2..863c59a1e3d 100644 --- a/lib/models/repository-states/present.js +++ b/lib/models/repository-states/present.js @@ -626,6 +626,11 @@ export default class Present extends State { return {stagedFiles, unstagedFiles, mergeConflictFiles}; } + // hack hack hack + async getChangedFilePatch(...args) { + return new MultiFilePatch([await this.getFilePatchForPath(...args)]); + } + getFilePatchForPath(filePath, {staged} = {staged: false}) { return this.cache.getOrSet(Keys.filePatch.oneWith(filePath, {staged}), async () => { const diffs = await this.git().getDiffsForFilePath(filePath, {staged}); From 9980a4ccae0ab254795550dee5000fe4b0421403 Mon Sep 17 00:00:00 2001 From: simurai Date: Thu, 1 Nov 2018 16:09:26 +0900 Subject: [PATCH 075/284] Fix scrollbar on macOS --- styles/commit-preview-view.less | 1 + 1 file changed, 1 insertion(+) diff --git a/styles/commit-preview-view.less b/styles/commit-preview-view.less index 63a07f36a2d..a0fa33e21b7 100644 --- a/styles/commit-preview-view.less +++ b/styles/commit-preview-view.less @@ -2,6 +2,7 @@ .github-CommitPreview-root { overflow: auto; + z-index: 1; // Fixes scrollbar on macOS .github-FilePatchView { height: auto; From 4160aec4657108474ad2b749720f562136db649c Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 1 Nov 2018 08:15:57 -0400 Subject: [PATCH 076/284] :shirt: add dangling comma --- 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 69c2d2099ff..92b9b08e4b7 100644 --- a/lib/views/file-patch-view.js +++ b/lib/views/file-patch-view.js @@ -69,7 +69,7 @@ export default class FilePatchView extends React.Component { 'didMouseDownOnHeader', 'didMouseDownOnLineNumber', 'didMouseMoveOnLineNumber', 'didMouseUp', 'didConfirm', 'didToggleSelectionMode', 'selectNextHunk', 'selectPreviousHunk', 'didOpenFile', 'didAddSelection', 'didChangeSelectionRange', 'didDestroySelection', - 'oldLineNumberLabel', 'newLineNumberLabel', 'handleMouseDown' + 'oldLineNumberLabel', 'newLineNumberLabel', 'handleMouseDown', ); this.mouseSelectionInProgress = false; From 197f0c131c8e09cc9fa4c9c85d723027bdf3bd26 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 1 Nov 2018 08:17:27 -0400 Subject: [PATCH 077/284] :shirt: Restore missing prop --- lib/views/commit-view.js | 1 + test/views/commit-view.test.js | 1 + 2 files changed, 2 insertions(+) diff --git a/lib/views/commit-view.js b/lib/views/commit-view.js index ebf83c698b1..400c96c4d0f 100644 --- a/lib/views/commit-view.js +++ b/lib/views/commit-view.js @@ -42,6 +42,7 @@ export default class CommitView extends React.Component { mergeConflictsExist: PropTypes.bool.isRequired, stagedChangesExist: PropTypes.bool.isRequired, isCommitting: PropTypes.bool.isRequired, + commitPreviewOpen: PropTypes.bool.isRequired, deactivateCommitBox: PropTypes.bool.isRequired, maximumCharacterLimit: PropTypes.number.isRequired, messageBuffer: PropTypes.object.isRequired, // FIXME more specific proptype diff --git a/test/views/commit-view.test.js b/test/views/commit-view.test.js index f7567a4e629..ad542a6837e 100644 --- a/test/views/commit-view.test.js +++ b/test/views/commit-view.test.js @@ -43,6 +43,7 @@ describe('CommitView', function() { stagedChangesExist={false} mergeConflictsExist={false} isCommitting={false} + commitPreviewOpen={false} deactivateCommitBox={false} maximumCharacterLimit={72} messageBuffer={messageBuffer} From 417f260e69c9c84a0786d5cace76ec0cca16e7ee Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 1 Nov 2018 08:25:39 -0400 Subject: [PATCH 078/284] Button caption tests don't depend on stagedChangesExist --- test/views/commit-view.test.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/views/commit-view.test.js b/test/views/commit-view.test.js index ad542a6837e..fd182e4e501 100644 --- a/test/views/commit-view.test.js +++ b/test/views/commit-view.test.js @@ -654,9 +654,7 @@ describe('CommitView', function() { }); it('displays correct button text depending on prop value', function() { - const wrapper = shallow(React.cloneElement(app, { - stagedChangesExist: false, - })); + const wrapper = shallow(app); assert.strictEqual(wrapper.find('.github-CommitView-commitPreview').text(), 'Preview Commit'); From 1e9e487721f34c6e9f88f15cb8ae664f012574f4 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 1 Nov 2018 08:29:59 -0400 Subject: [PATCH 079/284] Assert against the view prop instead of directly against state --- test/controllers/commit-controller.test.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/controllers/commit-controller.test.js b/test/controllers/commit-controller.test.js index 516d1fa3883..ffd4c1c9be3 100644 --- a/test/controllers/commit-controller.test.js +++ b/test/controllers/commit-controller.test.js @@ -424,19 +424,19 @@ describe('CommitController', function() { sinon.spy(workspace, 'toggle'); - assert.isFalse(wrapper.state('commitPreviewOpen')); + assert.isFalse(wrapper.find('CommitView').prop('commitPreviewOpen')); await wrapper.find('CommitView').prop('toggleCommitPreview')(); assert.isTrue(workspace.toggle.calledWith(CommitPreviewItem.buildURI(workdir))); - assert.isTrue(wrapper.state('commitPreviewOpen')); + assert.isTrue(wrapper.find('CommitView').prop('commitPreviewOpen')); await wrapper.find('CommitView').prop('toggleCommitPreview')(); assert.isTrue(workspace.toggle.calledTwice); - assert.isFalse(wrapper.state('commitPreviewOpen')); + assert.isFalse(wrapper.find('CommitView').prop('commitPreviewOpen')); await wrapper.find('CommitView').prop('toggleCommitPreview')(); assert.isTrue(workspace.toggle.calledThrice); - assert.isTrue(wrapper.state('commitPreviewOpen')); + assert.isTrue(wrapper.find('CommitView').prop('commitPreviewOpen')); }); }); }); From d218af9ad6d307a79e85a05d4f9a6472222c519f Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 1 Nov 2018 09:35:11 -0400 Subject: [PATCH 080/284] watchWorkspaceItem to track open pane items in React component state --- lib/watch-workspace-item.js | 40 ++++++++++ test/watch-workspace-item.test.js | 125 ++++++++++++++++++++++++++++++ 2 files changed, 165 insertions(+) create mode 100644 lib/watch-workspace-item.js create mode 100644 test/watch-workspace-item.test.js diff --git a/lib/watch-workspace-item.js b/lib/watch-workspace-item.js new file mode 100644 index 00000000000..0dd844bc468 --- /dev/null +++ b/lib/watch-workspace-item.js @@ -0,0 +1,40 @@ +import {CompositeDisposable} from 'atom'; + +import URIPattern from './atom/uri-pattern'; + +export function watchWorkspaceItem(workspace, pattern, component, stateKey) { + const uPattern = pattern instanceof URIPattern ? pattern : new URIPattern(pattern); + + function itemMatches(item) { + return item.getURI && uPattern.matches(item.getURI()).ok(); + } + + if (!component.state) { + component.state = {}; + } + let itemCount = workspace.getPaneItems().filter(itemMatches).length; + component.state[stateKey] = itemCount > 0; + + return new CompositeDisposable( + workspace.onDidAddPaneItem(({item}) => { + const hadOpen = itemCount > 0; + if (itemMatches(item)) { + itemCount++; + + if (itemCount > 0 && !hadOpen) { + component.setState({[stateKey]: true}); + } + } + }), + workspace.onDidDestroyPaneItem(({item}) => { + const hadOpen = itemCount > 0; + if (itemMatches(item)) { + itemCount--; + + if (itemCount <= 0 && hadOpen) { + component.setState({[stateKey]: false}); + } + } + }), + ); +} diff --git a/test/watch-workspace-item.test.js b/test/watch-workspace-item.test.js new file mode 100644 index 00000000000..9ea6826a5ba --- /dev/null +++ b/test/watch-workspace-item.test.js @@ -0,0 +1,125 @@ +import {watchWorkspaceItem} from '../lib/watch-workspace-item'; + +describe('watchWorkspaceItem', function() { + let sub, atomEnv, workspace, component; + + beforeEach(function() { + atomEnv = global.buildAtomEnvironment(); + workspace = atomEnv.workspace; + + component = { + state: {}, + setState: sinon.stub().resolves(), + }; + + workspace.addOpener(uri => { + if (uri.startsWith('atom-github://')) { + return { + getURI() { return uri; }, + }; + } else { + return undefined; + } + }); + }); + + afterEach(function() { + sub && sub.dispose(); + atomEnv.destroy(); + }); + + describe('initial state', function() { + it('creates component state if none is present', function() { + component.state = undefined; + + sub = watchWorkspaceItem(workspace, 'atom-github://item', component, 'aKey'); + assert.deepEqual(component.state, {aKey: false}); + }); + + it('is false when the pane is not open', async function() { + await workspace.open('atom-github://nonmatching'); + + sub = watchWorkspaceItem(workspace, 'atom-github://item', component, 'someKey'); + assert.isFalse(component.state.someKey); + }); + + it('is true when the pane is already open', async function() { + await workspace.open('atom-github://item/one'); + await workspace.open('atom-github://item/two'); + + sub = watchWorkspaceItem(workspace, 'atom-github://item/one', component, 'theKey'); + + assert.isTrue(component.state.theKey); + }); + + it('is true when multiple panes matching the URI pattern are open', async function() { + await workspace.open('atom-github://item/one'); + await workspace.open('atom-github://item/two'); + await workspace.open('atom-github://nonmatch'); + + sub = watchWorkspaceItem(workspace, 'atom-github://item/{pattern}', component, 'theKey'); + + assert.isTrue(component.state.theKey); + }); + }); + + describe('workspace events', function() { + it('becomes true when the pane is opened', async function() { + sub = watchWorkspaceItem(workspace, 'atom-github://item/{pattern}', component, 'theKey'); + + assert.isFalse(component.state.theKey); + + await workspace.open('atom-github://item/match'); + + assert.isTrue(component.setState.calledWith({theKey: true})); + }); + + it('remains true if another matching pane is opened', async function() { + await workspace.open('atom-github://item/match0'); + sub = watchWorkspaceItem(workspace, 'atom-github://item/{pattern}', component, 'theKey'); + + assert.isTrue(component.state.theKey); + + await workspace.open('atom-github://item/match1'); + + assert.isFalse(component.setState.called); + }); + + it('remains true if a matching pane is closed but another remains open', async function() { + await workspace.open('atom-github://item/match0'); + await workspace.open('atom-github://item/match1'); + + sub = watchWorkspaceItem(workspace, 'atom-github://item/{pattern}', component, 'theKey'); + assert.isTrue(component.state.theKey); + + assert.isTrue(workspace.hide('atom-github://item/match1')); + + assert.isFalse(component.setState.called); + }); + + it('becomes false if the last matching pane is closed', async function() { + await workspace.open('atom-github://item/match0'); + await workspace.open('atom-github://item/match1'); + + sub = watchWorkspaceItem(workspace, 'atom-github://item/{pattern}', component, 'theKey'); + assert.isTrue(component.state.theKey); + + assert.isTrue(workspace.hide('atom-github://item/match1')); + assert.isTrue(workspace.hide('atom-github://item/match0')); + + assert.isTrue(component.setState.calledWith({theKey: false})); + }); + }); + + it('stops updating when disposed', async function() { + sub = watchWorkspaceItem(workspace, 'atom-github://item', component, 'theKey'); + assert.isFalse(component.state.theKey); + + sub.dispose(); + await workspace.open('atom-github://item'); + assert.isFalse(component.setState.called); + + await workspace.hide('atom-github://item'); + assert.isFalse(component.setState.called); + }); +}); From f35b12ef43264c874f3068c18def2b96002c64eb Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 1 Nov 2018 09:42:14 -0400 Subject: [PATCH 081/284] Cover that last conditional :ok_hand: --- test/watch-workspace-item.test.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/watch-workspace-item.test.js b/test/watch-workspace-item.test.js index 9ea6826a5ba..25c21753c3c 100644 --- a/test/watch-workspace-item.test.js +++ b/test/watch-workspace-item.test.js @@ -1,4 +1,5 @@ import {watchWorkspaceItem} from '../lib/watch-workspace-item'; +import URIPattern from '../lib/atom/uri-pattern'; describe('watchWorkspaceItem', function() { let sub, atomEnv, workspace, component; @@ -61,6 +62,14 @@ describe('watchWorkspaceItem', function() { assert.isTrue(component.state.theKey); }); + + it('accepts a preconstructed URIPattern', async function() { + await workspace.open('atom-github://item/one'); + const u = new URIPattern('atom-github://item/{pattern}'); + + sub = watchWorkspaceItem(workspace, u, component, 'theKey'); + assert.isTrue(component.state.theKey); + }); }); describe('workspace events', function() { From 50c6ff13dc579b42fdd63b0490bdfee932524495 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 1 Nov 2018 10:24:19 -0400 Subject: [PATCH 082/284] Unescape *all* doubled dashes in a URI pattern --- lib/atom/uri-pattern.js | 2 +- test/atom/uri-pattern.test.js | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/atom/uri-pattern.js b/lib/atom/uri-pattern.js index 0a213f50e97..558a54f95d7 100644 --- a/lib/atom/uri-pattern.js +++ b/lib/atom/uri-pattern.js @@ -246,7 +246,7 @@ function dashEscape(raw) { * Reverse the escaping performed by `dashEscape` by un-doubling `-` characters. */ function dashUnescape(escaped) { - return escaped.replace('--', '-'); + return escaped.replace(/--/g, '-'); } /** diff --git a/test/atom/uri-pattern.test.js b/test/atom/uri-pattern.test.js index 4cb77a52ade..a36f332401c 100644 --- a/test/atom/uri-pattern.test.js +++ b/test/atom/uri-pattern.test.js @@ -38,6 +38,14 @@ describe('URIPattern', function() { assert.isTrue(pattern.matches('proto://host/foo#exact').ok()); assert.isFalse(pattern.matches('proto://host/foo#nope').ok()); }); + + it('escapes and unescapes dashes', function() { + assert.isTrue( + new URIPattern('atom-github://with-many-dashes') + .matches('atom-github://with-many-dashes') + .ok(), + ); + }); }); describe('parameter placeholders', function() { From e5440cb1ebf6517253bd54cf869a4555fbd81333 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 1 Nov 2018 10:25:29 -0400 Subject: [PATCH 083/284] Use watchWorkspaceItem to track open CommitPreviewItems --- lib/controllers/commit-controller.js | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/lib/controllers/commit-controller.js b/lib/controllers/commit-controller.js index 099ebfe8105..e5301ce8394 100644 --- a/lib/controllers/commit-controller.js +++ b/lib/controllers/commit-controller.js @@ -3,18 +3,18 @@ import {TextBuffer} from 'atom'; import React from 'react'; import PropTypes from 'prop-types'; -import {CompositeDisposable} from 'event-kit'; +import {CompositeDisposable, Disposable} from 'event-kit'; import fs from 'fs-extra'; import CommitView from '../views/commit-view'; import RefHolder from '../models/ref-holder'; import CommitPreviewItem from '../items/commit-preview-item'; import {AuthorPropType, UserStorePropType} from '../prop-types'; +import {watchWorkspaceItem} from '../watch-workspace-item'; import {autobind} from '../helpers'; import {addEvent} from '../reporter-proxy'; import URIPattern from '../atom/uri-pattern'; - export const COMMIT_GRAMMAR_SCOPE = 'text.git-commit'; export default class CommitController extends React.Component { @@ -57,9 +57,8 @@ export default class CommitController extends React.Component { this.commitMessageBuffer.onDidChange(this.handleMessageChange), ); - this.state = { - commitPreviewOpen: this.isCommitPreviewOpen(), - }; + this.previewWatcherSub = new Disposable(); + this.watchCommitPreviewItems(); } isCommitPreviewOpen() { @@ -147,6 +146,22 @@ export default class CommitController extends React.Component { this.subscriptions.dispose(); } + /** + * Track the presence of CommitPreviewItems corresponding to the current repository with this.state.commitPreviewOpen. + */ + watchCommitPreviewItems() { + this.subscriptions.remove(this.previewWatcherSub); + this.previewWatcherSub.dispose(); + + this.previewWatcherSub = watchWorkspaceItem( + this.props.workspace, + CommitPreviewItem.buildURI(this.props.repository.getWorkingDirectoryPath()), + this, + 'commitPreviewOpen', + ); + this.subscriptions.add(this.previewWatcherSub); + } + commit(message, coAuthors = [], amend = false) { let msg, verbatim; if (this.isCommitMessageEditorExpanded()) { @@ -283,7 +298,6 @@ export default class CommitController extends React.Component { } toggleCommitPreview() { - this.setState({commitPreviewOpen: !this.state.commitPreviewOpen}); return this.props.workspace.toggle( CommitPreviewItem.buildURI(this.props.repository.getWorkingDirectoryPath()), ); From e16b0f8bec92ed96142a9a9c2dfd49bf45e4da4d Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 1 Nov 2018 10:27:28 -0400 Subject: [PATCH 084/284] Register a fake opener for commit preview item URIs --- test/controllers/commit-controller.test.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test/controllers/commit-controller.test.js b/test/controllers/commit-controller.test.js index ffd4c1c9be3..9abb689022a 100644 --- a/test/controllers/commit-controller.test.js +++ b/test/controllers/commit-controller.test.js @@ -6,6 +6,7 @@ import {shallow, mount} from 'enzyme'; import Commit from '../../lib/models/commit'; import {nullBranch} from '../../lib/models/branch'; import UserStore from '../../lib/models/user-store'; +import URIPattern from '../../lib/atom/uri-pattern'; import CommitController, {COMMIT_GRAMMAR_SCOPE} from '../../lib/controllers/commit-controller'; import CommitPreviewItem from '../../lib/items/commit-preview-item'; @@ -29,6 +30,16 @@ describe('CommitController', function() { const noop = () => {}; const store = new UserStore({config}); + // Ensure the Workspace doesn't mangle atom-github://... URIs + const pattern = new URIPattern(CommitPreviewItem.uriPattern); + workspace.addOpener(uri => { + if (pattern.matches(uri).ok()) { + return {getURI() { return uri; }}; + } else { + return undefined; + } + }); + app = ( Date: Thu, 1 Nov 2018 10:46:04 -0400 Subject: [PATCH 085/284] Refactor watchWorkspaceItem to use a class --- lib/watch-workspace-item.js | 84 ++++++++++++++++++++++++------------- 1 file changed, 55 insertions(+), 29 deletions(-) diff --git a/lib/watch-workspace-item.js b/lib/watch-workspace-item.js index 0dd844bc468..0f231d7ba5d 100644 --- a/lib/watch-workspace-item.js +++ b/lib/watch-workspace-item.js @@ -2,39 +2,65 @@ import {CompositeDisposable} from 'atom'; import URIPattern from './atom/uri-pattern'; -export function watchWorkspaceItem(workspace, pattern, component, stateKey) { - const uPattern = pattern instanceof URIPattern ? pattern : new URIPattern(pattern); +class ItemWatcher { + constructor(workspace, pattern, component, stateKey) { + this.workspace = workspace; + this.pattern = pattern instanceof URIPattern ? pattern : new URIPattern(pattern); + this.component = component; + this.stateKey = stateKey; + + this.itemCount = workspace.getPaneItems().filter(this.itemMatches).length; + this.subs = new CompositeDisposable(); + } - function itemMatches(item) { - return item.getURI && uPattern.matches(item.getURI()).ok(); + setInitialState() { + if (!this.component.state) { + this.component.state = {}; + } + this.component.state[this.stateKey] = this.itemCount > 0; + return this; } - if (!component.state) { - component.state = {}; + subscribeToWorkspace() { + this.subs.dispose(); + this.subs = new CompositeDisposable( + this.workspace.onDidAddPaneItem(this.itemAdded), + this.workspace.onDidDestroyPaneItem(this.itemDestroyed), + ); + return this; } - let itemCount = workspace.getPaneItems().filter(itemMatches).length; - component.state[stateKey] = itemCount > 0; - - return new CompositeDisposable( - workspace.onDidAddPaneItem(({item}) => { - const hadOpen = itemCount > 0; - if (itemMatches(item)) { - itemCount++; - - if (itemCount > 0 && !hadOpen) { - component.setState({[stateKey]: true}); - } + + itemMatches = item => item.getURI && this.pattern.matches(item.getURI()).ok() + + itemAdded = ({item}) => { + const hadOpen = this.itemCount > 0; + if (this.itemMatches(item)) { + this.itemCount++; + + if (this.itemCount > 0 && !hadOpen) { + this.component.setState({[this.stateKey]: true}); } - }), - workspace.onDidDestroyPaneItem(({item}) => { - const hadOpen = itemCount > 0; - if (itemMatches(item)) { - itemCount--; - - if (itemCount <= 0 && hadOpen) { - component.setState({[stateKey]: false}); - } + } + } + + itemDestroyed = ({item}) => { + const hadOpen = this.itemCount > 0; + if (this.itemMatches(item)) { + this.itemCount--; + + if (this.itemCount <= 0 && hadOpen) { + this.component.setState({[this.stateKey]: false}); } - }), - ); + } + } + + dispose() { + this.subs.dispose(); + } +} + +export function watchWorkspaceItem(workspace, pattern, component, stateKey) { + return new ItemWatcher(workspace, pattern, component, stateKey) + .setInitialState() + .subscribeToWorkspace(); } From adecb128224fc4117d58a6fde60d7c0e9264f218 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 1 Nov 2018 11:07:57 -0400 Subject: [PATCH 086/284] Update an item watcher's pattern on a mounted component with setPattern --- lib/watch-workspace-item.js | 24 +++++++++++++++++++++++- test/watch-workspace-item.test.js | 27 ++++++++++++++++++++++++++- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/lib/watch-workspace-item.js b/lib/watch-workspace-item.js index 0f231d7ba5d..e55fdae9ba1 100644 --- a/lib/watch-workspace-item.js +++ b/lib/watch-workspace-item.js @@ -9,7 +9,7 @@ class ItemWatcher { this.component = component; this.stateKey = stateKey; - this.itemCount = workspace.getPaneItems().filter(this.itemMatches).length; + this.itemCount = this.getItemCount(); this.subs = new CompositeDisposable(); } @@ -30,8 +30,30 @@ class ItemWatcher { return this; } + setPattern(pattern) { + const wasTrue = this.itemCount > 0; + + this.pattern = pattern instanceof URIPattern ? pattern : new URIPattern(pattern); + + // Update the item count to match the new pattern + this.itemCount = this.getItemCount(); + + // Update the component's state if it's changed as a result + if (wasTrue && this.itemCount <= 0) { + return new Promise(resolve => this.component.setState({[this.stateKey]: false}, resolve)); + } else if (!wasTrue && this.itemCount > 0) { + return new Promise(resolve => this.component.setState({[this.stateKey]: true}, resolve)); + } else { + return Promise.resolve(); + } + } + itemMatches = item => item.getURI && this.pattern.matches(item.getURI()).ok() + getItemCount() { + return this.workspace.getPaneItems().filter(this.itemMatches).length; + } + itemAdded = ({item}) => { const hadOpen = this.itemCount > 0; if (this.itemMatches(item)) { diff --git a/test/watch-workspace-item.test.js b/test/watch-workspace-item.test.js index 25c21753c3c..f5926855466 100644 --- a/test/watch-workspace-item.test.js +++ b/test/watch-workspace-item.test.js @@ -10,7 +10,7 @@ describe('watchWorkspaceItem', function() { component = { state: {}, - setState: sinon.stub().resolves(), + setState: sinon.stub().callsFake((updater, cb) => cb && cb()), }; workspace.addOpener(uri => { @@ -131,4 +131,29 @@ describe('watchWorkspaceItem', function() { await workspace.hide('atom-github://item'); assert.isFalse(component.setState.called); }); + + describe('setPattern', function() { + it('immediately updates the state based on the new pattern', async function() { + sub = watchWorkspaceItem(workspace, 'atom-github://item0/{pattern}', component, 'theKey'); + assert.isFalse(component.state.theKey); + + await workspace.open('atom-github://item1/match'); + assert.isFalse(component.setState.called); + + await sub.setPattern('atom-github://item1/{pattern}'); + assert.isFalse(component.state.theKey); + assert.isTrue(component.setState.calledWith({theKey: true})); + }); + + it('uses the new pattern to keep state up to date', async function() { + sub = watchWorkspaceItem(workspace, 'atom-github://item0/{pattern}', component, 'theKey'); + await sub.setPattern('atom-github://item1/{pattern}'); + + await workspace.open('atom-github://item0/match'); + assert.isFalse(component.setState.called); + + await workspace.open('atom-github://item1/match'); + assert.isTrue(component.setState.calledWith({theKey: true})); + }); + }); }); From 60a9a8212277bfd2062e9c8de678400aded64c3b Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 1 Nov 2018 11:10:49 -0400 Subject: [PATCH 087/284] Use setPattern to update the existing preview item watcher --- lib/controllers/commit-controller.js | 42 +++++++--------------- test/controllers/commit-controller.test.js | 32 +++++++++++++++++ 2 files changed, 45 insertions(+), 29 deletions(-) diff --git a/lib/controllers/commit-controller.js b/lib/controllers/commit-controller.js index e5301ce8394..f4515eec8e2 100644 --- a/lib/controllers/commit-controller.js +++ b/lib/controllers/commit-controller.js @@ -57,19 +57,13 @@ export default class CommitController extends React.Component { this.commitMessageBuffer.onDidChange(this.handleMessageChange), ); - this.previewWatcherSub = new Disposable(); - this.watchCommitPreviewItems(); - } - - isCommitPreviewOpen() { - const items = this.props.workspace.getPaneItems(); - const uriPattern = new URIPattern(CommitPreviewItem.uriPattern); - for (const item of items) { - if (item.getURI && uriPattern.matches(item.getURI())) { - return true; - } - } - return false; + this.previewWatcher = watchWorkspaceItem( + this.props.workspace, + CommitPreviewItem.buildURI(this.props.repository.getWorkingDirectoryPath()), + this, + 'commitPreviewOpen', + ); + this.subscriptions.add(this.previewWatcher); } componentDidMount() { @@ -140,28 +134,18 @@ export default class CommitController extends React.Component { } else { this.commitMessageBuffer.setTextViaDiff(this.getCommitMessage()); } + + if (prevProps.repository !== this.props.repository) { + this.previewWatcher.setPattern( + CommitPreviewItem.buildURI(this.props.repository.getWorkingDirectoryPath()), + ); + } } componentWillUnmount() { this.subscriptions.dispose(); } - /** - * Track the presence of CommitPreviewItems corresponding to the current repository with this.state.commitPreviewOpen. - */ - watchCommitPreviewItems() { - this.subscriptions.remove(this.previewWatcherSub); - this.previewWatcherSub.dispose(); - - this.previewWatcherSub = watchWorkspaceItem( - this.props.workspace, - CommitPreviewItem.buildURI(this.props.repository.getWorkingDirectoryPath()), - this, - 'commitPreviewOpen', - ); - this.subscriptions.add(this.previewWatcherSub); - } - commit(message, coAuthors = [], amend = false) { let msg, verbatim; if (this.isCommitMessageEditorExpanded()) { diff --git a/test/controllers/commit-controller.test.js b/test/controllers/commit-controller.test.js index 9abb689022a..8e450e80dc3 100644 --- a/test/controllers/commit-controller.test.js +++ b/test/controllers/commit-controller.test.js @@ -449,5 +449,37 @@ describe('CommitController', function() { assert.isTrue(workspace.toggle.calledThrice); assert.isTrue(wrapper.find('CommitView').prop('commitPreviewOpen')); }); + + it('toggles the commit preview pane for the active repository', async function() { + const workdir0 = await cloneRepository('three-files'); + const repository0 = await buildRepository(workdir0); + + const workdir1 = await cloneRepository('three-files'); + const repository1 = await buildRepository(workdir1); + + const wrapper = shallow(React.cloneElement(app, {repository: repository0})); + + assert.isFalse(wrapper.find('CommitView').prop('commitPreviewOpen')); + + await wrapper.find('CommitView').prop('toggleCommitPreview')(); + assert.isTrue(workspace.getPaneItems().some(item => item.getURI() === CommitPreviewItem.buildURI(workdir0))); + assert.isFalse(workspace.getPaneItems().some(item => item.getURI() === CommitPreviewItem.buildURI(workdir1))); + assert.isTrue(wrapper.find('CommitView').prop('commitPreviewOpen')); + + wrapper.setProps({repository: repository1}); + assert.isTrue(workspace.getPaneItems().some(item => item.getURI() === CommitPreviewItem.buildURI(workdir0))); + assert.isFalse(workspace.getPaneItems().some(item => item.getURI() === CommitPreviewItem.buildURI(workdir1))); + assert.isFalse(wrapper.find('CommitView').prop('commitPreviewOpen')); + + await wrapper.find('CommitView').prop('toggleCommitPreview')(); + assert.isTrue(workspace.getPaneItems().some(item => item.getURI() === CommitPreviewItem.buildURI(workdir0))); + assert.isTrue(workspace.getPaneItems().some(item => item.getURI() === CommitPreviewItem.buildURI(workdir1))); + assert.isTrue(wrapper.find('CommitView').prop('commitPreviewOpen')); + + await wrapper.find('CommitView').prop('toggleCommitPreview')(); + assert.isTrue(workspace.getPaneItems().some(item => item.getURI() === CommitPreviewItem.buildURI(workdir0))); + assert.isFalse(workspace.getPaneItems().some(item => item.getURI() === CommitPreviewItem.buildURI(workdir1))); + assert.isFalse(wrapper.find('CommitView').prop('commitPreviewOpen')); + }); }); }); From afb2a1ee77f198d34640fd4a094e7322de4b456f Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 1 Nov 2018 11:21:23 -0400 Subject: [PATCH 088/284] :art: group related props --- 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 92b9b08e4b7..b3d9e348bd0 100644 --- a/lib/views/file-patch-view.js +++ b/lib/views/file-patch-view.js @@ -36,6 +36,7 @@ export default class FilePatchView extends React.Component { repository: PropTypes.object.isRequired, hasUndoHistory: PropTypes.bool.isRequired, useEditorAutoHeight: PropTypes.bool, + isActive: PropTypes.bool, workspace: PropTypes.object.isRequired, commands: PropTypes.object.isRequired, @@ -54,7 +55,6 @@ export default class FilePatchView extends React.Component { toggleSymlinkChange: PropTypes.func.isRequired, undoLastDiscard: PropTypes.func.isRequired, discardRows: PropTypes.func.isRequired, - isActive: PropTypes.bool, handleMouseDown: PropTypes.func, } From eb42fb24799fbed2b17c2350c32eab54058f7f27 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 1 Nov 2018 11:31:59 -0400 Subject: [PATCH 089/284] Require isActive prop in FilePatchView --- lib/views/file-patch-view.js | 2 +- test/views/file-patch-view.test.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/views/file-patch-view.js b/lib/views/file-patch-view.js index b3d9e348bd0..e40a8cc0212 100644 --- a/lib/views/file-patch-view.js +++ b/lib/views/file-patch-view.js @@ -36,7 +36,7 @@ export default class FilePatchView extends React.Component { repository: PropTypes.object.isRequired, hasUndoHistory: PropTypes.bool.isRequired, useEditorAutoHeight: PropTypes.bool, - isActive: PropTypes.bool, + isActive: PropTypes.bool.isRequired, workspace: PropTypes.object.isRequired, commands: PropTypes.object.isRequired, diff --git a/test/views/file-patch-view.test.js b/test/views/file-patch-view.test.js index 8964aac0909..da8b386b8c8 100644 --- a/test/views/file-patch-view.test.js +++ b/test/views/file-patch-view.test.js @@ -53,6 +53,7 @@ describe('FilePatchView', function() { selectionMode: 'line', selectedRows: new Set(), repository, + isActive: true, workspace, config: atomEnv.config, From 6bc589037442851a2fa9708f9e21d5274a91d277 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 1 Nov 2018 11:38:14 -0400 Subject: [PATCH 090/284] Add a CSS class to FilePatchView's root when inactive --- lib/views/file-patch-view.js | 1 + test/views/file-patch-view.test.js | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/lib/views/file-patch-view.js b/lib/views/file-patch-view.js index e40a8cc0212..4a52d2ed858 100644 --- a/lib/views/file-patch-view.js +++ b/lib/views/file-patch-view.js @@ -164,6 +164,7 @@ export default class FilePatchView extends React.Component { `github-FilePatchView--${this.props.stagingStatus}`, {'github-FilePatchView--blank': !this.props.filePatch.isPresent()}, {'github-FilePatchView--hunkMode': this.props.selectionMode === 'hunk'}, + {'github-FilePatchView--inactive': !this.props.isActive}, ); return ( diff --git a/test/views/file-patch-view.test.js b/test/views/file-patch-view.test.js index da8b386b8c8..ed9ebfdf95e 100644 --- a/test/views/file-patch-view.test.js +++ b/test/views/file-patch-view.test.js @@ -116,6 +116,13 @@ describe('FilePatchView', function() { assert.isTrue(wrapper.find('.github-FilePatchView--hunkMode').exists()); }); + it('sets the root class when inactive', function() { + const wrapper = shallow(buildApp({isActive: true})); + assert.isFalse(wrapper.find('.github-FilePatchView--inactive').exists()); + wrapper.setProps({isActive: false}); + assert.isTrue(wrapper.find('.github-FilePatchView--inactive').exists()); + }); + it('preserves the selection index when a new file patch arrives in line selection mode', function() { const selectedRowsChanged = sinon.spy(); const wrapper = mount(buildApp({ From 6a70de9c87dd4bceeee40c74df5966c089b680a5 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 1 Nov 2018 11:42:33 -0400 Subject: [PATCH 091/284] Wait I got that backwards --- lib/views/file-patch-view.js | 2 +- test/views/file-patch-view.test.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/views/file-patch-view.js b/lib/views/file-patch-view.js index 4a52d2ed858..a8e0f4f008d 100644 --- a/lib/views/file-patch-view.js +++ b/lib/views/file-patch-view.js @@ -164,7 +164,7 @@ export default class FilePatchView extends React.Component { `github-FilePatchView--${this.props.stagingStatus}`, {'github-FilePatchView--blank': !this.props.filePatch.isPresent()}, {'github-FilePatchView--hunkMode': this.props.selectionMode === 'hunk'}, - {'github-FilePatchView--inactive': !this.props.isActive}, + {'github-FilePatchView--active': this.props.isActive}, ); return ( diff --git a/test/views/file-patch-view.test.js b/test/views/file-patch-view.test.js index ed9ebfdf95e..7c7073bf1f5 100644 --- a/test/views/file-patch-view.test.js +++ b/test/views/file-patch-view.test.js @@ -116,11 +116,11 @@ describe('FilePatchView', function() { assert.isTrue(wrapper.find('.github-FilePatchView--hunkMode').exists()); }); - it('sets the root class when inactive', function() { + it('sets the root class when active', function() { const wrapper = shallow(buildApp({isActive: true})); - assert.isFalse(wrapper.find('.github-FilePatchView--inactive').exists()); + assert.isTrue(wrapper.find('.github-FilePatchView--active').exists()); wrapper.setProps({isActive: false}); - assert.isTrue(wrapper.find('.github-FilePatchView--inactive').exists()); + assert.isFalse(wrapper.find('.github-FilePatchView--active').exists()); }); it('preserves the selection index when a new file patch arrives in line selection mode', function() { From 17dbf1e4aef78c7a89474188f752142be56e5676 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 1 Nov 2018 11:44:45 -0400 Subject: [PATCH 092/284] Don't style cursor lines in inactive FilePatchView editors --- styles/file-patch-view.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/styles/file-patch-view.less b/styles/file-patch-view.less index c04a3445b02..a2694cc9b25 100644 --- a/styles/file-patch-view.less +++ b/styles/file-patch-view.less @@ -163,7 +163,7 @@ .hunk-line-mixin(@bg;) { background-color: fade(@bg, 18%); - &.line.cursor-line { + .github-FilePatchView--active &.line.cursor-line { background-color: fade(@bg, 28%); } } From b1bf5c89f956d3a5a4c30b32168c107be674eb16 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 1 Nov 2018 11:48:03 -0400 Subject: [PATCH 093/284] Okay fine let's do a class for both active and inactive --- lib/views/file-patch-view.js | 1 + test/views/file-patch-view.test.js | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/views/file-patch-view.js b/lib/views/file-patch-view.js index a8e0f4f008d..4cbf23538a3 100644 --- a/lib/views/file-patch-view.js +++ b/lib/views/file-patch-view.js @@ -165,6 +165,7 @@ export default class FilePatchView extends React.Component { {'github-FilePatchView--blank': !this.props.filePatch.isPresent()}, {'github-FilePatchView--hunkMode': this.props.selectionMode === 'hunk'}, {'github-FilePatchView--active': this.props.isActive}, + {'github-FilePatchView--inactive': !this.props.isActive}, ); return ( diff --git a/test/views/file-patch-view.test.js b/test/views/file-patch-view.test.js index 7c7073bf1f5..05a919011aa 100644 --- a/test/views/file-patch-view.test.js +++ b/test/views/file-patch-view.test.js @@ -116,11 +116,13 @@ describe('FilePatchView', function() { assert.isTrue(wrapper.find('.github-FilePatchView--hunkMode').exists()); }); - it('sets the root class when active', function() { + it('sets the root class when active or inactive', function() { const wrapper = shallow(buildApp({isActive: true})); assert.isTrue(wrapper.find('.github-FilePatchView--active').exists()); + assert.isFalse(wrapper.find('.github-FilePatchView--inactive').exists()); wrapper.setProps({isActive: false}); assert.isFalse(wrapper.find('.github-FilePatchView--active').exists()); + assert.isTrue(wrapper.find('.github-FilePatchView--inactive').exists()); }); it('preserves the selection index when a new file patch arrives in line selection mode', function() { From a2fc2acbac3a642d661f0b9411787873be617363 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 1 Nov 2018 11:52:00 -0400 Subject: [PATCH 094/284] Hide selection regions in inactive FilePatch editors --- styles/file-patch-view.less | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/styles/file-patch-view.less b/styles/file-patch-view.less index a2694cc9b25..8fe131c6c03 100644 --- a/styles/file-patch-view.less +++ b/styles/file-patch-view.less @@ -225,4 +225,10 @@ } } } + + // Inactive + + &--inactive .highlights .highlight.selection { + display: none; + } } From 48aa0e6c6896eee081ac735355aedf2230a7539e Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 1 Nov 2018 12:49:46 -0400 Subject: [PATCH 095/284] :shirt: unused imports --- lib/controllers/commit-controller.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/controllers/commit-controller.js b/lib/controllers/commit-controller.js index f4515eec8e2..059a7282bb0 100644 --- a/lib/controllers/commit-controller.js +++ b/lib/controllers/commit-controller.js @@ -3,7 +3,7 @@ import {TextBuffer} from 'atom'; import React from 'react'; import PropTypes from 'prop-types'; -import {CompositeDisposable, Disposable} from 'event-kit'; +import {CompositeDisposable} from 'event-kit'; import fs from 'fs-extra'; import CommitView from '../views/commit-view'; @@ -13,7 +13,6 @@ import {AuthorPropType, UserStorePropType} from '../prop-types'; import {watchWorkspaceItem} from '../watch-workspace-item'; import {autobind} from '../helpers'; import {addEvent} from '../reporter-proxy'; -import URIPattern from '../atom/uri-pattern'; export const COMMIT_GRAMMAR_SCOPE = 'text.git-commit'; From 72e1c611f63fb21b8973b46ca0329fbf0aeda9a4 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 1 Nov 2018 14:30:02 -0400 Subject: [PATCH 096/284] Shhhhhhh this is totally related --- keymaps/git.cson | 1 + 1 file changed, 1 insertion(+) diff --git a/keymaps/git.cson b/keymaps/git.cson index 368b2d5759b..2075d945599 100644 --- a/keymaps/git.cson +++ b/keymaps/git.cson @@ -26,6 +26,7 @@ 'shift-tab': 'core:focus-previous' 'o': 'github:open-file' 'left': 'core:move-left' + 'cmd-left': 'core:move-left' '.github-CommitView button': 'tab': 'core:focus-next' From bd43ddd1d30e22d4d60c6d3a0cbd1e45b85cd164 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 1 Nov 2018 14:49:51 -0400 Subject: [PATCH 097/284] Add .native-key-bindings to within CommitView --- lib/views/commit-view.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/views/commit-view.js b/lib/views/commit-view.js index 400c96c4d0f..69017069154 100644 --- a/lib/views/commit-view.js +++ b/lib/views/commit-view.js @@ -164,7 +164,7 @@ export default class CommitView extends React.Component {
{this.commitIsEnabled(false) && } From a8bb1e98e30072351f85d54a8573c3dd318da35b Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 2 Nov 2018 12:03:35 -0400 Subject: [PATCH 108/284] Tests to add an {active: true} flag on watchWorkspaceItem When set to true, the state key is set to `true` only when a matching item is the active item in some Pane. When set to false, the state key is set to true when a matching item is open anywhere in the Workspace. --- test/watch-workspace-item.test.js | 46 ++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/test/watch-workspace-item.test.js b/test/watch-workspace-item.test.js index f5926855466..533d74fb995 100644 --- a/test/watch-workspace-item.test.js +++ b/test/watch-workspace-item.test.js @@ -1,7 +1,7 @@ import {watchWorkspaceItem} from '../lib/watch-workspace-item'; import URIPattern from '../lib/atom/uri-pattern'; -describe('watchWorkspaceItem', function() { +describe.only('watchWorkspaceItem', function() { let sub, atomEnv, workspace, component; beforeEach(function() { @@ -70,6 +70,42 @@ describe('watchWorkspaceItem', function() { sub = watchWorkspaceItem(workspace, u, component, 'theKey'); assert.isTrue(component.state.theKey); }); + + describe('{active: true}', function() { + it('is false when the pane is not open', async function() { + await workspace.open('atom-github://nonmatching'); + + sub = watchWorkspaceItem(workspace, 'atom-github://item', component, 'someKey', {active: true}); + assert.isFalse(component.state.someKey); + }); + + it('is false when the pane is open, but not active', async function() { + await workspace.open('atom-github://item'); + await workspace.open('atom-github://nonmatching'); + + sub = watchWorkspaceItem(workspace, 'atom-github://item', component, 'someKey', {active: true}); + assert.isFalse(component.state.someKey); + }); + + it('is true when the pane is open and active in the workspace', async function() { + await workspace.open('atom-github://nonmatching'); + await workspace.open('atom-github://item'); + + sub = watchWorkspaceItem(workspace, 'atom-github://item', component, 'someKey', {active: true}); + assert.isTrue(component.state.someKey); + }); + + it('is true when the pane is open and active in any pane', async function() { + await workspace.open('atom-github://item', {location: 'right'}); + await workspace.open('atom-github://nonmatching'); + + assert.strictEqual(workspace.getRightDock().getActivePaneItem().getURI(), 'atom-github://item'); + assert.strictEqual(workspace.getActivePaneItem(), 'atom-github://nonmatching'); + + sub = watchWorkspaceItem(workspace, 'atom-github://item', component, 'someKey', {active: true}); + assert.isTrue(component.state.someKey); + }); + }); }); describe('workspace events', function() { @@ -118,6 +154,10 @@ describe('watchWorkspaceItem', function() { assert.isTrue(component.setState.calledWith({theKey: false})); }); + + describe('{active: true}', function() { + // + }); }); it('stops updating when disposed', async function() { @@ -155,5 +195,9 @@ describe('watchWorkspaceItem', function() { await workspace.open('atom-github://item1/match'); assert.isTrue(component.setState.calledWith({theKey: true})); }); + + describe('{active: true}', function() { + // + }); }); }); From e6214f93a6b3706dd655aec434b13d13fe426b3b Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 2 Nov 2018 12:04:05 -0400 Subject: [PATCH 109/284] Adjust CommitController tests for tri-state behavior --- test/controllers/commit-controller.test.js | 43 ++++++++++++++-------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/test/controllers/commit-controller.test.js b/test/controllers/commit-controller.test.js index 67e8654a662..6142c3d2b5a 100644 --- a/test/controllers/commit-controller.test.js +++ b/test/controllers/commit-controller.test.js @@ -430,24 +430,38 @@ describe('CommitController', function() { it('opens and closes commit preview pane', async function() { const workdir = await cloneRepository('three-files'); const repository = await buildRepository(workdir); + const previewURI = CommitPreviewItem.buildURI(workdir); const wrapper = shallow(React.cloneElement(app, {repository})); - sinon.spy(workspace, 'toggle'); - - assert.isFalse(wrapper.find('CommitView').prop('commitPreviewOpen')); + assert.isFalse(wrapper.find('CommitView').prop('commitPreviewActive')); await wrapper.find('CommitView').prop('toggleCommitPreview')(); - assert.isTrue(workspace.toggle.calledWith(CommitPreviewItem.buildURI(workdir))); - assert.isTrue(wrapper.find('CommitView').prop('commitPreviewOpen')); + + // Commit preview open as active pane item + assert.strictEqual(workspace.getActivePaneItem().getURI(), previewURI); + assert.strictEqual(workspace.getActivePaneItem(), workspace.paneForItem(previewURI).getPendingItem()); + assert.isTrue(wrapper.find('CommitView').prop('commitPreviewActive')); + + await workspace.open(__filename); + + // Commit preview open, but not active + assert.include(workspace.getAllPaneItems().map(i => i.getURI()), previewURI); + assert.notStrictEqual(workspace.getActivePaneItem().getURI(), previewURI); + assert.isFalse(wrapper.find('CommitView').prop('commitPreviewActive')); await wrapper.find('CommitView').prop('toggleCommitPreview')(); - assert.isTrue(workspace.toggle.calledTwice); - assert.isFalse(wrapper.find('CommitView').prop('commitPreviewOpen')); + + // Open as active pane item again + assert.strictEqual(workspace.getActivePaneItem().getURI(), previewURI); + assert.strictEqual(workspace.getActivePaneItem(), workspace.paneForItem(previewURI).getPendingItem()); + assert.isTrue(wrapper.find('CommitView').prop('commitPreviewActive')); await wrapper.find('CommitView').prop('toggleCommitPreview')(); - assert.isTrue(workspace.toggle.calledThrice); - assert.isTrue(wrapper.find('CommitView').prop('commitPreviewOpen')); + + // Commit preview closed + assert.notInclude(workspace.getAllPaneItems().map(i => i.getURI()), previewURI); + assert.isTrue(wrapper.find('CommitView').prop('commitPreviewActive')); }); it('records a metrics event when pane is toggled', async function() { @@ -464,7 +478,6 @@ describe('CommitController', function() { assert.isTrue(reporterProxy.addEvent.calledOnceWithExactly('toggle-commit-preview', {package: 'github'})); }); - it('toggles the commit preview pane for the active repository', async function() { const workdir0 = await cloneRepository('three-files'); const repository0 = await buildRepository(workdir0); @@ -474,27 +487,27 @@ describe('CommitController', function() { const wrapper = shallow(React.cloneElement(app, {repository: repository0})); - assert.isFalse(wrapper.find('CommitView').prop('commitPreviewOpen')); + assert.isFalse(wrapper.find('CommitView').prop('commitPreviewActive')); await wrapper.find('CommitView').prop('toggleCommitPreview')(); assert.isTrue(workspace.getPaneItems().some(item => item.getURI() === CommitPreviewItem.buildURI(workdir0))); assert.isFalse(workspace.getPaneItems().some(item => item.getURI() === CommitPreviewItem.buildURI(workdir1))); - assert.isTrue(wrapper.find('CommitView').prop('commitPreviewOpen')); + assert.isTrue(wrapper.find('CommitView').prop('commitPreviewActive')); wrapper.setProps({repository: repository1}); assert.isTrue(workspace.getPaneItems().some(item => item.getURI() === CommitPreviewItem.buildURI(workdir0))); assert.isFalse(workspace.getPaneItems().some(item => item.getURI() === CommitPreviewItem.buildURI(workdir1))); - assert.isFalse(wrapper.find('CommitView').prop('commitPreviewOpen')); + assert.isFalse(wrapper.find('CommitView').prop('commitPreviewActive')); await wrapper.find('CommitView').prop('toggleCommitPreview')(); assert.isTrue(workspace.getPaneItems().some(item => item.getURI() === CommitPreviewItem.buildURI(workdir0))); assert.isTrue(workspace.getPaneItems().some(item => item.getURI() === CommitPreviewItem.buildURI(workdir1))); - assert.isTrue(wrapper.find('CommitView').prop('commitPreviewOpen')); + assert.isTrue(wrapper.find('CommitView').prop('commitPreviewActive')); await wrapper.find('CommitView').prop('toggleCommitPreview')(); assert.isTrue(workspace.getPaneItems().some(item => item.getURI() === CommitPreviewItem.buildURI(workdir0))); assert.isFalse(workspace.getPaneItems().some(item => item.getURI() === CommitPreviewItem.buildURI(workdir1))); - assert.isFalse(wrapper.find('CommitView').prop('commitPreviewOpen')); + assert.isFalse(wrapper.find('CommitView').prop('commitPreviewActive')); }); }); }); From c91ba1dee175d6176a8a77d7f6573a7a477df8be Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 2 Nov 2018 12:04:22 -0400 Subject: [PATCH 110/284] Rename the prop because we care about active now instead of just open --- lib/controllers/commit-controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/controllers/commit-controller.js b/lib/controllers/commit-controller.js index 4ddf9c37289..601c084fa86 100644 --- a/lib/controllers/commit-controller.js +++ b/lib/controllers/commit-controller.js @@ -60,7 +60,7 @@ export default class CommitController extends React.Component { this.props.workspace, CommitPreviewItem.buildURI(this.props.repository.getWorkingDirectoryPath()), this, - 'commitPreviewOpen', + 'commitPreviewActive', ); this.subscriptions.add(this.previewWatcher); } From 67e70d081f7a1c79a287cee415305a476e6510a6 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 2 Nov 2018 12:04:37 -0400 Subject: [PATCH 111/284] Incomplete work to get {active: true} working --- lib/watch-workspace-item.js | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/lib/watch-workspace-item.js b/lib/watch-workspace-item.js index e55fdae9ba1..f047648334f 100644 --- a/lib/watch-workspace-item.js +++ b/lib/watch-workspace-item.js @@ -3,21 +3,31 @@ import {CompositeDisposable} from 'atom'; import URIPattern from './atom/uri-pattern'; class ItemWatcher { - constructor(workspace, pattern, component, stateKey) { + constructor(workspace, pattern, component, stateKey, opts) { this.workspace = workspace; this.pattern = pattern instanceof URIPattern ? pattern : new URIPattern(pattern); this.component = component; this.stateKey = stateKey; + this.opts = opts; - this.itemCount = this.getItemCount(); + this.itemCount = this.readItemCount(); + this.activeCount = this.readActiveCount(); this.subs = new CompositeDisposable(); } + getCurrentState() { + if (this.opts.active) { + return this.activeCount > 0; + } else { + return this.itemCount > 0; + } + } + setInitialState() { if (!this.component.state) { this.component.state = {}; } - this.component.state[this.stateKey] = this.itemCount > 0; + this.component.state[this.stateKey] = this.getCurrentState(); return this; } @@ -50,10 +60,14 @@ class ItemWatcher { itemMatches = item => item.getURI && this.pattern.matches(item.getURI()).ok() - getItemCount() { + readItemCount() { return this.workspace.getPaneItems().filter(this.itemMatches).length; } + readActiveCount() { + return this.workspace.getPanes().filter(pane => this.itemMatches(pane.getActiveItem())).length; + } + itemAdded = ({item}) => { const hadOpen = this.itemCount > 0; if (this.itemMatches(item)) { @@ -81,8 +95,13 @@ class ItemWatcher { } } -export function watchWorkspaceItem(workspace, pattern, component, stateKey) { - return new ItemWatcher(workspace, pattern, component, stateKey) +export function watchWorkspaceItem(workspace, pattern, component, stateKey, options = {}) { + const opts = { + active: false, + ...options, + }; + + return new ItemWatcher(workspace, pattern, component, stateKey, opts) .setInitialState() .subscribeToWorkspace(); } From 29f75d78fa3996599b4f49f8ef48797212d19725 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 2 Nov 2018 14:24:45 -0700 Subject: [PATCH 112/284] Check if item exists before calling its `getURI` method --- lib/watch-workspace-item.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/watch-workspace-item.js b/lib/watch-workspace-item.js index f047648334f..99f46c723d9 100644 --- a/lib/watch-workspace-item.js +++ b/lib/watch-workspace-item.js @@ -58,7 +58,9 @@ class ItemWatcher { } } - itemMatches = item => item.getURI && this.pattern.matches(item.getURI()).ok() + itemMatches = item => { + return item && item.getURI && this.pattern.matches(item.getURI()).ok(); + } readItemCount() { return this.workspace.getPaneItems().filter(this.itemMatches).length; From 8904f391413f39ca82006bf38f1d968177152b95 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 2 Nov 2018 15:30:24 -0700 Subject: [PATCH 113/284] Actually, let's inline that function --- lib/watch-workspace-item.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/watch-workspace-item.js b/lib/watch-workspace-item.js index 99f46c723d9..17c315542cc 100644 --- a/lib/watch-workspace-item.js +++ b/lib/watch-workspace-item.js @@ -58,9 +58,7 @@ class ItemWatcher { } } - itemMatches = item => { - return item && item.getURI && this.pattern.matches(item.getURI()).ok(); - } + itemMatches = item => item && item.getURI && this.pattern.matches(item.getURI()).ok() readItemCount() { return this.workspace.getPaneItems().filter(this.itemMatches).length; From b9ef77305e1abdf29f1952c5e47df6c66ab5aae0 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 2 Nov 2018 15:31:48 -0700 Subject: [PATCH 114/284] Revert "Incomplete work to get {active: true} working" This reverts commit 67e70d081f7a1c79a287cee415305a476e6510a6. --- lib/watch-workspace-item.js | 31 ++++++------------------------- 1 file changed, 6 insertions(+), 25 deletions(-) diff --git a/lib/watch-workspace-item.js b/lib/watch-workspace-item.js index 17c315542cc..05da51cf135 100644 --- a/lib/watch-workspace-item.js +++ b/lib/watch-workspace-item.js @@ -3,31 +3,21 @@ import {CompositeDisposable} from 'atom'; import URIPattern from './atom/uri-pattern'; class ItemWatcher { - constructor(workspace, pattern, component, stateKey, opts) { + constructor(workspace, pattern, component, stateKey) { this.workspace = workspace; this.pattern = pattern instanceof URIPattern ? pattern : new URIPattern(pattern); this.component = component; this.stateKey = stateKey; - this.opts = opts; - this.itemCount = this.readItemCount(); - this.activeCount = this.readActiveCount(); + this.itemCount = this.getItemCount(); this.subs = new CompositeDisposable(); } - getCurrentState() { - if (this.opts.active) { - return this.activeCount > 0; - } else { - return this.itemCount > 0; - } - } - setInitialState() { if (!this.component.state) { this.component.state = {}; } - this.component.state[this.stateKey] = this.getCurrentState(); + this.component.state[this.stateKey] = this.itemCount > 0; return this; } @@ -60,14 +50,10 @@ class ItemWatcher { itemMatches = item => item && item.getURI && this.pattern.matches(item.getURI()).ok() - readItemCount() { + getItemCount() { return this.workspace.getPaneItems().filter(this.itemMatches).length; } - readActiveCount() { - return this.workspace.getPanes().filter(pane => this.itemMatches(pane.getActiveItem())).length; - } - itemAdded = ({item}) => { const hadOpen = this.itemCount > 0; if (this.itemMatches(item)) { @@ -95,13 +81,8 @@ class ItemWatcher { } } -export function watchWorkspaceItem(workspace, pattern, component, stateKey, options = {}) { - const opts = { - active: false, - ...options, - }; - - return new ItemWatcher(workspace, pattern, component, stateKey, opts) +export function watchWorkspaceItem(workspace, pattern, component, stateKey) { + return new ItemWatcher(workspace, pattern, component, stateKey) .setInitialState() .subscribeToWorkspace(); } From 669015bac1df5f168eccd7d07e20ca4192d312a3 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 2 Nov 2018 17:19:18 -0700 Subject: [PATCH 115/284] Assert on the URI, not item --- test/watch-workspace-item.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/watch-workspace-item.test.js b/test/watch-workspace-item.test.js index 533d74fb995..96a857158f3 100644 --- a/test/watch-workspace-item.test.js +++ b/test/watch-workspace-item.test.js @@ -100,7 +100,7 @@ describe.only('watchWorkspaceItem', function() { await workspace.open('atom-github://nonmatching'); assert.strictEqual(workspace.getRightDock().getActivePaneItem().getURI(), 'atom-github://item'); - assert.strictEqual(workspace.getActivePaneItem(), 'atom-github://nonmatching'); + assert.strictEqual(workspace.getActivePaneItem().getURI(), 'atom-github://nonmatching'); sub = watchWorkspaceItem(workspace, 'atom-github://item', component, 'someKey', {active: true}); assert.isTrue(component.state.someKey); From 7258c297cc99d93366f40770683a1133614a7f3c Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 2 Nov 2018 17:23:00 -0700 Subject: [PATCH 116/284] Stopgap for funky test issue (`atom-github://item` opens in right dock) --- test/watch-workspace-item.test.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/test/watch-workspace-item.test.js b/test/watch-workspace-item.test.js index 96a857158f3..1a392132b7c 100644 --- a/test/watch-workspace-item.test.js +++ b/test/watch-workspace-item.test.js @@ -80,10 +80,11 @@ describe.only('watchWorkspaceItem', function() { }); it('is false when the pane is open, but not active', async function() { - await workspace.open('atom-github://item'); + // TODO: fix this test suite so that 'atom-github://item' works + await workspace.open('atom-github://some-item'); await workspace.open('atom-github://nonmatching'); - sub = watchWorkspaceItem(workspace, 'atom-github://item', component, 'someKey', {active: true}); + sub = watchWorkspaceItem(workspace, 'atom-github://some-item', component, 'someKey', {active: true}); assert.isFalse(component.state.someKey); }); @@ -161,14 +162,15 @@ describe.only('watchWorkspaceItem', function() { }); it('stops updating when disposed', async function() { - sub = watchWorkspaceItem(workspace, 'atom-github://item', component, 'theKey'); + // TODO: fix this test suite so that 'atom-github://item' works + sub = watchWorkspaceItem(workspace, 'atom-github://some-item', component, 'theKey'); assert.isFalse(component.state.theKey); sub.dispose(); - await workspace.open('atom-github://item'); + await workspace.open('atom-github://some-item'); assert.isFalse(component.setState.called); - await workspace.hide('atom-github://item'); + await workspace.hide('atom-github://some-item'); assert.isFalse(component.setState.called); }); From 7b252994482961db3ea5954749c022a7b61604dc Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 2 Nov 2018 17:27:41 -0700 Subject: [PATCH 117/284] Implement ActiveItemWatcher class --- lib/watch-workspace-item.js | 81 +++++++++++++++++++++++++++++++++++-- 1 file changed, 77 insertions(+), 4 deletions(-) diff --git a/lib/watch-workspace-item.js b/lib/watch-workspace-item.js index 05da51cf135..4d225ee49e0 100644 --- a/lib/watch-workspace-item.js +++ b/lib/watch-workspace-item.js @@ -81,8 +81,81 @@ class ItemWatcher { } } -export function watchWorkspaceItem(workspace, pattern, component, stateKey) { - return new ItemWatcher(workspace, pattern, component, stateKey) - .setInitialState() - .subscribeToWorkspace(); +class ActiveItemWatcher { + constructor(workspace, pattern, component, stateKey, opts) { + this.workspace = workspace; + this.pattern = pattern instanceof URIPattern ? pattern : new URIPattern(pattern); + this.component = component; + this.stateKey = stateKey; + this.opts = opts; + + this.activeItem = this.isActiveItem(); + this.subs = new CompositeDisposable(); + } + + isActiveItem() { + for (const pane of this.workspace.getPanes()) { + if (this.itemMatches(pane.getActiveItem())) { + return true; + } + } + return false; + } + + setInitialState() { + if (!this.component.state) { + this.component.state = {}; + } + this.component.state[this.stateKey] = this.activeItem; + return this; + } + + subscribeToWorkspace() { + this.subs.dispose(); + this.subs = new CompositeDisposable( + this.workspace.onDidChangeActivePaneItem(this.updateActiveState), + ); + return this; + } + + updateActiveState() { + const wasActive = this.activeItem; + + // Update the component's state if it's changed as a result + if (wasActive && !this.activeItem) { + return new Promise(resolve => this.component.setState({[this.stateKey]: false}, resolve)); + } else if (!wasActive && this.activeItem) { + return new Promise(resolve => this.component.setState({[this.stateKey]: true}, resolve)); + } else { + return Promise.resolve(); + } + } + + setPattern(pattern) { + this.pattern = pattern instanceof URIPattern ? pattern : new URIPattern(pattern); + + this.updateActiveState(); + } + + itemMatches = item => item && item.getURI && this.pattern.matches(item.getURI()).ok() + + dispose() { + this.subs.dispose(); + } +} + +export function watchWorkspaceItem(workspace, pattern, component, stateKey, options = {}) { + if (options.active) { + // I implemented this as a separate class because the logic differs enough + // and I suspect we can replace `ItemWatcher` with this. I don't see a clear use case for the `ItemWatcher` class + return new ActiveItemWatcher(workspace, pattern, component, stateKey, options) + .setInitialState() + .subscribeToWorkspace(); + } else { + // TODO: would we ever actually use this? If not, clean it up, along with tests + return new ItemWatcher(workspace, pattern, component, stateKey, options) + .setInitialState() + .subscribeToWorkspace(); + } + } From 692ea4cf590348f897cf541686f54ecc28d43c17 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 2 Nov 2018 19:08:17 -0700 Subject: [PATCH 118/284] Pass `{active: true}` to `watchWorkspaceItem` --- lib/controllers/commit-controller.js | 3 ++- lib/views/commit-view.js | 4 ++-- lib/watch-workspace-item.js | 6 ++++-- test/views/commit-view.test.js | 6 +++--- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/lib/controllers/commit-controller.js b/lib/controllers/commit-controller.js index 601c084fa86..e097b2b41b0 100644 --- a/lib/controllers/commit-controller.js +++ b/lib/controllers/commit-controller.js @@ -61,6 +61,7 @@ export default class CommitController extends React.Component { CommitPreviewItem.buildURI(this.props.repository.getWorkingDirectoryPath()), this, 'commitPreviewActive', + {active: true}, ); this.subscriptions.add(this.previewWatcher); } @@ -122,7 +123,7 @@ export default class CommitController extends React.Component { selectedCoAuthors={this.props.selectedCoAuthors} updateSelectedCoAuthors={this.props.updateSelectedCoAuthors} toggleCommitPreview={this.toggleCommitPreview} - commitPreviewOpen={this.state.commitPreviewOpen} + commitPreviewActive={this.state.commitPreviewActive} /> ); } diff --git a/lib/views/commit-view.js b/lib/views/commit-view.js index 358934d6bc6..af82d6da96e 100644 --- a/lib/views/commit-view.js +++ b/lib/views/commit-view.js @@ -42,7 +42,7 @@ export default class CommitView extends React.Component { mergeConflictsExist: PropTypes.bool.isRequired, stagedChangesExist: PropTypes.bool.isRequired, isCommitting: PropTypes.bool.isRequired, - commitPreviewOpen: PropTypes.bool.isRequired, + commitPreviewActive: PropTypes.bool.isRequired, deactivateCommitBox: PropTypes.bool.isRequired, maximumCharacterLimit: PropTypes.number.isRequired, messageBuffer: PropTypes.object.isRequired, // FIXME more specific proptype @@ -167,7 +167,7 @@ export default class CommitView extends React.Component { className="github-CommitView-commitPreview github-CommitView-button btn native-key-bindings" disabled={!this.props.stagedChangesExist} onClick={this.props.toggleCommitPreview}> - {this.props.commitPreviewOpen ? 'Close Commit Preview' : 'Preview Commit'} + {this.props.commitPreviewActive ? 'Close Commit Preview' : 'Preview Commit'}
diff --git a/lib/watch-workspace-item.js b/lib/watch-workspace-item.js index 4d225ee49e0..319568e6d4d 100644 --- a/lib/watch-workspace-item.js +++ b/lib/watch-workspace-item.js @@ -114,13 +114,15 @@ class ActiveItemWatcher { this.subs.dispose(); this.subs = new CompositeDisposable( this.workspace.onDidChangeActivePaneItem(this.updateActiveState), + this.workspace.onDidDestroyPaneItem(this.updateActiveState), ); return this; } - updateActiveState() { + updateActiveState = () => { const wasActive = this.activeItem; + this.activeItem = this.isActiveItem(); // Update the component's state if it's changed as a result if (wasActive && !this.activeItem) { return new Promise(resolve => this.component.setState({[this.stateKey]: false}, resolve)); @@ -134,7 +136,7 @@ class ActiveItemWatcher { setPattern(pattern) { this.pattern = pattern instanceof URIPattern ? pattern : new URIPattern(pattern); - this.updateActiveState(); + return this.updateActiveState(); } itemMatches = item => item && item.getURI && this.pattern.matches(item.getURI()).ok() diff --git a/test/views/commit-view.test.js b/test/views/commit-view.test.js index fd182e4e501..14aef600800 100644 --- a/test/views/commit-view.test.js +++ b/test/views/commit-view.test.js @@ -43,7 +43,7 @@ describe('CommitView', function() { stagedChangesExist={false} mergeConflictsExist={false} isCommitting={false} - commitPreviewOpen={false} + commitPreviewActive={false} deactivateCommitBox={false} maximumCharacterLimit={72} messageBuffer={messageBuffer} @@ -658,10 +658,10 @@ describe('CommitView', function() { assert.strictEqual(wrapper.find('.github-CommitView-commitPreview').text(), 'Preview Commit'); - wrapper.setProps({commitPreviewOpen: true}); + wrapper.setProps({commitPreviewActive: true}); assert.strictEqual(wrapper.find('.github-CommitView-commitPreview').text(), 'Close Commit Preview'); - wrapper.setProps({commitPreviewOpen: false}); + wrapper.setProps({commitPreviewActive: false}); assert.strictEqual(wrapper.find('.github-CommitView-commitPreview').text(), 'Preview Commit'); }); }); From 2ab46e71ada787685df0219370fa0f13699163bc Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 2 Nov 2018 19:20:09 -0700 Subject: [PATCH 119/284] Watch for active pane item change in workspace center only If we watch for changes on the workspace then we don't get an event in the case that an already-open file is clicked in tree-view, because tree-view remains the active item --- lib/watch-workspace-item.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/watch-workspace-item.js b/lib/watch-workspace-item.js index 319568e6d4d..dc2fcd1077a 100644 --- a/lib/watch-workspace-item.js +++ b/lib/watch-workspace-item.js @@ -113,8 +113,7 @@ class ActiveItemWatcher { subscribeToWorkspace() { this.subs.dispose(); this.subs = new CompositeDisposable( - this.workspace.onDidChangeActivePaneItem(this.updateActiveState), - this.workspace.onDidDestroyPaneItem(this.updateActiveState), + this.workspace.getCenter().onDidChangeActivePaneItem(this.updateActiveState), ); return this; } From baafe4a640bf714338b3ab8ffc5ff5851ca72ebf Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 2 Nov 2018 19:25:05 -0700 Subject: [PATCH 120/284] Make Commit Preview a pending item (honors config setting) --- lib/controllers/commit-controller.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/controllers/commit-controller.js b/lib/controllers/commit-controller.js index e097b2b41b0..4ddab2d3840 100644 --- a/lib/controllers/commit-controller.js +++ b/lib/controllers/commit-controller.js @@ -283,9 +283,12 @@ export default class CommitController extends React.Component { toggleCommitPreview() { addEvent('toggle-commit-preview', {package: 'github'}); - return this.props.workspace.toggle( - CommitPreviewItem.buildURI(this.props.repository.getWorkingDirectoryPath()), - ); + const uri = CommitPreviewItem.buildURI(this.props.repository.getWorkingDirectoryPath()); + if (this.props.workspace.hide(uri)) { + return Promise.resolve(); + } else { + return this.props.workspace.open(uri, {searchAllPanes: true, pending: true}); + } } } From 1f0e3b001e748637e2015c1013000662450c1830 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 2 Nov 2018 19:29:06 -0700 Subject: [PATCH 121/284] Use unique item name for test that places item in right dock --- test/watch-workspace-item.test.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/test/watch-workspace-item.test.js b/test/watch-workspace-item.test.js index 1a392132b7c..9448f74bebb 100644 --- a/test/watch-workspace-item.test.js +++ b/test/watch-workspace-item.test.js @@ -81,10 +81,10 @@ describe.only('watchWorkspaceItem', function() { it('is false when the pane is open, but not active', async function() { // TODO: fix this test suite so that 'atom-github://item' works - await workspace.open('atom-github://some-item'); + await workspace.open('atom-github://item'); await workspace.open('atom-github://nonmatching'); - sub = watchWorkspaceItem(workspace, 'atom-github://some-item', component, 'someKey', {active: true}); + sub = watchWorkspaceItem(workspace, 'atom-github://item', component, 'someKey', {active: true}); assert.isFalse(component.state.someKey); }); @@ -97,13 +97,13 @@ describe.only('watchWorkspaceItem', function() { }); it('is true when the pane is open and active in any pane', async function() { - await workspace.open('atom-github://item', {location: 'right'}); + await workspace.open('atom-github://some-item', {location: 'right'}); await workspace.open('atom-github://nonmatching'); - assert.strictEqual(workspace.getRightDock().getActivePaneItem().getURI(), 'atom-github://item'); + assert.strictEqual(workspace.getRightDock().getActivePaneItem().getURI(), 'atom-github://some-item'); assert.strictEqual(workspace.getActivePaneItem().getURI(), 'atom-github://nonmatching'); - sub = watchWorkspaceItem(workspace, 'atom-github://item', component, 'someKey', {active: true}); + sub = watchWorkspaceItem(workspace, 'atom-github://some-item', component, 'someKey', {active: true}); assert.isTrue(component.state.someKey); }); }); @@ -163,14 +163,14 @@ describe.only('watchWorkspaceItem', function() { it('stops updating when disposed', async function() { // TODO: fix this test suite so that 'atom-github://item' works - sub = watchWorkspaceItem(workspace, 'atom-github://some-item', component, 'theKey'); + sub = watchWorkspaceItem(workspace, 'atom-github://item', component, 'theKey'); assert.isFalse(component.state.theKey); sub.dispose(); - await workspace.open('atom-github://some-item'); + await workspace.open('atom-github://item'); assert.isFalse(component.setState.called); - await workspace.hide('atom-github://some-item'); + await workspace.hide('atom-github://item'); assert.isFalse(component.setState.called); }); From 45b379cbfa464dd94df55c8c4475e9466985ef7a Mon Sep 17 00:00:00 2001 From: Vanessa Yuen Date: Mon, 5 Nov 2018 12:25:07 +0100 Subject: [PATCH 122/284] fix commit controller test --- test/controllers/commit-controller.test.js | 19 ++++++++++--------- test/watch-workspace-item.test.js | 2 +- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/test/controllers/commit-controller.test.js b/test/controllers/commit-controller.test.js index 6142c3d2b5a..8e4a8685734 100644 --- a/test/controllers/commit-controller.test.js +++ b/test/controllers/commit-controller.test.js @@ -13,7 +13,7 @@ import CommitPreviewItem from '../../lib/items/commit-preview-item'; import {cloneRepository, buildRepository, buildRepositoryWithPipeline} from '../helpers'; import * as reporterProxy from '../../lib/reporter-proxy'; -describe('CommitController', function() { +describe.only('CommitController', function() { let atomEnvironment, workspace, commandRegistry, notificationManager, lastCommit, config, confirm, tooltips; let app; @@ -426,8 +426,8 @@ describe('CommitController', function() { }); }); - describe('toggleCommitPreview', function() { - it('opens and closes commit preview pane', async function() { + describe('tri-state toggle commit preview', function() { + it('opens, hides, and closes commit preview pane', async function() { const workdir = await cloneRepository('three-files'); const repository = await buildRepository(workdir); const previewURI = CommitPreviewItem.buildURI(workdir); @@ -436,17 +436,18 @@ describe('CommitController', function() { assert.isFalse(wrapper.find('CommitView').prop('commitPreviewActive')); + await workspace.open(path.join(workdir, 'a.txt')); await wrapper.find('CommitView').prop('toggleCommitPreview')(); // Commit preview open as active pane item assert.strictEqual(workspace.getActivePaneItem().getURI(), previewURI); - assert.strictEqual(workspace.getActivePaneItem(), workspace.paneForItem(previewURI).getPendingItem()); + assert.strictEqual(workspace.getActivePaneItem(), workspace.paneForURI(previewURI).getPendingItem()); assert.isTrue(wrapper.find('CommitView').prop('commitPreviewActive')); - await workspace.open(__filename); + await workspace.open(path.join(workdir, 'a.txt')); // Commit preview open, but not active - assert.include(workspace.getAllPaneItems().map(i => i.getURI()), previewURI); + assert.include(workspace.getPaneItems().map(i => i.getURI()), previewURI); assert.notStrictEqual(workspace.getActivePaneItem().getURI(), previewURI); assert.isFalse(wrapper.find('CommitView').prop('commitPreviewActive')); @@ -454,14 +455,14 @@ describe('CommitController', function() { // Open as active pane item again assert.strictEqual(workspace.getActivePaneItem().getURI(), previewURI); - assert.strictEqual(workspace.getActivePaneItem(), workspace.paneForItem(previewURI).getPendingItem()); + assert.strictEqual(workspace.getActivePaneItem(), workspace.paneForURI(previewURI).getPendingItem()); assert.isTrue(wrapper.find('CommitView').prop('commitPreviewActive')); await wrapper.find('CommitView').prop('toggleCommitPreview')(); // Commit preview closed - assert.notInclude(workspace.getAllPaneItems().map(i => i.getURI()), previewURI); - assert.isTrue(wrapper.find('CommitView').prop('commitPreviewActive')); + assert.notInclude(workspace.getPaneItems().map(i => i.getURI()), previewURI); + assert.isFalse(wrapper.find('CommitView').prop('commitPreviewActive')); }); it('records a metrics event when pane is toggled', async function() { diff --git a/test/watch-workspace-item.test.js b/test/watch-workspace-item.test.js index 9448f74bebb..672ae15dee5 100644 --- a/test/watch-workspace-item.test.js +++ b/test/watch-workspace-item.test.js @@ -1,7 +1,7 @@ import {watchWorkspaceItem} from '../lib/watch-workspace-item'; import URIPattern from '../lib/atom/uri-pattern'; -describe.only('watchWorkspaceItem', function() { +describe('watchWorkspaceItem', function() { let sub, atomEnv, workspace, component; beforeEach(function() { From f17da4b9a53bc493fb35dd54f81d907929367aaf Mon Sep 17 00:00:00 2001 From: Vanessa Yuen Date: Mon, 5 Nov 2018 12:34:49 +0100 Subject: [PATCH 123/284] =?UTF-8?q?got=20the=20tests=20to=20be=20green=20b?= =?UTF-8?q?ut=20do=20they=20REALLY=20work=20tho=20=F0=9F=A4=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/controllers/commit-controller.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/controllers/commit-controller.test.js b/test/controllers/commit-controller.test.js index 8e4a8685734..5cf9dd6b295 100644 --- a/test/controllers/commit-controller.test.js +++ b/test/controllers/commit-controller.test.js @@ -501,12 +501,12 @@ describe.only('CommitController', function() { assert.isFalse(wrapper.find('CommitView').prop('commitPreviewActive')); await wrapper.find('CommitView').prop('toggleCommitPreview')(); - assert.isTrue(workspace.getPaneItems().some(item => item.getURI() === CommitPreviewItem.buildURI(workdir0))); + assert.isFalse(workspace.getPaneItems().some(item => item.getURI() === CommitPreviewItem.buildURI(workdir0))); assert.isTrue(workspace.getPaneItems().some(item => item.getURI() === CommitPreviewItem.buildURI(workdir1))); assert.isTrue(wrapper.find('CommitView').prop('commitPreviewActive')); await wrapper.find('CommitView').prop('toggleCommitPreview')(); - assert.isTrue(workspace.getPaneItems().some(item => item.getURI() === CommitPreviewItem.buildURI(workdir0))); + assert.isFalse(workspace.getPaneItems().some(item => item.getURI() === CommitPreviewItem.buildURI(workdir0))); assert.isFalse(workspace.getPaneItems().some(item => item.getURI() === CommitPreviewItem.buildURI(workdir1))); assert.isFalse(wrapper.find('CommitView').prop('commitPreviewActive')); }); From 3d8fb2c1dd662307130fb02dd0ccb2060315ea43 Mon Sep 17 00:00:00 2001 From: Vanessa Yuen Date: Mon, 5 Nov 2018 12:35:06 +0100 Subject: [PATCH 124/284] take out the `.only` --- test/controllers/commit-controller.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/controllers/commit-controller.test.js b/test/controllers/commit-controller.test.js index 5cf9dd6b295..bbb187bfa62 100644 --- a/test/controllers/commit-controller.test.js +++ b/test/controllers/commit-controller.test.js @@ -13,7 +13,7 @@ import CommitPreviewItem from '../../lib/items/commit-preview-item'; import {cloneRepository, buildRepository, buildRepositoryWithPipeline} from '../helpers'; import * as reporterProxy from '../../lib/reporter-proxy'; -describe.only('CommitController', function() { +describe('CommitController', function() { let atomEnvironment, workspace, commandRegistry, notificationManager, lastCommit, config, confirm, tooltips; let app; From dde3a608a487283feb83caa6487890190792ffa2 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 5 Nov 2018 10:44:19 -0500 Subject: [PATCH 125/284] Test for a unified MultiFilePatch buffer --- test/models/patch/builder.test.js | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/test/models/patch/builder.test.js b/test/models/patch/builder.test.js index 987194ef824..2f295fdd4c5 100644 --- a/test/models/patch/builder.test.js +++ b/test/models/patch/builder.test.js @@ -1,7 +1,7 @@ import {buildFilePatch, buildMultiFilePatch} from '../../../lib/models/patch'; import {assertInPatch, assertInFilePatch} from '../../helpers'; -describe('buildFilePatch', function() { +describe.only('buildFilePatch', function() { it('returns a null patch for an empty diff list', function() { const p = buildFilePatch([]); assert.isFalse(p.getOldFile().isPresent()); @@ -556,6 +556,25 @@ describe('buildFilePatch', function() { ]); assert.lengthOf(mp.getFilePatches(), 3); + + assert.strictEqual( + mp.getBuffer().getText(), + 'line-0\nline-1\nline-2\nline-3\nline-4\nline-5\nline-6\n' + + 'line-5\nline-6\nline-7\nline-8\n' + + 'line-0\nline-1\nline-2\n', + ); + + const assertAllSame = getter => { + assert.lengthOf( + Array.from(new Set(mp.getFilePatches.map(p => p[getter]()))), + 1, + `FilePatches have different results from ${getter}`, + ); + }; + for (const getter of ['getUnchangedLayer', 'getAdditionLayer', 'getDeletionLayer', 'getNoNewlineLayer']) { + assertAllSame(getter); + } + assert.strictEqual(mp.getFilePatches()[0].getOldPath(), 'first'); assertInFilePatch(mp.getFilePatches()[0]).hunks( { From 97f6a1a8ae2430b3f59f6abdd77553719a5d3513 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 5 Nov 2018 13:01:37 -0500 Subject: [PATCH 126/284] Parse multi-file patches into a single buffer --- lib/models/patch/builder.js | 44 +++++++++++++++++++--------- lib/models/patch/multi-file-patch.js | 7 ++++- test/models/patch/builder.test.js | 30 +++++++++---------- 3 files changed, 51 insertions(+), 30 deletions(-) diff --git a/lib/models/patch/builder.js b/lib/models/patch/builder.js index c2bef5fe72e..6f700e1c6c2 100644 --- a/lib/models/patch/builder.js +++ b/lib/models/patch/builder.js @@ -20,8 +20,9 @@ export function buildFilePatch(diffs) { } export function buildMultiFilePatch(diffs) { + const layeredBuffer = initializeBuffer(); const byPath = new Map(); - const filePatches = []; + const actions = []; let index = 0; for (const diff of diffs) { @@ -34,7 +35,7 @@ export function buildMultiFilePatch(diffs) { if (otherHalf) { // The second half. Complete the paired diff, or fail if they have unexpected statuses or modes. const [otherDiff, otherIndex] = otherHalf; - filePatches[otherIndex] = dualDiffFilePatch(diff, otherDiff); + actions[otherIndex] = () => dualDiffFilePatch(diff, otherDiff, layeredBuffer); byPath.delete(thePath); } else { // The first half we've seen. @@ -42,27 +43,33 @@ export function buildMultiFilePatch(diffs) { index++; } } else { - filePatches[index] = singleDiffFilePatch(diff); + actions[index] = () => singleDiffFilePatch(diff, layeredBuffer); index++; } } // Populate unpaired diffs that looked like they could be part of a pair, but weren't. for (const [unpairedDiff, originalIndex] of byPath.values()) { - filePatches[originalIndex] = singleDiffFilePatch(unpairedDiff); + actions[originalIndex] = () => singleDiffFilePatch(unpairedDiff, layeredBuffer); } - return new MultiFilePatch(filePatches); + const filePatches = actions.map(action => action()); + + return new MultiFilePatch(layeredBuffer.buffer, filePatches); } function emptyDiffFilePatch() { return FilePatch.createNull(); } -function singleDiffFilePatch(diff) { +function singleDiffFilePatch(diff, layeredBuffer = null) { const wasSymlink = diff.oldMode === '120000'; const isSymlink = diff.newMode === '120000'; - const [hunks, buffer, layers] = buildHunks(diff); + + if (!layeredBuffer) { + layeredBuffer = initializeBuffer(); + } + const [hunks] = buildHunks(diff, layeredBuffer); let oldSymlink = null; let newSymlink = null; @@ -81,12 +88,16 @@ function singleDiffFilePatch(diff) { 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, buffer, layers}); + const patch = new Patch({status: diff.status, hunks, ...layeredBuffer}); return new FilePatch(oldFile, newFile, patch); } -function dualDiffFilePatch(diff1, diff2) { +function dualDiffFilePatch(diff1, diff2, layeredBuffer = null) { + if (!layeredBuffer) { + layeredBuffer = initializeBuffer(); + } + let modeChangeDiff, contentChangeDiff; if (diff1.oldMode === '120000' || diff1.newMode === '120000') { modeChangeDiff = diff1; @@ -96,7 +107,7 @@ function dualDiffFilePatch(diff1, diff2) { contentChangeDiff = diff1; } - const [hunks, buffer, layers] = buildHunks(contentChangeDiff); + const [hunks] = buildHunks(contentChangeDiff, layeredBuffer); const filePath = contentChangeDiff.oldPath || contentChangeDiff.newPath; const symlink = modeChangeDiff.hunks[0].lines[0].slice(1); @@ -122,7 +133,7 @@ function dualDiffFilePatch(diff1, diff2) { 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, buffer, layers}); + const patch = new Patch({status, hunks, ...layeredBuffer}); return new FilePatch(oldFile, newFile, patch); } @@ -134,12 +145,17 @@ const CHANGEKIND = { '\\': NoNewline, }; -function buildHunks(diff) { +function initializeBuffer() { const buffer = new TextBuffer(); const layers = ['hunk', 'unchanged', 'addition', 'deletion', 'noNewline'].reduce((obj, key) => { obj[key] = buffer.addMarkerLayer(); return obj; }, {}); + + return {buffer, layers}; +} + +function buildHunks(diff, {buffer, layers}) { const layersByKind = new Map([ [Unchanged, layers.unchanged], [Addition, layers.addition], @@ -148,7 +164,7 @@ function buildHunks(diff) { ]); const hunks = []; - let bufferRow = 0; + let bufferRow = buffer.getLastRow(); for (const hunkData of diff.hunks) { const bufferStartRow = bufferRow; @@ -210,5 +226,5 @@ function buildHunks(diff) { })); } - return [hunks, buffer, layers]; + return [hunks]; } diff --git a/lib/models/patch/multi-file-patch.js b/lib/models/patch/multi-file-patch.js index b47b90f5450..5c3356ece89 100644 --- a/lib/models/patch/multi-file-patch.js +++ b/lib/models/patch/multi-file-patch.js @@ -1,8 +1,13 @@ export default class MultiFilePatch { - constructor(filePatches) { + constructor(buffer, filePatches) { + this.buffer = buffer; this.filePatches = filePatches; } + getBuffer() { + return this.buffer; + } + getFilePatches() { return this.filePatches; } diff --git a/test/models/patch/builder.test.js b/test/models/patch/builder.test.js index 2f295fdd4c5..3fa8b748c0f 100644 --- a/test/models/patch/builder.test.js +++ b/test/models/patch/builder.test.js @@ -1,7 +1,7 @@ import {buildFilePatch, buildMultiFilePatch} from '../../../lib/models/patch'; import {assertInPatch, assertInFilePatch} from '../../helpers'; -describe.only('buildFilePatch', function() { +describe('buildFilePatch', function() { it('returns a null patch for an empty diff list', function() { const p = buildFilePatch([]); assert.isFalse(p.getOldFile().isPresent()); @@ -566,7 +566,7 @@ describe.only('buildFilePatch', function() { const assertAllSame = getter => { assert.lengthOf( - Array.from(new Set(mp.getFilePatches.map(p => p[getter]()))), + Array.from(new Set(mp.getFilePatches().map(p => p[getter]()))), 1, `FilePatches have different results from ${getter}`, ); @@ -595,19 +595,19 @@ describe.only('buildFilePatch', function() { assert.strictEqual(mp.getFilePatches()[1].getOldPath(), 'second'); assertInFilePatch(mp.getFilePatches()[1]).hunks( { - startRow: 0, endRow: 3, header: '@@ -5,3 +5,3 @@', regions: [ - {kind: 'unchanged', string: ' line-5\n', range: [[0, 0], [0, 6]]}, - {kind: 'addition', string: '+line-6\n', range: [[1, 0], [1, 6]]}, - {kind: 'deletion', string: '-line-7\n', range: [[2, 0], [2, 6]]}, - {kind: 'unchanged', string: ' line-8\n', range: [[3, 0], [3, 6]]}, + startRow: 7, endRow: 10, header: '@@ -5,3 +5,3 @@', regions: [ + {kind: 'unchanged', string: ' line-5\n', range: [[7, 0], [7, 6]]}, + {kind: 'addition', string: '+line-6\n', range: [[8, 0], [8, 6]]}, + {kind: 'deletion', string: '-line-7\n', range: [[9, 0], [9, 6]]}, + {kind: 'unchanged', string: ' line-8\n', range: [[10, 0], [10, 6]]}, ], }, ); assert.strictEqual(mp.getFilePatches()[2].getOldPath(), 'third'); assertInFilePatch(mp.getFilePatches()[2]).hunks( { - startRow: 0, endRow: 2, header: '@@ -1,0 +1,3 @@', regions: [ - {kind: 'addition', string: '+line-0\n+line-1\n+line-2\n', range: [[0, 0], [2, 6]]}, + startRow: 11, endRow: 13, header: '@@ -1,0 +1,3 @@', regions: [ + {kind: 'addition', string: '+line-0\n+line-1\n+line-2\n', range: [[11, 0], [13, 6]]}, ], }, ); @@ -691,8 +691,8 @@ describe.only('buildFilePatch', function() { assert.isTrue(fp1.hasTypechange()); assert.strictEqual(fp1.getNewSymlink(), 'was-non-symlink-destination'); assertInFilePatch(fp1).hunks({ - startRow: 0, endRow: 1, header: '@@ -1,2 +1,0 @@', regions: [ - {kind: 'deletion', string: '-line-0\n-line-1\n', range: [[0, 0], [1, 6]]}, + startRow: 3, endRow: 4, header: '@@ -1,2 +1,0 @@', regions: [ + {kind: 'deletion', string: '-line-0\n-line-1\n', range: [[3, 0], [4, 6]]}, ], }); @@ -700,15 +700,15 @@ describe.only('buildFilePatch', function() { assert.isTrue(fp2.hasTypechange()); assert.strictEqual(fp2.getOldSymlink(), 'was-symlink-destination'); assertInFilePatch(fp2).hunks({ - startRow: 0, endRow: 1, header: '@@ -1,0 +1,2 @@', regions: [ - {kind: 'addition', string: '+line-0\n+line-1\n', range: [[0, 0], [1, 6]]}, + startRow: 5, endRow: 6, header: '@@ -1,0 +1,2 @@', regions: [ + {kind: 'addition', string: '+line-0\n+line-1\n', range: [[5, 0], [6, 6]]}, ], }); assert.strictEqual(fp3.getNewPath(), 'third'); assertInFilePatch(fp3).hunks({ - startRow: 0, endRow: 2, header: '@@ -1,3 +1,0 @@', regions: [ - {kind: 'deletion', string: '-line-0\n-line-1\n-line-2\n', range: [[0, 0], [2, 6]]}, + startRow: 7, endRow: 9, header: '@@ -1,3 +1,0 @@', regions: [ + {kind: 'deletion', string: '-line-0\n-line-1\n-line-2\n', range: [[7, 0], [9, 6]]}, ], }); }); From 513d32306b93c830ffb50da7b35d9fd5ddf26275 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 5 Nov 2018 14:20:35 -0500 Subject: [PATCH 127/284] Accept specific FilePatch instances in controller methods --- lib/controllers/file-patch-controller.js | 51 ++++++++++++------------ 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/lib/controllers/file-patch-controller.js b/lib/controllers/file-patch-controller.js index ebda46ca87c..acf3d354b5c 100644 --- a/lib/controllers/file-patch-controller.js +++ b/lib/controllers/file-patch-controller.js @@ -4,6 +4,7 @@ import path from 'path'; import {autobind, equalSets} from '../helpers'; import {addEvent} from '../reporter-proxy'; +import {MultiFilePatchPropType} from '../prop-types'; import ChangedFileItem from '../items/changed-file-item'; import FilePatchView from '../views/file-patch-view'; @@ -11,8 +12,7 @@ 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, + multiFilePatch: MultiFilePatchPropType.isRequired, hasUndoHistory: PropTypes.bool, workspace: PropTypes.object.isRequired, @@ -39,7 +39,7 @@ export default class FilePatchController extends React.Component { ); this.state = { - lastFilePatch: this.props.filePatch, + lastMultiFilePatch: this.props.multiFilePatch, selectionMode: 'hunk', selectedRows: new Set(), }; @@ -53,7 +53,7 @@ export default class FilePatchController extends React.Component { } componentDidUpdate(prevProps) { - if (prevProps.filePatch !== this.props.filePatch) { + if (prevProps.multiFilePatch !== this.props.multiFilePatch) { this.resolvePatchChangePromise(); this.patchChangePromise = new Promise(resolve => { this.resolvePatchChangePromise = resolve; @@ -85,31 +85,31 @@ export default class FilePatchController extends React.Component { ); } - undoLastDiscard({eventSource} = {}) { + undoLastDiscard(filePatch, {eventSource} = {}) { addEvent('undo-last-discard', { package: 'github', component: 'FilePatchController', eventSource, }); - return this.props.undoLastDiscard(this.props.relPath, this.props.repository); + return this.props.undoLastDiscard(filePatch.getPath(), this.props.repository); } - diveIntoMirrorPatch() { + diveIntoMirrorPatch(filePatch) { const mirrorStatus = this.withStagingStatus({staged: 'unstaged', unstaged: 'staged'}); const workingDirectory = this.props.repository.getWorkingDirectoryPath(); - const uri = ChangedFileItem.buildURI(this.props.relPath, workingDirectory, mirrorStatus); + const uri = ChangedFileItem.buildURI(filePatch.getPath(), workingDirectory, mirrorStatus); this.props.destroy(); return this.props.workspace.open(uri); } - surfaceFile() { - return this.props.surfaceFileAtPath(this.props.relPath, this.props.stagingStatus); + surfaceFile(filePatch) { + return this.props.surfaceFileAtPath(filePatch.getPath(), this.props.stagingStatus); } - async openFile(positions) { - const absolutePath = path.join(this.props.repository.getWorkingDirectoryPath(), this.props.relPath); + async openFile(filePatch, positions) { + const absolutePath = path.join(this.props.repository.getWorkingDirectoryPath(), filePatch.getPath()); const editor = await this.props.workspace.open(absolutePath, {pending: true}); if (positions.length > 0) { editor.setCursorBufferPosition(positions[0], {autoscroll: false}); @@ -121,14 +121,14 @@ export default class FilePatchController extends React.Component { return editor; } - toggleFile() { + toggleFile(filePatch) { return this.stagingOperation(() => { const methodName = this.withStagingStatus({staged: 'unstageFiles', unstaged: 'stageFiles'}); - return this.props.repository[methodName]([this.props.relPath]); + return this.props.repository[methodName]([filePatch.getPath()]); }); } - async toggleRows(rowSet, nextSelectionMode) { + async toggleRows(filePatch, rowSet, nextSelectionMode) { let chosenRows = rowSet; if (chosenRows) { await this.selectedRowsChanged(chosenRows, nextSelectionMode); @@ -142,26 +142,27 @@ export default class FilePatchController extends React.Component { return this.stagingOperation(() => { const patch = this.withStagingStatus({ - staged: () => this.props.filePatch.getUnstagePatchForLines(chosenRows), - unstaged: () => this.props.filePatch.getStagePatchForLines(chosenRows), + staged: () => filePatch.getUnstagePatchForLines(chosenRows), + unstaged: () => filePatch.getStagePatchForLines(chosenRows), }); return this.props.repository.applyPatchToIndex(patch); }); } - toggleModeChange() { + toggleModeChange(filePatch) { return this.stagingOperation(() => { const targetMode = this.withStagingStatus({ - unstaged: this.props.filePatch.getNewMode(), - staged: this.props.filePatch.getOldMode(), + unstaged: filePatch.getNewMode(), + staged: filePatch.getOldMode(), }); - return this.props.repository.stageFileModeChange(this.props.relPath, targetMode); + return this.props.repository.stageFileModeChange(filePatch.getPath(), targetMode); }); } - toggleSymlinkChange() { + toggleSymlinkChange(filePatch) { return this.stagingOperation(() => { - const {filePatch, relPath, repository} = this.props; + const relPath = filePatch.getPath(); + const repository = this.props.repository; return this.withStagingStatus({ unstaged: () => { if (filePatch.hasTypechange() && filePatch.getStatus() === 'added') { @@ -181,7 +182,7 @@ export default class FilePatchController extends React.Component { }); } - async discardRows(rowSet, nextSelectionMode, {eventSource} = {}) { + async discardRows(filePatch, rowSet, nextSelectionMode, {eventSource} = {}) { let chosenRows = rowSet; if (chosenRows) { await this.selectedRowsChanged(chosenRows, nextSelectionMode); @@ -196,7 +197,7 @@ export default class FilePatchController extends React.Component { eventSource, }); - return this.props.discardLines(this.props.filePatch, chosenRows, this.props.repository); + return this.props.discardLines(filePatch, chosenRows, this.props.repository); } selectedRowsChanged(rows, nextSelectionMode) { From 601bdcb186a401fd1373af0c8766e371029b5357 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 5 Nov 2018 15:00:39 -0500 Subject: [PATCH 128/284] Render multiple FilePatches in one FilePatchView, take 1 --- lib/views/file-patch-view.js | 224 ++++++++++++++++++++++------------- 1 file changed, 140 insertions(+), 84 deletions(-) diff --git a/lib/views/file-patch-view.js b/lib/views/file-patch-view.js index 6cbba58939e..c67a96e394d 100644 --- a/lib/views/file-patch-view.js +++ b/lib/views/file-patch-view.js @@ -5,7 +5,7 @@ import {Range} from 'atom'; import {CompositeDisposable} from 'event-kit'; import {autobind} from '../helpers'; -import {RefHolderPropType} from '../prop-types'; +import {RefHolderPropType, MultiFilePatchPropType} from '../prop-types'; import AtomTextEditor from '../atom/atom-text-editor'; import Marker from '../atom/marker'; import MarkerLayer from '../atom/marker-layer'; @@ -28,10 +28,9 @@ const BLANK_LABEL = () => NBSP_CHARACTER; export default class FilePatchView extends React.Component { static propTypes = { - relPath: PropTypes.string.isRequired, stagingStatus: PropTypes.oneOf(['staged', 'unstaged']).isRequired, isPartiallyStaged: PropTypes.bool, - filePatch: PropTypes.object.isRequired, + multiFilePatch: MultiFilePatchPropType.isRequired, selectionMode: PropTypes.oneOf(['hunk', 'line']).isRequired, selectedRows: PropTypes.object.isRequired, repository: PropTypes.object.isRequired, @@ -56,7 +55,6 @@ export default class FilePatchView extends React.Component { toggleSymlinkChange: PropTypes.func.isRequired, undoLastDiscard: PropTypes.func.isRequired, discardRows: PropTypes.func.isRequired, - handleMouseDown: PropTypes.func, refInitialFocus: RefHolderPropType, } @@ -98,11 +96,18 @@ export default class FilePatchView extends React.Component { componentDidMount() { window.addEventListener('mouseup', this.didMouseUp); this.refEditor.map(editor => { - const [firstHunk] = this.props.filePatch.getHunks(); - if (firstHunk) { - this.nextSelectionMode = 'hunk'; - editor.setSelectedBufferRange(firstHunk.getRange()); + const [firstPatch] = this.props.multiFilePatch.getFilePatches(); + if (!firstPatch) { + return null; } + + const [firstHunk] = firstPatch.getHunks(); + if (!firstHunk) { + return null; + } + + this.nextSelectionMode = 'hunk'; + editor.setSelectedBufferRange(firstHunk.getRange()); return null; }); @@ -113,16 +118,16 @@ export default class FilePatchView extends React.Component { getSnapshotBeforeUpdate(prevProps) { let newSelectionRange = null; - if (this.props.filePatch !== prevProps.filePatch) { + if (this.props.multiFilePatch !== prevProps.multiFilePatch) { // Heuristically adjust the editor selection based on the old file patch, the old row selection state, and // the incoming patch. - newSelectionRange = this.props.filePatch.getNextSelectionRange( - prevProps.filePatch, + newSelectionRange = this.props.multiFilePatch.getNextSelectionRange( + prevProps.multiFilePatch, prevProps.selectedRows, ); this.suppressChanges = true; - this.props.filePatch.adoptBufferFrom(prevProps.filePatch); + this.props.multiFilePatch.adoptBufferFrom(prevProps.multiFilePatch); this.suppressChanges = false; } return newSelectionRange; @@ -142,7 +147,7 @@ export default class FilePatchView extends React.Component { } else { const nextHunks = new Set( Range.fromObject(newSelectionRange).getRows() - .map(row => this.props.filePatch.getHunkAt(row)) + .map(row => this.props.multiFilePatch.getHunkAt(row)) .filter(Boolean), ); const nextRanges = nextHunks.size > 0 @@ -165,63 +170,43 @@ export default class FilePatchView extends React.Component { this.subs.dispose(); } - handleMouseDown() { - this.props.handleMouseDown(this.props.relPath); - } - render() { const rootClass = cx( 'github-FilePatchView', `github-FilePatchView--${this.props.stagingStatus}`, - {'github-FilePatchView--blank': !this.props.filePatch.isPresent()}, + {'github-FilePatchView--blank': !this.props.multiFilePatch.anyPresent()}, {'github-FilePatchView--hunkMode': this.props.selectionMode === 'hunk'}, {'github-FilePatchView--active': this.props.isActive}, {'github-FilePatchView--inactive': !this.props.isActive}, ); return ( -
- +
{this.renderCommands()} - 0} - hasUndoHistory={this.props.hasUndoHistory} - - tooltips={this.props.tooltips} - - undoLastDiscard={this.undoLastDiscardFromButton} - diveIntoMirrorPatch={this.props.diveIntoMirrorPatch} - openFile={this.didOpenFile} - toggleFile={this.props.toggleFile} - /> -
- {this.props.filePatch.isPresent() ? this.renderNonEmptyPatch() : this.renderEmptyPatch()} + {this.props.multiFilePatch.anyPresent() ? this.renderNonEmptyPatch() : this.renderEmptyPatch()}
-
); } renderCommands() { let stageModeCommand = null; - if (this.props.filePatch.didChangeExecutableMode()) { + let stageSymlinkCommand = null; + + if (this.props.multiFilePatch.didAnyChangeExecutableMode()) { const command = this.props.stagingStatus === 'unstaged' ? 'github:stage-file-mode-change' : 'github:unstage-file-mode-change'; - stageModeCommand = ; + stageModeCommand = ; } - let stageSymlinkCommand = null; - if (this.props.filePatch.hasSymlink()) { + if (this.props.multiFilePatch.anyHaveSymlink()) { const command = this.props.stagingStatus === 'unstaged' ? 'github:stage-symlink-change' : 'github:unstage-symlink-change'; - stageSymlinkCommand = ; + stageSymlinkCommand = ; } return ( @@ -249,7 +234,7 @@ export default class FilePatchView extends React.Component { )} - - - - {this.renderExecutableModeChangeMeta()} - {this.renderSymlinkChangeMeta()} - - - - - {this.renderHunkHeaders()} + {this.props.multiFilePatch.getFilePatches().map(this.renderFilePatchDecorations)} {this.renderLineDecorations( Array.from(this.props.selectedRows, row => Range.fromObject([[row, 0], [row, Infinity]])), @@ -309,17 +285,17 @@ export default class FilePatchView extends React.Component { )} {this.renderDecorationsOnLayer( - this.props.filePatch.getAdditionLayer(), + this.props.multiFilePatch.getAdditionLayer(), 'github-FilePatchView-line--added', {icon: true, line: true}, )} {this.renderDecorationsOnLayer( - this.props.filePatch.getDeletionLayer(), + this.props.multiFilePatch.getDeletionLayer(), 'github-FilePatchView-line--deleted', {icon: true, line: true}, )} {this.renderDecorationsOnLayer( - this.props.filePatch.getNoNewlineLayer(), + this.props.multiFilePatch.getNoNewlineLayer(), 'github-FilePatchView-line--nonewline', {icon: true, line: true}, )} @@ -328,13 +304,45 @@ export default class FilePatchView extends React.Component { ); } - renderExecutableModeChangeMeta() { - if (!this.props.filePatch.didChangeExecutableMode()) { + renderFilePatchDecorations = filePatch => { + return ( + + + + + {this.renderExecutableModeChangeMeta(filePatch)} + {this.renderSymlinkChangeMeta(filePatch)} + + 0} + hasUndoHistory={this.props.hasUndoHistory} + + tooltips={this.props.tooltips} + + undoLastDiscard={this.undoLastDiscardFromButton} + diveIntoMirrorPatch={this.props.diveIntoMirrorPatch} + openFile={this.didOpenFile} + toggleFile={this.props.toggleFile} + /> + + + + + {this.renderHunkHeaders(filePatch)} + + ); + } + + renderExecutableModeChangeMeta(filePatch) { + if (!filePatch.didChangeExecutableMode()) { return null; } - const oldMode = this.props.filePatch.getOldMode(); - const newMode = this.props.filePatch.getNewMode(); + const oldMode = filePatch.getOldMode(); + const newMode = filePatch.getNewMode(); const attrs = this.props.stagingStatus === 'unstaged' ? { @@ -351,7 +359,7 @@ export default class FilePatchView extends React.Component { title="Mode change" actionIcon={attrs.actionIcon} actionText={attrs.actionText} - action={this.props.toggleModeChange}> + action={() => this.props.toggleModeChange(filePatch)}> File changed mode @@ -365,15 +373,15 @@ export default class FilePatchView extends React.Component { ); } - renderSymlinkChangeMeta() { - if (!this.props.filePatch.hasSymlink()) { + renderSymlinkChangeMeta(filePatch) { + if (!filePatch.hasSymlink()) { return null; } let detail =
; let title = ''; - const oldSymlink = this.props.filePatch.getOldSymlink(); - const newSymlink = this.props.filePatch.getNewSymlink(); + const oldSymlink = filePatch.getOldSymlink(); + const newSymlink = filePatch.getNewSymlink(); if (oldSymlink && newSymlink) { detail = ( @@ -434,7 +442,7 @@ export default class FilePatchView extends React.Component { title={title} actionIcon={attrs.actionIcon} actionText={attrs.actionText} - action={this.props.toggleSymlinkChange}> + action={() => this.props.toggleSymlinkChange(filePatch)}> {detail} @@ -442,16 +450,16 @@ export default class FilePatchView extends React.Component { ); } - renderHunkHeaders() { + renderHunkHeaders(filePatch) { const toggleVerb = this.props.stagingStatus === 'unstaged' ? 'Stage' : 'Unstage'; const selectedHunks = new Set( - Array.from(this.props.selectedRows, row => this.props.filePatch.getHunkAt(row)), + Array.from(this.props.selectedRows, row => filePatch.getHunkAt(row)), ); return ( - {this.props.filePatch.getHunks().map((hunk, index) => { + {filePatch.getHunks().map((hunk, index) => { const containsSelection = this.props.selectionMode === 'line' && selectedHunks.has(hunk); const isSelected = this.props.isActive && (this.props.selectionMode === 'hunk') && selectedHunks.has(hunk); @@ -595,9 +603,14 @@ export default class FilePatchView extends React.Component { ); } - toggleHunkSelection(hunk, containsSelection) { + toggleHunkSelection(filePatch, hunk, containsSelection) { if (containsSelection) { - return this.props.toggleRows(this.props.selectedRows, this.props.selectionMode, {eventSource: 'button'}); + return this.props.toggleRows( + filePatch, + this.props.selectedRows, + this.props.selectionMode, + {eventSource: 'button'}, + ); } else { const changeRows = new Set( hunk.getChanges() @@ -606,13 +619,18 @@ export default class FilePatchView extends React.Component { return rows; }, []), ); - return this.props.toggleRows(changeRows, 'hunk', {eventSource: 'button'}); + return this.props.toggleRows(filePatch, changeRows, 'hunk', {eventSource: 'button'}); } } - discardHunkSelection(hunk, containsSelection) { + discardHunkSelection(filePatch, hunk, containsSelection) { if (containsSelection) { - return this.props.discardRows(this.props.selectedRows, this.props.selectionMode, {eventSource: 'button'}); + return this.props.discardRows( + filePatch, + this.props.selectedRows, + this.props.selectionMode, + {eventSource: 'button'}, + ); } else { const changeRows = new Set( hunk.getChanges() @@ -621,7 +639,7 @@ export default class FilePatchView extends React.Component { return rows; }, []), ); - return this.props.discardRows(changeRows, 'hunk', {eventSource: 'button'}); + return this.props.discardRows(filePatch, changeRows, 'hunk', {eventSource: 'button'}); } } @@ -766,7 +784,12 @@ export default class FilePatchView extends React.Component { } didConfirm() { - return this.props.toggleRows(this.props.selectedRows, this.props.selectionMode); + return Promise.all( + Array.from( + this.getSelectedFilePatches(), + filePatch => this.props.toggleRows(filePatch, this.props.selectedRows, this.props.selectionMode), + ), + ); } didToggleSelectionMode() { @@ -795,6 +818,22 @@ export default class FilePatchView extends React.Component { }); } + didToggleModeChange = () => { + return Promise.all( + Array.from(this.getSelectedFilePatches()) + .filter(fp => fp.didChangeExecutableMode()) + .map(this.props.toggleModeChange), + ); + } + + didToggleSymlinkChange = () => { + return Promise.all( + Array.from(this.getSelectedFilePatches()) + .filter(fp => fp.hasSymlink()) + .map(this.props.toggleSymlinkChange), + ); + } + selectNextHunk() { this.refEditor.map(editor => { const nextHunks = new Set( @@ -827,7 +866,7 @@ export default class FilePatchView extends React.Component { for (const cursor of editor.getCursors()) { const cursorRow = cursor.getBufferPosition().row; - const hunk = this.props.filePatch.getHunkAt(cursorRow); + const hunk = this.props.multiFilePatch.getHunkAt(cursorRow); /* istanbul ignore next */ if (!hunk) { continue; @@ -913,7 +952,7 @@ export default class FilePatchView extends React.Component { } oldLineNumberLabel({bufferRow, softWrapped}) { - const hunk = this.props.filePatch.getHunkAt(bufferRow); + const hunk = this.props.multiFilePatch.getHunkAt(bufferRow); if (hunk === undefined) { return this.pad(''); } @@ -927,7 +966,7 @@ export default class FilePatchView extends React.Component { } newLineNumberLabel({bufferRow, softWrapped}) { - const hunk = this.props.filePatch.getHunkAt(bufferRow); + const hunk = this.props.multiFilePatch.getHunkAt(bufferRow); if (hunk === undefined) { return this.pad(''); } @@ -952,7 +991,7 @@ export default class FilePatchView extends React.Component { const seen = new Set(); return editor.getSelectedBufferRanges().reduce((acc, range) => { for (const row of range.getRows()) { - const hunk = this.props.filePatch.getHunkAt(row); + const hunk = this.props.multiFilePatch.getHunkAt(row); if (!hunk || seen.has(hunk)) { continue; } @@ -965,18 +1004,35 @@ export default class FilePatchView extends React.Component { }).getOr([]); } + /* + * Return a Set of FilePatches that include at least one editor selection. The selection need not contain an actual + * change row. + */ + getSelectedFilePatches() { + return this.refEditor.map(editor => { + const patches = new Set(); + for (const range of editor.getSelectedBufferRanges()) { + for (const row of range.getRows()) { + const patch = this.props.multiFilePatch.getFilePatchAt(row); + patches.add(patch); + } + } + return patches; + }).getOr(new Set()); + } + getHunkBefore(hunk) { const prevRow = hunk.getRange().start.row - 1; - return this.props.filePatch.getHunkAt(prevRow); + return this.props.multiFilePatch.getHunkAt(prevRow); } getHunkAfter(hunk) { const nextRow = hunk.getRange().end.row + 1; - return this.props.filePatch.getHunkAt(nextRow); + return this.props.multiFilePatch.getHunkAt(nextRow); } isChangeRow(bufferRow) { - const changeLayers = [this.props.filePatch.getAdditionLayer(), this.props.filePatch.getDeletionLayer()]; + const changeLayers = [this.props.multiFilePatch.getAdditionLayer(), this.props.multiFilePatch.getDeletionLayer()]; return changeLayers.some(layer => layer.findMarkers({intersectsRow: bufferRow}).length > 0); } @@ -990,7 +1046,7 @@ export default class FilePatchView extends React.Component { } pad(num) { - const maxDigits = this.props.filePatch.getMaxLineNumberWidth(); + const maxDigits = this.props.multiFilePatch.getMaxLineNumberWidth(); if (num === null) { return NBSP_CHARACTER.repeat(maxDigits); } else { From 04591fc8dbfc3afa66edf68b6dbb1551725c3f25 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 5 Nov 2018 15:28:10 -0500 Subject: [PATCH 129/284] Mark the contents of each FilePatch within a shared TextBuffer --- lib/models/patch/builder.js | 22 ++++++++++++++-------- lib/models/patch/file-patch.js | 4 ++++ lib/models/patch/patch.js | 7 ++++++- test/models/patch/builder.test.js | 3 +++ 4 files changed, 27 insertions(+), 9 deletions(-) diff --git a/lib/models/patch/builder.js b/lib/models/patch/builder.js index 6f700e1c6c2..3226963b7df 100644 --- a/lib/models/patch/builder.js +++ b/lib/models/patch/builder.js @@ -69,7 +69,7 @@ function singleDiffFilePatch(diff, layeredBuffer = null) { if (!layeredBuffer) { layeredBuffer = initializeBuffer(); } - const [hunks] = buildHunks(diff, layeredBuffer); + const [hunks, patchMarker] = buildHunks(diff, layeredBuffer); let oldSymlink = null; let newSymlink = null; @@ -88,7 +88,7 @@ function singleDiffFilePatch(diff, layeredBuffer = null) { 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, ...layeredBuffer}); + const patch = new Patch({status: diff.status, hunks, marker: patchMarker, ...layeredBuffer}); return new FilePatch(oldFile, newFile, patch); } @@ -107,7 +107,7 @@ function dualDiffFilePatch(diff1, diff2, layeredBuffer = null) { contentChangeDiff = diff1; } - const [hunks] = buildHunks(contentChangeDiff, layeredBuffer); + const [hunks, patchMarker] = buildHunks(contentChangeDiff, layeredBuffer); const filePath = contentChangeDiff.oldPath || contentChangeDiff.newPath; const symlink = modeChangeDiff.hunks[0].lines[0].slice(1); @@ -133,7 +133,7 @@ function dualDiffFilePatch(diff1, diff2, layeredBuffer = null) { 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, ...layeredBuffer}); + const patch = new Patch({status, hunks, marker: patchMarker, ...layeredBuffer}); return new FilePatch(oldFile, newFile, patch); } @@ -147,7 +147,7 @@ const CHANGEKIND = { function initializeBuffer() { const buffer = new TextBuffer(); - const layers = ['hunk', 'unchanged', 'addition', 'deletion', 'noNewline'].reduce((obj, key) => { + const layers = ['patch', 'hunk', 'unchanged', 'addition', 'deletion', 'noNewline'].reduce((obj, key) => { obj[key] = buffer.addMarkerLayer(); return obj; }, {}); @@ -164,7 +164,9 @@ function buildHunks(diff, {buffer, layers}) { ]); const hunks = []; - let bufferRow = buffer.getLastRow(); + const patchStartRow = buffer.getLastRow(); + let bufferRow = patchStartRow; + let nextLineLength = 0; for (const hunkData of diff.hunks) { const bufferStartRow = bufferRow; @@ -174,7 +176,6 @@ function buildHunks(diff, {buffer, layers}) { let LastChangeKind = null; let currentRangeStart = bufferRow; let lastLineLength = 0; - let nextLineLength = 0; const finishCurrentRange = () => { if (currentRangeStart === bufferRow) { @@ -226,5 +227,10 @@ function buildHunks(diff, {buffer, layers}) { })); } - return [hunks]; + const patchMarker = layers.patch.markRange( + [[patchStartRow, 0], [bufferRow - 1, nextLineLength]], + {invalidate: 'never', exclusive: false}, + ); + + return [hunks, patchMarker]; } diff --git a/lib/models/patch/file-patch.js b/lib/models/patch/file-patch.js index 76c98bccbc5..d657dedb724 100644 --- a/lib/models/patch/file-patch.js +++ b/lib/models/patch/file-patch.js @@ -36,6 +36,10 @@ export default class FilePatch { return this.patch; } + getMarker() { + return this.getPatch().getMarker(); + } + getOldPath() { return this.getOldFile().getPath(); } diff --git a/lib/models/patch/patch.js b/lib/models/patch/patch.js index 2a924f85f20..11b02e75332 100644 --- a/lib/models/patch/patch.js +++ b/lib/models/patch/patch.js @@ -8,10 +8,11 @@ export default class Patch { return new NullPatch(); } - constructor({status, hunks, buffer, layers}) { + constructor({status, hunks, buffer, layers, marker}) { this.status = status; this.hunks = hunks; this.buffer = buffer; + this.marker = marker; this.hunkLayer = layers.hunk; this.unchangedLayer = layers.unchanged; @@ -28,6 +29,10 @@ export default class Patch { return this.status; } + getMarker() { + return this.marker; + } + getHunks() { return this.hunks; } diff --git a/test/models/patch/builder.test.js b/test/models/patch/builder.test.js index 3fa8b748c0f..c4e2385e723 100644 --- a/test/models/patch/builder.test.js +++ b/test/models/patch/builder.test.js @@ -576,6 +576,7 @@ describe('buildFilePatch', function() { } assert.strictEqual(mp.getFilePatches()[0].getOldPath(), 'first'); + assert.deepEqual(mp.getFilePatches()[0].getMarker().getRange().serialize(), [[0, 0], [6, 6]]); assertInFilePatch(mp.getFilePatches()[0]).hunks( { startRow: 0, endRow: 3, header: '@@ -1,2 +1,4 @@', regions: [ @@ -593,6 +594,7 @@ describe('buildFilePatch', function() { }, ); assert.strictEqual(mp.getFilePatches()[1].getOldPath(), 'second'); + assert.deepEqual(mp.getFilePatches()[1].getMarker().getRange().serialize(), [[7, 0], [10, 6]]); assertInFilePatch(mp.getFilePatches()[1]).hunks( { startRow: 7, endRow: 10, header: '@@ -5,3 +5,3 @@', regions: [ @@ -604,6 +606,7 @@ describe('buildFilePatch', function() { }, ); assert.strictEqual(mp.getFilePatches()[2].getOldPath(), 'third'); + assert.deepEqual(mp.getFilePatches()[2].getMarker().getRange().serialize(), [[11, 0], [13, 6]]); assertInFilePatch(mp.getFilePatches()[2]).hunks( { startRow: 11, endRow: 13, header: '@@ -1,0 +1,3 @@', regions: [ From 91a49db58b0386c36103adfd8bfc41f7495c1926 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 5 Nov 2018 15:36:52 -0500 Subject: [PATCH 130/284] FilePatch::getStartRange() returns the Range for file controls --- lib/models/patch/file-patch.js | 4 ++++ lib/models/patch/patch.js | 7 ++++++- test/models/patch/file-patch.test.js | 28 +++++++++++++++++++++++++++- 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/lib/models/patch/file-patch.js b/lib/models/patch/file-patch.js index d657dedb724..16f91654694 100644 --- a/lib/models/patch/file-patch.js +++ b/lib/models/patch/file-patch.js @@ -40,6 +40,10 @@ export default class FilePatch { return this.getPatch().getMarker(); } + getStartRange() { + return this.getPatch().getStartRange(); + } + getOldPath() { return this.getOldFile().getPath(); } diff --git a/lib/models/patch/patch.js b/lib/models/patch/patch.js index 11b02e75332..585b1bf7e6b 100644 --- a/lib/models/patch/patch.js +++ b/lib/models/patch/patch.js @@ -1,4 +1,4 @@ -import {TextBuffer} from 'atom'; +import {TextBuffer, Range} from 'atom'; import Hunk from './hunk'; import {Unchanged, Addition, Deletion, NoNewline} from './region'; @@ -33,6 +33,11 @@ export default class Patch { return this.marker; } + getStartRange() { + const startPoint = this.getMarker().getRange().start; + return Range.fromObject([startPoint, startPoint]); + } + getHunks() { return this.hunks; } diff --git a/test/models/patch/file-patch.test.js b/test/models/patch/file-patch.test.js index 4df06258437..09414abb02d 100644 --- a/test/models/patch/file-patch.test.js +++ b/test/models/patch/file-patch.test.js @@ -22,7 +22,8 @@ describe('FilePatch', function() { ], }), ]; - const patch = new Patch({status: 'modified', hunks, buffer, layers}); + const marker = markRange(layers.patch); + const patch = new Patch({status: 'modified', hunks, buffer, layers, marker}); const oldFile = new File({path: 'a.txt', mode: '120000', symlink: 'dest.txt'}); const newFile = new File({path: 'b.txt', mode: '100755'}); @@ -44,6 +45,7 @@ describe('FilePatch', function() { assert.strictEqual(filePatch.getByteSize(), 15); assert.strictEqual(filePatch.getBuffer().getText(), '0000\n0001\n0002\n'); + assert.strictEqual(filePatch.getMarker(), marker); assert.strictEqual(filePatch.getMaxLineNumberWidth(), 1); assert.strictEqual(filePatch.getHunkAt(1), hunks[0]); @@ -158,6 +160,29 @@ describe('FilePatch', function() { ]); }); + it('returns the starting range of the patch', function() { + const buffer = new TextBuffer({text: '0000\n0001\n0002\n0003\n'}); + const layers = buildLayers(buffer); + const hunks = [ + new Hunk({ + oldStartRow: 2, oldRowCount: 1, newStartRow: 2, newRowCount: 3, + marker: markRange(layers.hunk, 1, 3), + regions: [ + new Unchanged(markRange(layers.unchanged, 1)), + new Addition(markRange(layers.addition, 2, 3)), + ], + }), + ]; + const marker = markRange(layers.patch, 1, 3); + const patch = new Patch({status: 'modified', hunks, buffer, layers, marker}); + 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.deepEqual(filePatch.getStartRange().serialize(), [[1, 0], [1, 0]]); + }); + it('adopts a buffer and layers from a prior FilePatch', function() { const oldFile = new File({path: 'a.txt', mode: '100755'}); const newFile = new File({path: 'b.txt', mode: '100755'}); @@ -903,6 +928,7 @@ describe('FilePatch', function() { function buildLayers(buffer) { return { + patch: buffer.addMarkerLayer(), hunk: buffer.addMarkerLayer(), unchanged: buffer.addMarkerLayer(), addition: buffer.addMarkerLayer(), From 885b81a6e07d1b87b3f550a71bbd0da6f1c3f142 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 5 Nov 2018 15:50:36 -0500 Subject: [PATCH 131/284] Locate a FilePatch within a shared TextBuffer by marker lookup --- lib/models/patch/builder.js | 2 +- lib/models/patch/multi-file-patch.js | 12 ++- test/models/patch/multi-file-patch.test.js | 108 ++++++++++++--------- 3 files changed, 76 insertions(+), 46 deletions(-) diff --git a/lib/models/patch/builder.js b/lib/models/patch/builder.js index 3226963b7df..f11ffb4a51b 100644 --- a/lib/models/patch/builder.js +++ b/lib/models/patch/builder.js @@ -55,7 +55,7 @@ export function buildMultiFilePatch(diffs) { const filePatches = actions.map(action => action()); - return new MultiFilePatch(layeredBuffer.buffer, filePatches); + return new MultiFilePatch(layeredBuffer.buffer, layeredBuffer.layers.patch, filePatches); } function emptyDiffFilePatch() { diff --git a/lib/models/patch/multi-file-patch.js b/lib/models/patch/multi-file-patch.js index 5c3356ece89..620448b311c 100644 --- a/lib/models/patch/multi-file-patch.js +++ b/lib/models/patch/multi-file-patch.js @@ -1,7 +1,12 @@ export default class MultiFilePatch { - constructor(buffer, filePatches) { + constructor(buffer, patchLayer, filePatches) { this.buffer = buffer; + this.patchLayer = patchLayer; this.filePatches = filePatches; + + this.filePatchesByMarker = new Map( + this.filePatches.map(filePatch => [filePatch.getMarker(), filePatch]), + ); } getBuffer() { @@ -11,4 +16,9 @@ export default class MultiFilePatch { getFilePatches() { return this.filePatches; } + + getFilePatchAt(bufferRow) { + const [marker] = this.patchLayer.findMarkers({intersectsRow: bufferRow}); + return this.filePatchesByMarker.get(marker); + } } diff --git a/test/models/patch/multi-file-patch.test.js b/test/models/patch/multi-file-patch.test.js index 3c0a38f5257..f866020cc33 100644 --- a/test/models/patch/multi-file-patch.test.js +++ b/test/models/patch/multi-file-patch.test.js @@ -8,58 +8,78 @@ import Hunk from '../../../lib/models/patch/hunk'; import {Unchanged, Addition, Deletion} from '../../../lib/models/patch/region'; describe('MultiFilePatch', function() { + let buffer, layers; + + beforeEach(function() { + buffer = new TextBuffer(); + layers = { + patch: buffer.addMarkerLayer(), + hunk: buffer.addMarkerLayer(), + unchanged: buffer.addMarkerLayer(), + addition: buffer.addMarkerLayer(), + deletion: buffer.addMarkerLayer(), + noNewline: buffer.addMarkerLayer(), + }; + }); + it('has an accessor for its file patches', function() { const filePatches = [buildFilePatchFixture(0), buildFilePatchFixture(1)]; - const mp = new MultiFilePatch(filePatches); + const mp = new MultiFilePatch(buffer, layers.patch, filePatches); assert.strictEqual(mp.getFilePatches(), filePatches); }); -}); -function buildFilePatchFixture(index) { - const buffer = new TextBuffer(); - for (let i = 0; i < 8; i++) { - buffer.append(`file-${index} line-${i}\n`); - } + it('locates an individual FilePatch by marker lookup', function() { + const filePatches = []; + for (let i = 0; i < 10; i++) { + filePatches.push(buildFilePatchFixture(i)); + } + const mp = new MultiFilePatch(buffer, layers.patch, filePatches); - const layers = { - hunk: buffer.addMarkerLayer(), - unchanged: buffer.addMarkerLayer(), - addition: buffer.addMarkerLayer(), - deletion: buffer.addMarkerLayer(), - noNewline: buffer.addMarkerLayer(), - }; + assert.strictEqual(mp.getFilePatchAt(0), filePatches[0]); + assert.strictEqual(mp.getFilePatchAt(7), filePatches[0]); + assert.strictEqual(mp.getFilePatchAt(8), filePatches[1]); + assert.strictEqual(mp.getFilePatchAt(79), filePatches[9]); + }); + + function buildFilePatchFixture(index) { + const rowOffset = buffer.getLastRow(); + for (let i = 0; i < 8; i++) { + buffer.append(`file-${index} line-${i}\n`); + } - const mark = (layer, start, end = start) => layer.markRange([[start, 0], [end, Infinity]]); + const mark = (layer, start, end = start) => layer.markRange([[rowOffset + start, 0], [rowOffset + end, Infinity]]); - const hunks = [ - new Hunk({ - oldStartRow: 0, newStartRow: 0, oldRowCount: 3, newRowCount: 3, - sectionHeading: `file-${index} hunk-0`, - marker: mark(layers.hunk, 0, 3), - regions: [ - new Unchanged(mark(layers.unchanged, 0)), - new Addition(mark(layers.addition, 1)), - new Deletion(mark(layers.deletion, 2)), - new Unchanged(mark(layers.unchanged, 3)), - ], - }), - new Hunk({ - oldStartRow: 10, newStartRow: 10, oldRowCount: 3, newRowCount: 3, - sectionHeading: `file-${index} hunk-1`, - marker: mark(layers.hunk, 4, 7), - regions: [ - new Unchanged(mark(layers.unchanged, 4)), - new Addition(mark(layers.addition, 5)), - new Deletion(mark(layers.deletion, 6)), - new Unchanged(mark(layers.unchanged, 7)), - ], - }), - ]; + const hunks = [ + new Hunk({ + oldStartRow: 0, newStartRow: 0, oldRowCount: 3, newRowCount: 3, + sectionHeading: `file-${index} hunk-0`, + marker: mark(layers.hunk, 0, 3), + regions: [ + new Unchanged(mark(layers.unchanged, 0)), + new Addition(mark(layers.addition, 1)), + new Deletion(mark(layers.deletion, 2)), + new Unchanged(mark(layers.unchanged, 3)), + ], + }), + new Hunk({ + oldStartRow: 10, newStartRow: 10, oldRowCount: 3, newRowCount: 3, + sectionHeading: `file-${index} hunk-1`, + marker: mark(layers.hunk, 4, 7), + regions: [ + new Unchanged(mark(layers.unchanged, 4)), + new Addition(mark(layers.addition, 5)), + new Deletion(mark(layers.deletion, 6)), + new Unchanged(mark(layers.unchanged, 7)), + ], + }), + ]; - const patch = new Patch({status: 'modified', hunks, buffer, layers}); + const marker = mark(layers.patch, 0, 7); + const patch = new Patch({status: 'modified', hunks, buffer, layers, marker}); - const oldFile = new File({path: `file-${index}.txt`, mode: '100644'}); - const newFile = new File({path: `file-${index}.txt`, mode: '100644'}); + const oldFile = new File({path: `file-${index}.txt`, mode: '100644'}); + const newFile = new File({path: `file-${index}.txt`, mode: '100644'}); - return new FilePatch(oldFile, newFile, patch); -} + return new FilePatch(oldFile, newFile, patch); + } +}); From d671a4fc23c47a74ef62c6e8e135543c424aef79 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 5 Nov 2018 16:07:36 -0500 Subject: [PATCH 132/284] Lift hunk layer tracking and indexing up to MultiFilePatch --- lib/models/patch/multi-file-patch.js | 20 +++++++++--- lib/models/patch/patch.js | 11 +------ lib/views/file-patch-view.js | 2 +- test/models/patch/file-patch.test.js | 2 -- test/models/patch/multi-file-patch.test.js | 22 +++++++++++-- test/models/patch/patch.test.js | 38 ---------------------- 6 files changed, 38 insertions(+), 57 deletions(-) diff --git a/lib/models/patch/multi-file-patch.js b/lib/models/patch/multi-file-patch.js index 620448b311c..00fb5950113 100644 --- a/lib/models/patch/multi-file-patch.js +++ b/lib/models/patch/multi-file-patch.js @@ -1,12 +1,19 @@ export default class MultiFilePatch { - constructor(buffer, patchLayer, filePatches) { + constructor(buffer, patchLayer, hunkLayer, filePatches) { this.buffer = buffer; this.patchLayer = patchLayer; + this.hunkLayer = hunkLayer; this.filePatches = filePatches; - this.filePatchesByMarker = new Map( - this.filePatches.map(filePatch => [filePatch.getMarker(), filePatch]), - ); + this.filePatchesByMarker = new Map(); + this.hunksByMarker = new Map(); + + for (const filePatch of this.filePatches) { + this.filePatchesByMarker.set(filePatch.getMarker(), filePatch); + for (const hunk of filePatch.getHunks()) { + this.hunksByMarker.set(hunk.getMarker(), hunk); + } + } } getBuffer() { @@ -21,4 +28,9 @@ export default class MultiFilePatch { const [marker] = this.patchLayer.findMarkers({intersectsRow: bufferRow}); return this.filePatchesByMarker.get(marker); } + + getHunkAt(bufferRow) { + const [marker] = this.hunkLayer.findMarkers({intersectsRow: bufferRow}); + return this.hunksByMarker.get(marker); + } } diff --git a/lib/models/patch/patch.js b/lib/models/patch/patch.js index 585b1bf7e6b..fdfa3de0925 100644 --- a/lib/models/patch/patch.js +++ b/lib/models/patch/patch.js @@ -21,7 +21,6 @@ export default class Patch { this.noNewlineLayer = layers.noNewline; this.buffer.retain(); - this.hunksByMarker = new Map(this.getHunks().map(hunk => [hunk.getMarker(), hunk])); this.changedLineCount = this.getHunks().reduce((acc, hunk) => acc + hunk.changedLineCount(), 0); } @@ -79,11 +78,6 @@ export default class Patch { return lastHunk ? lastHunk.getMaxLineNumberWidth() : 0; } - getHunkAt(bufferRow) { - const [marker] = this.hunkLayer.findMarkers({intersectsRow: bufferRow}); - return this.hunksByMarker.get(marker); - } - clone(opts = {}) { return new this.constructor({ status: opts.status !== undefined ? opts.status : this.getStatus(), @@ -336,6 +330,7 @@ export default class Patch { return [[newSelectionRow, 0], [newSelectionRow, Infinity]]; } + // TODO lift up to MultiFilePatch adoptBufferFrom(lastPatch) { lastPatch.getHunkLayer().clear(); lastPatch.getUnchangedLayer().clear(); @@ -508,10 +503,6 @@ class NullPatch { return 0; } - getHunkAt(bufferRow) { - return undefined; - } - toString() { return ''; } diff --git a/lib/views/file-patch-view.js b/lib/views/file-patch-view.js index c67a96e394d..6c992721ea7 100644 --- a/lib/views/file-patch-view.js +++ b/lib/views/file-patch-view.js @@ -453,7 +453,7 @@ export default class FilePatchView extends React.Component { renderHunkHeaders(filePatch) { const toggleVerb = this.props.stagingStatus === 'unstaged' ? 'Stage' : 'Unstage'; const selectedHunks = new Set( - Array.from(this.props.selectedRows, row => filePatch.getHunkAt(row)), + Array.from(this.props.selectedRows, row => this.multiFilePatch.getHunkAt(row)), ); return ( diff --git a/test/models/patch/file-patch.test.js b/test/models/patch/file-patch.test.js index 09414abb02d..0726890900a 100644 --- a/test/models/patch/file-patch.test.js +++ b/test/models/patch/file-patch.test.js @@ -48,8 +48,6 @@ describe('FilePatch', function() { assert.strictEqual(filePatch.getMarker(), marker); assert.strictEqual(filePatch.getMaxLineNumberWidth(), 1); - assert.strictEqual(filePatch.getHunkAt(1), hunks[0]); - const nBuffer = new TextBuffer({text: '0001\n0002\n'}); const nLayers = buildLayers(nBuffer); const nHunks = [ diff --git a/test/models/patch/multi-file-patch.test.js b/test/models/patch/multi-file-patch.test.js index f866020cc33..8719758fcf4 100644 --- a/test/models/patch/multi-file-patch.test.js +++ b/test/models/patch/multi-file-patch.test.js @@ -24,7 +24,7 @@ describe('MultiFilePatch', function() { it('has an accessor for its file patches', function() { const filePatches = [buildFilePatchFixture(0), buildFilePatchFixture(1)]; - const mp = new MultiFilePatch(buffer, layers.patch, filePatches); + const mp = new MultiFilePatch(buffer, layers.patch, layers.hunk, filePatches); assert.strictEqual(mp.getFilePatches(), filePatches); }); @@ -33,7 +33,7 @@ describe('MultiFilePatch', function() { for (let i = 0; i < 10; i++) { filePatches.push(buildFilePatchFixture(i)); } - const mp = new MultiFilePatch(buffer, layers.patch, filePatches); + const mp = new MultiFilePatch(buffer, layers.patch, layers.hunk, filePatches); assert.strictEqual(mp.getFilePatchAt(0), filePatches[0]); assert.strictEqual(mp.getFilePatchAt(7), filePatches[0]); @@ -41,6 +41,24 @@ describe('MultiFilePatch', function() { assert.strictEqual(mp.getFilePatchAt(79), filePatches[9]); }); + it('locates a Hunk by marker lookup', function() { + const filePatches = [ + buildFilePatchFixture(0), + buildFilePatchFixture(1), + buildFilePatchFixture(2), + ]; + const mp = new MultiFilePatch(buffer, layers.patch, layers.hunk, filePatches); + + assert.strictEqual(mp.getHunkAt(0), filePatches[0].getHunks()[0]); + assert.strictEqual(mp.getHunkAt(3), filePatches[0].getHunks()[0]); + assert.strictEqual(mp.getHunkAt(4), filePatches[0].getHunks()[1]); + assert.strictEqual(mp.getHunkAt(7), filePatches[0].getHunks()[1]); + assert.strictEqual(mp.getHunkAt(8), filePatches[1].getHunks()[0]); + assert.strictEqual(mp.getHunkAt(15), filePatches[1].getHunks()[1]); + assert.strictEqual(mp.getHunkAt(16), filePatches[2].getHunks()[0]); + assert.strictEqual(mp.getHunkAt(23), filePatches[2].getHunks()[1]); + }); + function buildFilePatchFixture(index) { const rowOffset = buffer.getLastRow(); for (let i = 0; i < 8; i++) { diff --git a/test/models/patch/patch.test.js b/test/models/patch/patch.test.js index 0ee6dccbef0..2de64df4df2 100644 --- a/test/models/patch/patch.test.js +++ b/test/models/patch/patch.test.js @@ -90,43 +90,6 @@ describe('Patch', function() { assert.strictEqual(p1.getMaxLineNumberWidth(), 0); }); - it('accesses the Hunk at a buffer row', function() { - const buffer = buildBuffer(8); - const layers = buildLayers(buffer); - const hunk0 = new Hunk({ - oldStartRow: 1, oldRowCount: 4, newStartRow: 1, newRowCount: 4, - marker: markRange(layers.hunk, 0, 3), - regions: [ - new Unchanged(markRange(layers.unchanged, 0)), - new Addition(markRange(layers.addition, 1)), - new Deletion(markRange(layers.deletion, 2)), - new Unchanged(markRange(layers.unchanged, 3)), - ], - }); - const hunk1 = new Hunk({ - oldStartRow: 10, oldRowCount: 4, newStartRow: 10, newRowCount: 4, - marker: markRange(layers.hunk, 4, 7), - regions: [ - new Unchanged(markRange(layers.unchanged, 4)), - new Deletion(markRange(layers.deletion, 5)), - new Addition(markRange(layers.addition, 6)), - new Unchanged(markRange(layers.unchanged, 7)), - ], - }); - const hunks = [hunk0, hunk1]; - const patch = new Patch({status: 'modified', hunks, buffer, layers}); - - assert.strictEqual(patch.getHunkAt(0), hunk0); - assert.strictEqual(patch.getHunkAt(1), hunk0); - assert.strictEqual(patch.getHunkAt(2), hunk0); - assert.strictEqual(patch.getHunkAt(3), hunk0); - assert.strictEqual(patch.getHunkAt(4), hunk1); - assert.strictEqual(patch.getHunkAt(5), hunk1); - assert.strictEqual(patch.getHunkAt(6), hunk1); - assert.strictEqual(patch.getHunkAt(7), hunk1); - assert.isUndefined(patch.getHunkAt(10)); - }); - it('clones itself with optionally overridden properties', function() { const buffer = new TextBuffer({text: 'bufferText'}); const layers = buildLayers(buffer); @@ -844,7 +807,6 @@ describe('Patch', function() { assert.strictEqual(nullPatch.toString(), ''); assert.strictEqual(nullPatch.getChangedLineCount(), 0); assert.strictEqual(nullPatch.getMaxLineNumberWidth(), 0); - assert.isUndefined(nullPatch.getHunkAt(0)); assert.deepEqual(nullPatch.getFirstChangeRange(), [[0, 0], [0, 0]]); assert.deepEqual(nullPatch.getNextSelectionRange(), [[0, 0], [0, 0]]); }); From a04f1e813a5ab967e3c42a9cc5030d6ec0acace4 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 5 Nov 2018 16:09:28 -0500 Subject: [PATCH 133/284] Construct MultiFilePatches correctly in the builder --- lib/models/patch/builder.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/models/patch/builder.js b/lib/models/patch/builder.js index f11ffb4a51b..7ec3e0a1bd0 100644 --- a/lib/models/patch/builder.js +++ b/lib/models/patch/builder.js @@ -55,7 +55,7 @@ export function buildMultiFilePatch(diffs) { const filePatches = actions.map(action => action()); - return new MultiFilePatch(layeredBuffer.buffer, layeredBuffer.layers.patch, filePatches); + return new MultiFilePatch(layeredBuffer.buffer, layeredBuffer.layers.patch, layeredBuffer.layers.hunk, filePatches); } function emptyDiffFilePatch() { From e9d33c9f5cd22fb979aefb7c2bdc30f727cd94da Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 5 Nov 2018 16:39:11 -0500 Subject: [PATCH 134/284] WIP: Move adoptBufferFrom() to MultiFilePatch --- lib/models/patch/multi-file-patch.js | 45 ++++++++++++ lib/models/patch/patch.js | 20 ------ test/models/patch/multi-file-patch.test.js | 39 +++++++++++ test/models/patch/patch.test.js | 81 ---------------------- 4 files changed, 84 insertions(+), 101 deletions(-) diff --git a/lib/models/patch/multi-file-patch.js b/lib/models/patch/multi-file-patch.js index 00fb5950113..b4c275d089d 100644 --- a/lib/models/patch/multi-file-patch.js +++ b/lib/models/patch/multi-file-patch.js @@ -33,4 +33,49 @@ export default class MultiFilePatch { const [marker] = this.hunkLayer.findMarkers({intersectsRow: bufferRow}); return this.hunksByMarker.get(marker); } + + adoptBufferFrom(lastMultiFilePatch) { + lastMultiFilePatch.getHunkLayer().clear(); + lastMultiFilePatch.getUnchangedLayer().clear(); + lastMultiFilePatch.getAdditionLayer().clear(); + lastMultiFilePatch.getDeletionLayer().clear(); + lastMultiFilePatch.getNoNewlineLayer().clear(); + + const nextBuffer = lastMultiFilePatch.getBuffer(); + nextBuffer.setText(this.getBuffer().getText()); + + for (const hunk of this.getHunks()) { + hunk.reMarkOn(lastMultiFilePatch.getHunkLayer()); + for (const region of hunk.getRegions()) { + const target = region.when({ + unchanged: () => lastMultiFilePatch.getUnchangedLayer(), + addition: () => lastMultiFilePatch.getAdditionLayer(), + deletion: () => lastMultiFilePatch.getDeletionLayer(), + nonewline: () => lastMultiFilePatch.getNoNewlineLayer(), + }); + region.reMarkOn(target); + } + } + + this.filePatchesByMarker.clear(); + this.hunksByMarker.clear(); + + for (const filePatch of this.filePatches) { + this.filePatchesByMarker.set(filePatch.getMarker(), filePatch); + for (const hunk of filePatch.getHunks()) { + this.hunksByMarker.set(hunk.getMarker(), hunk); + } + } + + this.hunkLayer = lastMultiFilePatch.getHunkLayer(); + + this.unchangedLayer = lastMultiFilePatch.getUnchangedLayer(); + + // FIXME + this.additionLayer = lastMultiFilePatch.getAdditionLayer(); + this.deletionLayer = lastMultiFilePatch.getDeletionLayer(); + this.noNewlineLayer = lastMultiFilePatch.getNoNewlineLayer(); + + this.buffer = nextBuffer; + } } diff --git a/lib/models/patch/patch.js b/lib/models/patch/patch.js index fdfa3de0925..7e137843dce 100644 --- a/lib/models/patch/patch.js +++ b/lib/models/patch/patch.js @@ -479,26 +479,6 @@ class NullPatch { return [[0, 0], [0, 0]]; } - adoptBufferFrom(lastPatch) { - lastPatch.getHunkLayer().clear(); - lastPatch.getUnchangedLayer().clear(); - lastPatch.getAdditionLayer().clear(); - lastPatch.getDeletionLayer().clear(); - lastPatch.getNoNewlineLayer().clear(); - - const nextBuffer = lastPatch.getBuffer(); - nextBuffer.setText(''); - - this.hunkLayer = lastPatch.getHunkLayer(); - this.unchangedLayer = lastPatch.getUnchangedLayer(); - this.additionLayer = lastPatch.getAdditionLayer(); - this.deletionLayer = lastPatch.getDeletionLayer(); - this.noNewlineLayer = lastPatch.getNoNewlineLayer(); - - this.buffer.release(); - this.buffer = nextBuffer; - } - getMaxLineNumberWidth() { return 0; } diff --git a/test/models/patch/multi-file-patch.test.js b/test/models/patch/multi-file-patch.test.js index 8719758fcf4..608eac6d161 100644 --- a/test/models/patch/multi-file-patch.test.js +++ b/test/models/patch/multi-file-patch.test.js @@ -59,6 +59,45 @@ describe('MultiFilePatch', function() { assert.strictEqual(mp.getHunkAt(23), filePatches[2].getHunks()[1]); }); + it('adopts a buffer from a previous patch', function() { + const lastBuffer = buffer; + const lastLayers = layers; + const lastFilePatches = [ + buildFilePatchFixture(0), + buildFilePatchFixture(1), + buildFilePatchFixture(2), + ]; + const lastPatch = new MultiFilePatch(lastBuffer, lastLayers.patch, lastLayers.hunk, lastFilePatches); + + buffer = new TextBuffer(); + layers = { + patch: buffer.addMarkerLayer(), + hunk: buffer.addMarkerLayer(), + unchanged: buffer.addMarkerLayer(), + addition: buffer.addMarkerLayer(), + deletion: buffer.addMarkerLayer(), + noNewline: buffer.addMarkerLayer(), + }; + const nextFilePatches = [ + buildFilePatchFixture(0), + buildFilePatchFixture(1), + buildFilePatchFixture(2), + buildFilePatchFixture(3), + ]; + const nextPatch = new MultiFilePatch(buffer, layers.patch, layers.hunk, nextFilePatches); + + nextPatch.adoptBufferFrom(lastPatch); + + assert.strictEqual(nextPatch.getBuffer(), lastBuffer); + assert.strictEqual(nextPatch.getHunkLayer(), lastLayers.hunk); + assert.strictEqual(nextPatch.getUnchangedLayer(), lastLayers.unchanged); + assert.strictEqual(nextPatch.getAdditionLayer(), lastLayers.addition); + assert.strictEqual(nextPatch.getDeletionLayer(), lastLayers.deletion); + assert.strictEqual(nextPatch.getNoNewlineLayer(), lastLayers.noNewline); + + assert.lengthOf(nextPatch.getHunkLayer().getMarkers(), 8); + }); + function buildFilePatchFixture(index) { const rowOffset = buffer.getLastRow(); for (let i = 0; i < 8; i++) { diff --git a/test/models/patch/patch.test.js b/test/models/patch/patch.test.js index 2de64df4df2..e20ebcc512f 100644 --- a/test/models/patch/patch.test.js +++ b/test/models/patch/patch.test.js @@ -810,87 +810,6 @@ describe('Patch', function() { assert.deepEqual(nullPatch.getFirstChangeRange(), [[0, 0], [0, 0]]); assert.deepEqual(nullPatch.getNextSelectionRange(), [[0, 0], [0, 0]]); }); - - it('adopts a buffer from a previous patch', function() { - const patch0 = buildPatchFixture(); - const buffer0 = patch0.getBuffer(); - const hunkLayer0 = patch0.getHunkLayer(); - const unchangedLayer0 = patch0.getUnchangedLayer(); - const additionLayer0 = patch0.getAdditionLayer(); - const deletionLayer0 = patch0.getDeletionLayer(); - const noNewlineLayer0 = patch0.getNoNewlineLayer(); - - const buffer1 = new TextBuffer({text: '0000\n0001\n0002\n0003\n0004\n No newline at end of file'}); - const layers1 = buildLayers(buffer1); - const hunks1 = [ - new Hunk({ - oldStartRow: 1, oldRowCount: 2, newStartRow: 1, newRowCount: 3, - sectionHeading: '0', - marker: markRange(layers1.hunk, 0, 2), - regions: [ - new Unchanged(markRange(layers1.unchanged, 0)), - new Addition(markRange(layers1.addition, 1)), - new Unchanged(markRange(layers1.unchanged, 2)), - ], - }), - new Hunk({ - oldStartRow: 5, oldRowCount: 2, newStartRow: 1, newRowCount: 3, - sectionHeading: '0', - marker: markRange(layers1.hunk, 3, 5), - regions: [ - new Unchanged(markRange(layers1.unchanged, 3)), - new Deletion(markRange(layers1.deletion, 4)), - new NoNewline(markRange(layers1.noNewline, 5)), - ], - }), - ]; - - const patch1 = new Patch({status: 'modified', hunks: hunks1, buffer: buffer1, layers: layers1}); - - assert.notStrictEqual(patch1.getBuffer(), patch0.getBuffer()); - assert.notStrictEqual(patch1.getHunkLayer(), hunkLayer0); - assert.notStrictEqual(patch1.getUnchangedLayer(), unchangedLayer0); - assert.notStrictEqual(patch1.getAdditionLayer(), additionLayer0); - assert.notStrictEqual(patch1.getDeletionLayer(), deletionLayer0); - assert.notStrictEqual(patch1.getNoNewlineLayer(), noNewlineLayer0); - - patch1.adoptBufferFrom(patch0); - - assert.strictEqual(patch1.getBuffer(), buffer0); - - const markerRanges = [ - ['hunk', patch1.getHunkLayer(), hunkLayer0], - ['unchanged', patch1.getUnchangedLayer(), unchangedLayer0], - ['addition', patch1.getAdditionLayer(), additionLayer0], - ['deletion', patch1.getDeletionLayer(), deletionLayer0], - ['noNewline', patch1.getNoNewlineLayer(), noNewlineLayer0], - ].reduce((obj, [key, layer1, layer0]) => { - assert.strictEqual(layer1, layer0, `Layer ${key} not inherited`); - obj[key] = layer1.getMarkers().map(marker => marker.getRange().serialize()); - return obj; - }, {}); - - assert.deepEqual(markerRanges, { - hunk: [ - [[0, 0], [2, 4]], - [[3, 0], [5, 26]], - ], - unchanged: [ - [[0, 0], [0, 4]], - [[2, 0], [2, 4]], - [[3, 0], [3, 4]], - ], - addition: [ - [[1, 0], [1, 4]], - ], - deletion: [ - [[4, 0], [4, 4]], - ], - noNewline: [ - [[5, 0], [5, 26]], - ], - }); - }); }); function buildBuffer(lines, noNewline = false) { From e034b77afa37966361b3e9f485c6582d429d3dfd Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Mon, 5 Nov 2018 17:22:23 -0800 Subject: [PATCH 135/284] Finish up implementing `adoptBufferFrom()` on MultiFilePatch --- lib/models/patch/multi-file-patch.js | 54 +++++++++++++++++++++------- 1 file changed, 41 insertions(+), 13 deletions(-) diff --git a/lib/models/patch/multi-file-patch.js b/lib/models/patch/multi-file-patch.js index b4c275d089d..25f1deae90a 100644 --- a/lib/models/patch/multi-file-patch.js +++ b/lib/models/patch/multi-file-patch.js @@ -1,8 +1,14 @@ export default class MultiFilePatch { - constructor(buffer, patchLayer, hunkLayer, filePatches) { + constructor(buffer, layers, filePatches) { this.buffer = buffer; - this.patchLayer = patchLayer; - this.hunkLayer = hunkLayer; + + this.patchLayer = layers.patch; + this.hunkLayer = layers.hunk; + this.unchangedLayer = layers.unchanged; + this.additionLayer = layers.addition; + this.deletionLayer = layers.deletion; + this.noNewlineLayer = layers.noNewline; + this.filePatches = filePatches; this.filePatchesByMarker = new Map(); @@ -20,6 +26,26 @@ export default class MultiFilePatch { return this.buffer; } + getHunkLayer() { + return this.hunkLayer; + } + + getUnchangedLayer() { + return this.unchangedLayer; + } + + getAdditionLayer() { + return this.additionLayer; + } + + getDeletionLayer() { + return this.deletionLayer; + } + + getNoNewlineLayer() { + return this.noNewlineLayer; + } + getFilePatches() { return this.filePatches; } @@ -44,16 +70,18 @@ export default class MultiFilePatch { const nextBuffer = lastMultiFilePatch.getBuffer(); nextBuffer.setText(this.getBuffer().getText()); - for (const hunk of this.getHunks()) { - hunk.reMarkOn(lastMultiFilePatch.getHunkLayer()); - for (const region of hunk.getRegions()) { - const target = region.when({ - unchanged: () => lastMultiFilePatch.getUnchangedLayer(), - addition: () => lastMultiFilePatch.getAdditionLayer(), - deletion: () => lastMultiFilePatch.getDeletionLayer(), - nonewline: () => lastMultiFilePatch.getNoNewlineLayer(), - }); - region.reMarkOn(target); + for (const patch of this.getFilePatches()) { + for (const hunk of patch.getHunks()) { + hunk.reMarkOn(lastMultiFilePatch.getHunkLayer()); + for (const region of hunk.getRegions()) { + const target = region.when({ + unchanged: () => lastMultiFilePatch.getUnchangedLayer(), + addition: () => lastMultiFilePatch.getAdditionLayer(), + deletion: () => lastMultiFilePatch.getDeletionLayer(), + nonewline: () => lastMultiFilePatch.getNoNewlineLayer(), + }); + region.reMarkOn(target); + } } } From 8f01004656f011650448e61cbcc9da2f2ddc602d Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Mon, 5 Nov 2018 17:23:12 -0800 Subject: [PATCH 136/284] Pass layers object to MultiFilePatch --- lib/models/patch/builder.js | 6 +++++- test/models/patch/multi-file-patch.test.js | 10 +++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/lib/models/patch/builder.js b/lib/models/patch/builder.js index 7ec3e0a1bd0..083ac2b657e 100644 --- a/lib/models/patch/builder.js +++ b/lib/models/patch/builder.js @@ -55,7 +55,11 @@ export function buildMultiFilePatch(diffs) { const filePatches = actions.map(action => action()); - return new MultiFilePatch(layeredBuffer.buffer, layeredBuffer.layers.patch, layeredBuffer.layers.hunk, filePatches); + const layers = { + patch: layeredBuffer.layers.patch, + hunk: layeredBuffer.layers.hunk, + }; + return new MultiFilePatch(layeredBuffer.buffer, layers, filePatches); } function emptyDiffFilePatch() { diff --git a/test/models/patch/multi-file-patch.test.js b/test/models/patch/multi-file-patch.test.js index 608eac6d161..7a64d746146 100644 --- a/test/models/patch/multi-file-patch.test.js +++ b/test/models/patch/multi-file-patch.test.js @@ -24,7 +24,7 @@ describe('MultiFilePatch', function() { it('has an accessor for its file patches', function() { const filePatches = [buildFilePatchFixture(0), buildFilePatchFixture(1)]; - const mp = new MultiFilePatch(buffer, layers.patch, layers.hunk, filePatches); + const mp = new MultiFilePatch(buffer, layers, filePatches); assert.strictEqual(mp.getFilePatches(), filePatches); }); @@ -33,7 +33,7 @@ describe('MultiFilePatch', function() { for (let i = 0; i < 10; i++) { filePatches.push(buildFilePatchFixture(i)); } - const mp = new MultiFilePatch(buffer, layers.patch, layers.hunk, filePatches); + const mp = new MultiFilePatch(buffer, layers, filePatches); assert.strictEqual(mp.getFilePatchAt(0), filePatches[0]); assert.strictEqual(mp.getFilePatchAt(7), filePatches[0]); @@ -47,7 +47,7 @@ describe('MultiFilePatch', function() { buildFilePatchFixture(1), buildFilePatchFixture(2), ]; - const mp = new MultiFilePatch(buffer, layers.patch, layers.hunk, filePatches); + const mp = new MultiFilePatch(buffer, layers, filePatches); assert.strictEqual(mp.getHunkAt(0), filePatches[0].getHunks()[0]); assert.strictEqual(mp.getHunkAt(3), filePatches[0].getHunks()[0]); @@ -67,7 +67,7 @@ describe('MultiFilePatch', function() { buildFilePatchFixture(1), buildFilePatchFixture(2), ]; - const lastPatch = new MultiFilePatch(lastBuffer, lastLayers.patch, lastLayers.hunk, lastFilePatches); + const lastPatch = new MultiFilePatch(lastBuffer, layers, lastFilePatches); buffer = new TextBuffer(); layers = { @@ -84,7 +84,7 @@ describe('MultiFilePatch', function() { buildFilePatchFixture(2), buildFilePatchFixture(3), ]; - const nextPatch = new MultiFilePatch(buffer, layers.patch, layers.hunk, nextFilePatches); + const nextPatch = new MultiFilePatch(buffer, layers, nextFilePatches); nextPatch.adoptBufferFrom(lastPatch); From 3247c0d62123b7d5402f2f98506afffeb3984e8b Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Mon, 5 Nov 2018 19:38:07 -0800 Subject: [PATCH 137/284] WIP clean up Patch model and remove layer code Question -- do we still need the layer stuff in BufferBuilder? My guess is no, but there's a bunch of region and marker logic in `getStagePatchForLines` --- lib/models/patch/patch.js | 108 ++------------------------------------ 1 file changed, 4 insertions(+), 104 deletions(-) diff --git a/lib/models/patch/patch.js b/lib/models/patch/patch.js index 7e137843dce..38d8e3a0eb8 100644 --- a/lib/models/patch/patch.js +++ b/lib/models/patch/patch.js @@ -8,18 +8,12 @@ export default class Patch { return new NullPatch(); } - constructor({status, hunks, buffer, layers, marker}) { + constructor({status, hunks, buffer, marker}) { this.status = status; this.hunks = hunks; this.buffer = buffer; this.marker = marker; - this.hunkLayer = layers.hunk; - this.unchangedLayer = layers.unchanged; - this.additionLayer = layers.addition; - this.deletionLayer = layers.deletion; - this.noNewlineLayer = layers.noNewline; - this.buffer.retain(); this.changedLineCount = this.getHunks().reduce((acc, hunk) => acc + hunk.changedLineCount(), 0); } @@ -45,26 +39,6 @@ export default class Patch { return this.buffer; } - getHunkLayer() { - return this.hunkLayer; - } - - getUnchangedLayer() { - return this.unchangedLayer; - } - - getAdditionLayer() { - return this.additionLayer; - } - - getDeletionLayer() { - return this.deletionLayer; - } - - getNoNewlineLayer() { - return this.noNewlineLayer; - } - getByteSize() { return Buffer.byteLength(this.buffer.getText(), 'utf8'); } @@ -83,13 +57,6 @@ export default class Patch { status: opts.status !== undefined ? opts.status : this.getStatus(), hunks: opts.hunks !== undefined ? opts.hunks : this.getHunks(), buffer: opts.buffer !== undefined ? opts.buffer : this.getBuffer(), - layers: opts.layers !== undefined ? opts.layers : { - hunk: this.getHunkLayer(), - unchanged: this.getUnchangedLayer(), - addition: this.getAdditionLayer(), - deletion: this.getDeletionLayer(), - noNewline: this.getNoNewlineLayer(), - }, }); } @@ -173,7 +140,7 @@ export default class Patch { const wholeFile = rowSet.size === this.changedLineCount; const status = this.getStatus() === 'deleted' && !wholeFile ? 'modified' : this.getStatus(); - return this.clone({hunks, status, buffer: builder.getBuffer(), layers: builder.getLayers()}); + return this.clone({hunks, status, buffer: builder.getBuffer()}); } getUnstagePatchForLines(rowSet) { @@ -261,7 +228,7 @@ export default class Patch { status = 'added'; } - return this.clone({hunks, status, buffer: builder.getBuffer(), layers: builder.getLayers()}); + return this.clone({hunks, status, buffer: builder.getBuffer()}); } getFirstChangeRange() { @@ -330,40 +297,6 @@ export default class Patch { return [[newSelectionRow, 0], [newSelectionRow, Infinity]]; } - // TODO lift up to MultiFilePatch - adoptBufferFrom(lastPatch) { - lastPatch.getHunkLayer().clear(); - lastPatch.getUnchangedLayer().clear(); - lastPatch.getAdditionLayer().clear(); - lastPatch.getDeletionLayer().clear(); - lastPatch.getNoNewlineLayer().clear(); - - const nextBuffer = lastPatch.getBuffer(); - nextBuffer.setText(this.getBuffer().getText()); - - for (const hunk of this.getHunks()) { - hunk.reMarkOn(lastPatch.getHunkLayer()); - for (const region of hunk.getRegions()) { - const target = region.when({ - unchanged: () => lastPatch.getUnchangedLayer(), - addition: () => lastPatch.getAdditionLayer(), - deletion: () => lastPatch.getDeletionLayer(), - nonewline: () => lastPatch.getNoNewlineLayer(), - }); - region.reMarkOn(target); - } - } - - this.hunkLayer = lastPatch.getHunkLayer(); - this.unchangedLayer = lastPatch.getUnchangedLayer(); - this.additionLayer = lastPatch.getAdditionLayer(); - this.deletionLayer = lastPatch.getDeletionLayer(); - this.noNewlineLayer = lastPatch.getNoNewlineLayer(); - - this.buffer = nextBuffer; - this.hunksByMarker = new Map(this.getHunks().map(hunk => [hunk.getMarker(), hunk])); - } - toString() { return this.getHunks().reduce((str, hunk) => str + hunk.toStringIn(this.getBuffer()), ''); } @@ -390,11 +323,6 @@ export default class Patch { class NullPatch { constructor() { this.buffer = new TextBuffer(); - this.hunkLayer = this.buffer.addMarkerLayer(); - this.unchangedLayer = this.buffer.addMarkerLayer(); - this.additionLayer = this.buffer.addMarkerLayer(); - this.deletionLayer = this.buffer.addMarkerLayer(); - this.noNewlineLayer = this.buffer.addMarkerLayer(); this.buffer.retain(); } @@ -411,26 +339,6 @@ class NullPatch { return this.buffer; } - getHunkLayer() { - return this.hunkLayer; - } - - getUnchangedLayer() { - return this.unchangedLayer; - } - - getAdditionLayer() { - return this.additionLayer; - } - - getDeletionLayer() { - return this.deletionLayer; - } - - getNoNewlineLayer() { - return this.noNewlineLayer; - } - getByteSize() { return 0; } @@ -443,8 +351,7 @@ class NullPatch { if ( opts.status === undefined && opts.hunks === undefined && - opts.buffer === undefined && - opts.layers === undefined + opts.buffer === undefined ) { return this; } else { @@ -452,13 +359,6 @@ class NullPatch { status: opts.status !== undefined ? opts.status : this.getStatus(), hunks: opts.hunks !== undefined ? opts.hunks : this.getHunks(), buffer: opts.buffer !== undefined ? opts.buffer : this.getBuffer(), - layers: opts.layers !== undefined ? opts.layers : { - hunk: this.getHunkLayer(), - unchanged: this.getUnchangedLayer(), - addition: this.getAdditionLayer(), - deletion: this.getDeletionLayer(), - noNewline: this.getNoNewlineLayer(), - }, }); } } From 178f50491fc4a9ad71ed1e1057b8dd43b9079940 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Tue, 6 Nov 2018 00:24:07 -0800 Subject: [PATCH 138/284] Don't pass layer to Patch constructor --- lib/models/patch/builder.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/models/patch/builder.js b/lib/models/patch/builder.js index 083ac2b657e..9dbd146c574 100644 --- a/lib/models/patch/builder.js +++ b/lib/models/patch/builder.js @@ -92,7 +92,7 @@ function singleDiffFilePatch(diff, layeredBuffer = null) { 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, marker: patchMarker, ...layeredBuffer}); + const patch = new Patch({status: diff.status, hunks, marker: patchMarker, buffer: layeredBuffer.buffer}); return new FilePatch(oldFile, newFile, patch); } @@ -137,7 +137,7 @@ function dualDiffFilePatch(diff1, diff2, layeredBuffer = null) { 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, marker: patchMarker, ...layeredBuffer}); + const patch = new Patch({status, hunks, marker: patchMarker, buffer: layeredBuffer.buffer}); return new FilePatch(oldFile, newFile, patch); } From a1badc1f64a3d957fc10f4d7ad9ee1239d003c9f Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 6 Nov 2018 09:50:17 -0500 Subject: [PATCH 139/284] Lift all layer and buffer management to MultiFilePatch --- lib/models/patch/file-patch.js | 103 +++++++++++++++++---------- lib/models/patch/multi-file-patch.js | 80 ++++++++++++++++++++- lib/models/patch/patch.js | 56 ++++++++++----- 3 files changed, 180 insertions(+), 59 deletions(-) diff --git a/lib/models/patch/file-patch.js b/lib/models/patch/file-patch.js index 16f91654694..494d663982f 100644 --- a/lib/models/patch/file-patch.js +++ b/lib/models/patch/file-patch.js @@ -104,6 +104,10 @@ export default class FilePatch { return this.getPatch().getNoNewlineLayer(); } + containsRow(row) { + return this.getPatch().containsRow(row); + } + // TODO delete if unused getAdditionRanges() { return this.getHunks().reduce((acc, hunk) => { @@ -136,10 +140,6 @@ export default class FilePatch { return [range]; } - adoptBufferFrom(prevFilePatch) { - this.getPatch().adoptBufferFrom(prevFilePatch.getPatch()); - } - didChangeExecutableMode() { if (!this.oldFile.isPresent() || !this.newFile.isPresent()) { return false; @@ -182,52 +182,77 @@ export default class FilePatch { ); } - getStagePatchForLines(selectedLineSet) { - 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 - return this.clone({newFile: nullFile}); - } else { - return this; - } - } else { - const patch = this.patch.getStagePatchForLines(selectedLineSet); - if (this.getStatus() === 'deleted') { - // Populate newFile - return this.clone({newFile: this.getOldFile(), patch}); - } else { - return this.clone({patch}); + buildStagePatchForLines(originalBuffer, nextLayeredBuffer, selectedLineSet) { + let newFile = this.getOldFile(); + + if (this.hasTypechange() && this.getStatus() === 'deleted') { + // Handle the special case when symlink is created where an entire file was deleted. In order to stage the file + // deletion, we must ensure that the created file patch has no new file. + if ( + this.patch.getChangedLineCount() === selectedLineSet.size && + Array.from(selectedLineSet, row => this.patch.containsRow(row)).every(Boolean) + ) { + newFile = nullFile; } } - } - getStagePatchForHunk(selectedHunk) { - return this.getStagePatchForLines(new Set(selectedHunk.getBufferRows())); + const {patch, buffer, layers} = this.patch.getStagePatchForLines( + originalBuffer, + nextLayeredBuffer, + selectedLineSet, + ); + if (this.getStatus() === 'deleted') { + // Populate newFile + return { + filePatch: this.clone({newFile, patch}), + buffer, + layers, + }; + } else { + return { + filePatch: this.clone({patch}), + buffer, + layers, + }; + } } - getUnstagePatchForLines(selectedLineSet) { - const wholeFile = this.patch.getChangedLineCount() === selectedLineSet.size; + buildUnstagePatchForLines(originalBuffer, nextLayeredBuffer, selectedLineSet) { const nonNullFile = this.getNewFile().isPresent() ? this.getNewFile() : this.getOldFile(); let oldFile = this.getNewFile(); let newFile = nonNullFile; - if (wholeFile && this.getStatus() === 'added') { - // Ensure that newFile is null if the patch is an addition because we're deleting the entire file from the - // index. If a symlink was deleted and replaced by a non-symlink file, we don't want the symlink entry to muck - // up the patch. - oldFile = nonNullFile; - newFile = nullFile; - } else if (wholeFile && this.getStatus() === 'deleted') { - oldFile = nullFile; - newFile = nonNullFile; + if (this.getStatus() === 'added') { + if ( + selectedLineSet.size === this.patch.getChangedLineCount() && + Array.from(selectedLineSet, row => this.patch.containsRow(row)).every(Boolean) + ) { + // Ensure that newFile is null if the patch is an addition because we're deleting the entire file from the + // index. If a symlink was deleted and replaced by a non-symlink file, we don't want the symlink entry to muck + // up the patch. + oldFile = nonNullFile; + newFile = nullFile; + } + } else if (this.getStatus() === 'deleted') { + if ( + selectedLineSet.size === this.patch.getChangedLineCount() && + Array.from(selectedLineSet, row => this.patch.containsRow(row)).every(Boolean) + ) { + oldFile = nullFile; + newFile = nonNullFile; + } } - return this.clone({oldFile, newFile, patch: this.patch.getUnstagePatchForLines(selectedLineSet)}); - } - - getUnstagePatchForHunk(hunk) { - return this.getUnstagePatchForLines(new Set(hunk.getBufferRows())); + const {patch, buffer, layers} = this.patch.buildUnstagePatchForLines( + originalBuffer, + nextLayeredBuffer, + selectedLineSet, + ); + return { + filePatch: this.clone({oldFile, newFile, patch}), + buffer, + layers, + }; } getNextSelectionRange(lastFilePatch, lastSelectedRows) { diff --git a/lib/models/patch/multi-file-patch.js b/lib/models/patch/multi-file-patch.js index 25f1deae90a..eb2b022e171 100644 --- a/lib/models/patch/multi-file-patch.js +++ b/lib/models/patch/multi-file-patch.js @@ -1,3 +1,5 @@ +import {TextBuffer} from 'atom'; + export default class MultiFilePatch { constructor(buffer, layers, filePatches) { this.buffer = buffer; @@ -60,6 +62,40 @@ export default class MultiFilePatch { return this.hunksByMarker.get(marker); } + getStagePatchForLines(selectedLineSet) { + const nextLayeredBuffer = this.buildLayeredBuffer(); + const nextFilePatches = this.getFilePatchesContaining(selectedLineSet).map(fp => { + return fp.buildStagePatchForLines(this.getBuffer(), nextLayeredBuffer, selectedLineSet); + }); + + return new MultiFilePatch( + nextLayeredBuffer.buffer, + nextLayeredBuffer.layers, + nextFilePatches, + ); + } + + getStagePatchForHunk(hunk) { + return this.getStagePatchForLines(new Set(hunk.getBufferRows())); + } + + getUnstagePatchForLines(selectedLineSet) { + const nextLayeredBuffer = this.buildLayeredBuffer(); + const nextFilePatches = this.getFilePatchesContaining(selectedLineSet).map(fp => { + return fp.buildUnstagePatchForLines(this.getBuffer(), nextLayeredBuffer, selectedLineSet); + }); + + return new MultiFilePatch( + nextLayeredBuffer.buffer, + nextLayeredBuffer.layers, + nextFilePatches, + ); + } + + getUnstagePatchForHunk(hunk) { + return this.getUnstagePatchForLines(new Set(hunk.getBufferRows())); + } + adoptBufferFrom(lastMultiFilePatch) { lastMultiFilePatch.getHunkLayer().clear(); lastMultiFilePatch.getUnchangedLayer().clear(); @@ -95,15 +131,53 @@ export default class MultiFilePatch { } } + this.patchLayer = lastMultiFilePatch.getPatchLayer(); this.hunkLayer = lastMultiFilePatch.getHunkLayer(); - this.unchangedLayer = lastMultiFilePatch.getUnchangedLayer(); - - // FIXME this.additionLayer = lastMultiFilePatch.getAdditionLayer(); this.deletionLayer = lastMultiFilePatch.getDeletionLayer(); this.noNewlineLayer = lastMultiFilePatch.getNoNewlineLayer(); this.buffer = nextBuffer; } + + buildLayeredBuffer() { + const buffer = new TextBuffer(); + buffer.retain(); + + return { + buffer, + layers: { + patch: buffer.addMarkerLayer(), + hunk: buffer.addMarkerLayer(), + unchanged: buffer.addMarkerLayer(), + addition: buffer.addMarkerLayer(), + deletion: buffer.addMarkerLayer(), + noNewline: buffer.addMarkerLayer(), + }, + }; + } + + /* + * Efficiently locate the FilePatch instances that contain at least one row from a Set. + */ + getFilePatchesContaining(rowSet) { + const sortedRowSet = Array.from(rowSet); + sortedRowSet.sort((a, b) => b - a); + + const filePatches = new Set(); + let lastFilePatch = null; + for (const row in sortedRowSet) { + // Because the rows are sorted, consecutive rows will almost certainly belong to the same patch, so we can save + // many avoidable marker index lookups by comparing with the last. + if (lastFilePatch && lastFilePatch.containsRow(row)) { + continue; + } + + lastFilePatch = this.getFilePatchAt(row); + filePatches.add(lastFilePatch); + } + + return filePatches; + } } diff --git a/lib/models/patch/patch.js b/lib/models/patch/patch.js index 38d8e3a0eb8..1c3f380b258 100644 --- a/lib/models/patch/patch.js +++ b/lib/models/patch/patch.js @@ -8,13 +8,11 @@ export default class Patch { return new NullPatch(); } - constructor({status, hunks, buffer, marker}) { + constructor({status, hunks, marker}) { this.status = status; this.hunks = hunks; - this.buffer = buffer; this.marker = marker; - this.buffer.retain(); this.changedLineCount = this.getHunks().reduce((acc, hunk) => acc + hunk.changedLineCount(), 0); } @@ -47,6 +45,10 @@ export default class Patch { return this.changedLineCount; } + containsRow(row) { + return this.marker.getRange().intersectsRow(row); + } + getMaxLineNumberWidth() { const lastHunk = this.hunks[this.hunks.length - 1]; return lastHunk ? lastHunk.getMaxLineNumberWidth() : 0; @@ -60,8 +62,8 @@ export default class Patch { }); } - getStagePatchForLines(rowSet) { - const builder = new BufferBuilder(this.getBuffer()); + buildStagePatchForLines(originalBuffer, nextLayeredBuffer, rowSet) { + const builder = new BufferBuilder(originalBuffer, nextLayeredBuffer); const hunks = []; let newRowDelta = 0; @@ -138,13 +140,21 @@ export default class Patch { } } + const buffer = builder.getBuffer(); + const layers = builder.getLayers(); + const marker = layers.patch.markRange([[0, 0], [buffer.getLastRow(), Infinity]]); + const wholeFile = rowSet.size === this.changedLineCount; const status = this.getStatus() === 'deleted' && !wholeFile ? 'modified' : this.getStatus(); - return this.clone({hunks, status, buffer: builder.getBuffer()}); + return { + patch: this.clone({hunks, status, marker}), + buffer, + layers, + }; } - getUnstagePatchForLines(rowSet) { - const builder = new BufferBuilder(this.getBuffer()); + buildUnstagePatchForLines(originalBuffer, nextLayeredBuffer, rowSet) { + const builder = new BufferBuilder(originalBuffer, nextLayeredBuffer); const hunks = []; let newRowDelta = 0; @@ -228,7 +238,15 @@ export default class Patch { status = 'added'; } - return this.clone({hunks, status, buffer: builder.getBuffer()}); + const buffer = builder.getBuffer(); + const layers = builder.getLayers(); + const marker = layers.patch.markRange([[0, 0], [buffer.getLastRow(), Infinity]]); + + return { + patch: this.clone({hunks, status, marker}), + buffer, + layers, + }; } getFirstChangeRange() { @@ -397,15 +415,19 @@ class NullPatch { } class BufferBuilder { - constructor(original) { + constructor(original, nextLayeredBuffer) { this.originalBuffer = original; - this.buffer = new TextBuffer(); - this.buffer.retain(); - this.layers = new Map( - [Unchanged, Addition, Deletion, NoNewline, 'hunk'].map(key => { - return [key, this.buffer.addMarkerLayer()]; - }), - ); + + this.buffer = nextLayeredBuffer.buffer; + this.layers = new Map([ + [Unchanged, nextLayeredBuffer.layers.unchanged], + [Addition, nextLayeredBuffer.layers.addition], + [Deletion, nextLayeredBuffer.layers.deletion], + [NoNewline, nextLayeredBuffer.layers.noNewline], + ['hunk', nextLayeredBuffer.layers.hunk], + ['patch', nextLayeredBuffer.layers.patch], + ]); + this.offset = 0; this.hunkBufferText = ''; From d6d5ff717d8b65ffcd30809cf36037a5e50572a2 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 6 Nov 2018 10:25:28 -0500 Subject: [PATCH 140/284] Applyable MultiFilePatch strings through the magic of concatenation --- lib/models/patch/multi-file-patch.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/models/patch/multi-file-patch.js b/lib/models/patch/multi-file-patch.js index eb2b022e171..d406b35e445 100644 --- a/lib/models/patch/multi-file-patch.js +++ b/lib/models/patch/multi-file-patch.js @@ -180,4 +180,11 @@ export default class MultiFilePatch { return filePatches; } + + /* + * Construct an apply-able patch String. + */ + toString() { + return this.filePatches.map(fp => fp.toString()).join(''); + } } From a9b9a76795cb3b28dd8c1d1a1db98c41ecdb859a Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 6 Nov 2018 10:28:57 -0500 Subject: [PATCH 141/284] Use lifted MultiFilePatch methods in FilePatchView and FilePatchController --- lib/controllers/file-patch-controller.js | 10 +++++----- lib/views/file-patch-view.js | 25 ++++++++++++------------ 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/lib/controllers/file-patch-controller.js b/lib/controllers/file-patch-controller.js index acf3d354b5c..d6092f9a333 100644 --- a/lib/controllers/file-patch-controller.js +++ b/lib/controllers/file-patch-controller.js @@ -128,7 +128,7 @@ export default class FilePatchController extends React.Component { }); } - async toggleRows(filePatch, rowSet, nextSelectionMode) { + async toggleRows(rowSet, nextSelectionMode) { let chosenRows = rowSet; if (chosenRows) { await this.selectedRowsChanged(chosenRows, nextSelectionMode); @@ -142,8 +142,8 @@ export default class FilePatchController extends React.Component { return this.stagingOperation(() => { const patch = this.withStagingStatus({ - staged: () => filePatch.getUnstagePatchForLines(chosenRows), - unstaged: () => filePatch.getStagePatchForLines(chosenRows), + staged: () => this.props.multiFilePatch.getUnstagePatchForLines(chosenRows), + unstaged: () => this.props.multiFilePatch.getStagePatchForLines(chosenRows), }); return this.props.repository.applyPatchToIndex(patch); }); @@ -182,7 +182,7 @@ export default class FilePatchController extends React.Component { }); } - async discardRows(filePatch, rowSet, nextSelectionMode, {eventSource} = {}) { + async discardRows(rowSet, nextSelectionMode, {eventSource} = {}) { let chosenRows = rowSet; if (chosenRows) { await this.selectedRowsChanged(chosenRows, nextSelectionMode); @@ -197,7 +197,7 @@ export default class FilePatchController extends React.Component { eventSource, }); - return this.props.discardLines(filePatch, chosenRows, this.props.repository); + return this.props.discardLines(this.props.multiFilePatch, chosenRows, this.props.repository); } selectedRowsChanged(rows, nextSelectionMode) { diff --git a/lib/views/file-patch-view.js b/lib/views/file-patch-view.js index 6c992721ea7..38e4f9887de 100644 --- a/lib/views/file-patch-view.js +++ b/lib/views/file-patch-view.js @@ -597,16 +597,17 @@ export default class FilePatchView extends React.Component { discardSelectionFromCommand = () => { return this.props.discardRows( + this.props.multiFilePatch, this.props.selectedRows, this.props.selectionMode, {eventSource: {command: 'github:discard-selected-lines'}}, ); } - toggleHunkSelection(filePatch, hunk, containsSelection) { + toggleHunkSelection(hunk, containsSelection) { if (containsSelection) { return this.props.toggleRows( - filePatch, + this.props.multiFilePatch, this.props.selectedRows, this.props.selectionMode, {eventSource: 'button'}, @@ -619,14 +620,19 @@ export default class FilePatchView extends React.Component { return rows; }, []), ); - return this.props.toggleRows(filePatch, changeRows, 'hunk', {eventSource: 'button'}); + return this.props.toggleRows( + this.props.multiFilePatch, + changeRows, + 'hunk', + {eventSource: 'button'}, + ); } } - discardHunkSelection(filePatch, hunk, containsSelection) { + discardHunkSelection(hunk, containsSelection) { if (containsSelection) { return this.props.discardRows( - filePatch, + this.props.multiFilePatch, this.props.selectedRows, this.props.selectionMode, {eventSource: 'button'}, @@ -639,7 +645,7 @@ export default class FilePatchView extends React.Component { return rows; }, []), ); - return this.props.discardRows(filePatch, changeRows, 'hunk', {eventSource: 'button'}); + return this.props.discardRows(this.props.multiFilePatch, changeRows, 'hunk', {eventSource: 'button'}); } } @@ -784,12 +790,7 @@ export default class FilePatchView extends React.Component { } didConfirm() { - return Promise.all( - Array.from( - this.getSelectedFilePatches(), - filePatch => this.props.toggleRows(filePatch, this.props.selectedRows, this.props.selectionMode), - ), - ); + return this.props.toggleRows(this.props.multiFilePatch, this.props.selectedRows, this.props.selectionMode); } didToggleSelectionMode() { From 5bdd388c67d3beae38b2e29bc1a7887aba3bb117 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 6 Nov 2018 10:29:16 -0500 Subject: [PATCH 142/284] Hack to get discardLines() happy with a MultiFilePatch --- lib/controllers/root-controller.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/controllers/root-controller.js b/lib/controllers/root-controller.js index 5622941a053..db41139e7b6 100644 --- a/lib/controllers/root-controller.js +++ b/lib/controllers/root-controller.js @@ -677,17 +677,19 @@ export default class RootController extends React.Component { ); } - async discardLines(filePatch, lines, repository = this.props.repository) { - const filePath = filePatch.getPath(); + async discardLines(multiFilePatch, lines, repository = this.props.repository) { + const filePaths = multiFilePatch.getFilePatches().map(fp => fp.getPath()); const destructiveAction = async () => { - const discardFilePatch = filePatch.getUnstagePatchForLines(lines); + const discardFilePatch = multiFilePatch.getUnstagePatchForLines(lines); await repository.applyPatchToWorkdir(discardFilePatch); }; return await repository.storeBeforeAndAfterBlobs( - [filePath], - () => this.ensureNoUnsavedFiles([filePath], 'Cannot discard lines.', repository.getWorkingDirectoryPath()), + [filePaths], + () => this.ensureNoUnsavedFiles(filePaths, 'Cannot discard lines.', repository.getWorkingDirectoryPath()), destructiveAction, - filePath, + // FIXME: Present::storeBeforeAndAfterBlobs() and DiscardHistory::storeBeforeAndAfterBlobs() need a way to store + // multiple partial paths + filePaths[0], ); } From d0a6e362864f7323b26eb3cd2e907e1a1024b930 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 6 Nov 2018 12:57:54 -0500 Subject: [PATCH 143/284] WIP Patch tests --- lib/models/patch/patch.js | 8 ----- test/models/patch/patch.test.js | 56 ++++++++++++++++----------------- 2 files changed, 28 insertions(+), 36 deletions(-) diff --git a/lib/models/patch/patch.js b/lib/models/patch/patch.js index 1c3f380b258..7416215839a 100644 --- a/lib/models/patch/patch.js +++ b/lib/models/patch/patch.js @@ -33,14 +33,6 @@ export default class Patch { return this.hunks; } - getBuffer() { - return this.buffer; - } - - getByteSize() { - return Buffer.byteLength(this.buffer.getText(), 'utf8'); - } - getChangedLineCount() { return this.changedLineCount; } diff --git a/test/models/patch/patch.test.js b/test/models/patch/patch.test.js index e20ebcc512f..b412df6c45a 100644 --- a/test/models/patch/patch.test.js +++ b/test/models/patch/patch.test.js @@ -9,23 +9,11 @@ describe('Patch', function() { it('has some standard accessors', function() { const buffer = new TextBuffer({text: 'bufferText'}); const layers = buildLayers(buffer); - const p = new Patch({status: 'modified', hunks: [], buffer, layers}); + const marker = markRange(layers.patch, 0, Infinity); + const p = new Patch({status: 'modified', hunks: [], marker}); assert.strictEqual(p.getStatus(), 'modified'); assert.deepEqual(p.getHunks(), []); - assert.strictEqual(p.getBuffer().getText(), 'bufferText'); assert.isTrue(p.isPresent()); - - assert.strictEqual(p.getUnchangedLayer().getMarkerCount(), 0); - assert.strictEqual(p.getAdditionLayer().getMarkerCount(), 0); - assert.strictEqual(p.getDeletionLayer().getMarkerCount(), 0); - assert.strictEqual(p.getNoNewlineLayer().getMarkerCount(), 0); - }); - - it('computes the byte size of the total patch data', function() { - const buffer = new TextBuffer({text: '\u00bd + \u00bc = \u00be'}); - const layers = buildLayers(buffer); - const p = new Patch({status: 'modified', hunks: [], buffer, layers}); - assert.strictEqual(p.getByteSize(), 12); }); it('computes the total changed line count', function() { @@ -58,7 +46,9 @@ describe('Patch', function() { ], }), ]; - const p = new Patch({status: 'modified', hunks, buffer, layers}); + const marker = markRange(layers.patch, 0, Infinity); + + const p = new Patch({status: 'modified', hunks, marker}); assert.strictEqual(p.getChangedLineCount(), 10); }); @@ -93,34 +83,37 @@ describe('Patch', function() { it('clones itself with optionally overridden properties', function() { const buffer = new TextBuffer({text: 'bufferText'}); const layers = buildLayers(buffer); - const original = new Patch({status: 'modified', hunks: [], buffer, layers}); + const marker = markRange(layers.patch, 0, Infinity); + + const original = new Patch({status: 'modified', hunks: [], marker}); const dup0 = original.clone(); assert.notStrictEqual(dup0, original); assert.strictEqual(dup0.getStatus(), 'modified'); assert.deepEqual(dup0.getHunks(), []); - assert.strictEqual(dup0.getBuffer().getText(), 'bufferText'); + assert.strictEqual(dup0.getMarker(), marker); const dup1 = original.clone({status: 'added'}); assert.notStrictEqual(dup1, original); assert.strictEqual(dup1.getStatus(), 'added'); assert.deepEqual(dup1.getHunks(), []); - assert.strictEqual(dup1.getBuffer().getText(), 'bufferText'); + assert.strictEqual(dup0.getMarker(), marker); const hunks = [new Hunk({regions: []})]; const dup2 = original.clone({hunks}); assert.notStrictEqual(dup2, original); assert.strictEqual(dup2.getStatus(), 'modified'); assert.deepEqual(dup2.getHunks(), hunks); - assert.strictEqual(dup2.getBuffer().getText(), 'bufferText'); + assert.strictEqual(dup0.getMarker(), marker); const nBuffer = new TextBuffer({text: 'changed'}); const nLayers = buildLayers(nBuffer); - const dup3 = original.clone({buffer: nBuffer, layers: nLayers}); + const nMarker = markRange(nLayers.patch, 0, Infinity); + const dup3 = original.clone({marker: nMarker}); assert.notStrictEqual(dup3, original); assert.strictEqual(dup3.getStatus(), 'modified'); assert.deepEqual(dup3.getHunks(), []); - assert.strictEqual(dup3.getBuffer().getText(), 'changed'); + assert.strictEqual(dup3.getMarker(), nMarker); }); it('clones a nullPatch as a nullPatch', function() { @@ -135,22 +128,23 @@ describe('Patch', function() { assert.notStrictEqual(dup0, nullPatch); assert.strictEqual(dup0.getStatus(), 'added'); assert.deepEqual(dup0.getHunks(), []); - assert.strictEqual(dup0.getBuffer().getText(), ''); + assert.deepEqual(dup0.getMarker().getRange().serialize(), [[0, 0], [0, 0]]); const hunks = [new Hunk({regions: []})]; const dup1 = nullPatch.clone({hunks}); assert.notStrictEqual(dup1, nullPatch); assert.isNull(dup1.getStatus()); assert.deepEqual(dup1.getHunks(), hunks); - assert.strictEqual(dup1.getBuffer().getText(), ''); + assert.deepEqual(dup0.getMarker().getRange().serialize(), [[0, 0], [0, 0]]); const nBuffer = new TextBuffer({text: 'changed'}); const nLayers = buildLayers(nBuffer); - const dup2 = nullPatch.clone({buffer: nBuffer, layers: nLayers}); + const nMarker = markRange(nLayers.patch, 0, Infinity); + const dup2 = nullPatch.clone({marker: nMarker}); assert.notStrictEqual(dup2, nullPatch); assert.isNull(dup2.getStatus()); assert.deepEqual(dup2.getHunks(), []); - assert.strictEqual(dup2.getBuffer().getText(), 'changed'); + assert.strictEqual(dup2.getMarker(), nMarker); }); describe('stage patch generation', function() { @@ -801,8 +795,7 @@ describe('Patch', function() { const nullPatch = Patch.createNull(); assert.isNull(nullPatch.getStatus()); assert.deepEqual(nullPatch.getHunks(), []); - assert.strictEqual(nullPatch.getBuffer().getText(), ''); - assert.strictEqual(nullPatch.getByteSize(), 0); + assert.deepEqual(nullPatch.getMarker().getRange().serialize(), [[0, 0], [0, 0]]); assert.isFalse(nullPatch.isPresent()); assert.strictEqual(nullPatch.toString(), ''); assert.strictEqual(nullPatch.getChangedLineCount(), 0); @@ -832,6 +825,7 @@ function buildBuffer(lines, noNewline = false) { function buildLayers(buffer) { return { + patch: buffer.addMarkerLayer(), hunk: buffer.addMarkerLayer(), unchanged: buffer.addMarkerLayer(), addition: buffer.addMarkerLayer(), @@ -895,6 +889,12 @@ function buildPatchFixture() { ], }), ]; + const marker = markRange(layers.patch, 0, Infinity); - return new Patch({status: 'modified', hunks, buffer, layers}); + return { + patch: new Patch({status: 'modified', hunks, marker}), + buffer, + layers, + marker, + }; } From bcf8e8c825313a7f630d16cdae13e647ae2d57f9 Mon Sep 17 00:00:00 2001 From: Vanessa Yuen Date: Tue, 6 Nov 2018 22:12:56 +0100 Subject: [PATCH 144/284] use FilePatchController instead of MultiFilePatchController --- lib/containers/changed-file-container.js | 6 ++---- lib/containers/commit-preview-container.js | 6 +++--- lib/models/patch/file-patch.js | 12 ++++++------ 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/lib/containers/changed-file-container.js b/lib/containers/changed-file-container.js index 6ee00e7df33..da4d8193829 100644 --- a/lib/containers/changed-file-container.js +++ b/lib/containers/changed-file-container.js @@ -5,7 +5,7 @@ import yubikiri from 'yubikiri'; import {autobind} from '../helpers'; import ObserveModel from '../views/observe-model'; import LoadingView from '../views/loading-view'; -import MultiFilePatchController from '../controllers/multi-file-patch-controller'; +import FilePatchController from '../controllers/file-patch-controller'; export default class ChangedFileContainer extends React.Component { static propTypes = { @@ -53,11 +53,9 @@ export default class ChangedFileContainer extends React.Component { } return ( - ); diff --git a/lib/containers/commit-preview-container.js b/lib/containers/commit-preview-container.js index 877fa76cfbe..abc17390708 100644 --- a/lib/containers/commit-preview-container.js +++ b/lib/containers/commit-preview-container.js @@ -4,7 +4,7 @@ import yubikiri from 'yubikiri'; import ObserveModel from '../views/observe-model'; import LoadingView from '../views/loading-view'; -import MultiFilePatchController from '../controllers/multi-file-patch-controller'; +import FilePatchController from '../controllers/file-patch-controller'; export default class CommitPreviewContainer extends React.Component { static propTypes = { @@ -31,8 +31,8 @@ export default class CommitPreviewContainer extends React.Component { } return ( - diff --git a/lib/models/patch/file-patch.js b/lib/models/patch/file-patch.js index 494d663982f..7d7199f0bc2 100644 --- a/lib/models/patch/file-patch.js +++ b/lib/models/patch/file-patch.js @@ -12,12 +12,12 @@ export default class FilePatch { this.oldFile = oldFile; this.newFile = newFile; this.patch = patch; - const metricsData = {package: 'github'}; - if (this.getPatch()) { - metricsData.sizeInBytes = this.getByteSize(); - } - - addEvent('file-patch-constructed', metricsData); + // const metricsData = {package: 'github'}; + // if (this.getPatch()) { + // metricsData.sizeInBytes = this.getByteSize(); + // } + // + // addEvent('file-patch-constructed', metricsData); } isPresent() { From 74d859371af89153a5ed507470c47ac55c4884b2 Mon Sep 17 00:00:00 2001 From: Vanessa Yuen Date: Tue, 6 Nov 2018 22:13:23 +0100 Subject: [PATCH 145/284] get rid of anything to do with active states --- lib/controllers/file-patch-controller.js | 1 - lib/views/file-patch-view.js | 10 ++-------- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/lib/controllers/file-patch-controller.js b/lib/controllers/file-patch-controller.js index d6092f9a333..7625ed485d5 100644 --- a/lib/controllers/file-patch-controller.js +++ b/lib/controllers/file-patch-controller.js @@ -26,7 +26,6 @@ export default class FilePatchController extends React.Component { undoLastDiscard: PropTypes.func, surfaceFileAtPath: PropTypes.func, handleClick: PropTypes.func, - isActive: PropTypes.bool, } constructor(props) { diff --git a/lib/views/file-patch-view.js b/lib/views/file-patch-view.js index 38e4f9887de..3f6bdc9885c 100644 --- a/lib/views/file-patch-view.js +++ b/lib/views/file-patch-view.js @@ -36,7 +36,6 @@ export default class FilePatchView extends React.Component { repository: PropTypes.object.isRequired, hasUndoHistory: PropTypes.bool, useEditorAutoHeight: PropTypes.bool, - isActive: PropTypes.bool.isRequired, workspace: PropTypes.object.isRequired, commands: PropTypes.object.isRequired, @@ -70,7 +69,7 @@ export default class FilePatchView extends React.Component { 'didMouseDownOnHeader', 'didMouseDownOnLineNumber', 'didMouseMoveOnLineNumber', 'didMouseUp', 'didConfirm', 'didToggleSelectionMode', 'selectNextHunk', 'selectPreviousHunk', 'didOpenFile', 'didAddSelection', 'didChangeSelectionRange', 'didDestroySelection', - 'oldLineNumberLabel', 'newLineNumberLabel', 'handleMouseDown', + 'oldLineNumberLabel', 'newLineNumberLabel', ); this.mouseSelectionInProgress = false; @@ -176,8 +175,6 @@ export default class FilePatchView extends React.Component { `github-FilePatchView--${this.props.stagingStatus}`, {'github-FilePatchView--blank': !this.props.multiFilePatch.anyPresent()}, {'github-FilePatchView--hunkMode': this.props.selectionMode === 'hunk'}, - {'github-FilePatchView--active': this.props.isActive}, - {'github-FilePatchView--inactive': !this.props.isActive}, ); return ( @@ -461,7 +458,7 @@ export default class FilePatchView extends React.Component { {filePatch.getHunks().map((hunk, index) => { const containsSelection = this.props.selectionMode === 'line' && selectedHunks.has(hunk); - const isSelected = this.props.isActive && (this.props.selectionMode === 'hunk') && selectedHunks.has(hunk); + const isSelected = (this.props.selectionMode === 'hunk') && selectedHunks.has(hunk); let buttonSuffix = ''; if (containsSelection) { @@ -514,9 +511,6 @@ export default class FilePatchView extends React.Component { if (ranges.length === 0) { return null; } - if (!this.props.isActive) { - return null; - } const holder = refHolder || new RefHolder(); return ( From ae3e09875fda0315ba4c07e210d46f7d848a6302 Mon Sep 17 00:00:00 2001 From: Vanessa Yuen Date: Tue, 6 Nov 2018 22:25:51 +0100 Subject: [PATCH 146/284] rename: - `FilePatchController` to `MultiFilePatchController` - `FilePatchView` to `MultiFilePatchView` and getting rid of the old `MultiFilePatchController` --- lib/containers/changed-file-container.js | 4 +- lib/containers/commit-preview-container.js | 4 +- lib/controllers/file-patch-controller.js | 232 --------- .../multi-file-patch-controller.js | 244 +++++++-- lib/helpers.js | 4 +- ...patch-view.js => multi-file-patch-view.js} | 2 +- .../controllers/file-patch-controller.test.js | 441 ---------------- .../multi-file-patch-controller.test.js | 486 +++++++++++++++--- test/views/file-patch-view.test.js | 6 +- 9 files changed, 639 insertions(+), 784 deletions(-) delete mode 100644 lib/controllers/file-patch-controller.js rename lib/views/{file-patch-view.js => multi-file-patch-view.js} (99%) delete mode 100644 test/controllers/file-patch-controller.test.js diff --git a/lib/containers/changed-file-container.js b/lib/containers/changed-file-container.js index da4d8193829..30c9a245e4d 100644 --- a/lib/containers/changed-file-container.js +++ b/lib/containers/changed-file-container.js @@ -5,7 +5,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'; +import MultiFilePatchController from '../controllers/multi-file-patch-controller'; export default class ChangedFileContainer extends React.Component { static propTypes = { @@ -53,7 +53,7 @@ export default class ChangedFileContainer extends React.Component { } return ( - { - this.resolvePatchChangePromise = resolve; - }); - } - - componentDidUpdate(prevProps) { - if (prevProps.multiFilePatch !== this.props.multiFilePatch) { - this.resolvePatchChangePromise(); - this.patchChangePromise = new Promise(resolve => { - this.resolvePatchChangePromise = resolve; - }); - } - } - - render() { - return ( - - ); - } - - undoLastDiscard(filePatch, {eventSource} = {}) { - addEvent('undo-last-discard', { - package: 'github', - component: 'FilePatchController', - eventSource, - }); - - return this.props.undoLastDiscard(filePatch.getPath(), this.props.repository); - } - - diveIntoMirrorPatch(filePatch) { - const mirrorStatus = this.withStagingStatus({staged: 'unstaged', unstaged: 'staged'}); - const workingDirectory = this.props.repository.getWorkingDirectoryPath(); - const uri = ChangedFileItem.buildURI(filePatch.getPath(), workingDirectory, mirrorStatus); - - this.props.destroy(); - return this.props.workspace.open(uri); - } - - surfaceFile(filePatch) { - return this.props.surfaceFileAtPath(filePatch.getPath(), this.props.stagingStatus); - } - - async openFile(filePatch, positions) { - const absolutePath = path.join(this.props.repository.getWorkingDirectoryPath(), filePatch.getPath()); - const editor = await this.props.workspace.open(absolutePath, {pending: true}); - if (positions.length > 0) { - editor.setCursorBufferPosition(positions[0], {autoscroll: false}); - for (const position of positions.slice(1)) { - editor.addCursorAtBufferPosition(position); - } - editor.scrollToBufferPosition(positions[positions.length - 1], {center: true}); - } - return editor; - } - - toggleFile(filePatch) { - return this.stagingOperation(() => { - const methodName = this.withStagingStatus({staged: 'unstageFiles', unstaged: 'stageFiles'}); - return this.props.repository[methodName]([filePatch.getPath()]); - }); - } - - 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.multiFilePatch.getUnstagePatchForLines(chosenRows), - unstaged: () => this.props.multiFilePatch.getStagePatchForLines(chosenRows), - }); - return this.props.repository.applyPatchToIndex(patch); - }); - } - - toggleModeChange(filePatch) { - return this.stagingOperation(() => { - const targetMode = this.withStagingStatus({ - unstaged: filePatch.getNewMode(), - staged: filePatch.getOldMode(), - }); - return this.props.repository.stageFileModeChange(filePatch.getPath(), targetMode); - }); - } - - toggleSymlinkChange(filePatch) { - return this.stagingOperation(() => { - const relPath = filePatch.getPath(); - const repository = this.props.repository; - 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]); - }, - }); - }); - } - - async discardRows(rowSet, nextSelectionMode, {eventSource} = {}) { - let chosenRows = rowSet; - if (chosenRows) { - await this.selectedRowsChanged(chosenRows, nextSelectionMode); - } else { - chosenRows = this.state.selectedRows; - } - - addEvent('discard-unstaged-changes', { - package: 'github', - component: 'FilePatchController', - lineCount: chosenRows.size, - eventSource, - }); - - return this.props.discardLines(this.props.multiFilePatch, chosenRows, this.props.repository); - } - - selectedRowsChanged(rows, nextSelectionMode) { - if (equalSets(this.state.selectedRows, rows) && this.state.selectionMode === nextSelectionMode) { - return Promise.resolve(); - } - - return new Promise(resolve => { - this.setState({selectedRows: rows, selectionMode: nextSelectionMode}, resolve); - }); - } - - withStagingStatus(callbacks) { - const callback = callbacks[this.props.stagingStatus]; - /* istanbul ignore if */ - if (!callback) { - throw new Error(`Unknown staging status: ${this.props.stagingStatus}`); - } - 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/multi-file-patch-controller.js b/lib/controllers/multi-file-patch-controller.js index a1ad50a2512..ebcd8171abf 100644 --- a/lib/controllers/multi-file-patch-controller.js +++ b/lib/controllers/multi-file-patch-controller.js @@ -1,52 +1,232 @@ -import React, {Fragment} from 'react'; +import React from 'react'; +import PropTypes from 'prop-types'; +import path from 'path'; -import {MultiFilePatchPropType, RefHolderPropType} from '../prop-types'; -import FilePatchController from '../controllers/file-patch-controller'; -import {autobind} from '../helpers'; +import {autobind, equalSets} from '../helpers'; +import {addEvent} from '../reporter-proxy'; +import {MultiFilePatchPropType} from '../prop-types'; +import ChangedFileItem from '../items/changed-file-item'; +import MultiFilePatchView from '../views/multi-file-patch-view'; export default class MultiFilePatchController extends React.Component { static propTypes = { + repository: PropTypes.object.isRequired, + stagingStatus: PropTypes.oneOf(['staged', 'unstaged']), multiFilePatch: MultiFilePatchPropType.isRequired, - refInitialFocus: RefHolderPropType, + hasUndoHistory: PropTypes.bool, + + workspace: PropTypes.object.isRequired, + commands: PropTypes.object.isRequired, + keymaps: PropTypes.object.isRequired, + tooltips: PropTypes.object.isRequired, + config: PropTypes.object.isRequired, + + destroy: PropTypes.func.isRequired, + discardLines: PropTypes.func, + undoLastDiscard: PropTypes.func, + surfaceFileAtPath: PropTypes.func, + handleClick: PropTypes.func, } constructor(props) { super(props); - autobind(this, 'handleMouseDown'); - const firstFilePatch = this.props.multiFilePatch.getFilePatches()[0]; + autobind( + this, + 'selectedRowsChanged', + 'undoLastDiscard', 'diveIntoMirrorPatch', 'surfaceFile', 'openFile', + 'toggleFile', 'toggleRows', 'toggleModeChange', 'toggleSymlinkChange', 'discardRows', + ); - this.state = {activeFilePatch: firstFilePatch ? firstFilePatch.getPath() : null}; + this.state = { + lastMultiFilePatch: this.props.multiFilePatch, + selectionMode: 'hunk', + selectedRows: new Set(), + }; + + this.mouseSelectionInProgress = false; + this.stagingOperationInProgress = false; + + this.patchChangePromise = new Promise(resolve => { + this.resolvePatchChangePromise = resolve; + }); } - handleMouseDown(relPath) { - this.setState({activeFilePatch: relPath}); + componentDidUpdate(prevProps) { + if (prevProps.multiFilePatch !== this.props.multiFilePatch) { + this.resolvePatchChangePromise(); + this.patchChangePromise = new Promise(resolve => { + this.resolvePatchChangePromise = resolve; + }); + } } render() { return ( - - {this.props.multiFilePatch.getFilePatches().map(filePatch => { - const relPath = filePatch.getPath(); - const isActive = this.state.activeFilePatch === relPath; - let props = this.props; - if (!isActive) { - props = {...props}; - delete props.refInitialFocus; - } + - ); - })} - + selectedRows={this.state.selectedRows} + selectionMode={this.state.selectionMode} + selectedRowsChanged={this.selectedRowsChanged} + + diveIntoMirrorPatch={this.diveIntoMirrorPatch} + surfaceFile={this.surfaceFile} + openFile={this.openFile} + toggleFile={this.toggleFile} + toggleRows={this.toggleRows} + toggleModeChange={this.toggleModeChange} + toggleSymlinkChange={this.toggleSymlinkChange} + undoLastDiscard={this.undoLastDiscard} + discardRows={this.discardRows} + selectNextHunk={this.selectNextHunk} + selectPreviousHunk={this.selectPreviousHunk} + /> ); } + + undoLastDiscard(filePatch, {eventSource} = {}) { + addEvent('undo-last-discard', { + package: 'github', + component: 'FilePatchController', + eventSource, + }); + + return this.props.undoLastDiscard(filePatch.getPath(), this.props.repository); + } + + diveIntoMirrorPatch(filePatch) { + const mirrorStatus = this.withStagingStatus({staged: 'unstaged', unstaged: 'staged'}); + const workingDirectory = this.props.repository.getWorkingDirectoryPath(); + const uri = ChangedFileItem.buildURI(filePatch.getPath(), workingDirectory, mirrorStatus); + + this.props.destroy(); + return this.props.workspace.open(uri); + } + + surfaceFile(filePatch) { + return this.props.surfaceFileAtPath(filePatch.getPath(), this.props.stagingStatus); + } + + async openFile(filePatch, positions) { + const absolutePath = path.join(this.props.repository.getWorkingDirectoryPath(), filePatch.getPath()); + const editor = await this.props.workspace.open(absolutePath, {pending: true}); + if (positions.length > 0) { + editor.setCursorBufferPosition(positions[0], {autoscroll: false}); + for (const position of positions.slice(1)) { + editor.addCursorAtBufferPosition(position); + } + editor.scrollToBufferPosition(positions[positions.length - 1], {center: true}); + } + return editor; + } + + toggleFile(filePatch) { + return this.stagingOperation(() => { + const methodName = this.withStagingStatus({staged: 'unstageFiles', unstaged: 'stageFiles'}); + return this.props.repository[methodName]([filePatch.getPath()]); + }); + } + + 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.multiFilePatch.getUnstagePatchForLines(chosenRows), + unstaged: () => this.props.multiFilePatch.getStagePatchForLines(chosenRows), + }); + return this.props.repository.applyPatchToIndex(patch); + }); + } + + toggleModeChange(filePatch) { + return this.stagingOperation(() => { + const targetMode = this.withStagingStatus({ + unstaged: filePatch.getNewMode(), + staged: filePatch.getOldMode(), + }); + return this.props.repository.stageFileModeChange(filePatch.getPath(), targetMode); + }); + } + + toggleSymlinkChange(filePatch) { + return this.stagingOperation(() => { + const relPath = filePatch.getPath(); + const repository = this.props.repository; + 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]); + }, + }); + }); + } + + async discardRows(rowSet, nextSelectionMode, {eventSource} = {}) { + let chosenRows = rowSet; + if (chosenRows) { + await this.selectedRowsChanged(chosenRows, nextSelectionMode); + } else { + chosenRows = this.state.selectedRows; + } + + addEvent('discard-unstaged-changes', { + package: 'github', + component: 'FilePatchController', + lineCount: chosenRows.size, + eventSource, + }); + + return this.props.discardLines(this.props.multiFilePatch, chosenRows, this.props.repository); + } + + selectedRowsChanged(rows, nextSelectionMode) { + if (equalSets(this.state.selectedRows, rows) && this.state.selectionMode === nextSelectionMode) { + return Promise.resolve(); + } + + return new Promise(resolve => { + this.setState({selectedRows: rows, selectionMode: nextSelectionMode}, resolve); + }); + } + + withStagingStatus(callbacks) { + const callback = callbacks[this.props.stagingStatus]; + /* istanbul ignore if */ + if (!callback) { + throw new Error(`Unknown staging status: ${this.props.stagingStatus}`); + } + 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/helpers.js b/lib/helpers.js index 37f1854de12..5acc2f6cd5c 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -3,7 +3,7 @@ import fs from 'fs-extra'; import os from 'os'; import temp from 'temp'; -import FilePatchController from './controllers/file-patch-controller'; +import MultiFilePatchController from './controllers/multi-file-patch-controller'; import RefHolder from './models/ref-holder'; export const LINE_ENDING_REGEX = /\r?\n/; @@ -374,7 +374,7 @@ export function getCommitMessageEditors(repository, workspace) { export function getFilePatchPaneItems({onlyStaged, empty} = {}, workspace) { return workspace.getPaneItems().filter(item => { - const isFilePatchItem = item && item.getRealItem && item.getRealItem() instanceof FilePatchController; + const isFilePatchItem = item && item.getRealItem && item.getRealItem() instanceof MultiFilePatchController; if (onlyStaged) { return isFilePatchItem && item.stagingStatus === 'staged'; } else if (empty) { diff --git a/lib/views/file-patch-view.js b/lib/views/multi-file-patch-view.js similarity index 99% rename from lib/views/file-patch-view.js rename to lib/views/multi-file-patch-view.js index 3f6bdc9885c..670acf49c80 100644 --- a/lib/views/file-patch-view.js +++ b/lib/views/multi-file-patch-view.js @@ -26,7 +26,7 @@ const NBSP_CHARACTER = '\u00a0'; const BLANK_LABEL = () => NBSP_CHARACTER; -export default class FilePatchView extends React.Component { +export default class MultiFilePatchView extends React.Component { static propTypes = { stagingStatus: PropTypes.oneOf(['staged', 'unstaged']).isRequired, isPartiallyStaged: PropTypes.bool, diff --git a/test/controllers/file-patch-controller.test.js b/test/controllers/file-patch-controller.test.js deleted file mode 100644 index b5177297e01..00000000000 --- a/test/controllers/file-patch-controller.test.js +++ /dev/null @@ -1,441 +0,0 @@ -import path from 'path'; -import fs from 'fs-extra'; -import React from 'react'; -import {shallow} from 'enzyme'; - -import FilePatchController from '../../lib/controllers/file-patch-controller'; -import * as reporterProxy from '../../lib/reporter-proxy'; -import {cloneRepository, buildRepository} from '../helpers'; - -describe('FilePatchController', 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'), '00\n01\n02\n03\n04\n05\n06'); - - filePatch = await repository.getFilePatchForPath('a.txt', {staged: false}); - }); - - afterEach(function() { - atomEnv.destroy(); - }); - - function buildApp(overrideProps = {}) { - const props = { - repository, - stagingStatus: 'unstaged', - relPath: 'a.txt', - isPartiallyStaged: false, - filePatch, - hasUndoHistory: false, - workspace: atomEnv.workspace, - commands: atomEnv.commands, - keymaps: atomEnv.keymaps, - tooltips: atomEnv.tooltips, - config: atomEnv.config, - destroy: () => {}, - discardLines: () => {}, - undoLastDiscard: () => {}, - surfaceFileAtPath: () => {}, - ...overrideProps, - }; - - return ; - } - - it('passes extra props to the FilePatchView', function() { - const extra = Symbol('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)); - }); - - it('calls surfaceFileAtPath with set arguments', function() { - const surfaceFileAtPath = sinon.spy(); - const wrapper = shallow(buildApp({relPath: 'c.txt', surfaceFileAtPath})); - wrapper.find('FilePatchView').prop('surfaceFile')(); - - assert.isTrue(surfaceFileAtPath.calledWith('c.txt', 'unstaged')); - }); - - 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 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'), 'hunk'); - - wrapper.find('FilePatchView').prop('selectedRowsChanged')(new Set([1, 2]), 'line'); - assert.sameMembers(Array.from(wrapper.find('FilePatchView').prop('selectedRows')), [1, 2]); - assert.strictEqual(wrapper.find('FilePatchView').prop('selectionMode'), 'line'); - }); - - 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')), []); - assert.strictEqual(wrapper.find('FilePatchView').prop('selectionMode'), 'hunk'); - - sinon.spy(wrapper.instance(), 'render'); - - 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('discardLines()', function() { - it('records an event', async function() { - const wrapper = shallow(buildApp()); - sinon.stub(reporterProxy, 'addEvent'); - await wrapper.find('FilePatchView').prop('discardRows')(new Set([1, 2])); - assert.isTrue(reporterProxy.addEvent.calledWith('discard-unstaged-changes', { - package: 'github', - component: 'FilePatchController', - lineCount: 2, - eventSource: undefined, - })); - }); - }); - - describe('undoLastDiscard()', function() { - it('records an event', function() { - const wrapper = shallow(buildApp()); - sinon.stub(reporterProxy, 'addEvent'); - wrapper.find('FilePatchView').prop('undoLastDiscard')(); - assert.isTrue(reporterProxy.addEvent.calledWith('undo-last-discard', { - package: 'github', - component: 'FilePatchController', - eventSource: undefined, - })); - }); - }); - }); - - describe('toggleRows()', function() { - it('is a no-op with no selected rows', async function() { - const wrapper = shallow(buildApp()); - - 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.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() { - 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.sameMembers(Array.from(otherPatch.getUnstagePatchForLines.lastCall.args[0]), [2]); - assert.isTrue(repository.applyPatchToIndex.calledWith(otherPatch.getUnstagePatchForLines.returnValues[0])); - }); - }); - - 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'})); - - 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', 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])); - - 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.sameMembers(Array.from(wrapper.find('FilePatchView').prop('selectedRows')), [4, 5]); - assert.strictEqual(wrapper.find('FilePatchView').prop('selectionMode'), 'hunk'); - }); -}); diff --git a/test/controllers/multi-file-patch-controller.test.js b/test/controllers/multi-file-patch-controller.test.js index 858932cab78..7079614ddff 100644 --- a/test/controllers/multi-file-patch-controller.test.js +++ b/test/controllers/multi-file-patch-controller.test.js @@ -1,93 +1,441 @@ +import path from 'path'; +import fs from 'fs-extra'; import React from 'react'; import {shallow} from 'enzyme'; import MultiFilePatchController from '../../lib/controllers/multi-file-patch-controller'; -import {buildMultiFilePatch} from '../../lib/models/patch'; -import RefHolder from '../../lib/models/ref-holder'; - -describe('MultiFilePatchController', function() { - let multiFilePatch; - - beforeEach(function() { - multiFilePatch = buildMultiFilePatch([ - { - oldPath: 'first', oldMode: '100644', newPath: 'first', newMode: '100755', status: 'modified', - hunks: [ - { - oldStartLine: 1, oldLineCount: 2, newStartLine: 1, newLineCount: 4, - lines: [' line-0', '+line-1', '+line-2', ' line-3'], - }, - ], - }, - { - oldPath: 'second', oldMode: '100644', newPath: 'second', newMode: '100644', status: 'modified', - hunks: [ - { - oldStartLine: 5, oldLineCount: 3, newStartLine: 5, newLineCount: 3, - lines: [' line-5', '+line-6', '-line-7', ' line-8'], - }, - ], - }, - { - oldPath: 'third', oldMode: '100755', newPath: 'third', newMode: '100755', status: 'added', - hunks: [ - { - oldStartLine: 1, oldLineCount: 0, newStartLine: 1, newLineCount: 3, - lines: ['+line-0', '+line-1', '+line-2'], - }, - ], - }, - ]); +import * as reporterProxy from '../../lib/reporter-proxy'; +import {cloneRepository, buildRepository} from '../helpers'; + +describe.only('MultiFilePatchController', 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'), '00\n01\n02\n03\n04\n05\n06'); + + filePatch = await repository.getFilePatchForPath('a.txt', {staged: false}); + }); + + afterEach(function() { + atomEnv.destroy(); }); - function buildApp(override = {}) { + function buildApp(overrideProps = {}) { const props = { - multiFilePatch, - ...override, + repository, + stagingStatus: 'unstaged', + relPath: 'a.txt', + isPartiallyStaged: false, + filePatch, + hasUndoHistory: false, + workspace: atomEnv.workspace, + commands: atomEnv.commands, + keymaps: atomEnv.keymaps, + tooltips: atomEnv.tooltips, + config: atomEnv.config, + destroy: () => {}, + discardLines: () => {}, + undoLastDiscard: () => {}, + surfaceFileAtPath: () => {}, + ...overrideProps, }; return ; } - it('renders a FilePatchController for each file patch', function() { - const wrapper = shallow(buildApp()); + it('passes extra props to the FilePatchView', function() { + const extra = Symbol('extra'); + const wrapper = shallow(buildApp({extra})); + + assert.strictEqual(wrapper.find('MultiFilePatchView').prop('extra'), extra); + }); - assert.lengthOf(wrapper.find('FilePatchController'), 3); + it('calls undoLastDiscard through with set arguments', function() { + const undoLastDiscard = sinon.spy(); + const wrapper = shallow(buildApp({relPath: 'b.txt', undoLastDiscard})); + wrapper.find('MultiFilePatchView').prop('undoLastDiscard')(); - // O(n^2) doesn't matter when n is small :stars: - assert.isTrue( - multiFilePatch.getFilePatches().every(fp => { - return wrapper - .find('FilePatchController') - .someWhere(w => w.prop('filePatch') === fp); - }), - ); + assert.isTrue(undoLastDiscard.calledWith('b.txt', repository)); }); - it('passes additional props to each controller', function() { - const extra = Symbol('hooray'); - const wrapper = shallow(buildApp({extra})); + it('calls surfaceFileAtPath with set arguments', function() { + const surfaceFileAtPath = sinon.spy(); + const wrapper = shallow(buildApp({relPath: 'c.txt', surfaceFileAtPath})); + wrapper.find('MultiFilePatchView').prop('surfaceFile')(); + + assert.isTrue(surfaceFileAtPath.calledWith('c.txt', 'unstaged')); + }); + + 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('MultiFilePatchView').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('MultiFilePatchView').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('MultiFilePatchView').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('MultiFilePatchView').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('MultiFilePatchView').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('MultiFilePatchView').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('MultiFilePatchView').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('MultiFilePatchView').prop('toggleFile')(), 'staged'); + + wrapper.setProps({stagingStatus: 'staged'}); + assert.isNull(await wrapper.find('MultiFilePatchView').prop('toggleFile')()); + + const promise = wrapper.instance().patchChangePromise; + wrapper.setProps({filePatch: filePatch.clone()}); + await promise; + + assert.strictEqual(await wrapper.find('MultiFilePatchView').prop('toggleFile')(), 'unstaged'); + }); + }); + + 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('MultiFilePatchView').prop('selectedRows')), []); + assert.strictEqual(wrapper.find('MultiFilePatchView').prop('selectionMode'), 'hunk'); + + wrapper.find('MultiFilePatchView').prop('selectedRowsChanged')(new Set([1, 2]), 'line'); + assert.sameMembers(Array.from(wrapper.find('MultiFilePatchView').prop('selectedRows')), [1, 2]); + assert.strictEqual(wrapper.find('MultiFilePatchView').prop('selectionMode'), 'line'); + }); + + 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('MultiFilePatchView').prop('selectedRows')), []); + assert.strictEqual(wrapper.find('MultiFilePatchView').prop('selectionMode'), 'hunk'); + + sinon.spy(wrapper.instance(), 'render'); + + wrapper.find('MultiFilePatchView').prop('selectedRowsChanged')(new Set([1, 2]), 'line'); + + assert.isTrue(wrapper.instance().render.called); + assert.sameMembers(Array.from(wrapper.find('MultiFilePatchView').prop('selectedRows')), [1, 2]); + assert.strictEqual(wrapper.find('MultiFilePatchView').prop('selectionMode'), 'line'); + + wrapper.instance().render.resetHistory(); + wrapper.find('MultiFilePatchView').prop('selectedRowsChanged')(new Set([2, 1]), 'line'); + + assert.sameMembers(Array.from(wrapper.find('MultiFilePatchView').prop('selectedRows')), [1, 2]); + assert.strictEqual(wrapper.find('MultiFilePatchView').prop('selectionMode'), 'line'); + assert.isFalse(wrapper.instance().render.called); + + wrapper.instance().render.resetHistory(); + wrapper.find('MultiFilePatchView').prop('selectedRowsChanged')(new Set([1, 2]), 'hunk'); + + assert.sameMembers(Array.from(wrapper.find('MultiFilePatchView').prop('selectedRows')), [1, 2]); + assert.strictEqual(wrapper.find('MultiFilePatchView').prop('selectionMode'), 'hunk'); + assert.isTrue(wrapper.instance().render.called); + }); + + describe('discardLines()', function() { + it('records an event', async function() { + const wrapper = shallow(buildApp()); + sinon.stub(reporterProxy, 'addEvent'); + await wrapper.find('MultiFilePatchView').prop('discardRows')(new Set([1, 2])); + assert.isTrue(reporterProxy.addEvent.calledWith('discard-unstaged-changes', { + package: 'github', + component: 'MultiFilePatchController', + lineCount: 2, + eventSource: undefined, + })); + }); + }); + + describe('undoLastDiscard()', function() { + it('records an event', function() { + const wrapper = shallow(buildApp()); + sinon.stub(reporterProxy, 'addEvent'); + wrapper.find('MultiFilePatchView').prop('undoLastDiscard')(); + assert.isTrue(reporterProxy.addEvent.calledWith('undo-last-discard', { + package: 'github', + component: 'MultiFilePatchController', + eventSource: undefined, + })); + }); + }); + }); + + describe('toggleRows()', function() { + it('is a no-op with no selected rows', async function() { + const wrapper = shallow(buildApp()); + + sinon.spy(repository, 'applyPatchToIndex'); + + await wrapper.find('MultiFilePatchView').prop('toggleRows')(); + assert.isFalse(repository.applyPatchToIndex.called); + }); + + it('applies a stage patch to the index', async function() { + const wrapper = shallow(buildApp()); + wrapper.find('MultiFilePatchView').prop('selectedRowsChanged')(new Set([1])); + + sinon.spy(filePatch, 'getStagePatchForLines'); + sinon.spy(repository, 'applyPatchToIndex'); + + await wrapper.find('MultiFilePatchView').prop('toggleRows')(); + + 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('MultiFilePatchView').prop('selectedRowsChanged')(new Set([1]), 'line'); + + sinon.spy(filePatch, 'getStagePatchForLines'); + sinon.spy(repository, 'applyPatchToIndex'); + + await wrapper.find('MultiFilePatchView').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('MultiFilePatchView').prop('selectedRows')), [2]); + assert.strictEqual(wrapper.find('MultiFilePatchView').prop('selectionMode'), 'hunk'); + }); + + 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('MultiFilePatchView').prop('selectedRowsChanged')(new Set([2])); + + sinon.spy(otherPatch, 'getUnstagePatchForLines'); + sinon.spy(repository, 'applyPatchToIndex'); + + await wrapper.find('MultiFilePatchView').prop('toggleRows')(); + + assert.sameMembers(Array.from(otherPatch.getUnstagePatchForLines.lastCall.args[0]), [2]); + assert.isTrue(repository.applyPatchToIndex.calledWith(otherPatch.getUnstagePatchForLines.returnValues[0])); + }); + }); + + 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'})); + + sinon.spy(repository, 'stageFileModeChange'); + await wrapper.find('MultiFilePatchView').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('MultiFilePatchView').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('MultiFilePatchView').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('MultiFilePatchView').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('MultiFilePatchView').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('MultiFilePatchView').prop('toggleSymlinkChange')(); + + assert.isTrue(repository.unstageFiles.calledWith(['waslink.txt'])); + }); + }); + } + + it('calls discardLines with selected rows', async function() { + const discardLines = sinon.spy(); + const wrapper = shallow(buildApp({discardLines})); + wrapper.find('MultiFilePatchView').prop('selectedRowsChanged')(new Set([1, 2])); + + await wrapper.find('MultiFilePatchView').prop('discardRows')(); - assert.isTrue( - wrapper - .find('FilePatchController') - .everyWhere(w => w.prop('extra') === extra), - ); + const lastArgs = discardLines.lastCall.args; + assert.strictEqual(lastArgs[0], filePatch); + assert.sameMembers(Array.from(lastArgs[1]), [1, 2]); + assert.strictEqual(lastArgs[2], repository); }); - it('passes a refInitialFocus only to the active FilePatchController', function() { - const refInitialFocus = new RefHolder(); - const wrapper = shallow(buildApp({refInitialFocus})); + it('calls discardLines with explicitly provided rows', async function() { + const discardLines = sinon.spy(); + const wrapper = shallow(buildApp({discardLines})); + wrapper.find('MultiFilePatchView').prop('selectedRowsChanged')(new Set([1, 2])); - assert.strictEqual(wrapper.find('FilePatchController[relPath="first"]').prop('refInitialFocus'), refInitialFocus); - assert.notExists(wrapper.find('FilePatchController[relPath="second"]').prop('refInitialFocus')); - assert.notExists(wrapper.find('FilePatchController[relPath="third"]').prop('refInitialFocus')); + await wrapper.find('MultiFilePatchView').prop('discardRows')(new Set([4, 5]), 'hunk'); - wrapper.find('FilePatchController[relPath="second"]').prop('handleMouseDown')('second'); - wrapper.update(); + const lastArgs = discardLines.lastCall.args; + assert.strictEqual(lastArgs[0], filePatch); + assert.sameMembers(Array.from(lastArgs[1]), [4, 5]); + assert.strictEqual(lastArgs[2], repository); - assert.notExists(wrapper.find('FilePatchController[relPath="first"]').prop('refInitialFocus')); - assert.strictEqual(wrapper.find('FilePatchController[relPath="second"]').prop('refInitialFocus'), refInitialFocus); - assert.notExists(wrapper.find('FilePatchController[relPath="third"]').prop('refInitialFocus')); + assert.sameMembers(Array.from(wrapper.find('MultiFilePatchView').prop('selectedRows')), [4, 5]); + assert.strictEqual(wrapper.find('MultiFilePatchView').prop('selectionMode'), 'hunk'); }); }); diff --git a/test/views/file-patch-view.test.js b/test/views/file-patch-view.test.js index 4f8ca64ad43..de957868a7e 100644 --- a/test/views/file-patch-view.test.js +++ b/test/views/file-patch-view.test.js @@ -2,13 +2,13 @@ import React from 'react'; import {shallow, mount} from 'enzyme'; import {cloneRepository, buildRepository} from '../helpers'; -import FilePatchView from '../../lib/views/file-patch-view'; +import MultiFilePatchView from '../../lib/views/multi-file-patch-view'; import {buildFilePatch} from '../../lib/models/patch'; import {nullFile} from '../../lib/models/patch/file'; import FilePatch from '../../lib/models/patch/file-patch'; import RefHolder from '../../lib/models/ref-holder'; -describe('FilePatchView', function() { +describe('MultiFilePatchView', function() { let atomEnv, workspace, repository, filePatch; beforeEach(async function() { @@ -77,7 +77,7 @@ describe('FilePatchView', function() { ...overrideProps, }; - return ; + return ; } it('renders the file header', function() { From 040e44981252603e3c8250333a7dbf721ecab8bc Mon Sep 17 00:00:00 2001 From: Vanessa Yuen Date: Tue, 6 Nov 2018 22:48:39 +0100 Subject: [PATCH 147/284] more renaming --- .../multi-file-patch-controller.test.js | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/test/controllers/multi-file-patch-controller.test.js b/test/controllers/multi-file-patch-controller.test.js index 7079614ddff..29b86f23cec 100644 --- a/test/controllers/multi-file-patch-controller.test.js +++ b/test/controllers/multi-file-patch-controller.test.js @@ -8,7 +8,7 @@ import * as reporterProxy from '../../lib/reporter-proxy'; import {cloneRepository, buildRepository} from '../helpers'; describe.only('MultiFilePatchController', function() { - let atomEnv, repository, filePatch; + let atomEnv, repository, multiFilePatch; beforeEach(async function() { atomEnv = global.buildAtomEnvironment(); @@ -19,7 +19,7 @@ describe.only('MultiFilePatchController', function() { // a.txt: unstaged changes await fs.writeFile(path.join(workdirPath, 'a.txt'), '00\n01\n02\n03\n04\n05\n06'); - filePatch = await repository.getFilePatchForPath('a.txt', {staged: false}); + multiFilePatch = await repository.getStagedChangesPatch(); }); afterEach(function() { @@ -32,7 +32,7 @@ describe.only('MultiFilePatchController', function() { stagingStatus: 'unstaged', relPath: 'a.txt', isPartiallyStaged: false, - filePatch, + multiFilePatch, hasUndoHistory: false, workspace: atomEnv.workspace, commands: atomEnv.commands, @@ -155,7 +155,7 @@ describe.only('MultiFilePatchController', function() { assert.isNull(await wrapper.find('MultiFilePatchView').prop('toggleFile')()); const promise = wrapper.instance().patchChangePromise; - wrapper.setProps({filePatch: filePatch.clone()}); + wrapper.setProps({multiFilePatch: multiFilePatch.clone()}); await promise; assert.strictEqual(await wrapper.find('MultiFilePatchView').prop('toggleFile')(), 'unstaged'); @@ -243,26 +243,26 @@ describe.only('MultiFilePatchController', function() { const wrapper = shallow(buildApp()); wrapper.find('MultiFilePatchView').prop('selectedRowsChanged')(new Set([1])); - sinon.spy(filePatch, 'getStagePatchForLines'); + sinon.spy(multiFilePatch, 'getStagePatchForLines'); sinon.spy(repository, 'applyPatchToIndex'); await wrapper.find('MultiFilePatchView').prop('toggleRows')(); - assert.sameMembers(Array.from(filePatch.getStagePatchForLines.lastCall.args[0]), [1]); - assert.isTrue(repository.applyPatchToIndex.calledWith(filePatch.getStagePatchForLines.returnValues[0])); + assert.sameMembers(Array.from(multiFilePatch.getStagePatchForLines.lastCall.args[0]), [1]); + assert.isTrue(repository.applyPatchToIndex.calledWith(multiFilePatch.getStagePatchForLines.returnValues[0])); }); it('toggles a different row set if provided', async function() { const wrapper = shallow(buildApp()); wrapper.find('MultiFilePatchView').prop('selectedRowsChanged')(new Set([1]), 'line'); - sinon.spy(filePatch, 'getStagePatchForLines'); + sinon.spy(multiFilePatch, 'getStagePatchForLines'); sinon.spy(repository, 'applyPatchToIndex'); await wrapper.find('MultiFilePatchView').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(multiFilePatch.getStagePatchForLines.lastCall.args[0]), [2]); + assert.isTrue(repository.applyPatchToIndex.calledWith(multiFilePatch.getStagePatchForLines.returnValues[0])); assert.sameMembers(Array.from(wrapper.find('MultiFilePatchView').prop('selectedRows')), [2]); assert.strictEqual(wrapper.find('MultiFilePatchView').prop('selectionMode'), 'hunk'); @@ -418,7 +418,7 @@ describe.only('MultiFilePatchController', function() { await wrapper.find('MultiFilePatchView').prop('discardRows')(); const lastArgs = discardLines.lastCall.args; - assert.strictEqual(lastArgs[0], filePatch); + assert.strictEqual(lastArgs[0], multiFilePatch); assert.sameMembers(Array.from(lastArgs[1]), [1, 2]); assert.strictEqual(lastArgs[2], repository); }); @@ -431,7 +431,7 @@ describe.only('MultiFilePatchController', function() { await wrapper.find('MultiFilePatchView').prop('discardRows')(new Set([4, 5]), 'hunk'); const lastArgs = discardLines.lastCall.args; - assert.strictEqual(lastArgs[0], filePatch); + assert.strictEqual(lastArgs[0], multiFilePatch); assert.sameMembers(Array.from(lastArgs[1]), [4, 5]); assert.strictEqual(lastArgs[2], repository); From 462150dfe61cdcdfe271490800f1ab441b17bdea Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Tue, 6 Nov 2018 16:18:20 -0800 Subject: [PATCH 148/284] Assign default for params in MultiFilePatch constructor --- lib/models/patch/multi-file-patch.js | 3 ++- lib/models/repository-states/state.js | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/models/patch/multi-file-patch.js b/lib/models/patch/multi-file-patch.js index d406b35e445..61045efab10 100644 --- a/lib/models/patch/multi-file-patch.js +++ b/lib/models/patch/multi-file-patch.js @@ -1,7 +1,8 @@ import {TextBuffer} from 'atom'; export default class MultiFilePatch { - constructor(buffer, layers, filePatches) { + constructor(buffer = null, layers = {}, filePatches = []) { + this.buffer = buffer; this.patchLayer = layers.patch; diff --git a/lib/models/repository-states/state.js b/lib/models/repository-states/state.js index 791b52174d9..39f3bf598af 100644 --- a/lib/models/repository-states/state.js +++ b/lib/models/repository-states/state.js @@ -280,11 +280,11 @@ export default class State { } getChangedFilePatch() { - return Promise.resolve(new MultiFilePatch([])); + return Promise.resolve(new MultiFilePatch()); } getStagedChangesPatch() { - return Promise.resolve(new MultiFilePatch([])); + return Promise.resolve(new MultiFilePatch()); } readFileFromIndex(filePath) { From 7ae17364184f32c7d82878bc30571e47f3f0b813 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Tue, 6 Nov 2018 16:19:51 -0800 Subject: [PATCH 149/284] Add methods to MultiFilePatch Added `anyPresent`, `didAnyChangeExecutableMode`, `anyHaveSymlink` --- lib/models/patch/multi-file-patch.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/lib/models/patch/multi-file-patch.js b/lib/models/patch/multi-file-patch.js index 61045efab10..c89eed6f00b 100644 --- a/lib/models/patch/multi-file-patch.js +++ b/lib/models/patch/multi-file-patch.js @@ -25,6 +25,28 @@ export default class MultiFilePatch { } } + anyPresent() { + return this.buffer !== null; + } + + didAnyChangeExecutableMode() { + for (const filePatch of this.getFilePatches()) { + if (filePatch.didAnyChangeExecutableMode()) { + return true; + } + } + return false; + } + + anyHaveSymlink() { + for (const filePatch of this.getFilePatches()) { + if (filePatch.hasSymlink()) { + return true; + } + } + return false; + } + getBuffer() { return this.buffer; } From 95f17f5e26c6ca5f34efd3def6a96503c83350f9 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Tue, 6 Nov 2018 16:25:34 -0800 Subject: [PATCH 150/284] Make `buildFilePatch` return a MultiFilePatch --- lib/models/patch/builder.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/lib/models/patch/builder.js b/lib/models/patch/builder.js index 9dbd146c574..e35b31da2eb 100644 --- a/lib/models/patch/builder.js +++ b/lib/models/patch/builder.js @@ -8,15 +8,24 @@ import FilePatch from './file-patch'; import MultiFilePatch from './multi-file-patch'; export function buildFilePatch(diffs) { + const layeredBuffer = initializeBuffer(); + + let filePatch; if (diffs.length === 0) { - return emptyDiffFilePatch(); + filePatch = emptyDiffFilePatch(); } else if (diffs.length === 1) { - return singleDiffFilePatch(diffs[0]); + filePatch = singleDiffFilePatch(diffs[0], layeredBuffer); } else if (diffs.length === 2) { - return dualDiffFilePatch(...diffs); + filePatch = dualDiffFilePatch(diffs[0], diffs[1], layeredBuffer); } else { throw new Error(`Unexpected number of diffs: ${diffs.length}`); } + + const layers = { + patch: layeredBuffer.layers.patch, + hunk: layeredBuffer.layers.hunk, + }; + return new MultiFilePatch(layeredBuffer.buffer, layers, [filePatch]); } export function buildMultiFilePatch(diffs) { From 91f587c3bfeabb7799a2039d75a0c3745ba6f01b Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Tue, 6 Nov 2018 16:34:02 -0800 Subject: [PATCH 151/284] :fire: `getChagnedFilepath` hack --- lib/containers/changed-file-container.js | 2 +- lib/models/repository-states/present.js | 5 ----- lib/models/repository-states/state.js | 4 ---- lib/models/repository.js | 1 - 4 files changed, 1 insertion(+), 11 deletions(-) diff --git a/lib/containers/changed-file-container.js b/lib/containers/changed-file-container.js index 30c9a245e4d..eb6a8a7f8a3 100644 --- a/lib/containers/changed-file-container.js +++ b/lib/containers/changed-file-container.js @@ -33,7 +33,7 @@ export default class ChangedFileContainer extends React.Component { const staged = this.props.stagingStatus === 'staged'; return yubikiri({ - multiFilePatch: repository.getChangedFilePatch(this.props.relPath, {staged}), + multiFilePatch: repository.getFilePatchForPath(this.props.relPath, {staged}), isPartiallyStaged: repository.isPartiallyStaged(this.props.relPath), hasUndoHistory: repository.hasDiscardHistory(this.props.relPath), }); diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js index 863c59a1e3d..97ebccd24e2 100644 --- a/lib/models/repository-states/present.js +++ b/lib/models/repository-states/present.js @@ -626,11 +626,6 @@ export default class Present extends State { return {stagedFiles, unstagedFiles, mergeConflictFiles}; } - // hack hack hack - async getChangedFilePatch(...args) { - return new MultiFilePatch([await this.getFilePatchForPath(...args)]); - } - getFilePatchForPath(filePath, {staged} = {staged: false}) { return this.cache.getOrSet(Keys.filePatch.oneWith(filePath, {staged}), async () => { const diffs = await this.git().getDiffsForFilePath(filePath, {staged}); diff --git a/lib/models/repository-states/state.js b/lib/models/repository-states/state.js index 39f3bf598af..fe0ff5a4a91 100644 --- a/lib/models/repository-states/state.js +++ b/lib/models/repository-states/state.js @@ -279,10 +279,6 @@ export default class State { return Promise.resolve(FilePatch.createNull()); } - getChangedFilePatch() { - return Promise.resolve(new MultiFilePatch()); - } - getStagedChangesPatch() { return Promise.resolve(new MultiFilePatch()); } diff --git a/lib/models/repository.js b/lib/models/repository.js index 08970a981d6..6801b952626 100644 --- a/lib/models/repository.js +++ b/lib/models/repository.js @@ -328,7 +328,6 @@ const delegates = [ 'getFilePatchForPath', 'getStagedChangesPatch', 'readFileFromIndex', - 'getChangedFilePatch', 'getLastCommit', 'getRecentCommits', From 118761345a16500ffe99c749230b96bbe523146d Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Tue, 6 Nov 2018 16:44:35 -0800 Subject: [PATCH 152/284] Oops, call the right method `didChangeExecutableMode` --- lib/models/patch/multi-file-patch.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/models/patch/multi-file-patch.js b/lib/models/patch/multi-file-patch.js index c89eed6f00b..e4d30fda23e 100644 --- a/lib/models/patch/multi-file-patch.js +++ b/lib/models/patch/multi-file-patch.js @@ -31,7 +31,7 @@ export default class MultiFilePatch { didAnyChangeExecutableMode() { for (const filePatch of this.getFilePatches()) { - if (filePatch.didAnyChangeExecutableMode()) { + if (filePatch.didChangeExecutableMode()) { return true; } } From 9d2becd754baf5fcfb68dfd33b7880a9bc6009a0 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Tue, 6 Nov 2018 16:56:48 -0800 Subject: [PATCH 153/284] Make State#getFilePatchForPath return a MultiFilePatch instead of FilePatch --- lib/models/repository-states/state.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/models/repository-states/state.js b/lib/models/repository-states/state.js index fe0ff5a4a91..72c451e53db 100644 --- a/lib/models/repository-states/state.js +++ b/lib/models/repository-states/state.js @@ -276,7 +276,7 @@ export default class State { } getFilePatchForPath(filePath, options = {}) { - return Promise.resolve(FilePatch.createNull()); + return Promise.resolve(new MultiFilePatch()); } getStagedChangesPatch() { From db5ad10ce3bb5561dab268a8160b2e2190216bad Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Tue, 6 Nov 2018 16:57:20 -0800 Subject: [PATCH 154/284] Pass all layers to MultiFilePatch in `buildFilePatch` --- lib/models/patch/builder.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/models/patch/builder.js b/lib/models/patch/builder.js index e35b31da2eb..458aa9dbc44 100644 --- a/lib/models/patch/builder.js +++ b/lib/models/patch/builder.js @@ -21,11 +21,7 @@ export function buildFilePatch(diffs) { throw new Error(`Unexpected number of diffs: ${diffs.length}`); } - const layers = { - patch: layeredBuffer.layers.patch, - hunk: layeredBuffer.layers.hunk, - }; - return new MultiFilePatch(layeredBuffer.buffer, layers, [filePatch]); + return new MultiFilePatch(layeredBuffer.buffer, layeredBuffer.layers, [filePatch]); } export function buildMultiFilePatch(diffs) { From 0cda4425efe49d6950e9f91246778cf941bc7e86 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Tue, 6 Nov 2018 17:11:29 -0800 Subject: [PATCH 155/284] this.multiFilePatch --> this.props.multiFilPatch --- lib/views/multi-file-patch-view.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/views/multi-file-patch-view.js b/lib/views/multi-file-patch-view.js index 670acf49c80..600067d1494 100644 --- a/lib/views/multi-file-patch-view.js +++ b/lib/views/multi-file-patch-view.js @@ -450,7 +450,7 @@ export default class MultiFilePatchView extends React.Component { renderHunkHeaders(filePatch) { const toggleVerb = this.props.stagingStatus === 'unstaged' ? 'Stage' : 'Unstage'; const selectedHunks = new Set( - Array.from(this.props.selectedRows, row => this.multiFilePatch.getHunkAt(row)), + Array.from(this.props.selectedRows, row => this.props.multiFilePatch.getHunkAt(row)), ); return ( From 6023fe778761296d0516ba39b07de520fde15026 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Tue, 6 Nov 2018 17:11:51 -0800 Subject: [PATCH 156/284] Add MultiFilePatch#getMaxLineWidth --- lib/models/patch/multi-file-patch.js | 51 ++++++++++++++++------------ 1 file changed, 29 insertions(+), 22 deletions(-) diff --git a/lib/models/patch/multi-file-patch.js b/lib/models/patch/multi-file-patch.js index e4d30fda23e..730f0662d6a 100644 --- a/lib/models/patch/multi-file-patch.js +++ b/lib/models/patch/multi-file-patch.js @@ -25,28 +25,6 @@ export default class MultiFilePatch { } } - anyPresent() { - return this.buffer !== null; - } - - didAnyChangeExecutableMode() { - for (const filePatch of this.getFilePatches()) { - if (filePatch.didChangeExecutableMode()) { - return true; - } - } - return false; - } - - anyHaveSymlink() { - for (const filePatch of this.getFilePatches()) { - if (filePatch.hasSymlink()) { - return true; - } - } - return false; - } - getBuffer() { return this.buffer; } @@ -204,6 +182,35 @@ export default class MultiFilePatch { return filePatches; } + anyPresent() { + return this.buffer !== null; + } + + didAnyChangeExecutableMode() { + for (const filePatch of this.getFilePatches()) { + if (filePatch.didChangeExecutableMode()) { + return true; + } + } + return false; + } + + anyHaveSymlink() { + for (const filePatch of this.getFilePatches()) { + if (filePatch.hasSymlink()) { + return true; + } + } + return false; + } + + getMaxLineNumberWidth() { + return this.getFilePatches().reduce((maxWidth, filePatch) => { + const width = filePatch.getMaxLineNumberWidth(); + return maxWidth >= width ? maxWidth : width; + }, 0); + } + /* * Construct an apply-able patch String. */ From f50d098588d37ab03a6b853d3085f44fc85a06b1 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Tue, 6 Nov 2018 17:13:07 -0800 Subject: [PATCH 157/284] Pass all layers through to MultiFilePatch in `buildMultiFilePatch` --- lib/models/patch/builder.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/models/patch/builder.js b/lib/models/patch/builder.js index 458aa9dbc44..9ab2eca8c1c 100644 --- a/lib/models/patch/builder.js +++ b/lib/models/patch/builder.js @@ -60,11 +60,7 @@ export function buildMultiFilePatch(diffs) { const filePatches = actions.map(action => action()); - const layers = { - patch: layeredBuffer.layers.patch, - hunk: layeredBuffer.layers.hunk, - }; - return new MultiFilePatch(layeredBuffer.buffer, layers, filePatches); + return new MultiFilePatch(layeredBuffer.buffer, layeredBuffer.layers, filePatches); } function emptyDiffFilePatch() { From 843f1f80af8952dae62c31e3f2cbffc5abbb0cf0 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Tue, 6 Nov 2018 18:11:42 -0800 Subject: [PATCH 158/284] :fire: auto height now that we're in a single editor Co-Authored-By: Tilde Ann Thurium --- styles/commit-preview-view.less | 1 - 1 file changed, 1 deletion(-) diff --git a/styles/commit-preview-view.less b/styles/commit-preview-view.less index a0fa33e21b7..aff47bd9a6c 100644 --- a/styles/commit-preview-view.less +++ b/styles/commit-preview-view.less @@ -5,7 +5,6 @@ z-index: 1; // Fixes scrollbar on macOS .github-FilePatchView { - height: auto; border-bottom: 1px solid @base-border-color; &:last-child { From 381f26b4ec418c2b7fff7b4d2e65d9bc14167ba2 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 7 Nov 2018 08:27:52 -0500 Subject: [PATCH 159/284] Retain the buffer created from a builder --- lib/models/patch/builder.js | 2 ++ lib/models/patch/multi-file-patch.js | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/models/patch/builder.js b/lib/models/patch/builder.js index 9ab2eca8c1c..7e3c9c0e699 100644 --- a/lib/models/patch/builder.js +++ b/lib/models/patch/builder.js @@ -152,6 +152,8 @@ const CHANGEKIND = { function initializeBuffer() { const buffer = new TextBuffer(); + buffer.retain(); + const layers = ['patch', 'hunk', 'unchanged', 'addition', 'deletion', 'noNewline'].reduce((obj, key) => { obj[key] = buffer.addMarkerLayer(); return obj; diff --git a/lib/models/patch/multi-file-patch.js b/lib/models/patch/multi-file-patch.js index 730f0662d6a..de6dd978af4 100644 --- a/lib/models/patch/multi-file-patch.js +++ b/lib/models/patch/multi-file-patch.js @@ -2,7 +2,6 @@ import {TextBuffer} from 'atom'; export default class MultiFilePatch { constructor(buffer = null, layers = {}, filePatches = []) { - this.buffer = buffer; this.patchLayer = layers.patch; From 1e0385b16bef827e3240a211da5ae98065c64e77 Mon Sep 17 00:00:00 2001 From: Vanessa Yuen Date: Wed, 7 Nov 2018 15:13:48 +0100 Subject: [PATCH 160/284] don't pass multifilepatch to toggle rows --- lib/views/multi-file-patch-view.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/views/multi-file-patch-view.js b/lib/views/multi-file-patch-view.js index 600067d1494..2a1df7781c4 100644 --- a/lib/views/multi-file-patch-view.js +++ b/lib/views/multi-file-patch-view.js @@ -601,7 +601,6 @@ export default class MultiFilePatchView extends React.Component { toggleHunkSelection(hunk, containsSelection) { if (containsSelection) { return this.props.toggleRows( - this.props.multiFilePatch, this.props.selectedRows, this.props.selectionMode, {eventSource: 'button'}, @@ -615,7 +614,6 @@ export default class MultiFilePatchView extends React.Component { }, []), ); return this.props.toggleRows( - this.props.multiFilePatch, changeRows, 'hunk', {eventSource: 'button'}, @@ -784,7 +782,7 @@ export default class MultiFilePatchView extends React.Component { } didConfirm() { - return this.props.toggleRows(this.props.multiFilePatch, this.props.selectedRows, this.props.selectionMode); + return this.props.toggleRows(this.props.selectedRows, this.props.selectionMode); } didToggleSelectionMode() { From fc198875dcd21e46dc98144f9847ac0c02c53b22 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 7 Nov 2018 09:20:25 -0500 Subject: [PATCH 161/284] Move getNextSelectionRange() up to MultiFilePatch --- lib/models/patch/file-patch.js | 4 -- lib/models/patch/multi-file-patch.js | 60 ++++++++++++++++++++++++++++ lib/models/patch/patch.js | 51 ----------------------- 3 files changed, 60 insertions(+), 55 deletions(-) diff --git a/lib/models/patch/file-patch.js b/lib/models/patch/file-patch.js index 7d7199f0bc2..f6aee78c629 100644 --- a/lib/models/patch/file-patch.js +++ b/lib/models/patch/file-patch.js @@ -255,10 +255,6 @@ export default class FilePatch { }; } - getNextSelectionRange(lastFilePatch, lastSelectedRows) { - return this.getPatch().getNextSelectionRange(lastFilePatch.getPatch(), lastSelectedRows); - } - isEqual(other) { if (!(other instanceof this.constructor)) { return false; } diff --git a/lib/models/patch/multi-file-patch.js b/lib/models/patch/multi-file-patch.js index de6dd978af4..e2640424fb5 100644 --- a/lib/models/patch/multi-file-patch.js +++ b/lib/models/patch/multi-file-patch.js @@ -96,6 +96,66 @@ export default class MultiFilePatch { return this.getUnstagePatchForLines(new Set(hunk.getBufferRows())); } + getNextSelectionRange(lastMultiFilePatch, lastSelectedRows) { + if (lastSelectedRows.size === 0) { + const [firstPatch] = this.getFilePatches(); + if (!firstPatch) { + return [[0, 0], [0, 0]]; + } + + return firstPatch.getFirstChangeRange(); + } + + const lastMax = Math.max(...lastSelectedRows); + + let lastSelectionIndex = 0; + for (const lastFilePatch of lastMultiFilePatch.getFilePatches()) { + for (const hunk of lastFilePatch.getHunks()) { + let includesMax = false; + let hunkSelectionOffset = 0; + + changeLoop: for (const change of hunk.getChanges()) { + for (const {intersection, gap} of change.intersectRows(lastSelectedRows, true)) { + // Only include a partial range if this intersection includes the last selected buffer row. + includesMax = intersection.intersectsRow(lastMax); + const delta = includesMax ? lastMax - intersection.start.row + 1 : intersection.getRowCount(); + + if (gap) { + // Range of unselected changes. + hunkSelectionOffset += delta; + } + + if (includesMax) { + break changeLoop; + } + } + } + + lastSelectionIndex += hunkSelectionOffset; + + if (includesMax) { + break; + } + } + } + + let newSelectionRow = 0; + patchLoop: for (const filePatch of this.getFilePatches()) { + for (const hunk of filePatch.getHunks()) { + for (const change of hunk.getChanges()) { + if (lastSelectionIndex < change.bufferRowCount()) { + newSelectionRow = change.getStartBufferRow() + lastSelectionIndex; + break patchLoop; + } else { + lastSelectionIndex -= change.bufferRowCount(); + } + } + } + } + + return [[newSelectionRow, 0], [newSelectionRow, Infinity]]; + } + adoptBufferFrom(lastMultiFilePatch) { lastMultiFilePatch.getHunkLayer().clear(); lastMultiFilePatch.getUnchangedLayer().clear(); diff --git a/lib/models/patch/patch.js b/lib/models/patch/patch.js index 7416215839a..223e549302b 100644 --- a/lib/models/patch/patch.js +++ b/lib/models/patch/patch.js @@ -256,57 +256,6 @@ export default class Patch { return [[firstRow, 0], [firstRow, Infinity]]; } - getNextSelectionRange(lastPatch, lastSelectedRows) { - if (lastSelectedRows.size === 0) { - return this.getFirstChangeRange(); - } - - const lastMax = Math.max(...lastSelectedRows); - - let lastSelectionIndex = 0; - for (const hunk of lastPatch.getHunks()) { - let includesMax = false; - let hunkSelectionOffset = 0; - - changeLoop: for (const change of hunk.getChanges()) { - for (const {intersection, gap} of change.intersectRows(lastSelectedRows, true)) { - // Only include a partial range if this intersection includes the last selected buffer row. - includesMax = intersection.intersectsRow(lastMax); - const delta = includesMax ? lastMax - intersection.start.row + 1 : intersection.getRowCount(); - - if (gap) { - // Range of unselected changes. - hunkSelectionOffset += delta; - } - - if (includesMax) { - break changeLoop; - } - } - } - - lastSelectionIndex += hunkSelectionOffset; - - if (includesMax) { - break; - } - } - - let newSelectionRow = 0; - hunkLoop: for (const hunk of this.getHunks()) { - for (const change of hunk.getChanges()) { - if (lastSelectionIndex < change.bufferRowCount()) { - newSelectionRow = change.getStartBufferRow() + lastSelectionIndex; - break hunkLoop; - } else { - lastSelectionIndex -= change.bufferRowCount(); - } - } - } - - return [[newSelectionRow, 0], [newSelectionRow, Infinity]]; - } - toString() { return this.getHunks().reduce((str, hunk) => str + hunk.toStringIn(this.getBuffer()), ''); } From 4ad126a7021fa5bf183c0c0a2eeb8a42581ec2e5 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 7 Nov 2018 10:07:27 -0500 Subject: [PATCH 162/284] Pass bound filePatch arguments in FilePatchHeaderView callbacks --- lib/views/multi-file-patch-view.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/views/multi-file-patch-view.js b/lib/views/multi-file-patch-view.js index 2a1df7781c4..d306baeb176 100644 --- a/lib/views/multi-file-patch-view.js +++ b/lib/views/multi-file-patch-view.js @@ -319,10 +319,10 @@ export default class MultiFilePatchView extends React.Component { tooltips={this.props.tooltips} - undoLastDiscard={this.undoLastDiscardFromButton} - diveIntoMirrorPatch={this.props.diveIntoMirrorPatch} + undoLastDiscard={() => this.undoLastDiscardFromButton(filePatch)} + diveIntoMirrorPatch={() => this.props.diveIntoMirrorPatch(filePatch)} openFile={this.didOpenFile} - toggleFile={this.props.toggleFile} + toggleFile={() => this.props.toggleFile(filePatch)} /> From e6d861f09b36837e53aa2640dcc7ff28f35c338e Mon Sep 17 00:00:00 2001 From: Vanessa Yuen Date: Wed, 7 Nov 2018 16:37:52 +0100 Subject: [PATCH 163/284] discardRows don't need multifilepatch as arg either --- lib/views/multi-file-patch-view.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/views/multi-file-patch-view.js b/lib/views/multi-file-patch-view.js index d306baeb176..d72d7982dec 100644 --- a/lib/views/multi-file-patch-view.js +++ b/lib/views/multi-file-patch-view.js @@ -591,7 +591,6 @@ export default class MultiFilePatchView extends React.Component { discardSelectionFromCommand = () => { return this.props.discardRows( - this.props.multiFilePatch, this.props.selectedRows, this.props.selectionMode, {eventSource: {command: 'github:discard-selected-lines'}}, @@ -624,7 +623,6 @@ export default class MultiFilePatchView extends React.Component { discardHunkSelection(hunk, containsSelection) { if (containsSelection) { return this.props.discardRows( - this.props.multiFilePatch, this.props.selectedRows, this.props.selectionMode, {eventSource: 'button'}, From e1786d057a5cf0558a9df392527643d6e6ef1fed Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 7 Nov 2018 10:51:26 -0500 Subject: [PATCH 164/284] Use the TextBuffer from MultiFilePatch to render patch strings --- lib/models/patch/file-patch.js | 6 +++--- lib/models/patch/multi-file-patch.js | 2 +- lib/models/patch/patch.js | 4 ++-- test/models/patch/patch.test.js | 10 ++++++---- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/lib/models/patch/file-patch.js b/lib/models/patch/file-patch.js index f6aee78c629..97e3b75b560 100644 --- a/lib/models/patch/file-patch.js +++ b/lib/models/patch/file-patch.js @@ -265,7 +265,7 @@ export default class FilePatch { ); } - toString() { + toStringIn(buffer) { if (!this.isPresent()) { return ''; } @@ -281,7 +281,7 @@ export default class FilePatch { patch: this.getNewSymlink() ? this.getPatch().clone({status: 'added'}) : this.getPatch(), }); - return left.toString() + right.toString(); + return left.toStringIn(buffer) + right.toStringIn(buffer); } 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`; @@ -289,7 +289,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.getPatch().toString(); + return this.getHeaderString() + this.getPatch().toStringIn(buffer); } } diff --git a/lib/models/patch/multi-file-patch.js b/lib/models/patch/multi-file-patch.js index e2640424fb5..02007ea785c 100644 --- a/lib/models/patch/multi-file-patch.js +++ b/lib/models/patch/multi-file-patch.js @@ -274,6 +274,6 @@ export default class MultiFilePatch { * Construct an apply-able patch String. */ toString() { - return this.filePatches.map(fp => fp.toString()).join(''); + return this.filePatches.map(fp => fp.toStringIn(this.buffer)).join(''); } } diff --git a/lib/models/patch/patch.js b/lib/models/patch/patch.js index 223e549302b..697381747e1 100644 --- a/lib/models/patch/patch.js +++ b/lib/models/patch/patch.js @@ -256,8 +256,8 @@ export default class Patch { return [[firstRow, 0], [firstRow, Infinity]]; } - toString() { - return this.getHunks().reduce((str, hunk) => str + hunk.toStringIn(this.getBuffer()), ''); + toStringIn(buffer) { + return this.getHunks().reduce((str, hunk) => str + hunk.toStringIn(buffer), ''); } isPresent() { diff --git a/test/models/patch/patch.test.js b/test/models/patch/patch.test.js index b412df6c45a..b3d0c857f13 100644 --- a/test/models/patch/patch.test.js +++ b/test/models/patch/patch.test.js @@ -746,10 +746,11 @@ describe('Patch', function() { new Unchanged(markRange(layers.unchanged, 9)), ], }); + const marker = markRange(layers.patch, 0, 9); - const p = new Patch({status: 'modified', hunks: [hunk0, hunk1], buffer, layers}); + const p = new Patch({status: 'modified', hunks: [hunk0, hunk1], marker}); - assert.strictEqual(p.toString(), [ + assert.strictEqual(p.toStringIn(buffer), [ '@@ -0,2 +0,3 @@\n', ' 0000\n', '+0001\n', @@ -777,9 +778,10 @@ describe('Patch', function() { new Unchanged(markRange(layers.unchanged, 5)), ], }); + const marker = markRange(layers.patch, 0, 5); - const p = new Patch({status: 'modified', hunks: [hunk], buffer, layers}); - assert.strictEqual(p.toString(), [ + const p = new Patch({status: 'modified', hunks: [hunk], marker}); + assert.strictEqual(p.toStringIn(buffer), [ '@@ -1,5 +1,5 @@\n', ' \n', ' \n', From 42f4602b8d52c80a45b793abc2adc13d0b170a44 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 7 Nov 2018 12:01:28 -0500 Subject: [PATCH 165/284] Patch assertion helpers need to accept a TextBuffer --- test/helpers.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/test/helpers.js b/test/helpers.js index 4c58f0475a7..86c39ab3e21 100644 --- a/test/helpers.js +++ b/test/helpers.js @@ -156,8 +156,9 @@ export function assertEqualSortedArraysByKey(arr1, arr2, key) { // Helpers for test/models/patch classes class PatchBufferAssertions { - constructor(patch) { + constructor(patch, buffer) { this.patch = patch; + this.buffer = buffer; } hunk(hunkIndex, {startRow, endRow, header, regions}) { @@ -174,7 +175,7 @@ class PatchBufferAssertions { const spec = regions[i]; assert.strictEqual(region.constructor.name.toLowerCase(), spec.kind); - assert.strictEqual(region.toStringIn(this.patch.getBuffer()), spec.string); + assert.strictEqual(region.toStringIn(this.buffer), spec.string); assert.deepEqual(region.getRange().serialize(), spec.range); } } @@ -187,12 +188,12 @@ class PatchBufferAssertions { } } -export function assertInPatch(patch) { - return new PatchBufferAssertions(patch); +export function assertInPatch(patch, buffer) { + return new PatchBufferAssertions(patch, buffer); } -export function assertInFilePatch(filePatch) { - return assertInPatch(filePatch.getPatch()); +export function assertInFilePatch(filePatch, buffer) { + return assertInPatch(filePatch.getPatch(), buffer); } let activeRenderers = []; From 45c2ddf4b794169daf5f7b06b438ea89db5a4c24 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 7 Nov 2018 12:02:23 -0500 Subject: [PATCH 166/284] Patch tests are :white_check_mark: :fireworks: --- lib/models/patch/patch.js | 32 ++-- test/models/patch/patch.test.js | 282 ++++++++------------------------ 2 files changed, 84 insertions(+), 230 deletions(-) diff --git a/lib/models/patch/patch.js b/lib/models/patch/patch.js index 697381747e1..d7e7a53391d 100644 --- a/lib/models/patch/patch.js +++ b/lib/models/patch/patch.js @@ -50,7 +50,7 @@ export default class Patch { return new this.constructor({ status: opts.status !== undefined ? opts.status : this.getStatus(), hunks: opts.hunks !== undefined ? opts.hunks : this.getHunks(), - buffer: opts.buffer !== undefined ? opts.buffer : this.getBuffer(), + marker: opts.marker !== undefined ? opts.marker : this.getMarker(), }); } @@ -138,11 +138,7 @@ export default class Patch { const wholeFile = rowSet.size === this.changedLineCount; const status = this.getStatus() === 'deleted' && !wholeFile ? 'modified' : this.getStatus(); - return { - patch: this.clone({hunks, status, marker}), - buffer, - layers, - }; + return this.clone({hunks, status, marker}); } buildUnstagePatchForLines(originalBuffer, nextLayeredBuffer, rowSet) { @@ -234,11 +230,7 @@ export default class Patch { const layers = builder.getLayers(); const marker = layers.patch.markRange([[0, 0], [buffer.getLastRow(), Infinity]]); - return { - patch: this.clone({hunks, status, marker}), - buffer, - layers, - }; + return this.clone({hunks, status, marker}); } getFirstChangeRange() { @@ -281,9 +273,8 @@ export default class Patch { class NullPatch { constructor() { - this.buffer = new TextBuffer(); - - this.buffer.retain(); + const buffer = new TextBuffer(); + this.marker = buffer.markRange([[0, 0], [0, 0]]); } getStatus() { @@ -294,8 +285,8 @@ class NullPatch { return []; } - getBuffer() { - return this.buffer; + getMarker() { + return this.marker; } getByteSize() { @@ -310,23 +301,23 @@ class NullPatch { if ( opts.status === undefined && opts.hunks === undefined && - opts.buffer === undefined + opts.marker === undefined ) { return this; } else { return new Patch({ status: opts.status !== undefined ? opts.status : this.getStatus(), hunks: opts.hunks !== undefined ? opts.hunks : this.getHunks(), - buffer: opts.buffer !== undefined ? opts.buffer : this.getBuffer(), + marker: opts.marker !== undefined ? opts.marker : this.getMarker(), }); } } - getStagePatchForLines() { + buildStagePatchForLines() { return this; } - getUnstagePatchForLines() { + buildUnstagePatchForLines() { return this; } @@ -449,6 +440,7 @@ class BufferBuilder { getLayers() { return { + patch: this.layers.get('patch'), hunk: this.layers.get('hunk'), unchanged: this.layers.get(Unchanged), addition: this.layers.get(Addition), diff --git a/test/models/patch/patch.test.js b/test/models/patch/patch.test.js index b3d0c857f13..b6c923f1a87 100644 --- a/test/models/patch/patch.test.js +++ b/test/models/patch/patch.test.js @@ -148,13 +148,21 @@ describe('Patch', function() { }); describe('stage patch generation', function() { + let stageLayeredBuffer; + + beforeEach(function() { + const stageBuffer = new TextBuffer(); + const stageLayers = buildLayers(stageBuffer); + stageLayeredBuffer = {buffer: stageBuffer, layers: stageLayers}; + }); + it('creates a patch that applies selected lines from only the first hunk', function() { - const patch = buildPatchFixture(); - const stagePatch = patch.getStagePatchForLines(new Set([2, 3, 4, 5])); + const {patch, buffer: originalBuffer} = buildPatchFixture(); + const stagePatch = patch.buildStagePatchForLines(originalBuffer, stageLayeredBuffer, new Set([2, 3, 4, 5])); // buffer rows: 0 1 2 3 4 5 6 const expectedBufferText = '0000\n0001\n0002\n0003\n0004\n0005\n0006\n'; - assert.strictEqual(stagePatch.getBuffer().getText(), expectedBufferText); - assertInPatch(stagePatch).hunks( + assert.strictEqual(stageLayeredBuffer.buffer.getText(), expectedBufferText); + assertInPatch(stagePatch, stageLayeredBuffer.buffer).hunks( { startRow: 0, endRow: 6, @@ -170,12 +178,12 @@ describe('Patch', function() { }); it('creates a patch that applies selected lines from a single non-first hunk', function() { - const patch = buildPatchFixture(); - const stagePatch = patch.getStagePatchForLines(new Set([8, 13, 14, 16])); + const {patch, buffer: originalBuffer} = buildPatchFixture(); + const stagePatch = patch.buildStagePatchForLines(originalBuffer, stageLayeredBuffer, 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.getBuffer().getText(), expectedBufferText); - assertInPatch(stagePatch).hunks( + assert.strictEqual(stageLayeredBuffer.buffer.getText(), expectedBufferText); + assertInPatch(stagePatch, stageLayeredBuffer.buffer).hunks( { startRow: 0, endRow: 9, @@ -194,8 +202,8 @@ describe('Patch', function() { }); 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 {patch, buffer: originalBuffer} = buildPatchFixture(); + const stagePatch = patch.buildStagePatchForLines(originalBuffer, stageLayeredBuffer, new Set([1, 5, 15, 16, 17, 25])); const expectedBufferText = // buffer rows // 0 1 2 3 4 @@ -204,8 +212,8 @@ describe('Patch', function() { '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.getBuffer().getText(), expectedBufferText); - assertInPatch(stagePatch).hunks( + assert.strictEqual(stageLayeredBuffer.buffer.getText(), expectedBufferText); + assertInPatch(stagePatch, stageLayeredBuffer.buffer).hunks( { startRow: 0, endRow: 4, @@ -243,15 +251,15 @@ describe('Patch', function() { }); it('marks ranges for each change region on the correct marker layer', function() { - const patch = buildPatchFixture(); - const stagePatch = patch.getStagePatchForLines(new Set([1, 5, 15, 16, 17, 25])); + const {patch, buffer: originalBuffer} = buildPatchFixture(); + patch.buildStagePatchForLines(originalBuffer, stageLayeredBuffer, new Set([1, 5, 15, 16, 17, 25])); const layerRanges = [ - ['hunk', stagePatch.getHunkLayer()], - ['unchanged', stagePatch.getUnchangedLayer()], - ['addition', stagePatch.getAdditionLayer()], - ['deletion', stagePatch.getDeletionLayer()], - ['noNewline', stagePatch.getNoNewlineLayer()], + ['hunk', stageLayeredBuffer.layers.hunk], + ['unchanged', stageLayeredBuffer.layers.unchanged], + ['addition', stageLayeredBuffer.layers.addition], + ['deletion', stageLayeredBuffer.layers.deletion], + ['noNewline', stageLayeredBuffer.layers.noNewline], ].reduce((obj, [key, layer]) => { obj[key] = layer.getMarkers().map(marker => marker.getRange().serialize()); return obj; @@ -299,12 +307,13 @@ describe('Patch', function() { ], }), ]; + const marker = markRange(layers.patch, 0, 5); - const patch = new Patch({status: 'deleted', hunks, buffer, layers}); + const patch = new Patch({status: 'deleted', hunks, marker}); - const stagedPatch = patch.getStagePatchForLines(new Set([1, 3, 4])); + const stagedPatch = patch.buildStagePatchForLines(buffer, stageLayeredBuffer, new Set([1, 3, 4])); assert.strictEqual(stagedPatch.getStatus(), 'modified'); - assertInPatch(stagedPatch).hunks( + assertInPatch(stagedPatch, stageLayeredBuffer.buffer).hunks( { startRow: 0, endRow: 5, @@ -332,28 +341,37 @@ describe('Patch', function() { ], }), ]; - const patch = new Patch({status: 'deleted', hunks, buffer, layers}); + const marker = markRange(layers.patch, 0, 2); + const patch = new Patch({status: 'deleted', hunks, marker}); - const stagePatch0 = patch.getStagePatchForLines(new Set([0, 1, 2])); + const stagePatch0 = patch.buildStagePatchForLines(buffer, stageLayeredBuffer, new Set([0, 1, 2])); assert.strictEqual(stagePatch0.getStatus(), 'deleted'); }); it('returns a nullPatch as a nullPatch', function() { const nullPatch = Patch.createNull(); - assert.strictEqual(nullPatch.getStagePatchForLines(new Set([1, 2, 3])), nullPatch); + assert.strictEqual(nullPatch.buildStagePatchForLines(new Set([1, 2, 3])), nullPatch); }); }); describe('unstage patch generation', function() { + let unstageLayeredBuffer; + + beforeEach(function() { + const unstageBuffer = new TextBuffer(); + const unstageLayers = buildLayers(unstageBuffer); + unstageLayeredBuffer = {buffer: unstageBuffer, layers: unstageLayers}; + }); + 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])); + const {patch, buffer: originalBuffer} = buildPatchFixture(); + const unstagePatch = patch.buildUnstagePatchForLines(originalBuffer, unstageLayeredBuffer, new Set([8, 12, 13])); assert.strictEqual( - unstagePatch.getBuffer().getText(), + unstageLayeredBuffer.buffer.getText(), // 0 1 2 3 4 5 6 7 8 '0007\n0008\n0009\n0010\n0011\n0012\n0013\n0017\n0018\n', ); - assertInPatch(unstagePatch).hunks( + assertInPatch(unstagePatch, unstageLayeredBuffer.buffer).hunks( { startRow: 0, endRow: 8, @@ -370,10 +388,10 @@ describe('Patch', function() { }); 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])); + const {patch, buffer: originalBuffer} = buildPatchFixture(); + const unstagePatch = patch.buildUnstagePatchForLines(originalBuffer, unstageLayeredBuffer, new Set([1, 4, 5, 16, 17, 20, 25])); assert.strictEqual( - unstagePatch.getBuffer().getText(), + unstageLayeredBuffer.buffer.getText(), // 0 1 2 3 4 5 '0000\n0001\n0003\n0004\n0005\n0006\n' + // 6 7 8 9 10 11 12 13 @@ -383,7 +401,7 @@ describe('Patch', function() { // 17 18 19 '0024\n0025\n No newline at end of file\n', ); - assertInPatch(unstagePatch).hunks( + assertInPatch(unstagePatch, unstageLayeredBuffer.buffer).hunks( { startRow: 0, endRow: 5, @@ -431,15 +449,14 @@ describe('Patch', function() { }); it('marks ranges for each change region on the correct marker layer', function() { - const patch = buildPatchFixture(); - const unstagePatch = patch.getUnstagePatchForLines(new Set([1, 4, 5, 16, 17, 20, 25])); - + const {patch, buffer: originalBuffer} = buildPatchFixture(); + patch.buildUnstagePatchForLines(originalBuffer, unstageLayeredBuffer, new Set([1, 4, 5, 16, 17, 20, 25])); const layerRanges = [ - ['hunk', unstagePatch.getHunkLayer()], - ['unchanged', unstagePatch.getUnchangedLayer()], - ['addition', unstagePatch.getAdditionLayer()], - ['deletion', unstagePatch.getDeletionLayer()], - ['noNewline', unstagePatch.getNoNewlineLayer()], + ['hunk', unstageLayeredBuffer.layers.hunk], + ['unchanged', unstageLayeredBuffer.layers.unchanged], + ['addition', unstageLayeredBuffer.layers.addition], + ['deletion', unstageLayeredBuffer.layers.deletion], + ['noNewline', unstageLayeredBuffer.layers.noNewline], ].reduce((obj, [key, layer]) => { obj[key] = layer.getMarkers().map(marker => marker.getRange().serialize()); return obj; @@ -490,11 +507,12 @@ describe('Patch', function() { ], }), ]; - const patch = new Patch({status: 'added', hunks, buffer, layers}); - const unstagePatch = patch.getUnstagePatchForLines(new Set([1, 2])); + const marker = markRange(layers.patch, 0, 2); + const patch = new Patch({status: 'added', hunks, marker}); + const unstagePatch = patch.buildUnstagePatchForLines(buffer, unstageLayeredBuffer, new Set([1, 2])); assert.strictEqual(unstagePatch.getStatus(), 'modified'); - assert.strictEqual(unstagePatch.getBuffer().getText(), '0000\n0001\n0002\n'); - assertInPatch(unstagePatch).hunks( + assert.strictEqual(unstageLayeredBuffer.buffer.getText(), '0000\n0001\n0002\n'); + assertInPatch(unstagePatch, unstageLayeredBuffer.buffer).hunks( { startRow: 0, endRow: 2, @@ -522,21 +540,22 @@ describe('Patch', function() { ], }), ]; - const patch = new Patch({status: 'added', hunks, buffer, layers}); + const marker = markRange(layers.patch, 0, 2); + const patch = new Patch({status: 'added', hunks, marker}); - const unstagePatch = patch.getUnstagePatchForLines(new Set([0, 1, 2])); + const unstagePatch = patch.buildUnstagePatchForLines(buffer, unstageLayeredBuffer, new Set([0, 1, 2])); assert.strictEqual(unstagePatch.getStatus(), 'deleted'); }); it('returns a nullPatch as a nullPatch', function() { const nullPatch = Patch.createNull(); - assert.strictEqual(nullPatch.getUnstagePatchForLines(new Set([1, 2, 3])), nullPatch); + assert.strictEqual(nullPatch.buildUnstagePatchForLines(new Set([1, 2, 3])), nullPatch); }); }); describe('getFirstChangeRange', function() { it('accesses the range of the first change from the first hunk', function() { - const patch = buildPatchFixture(); + const {patch} = buildPatchFixture(); assert.deepEqual(patch.getFirstChangeRange(), [[1, 0], [1, Infinity]]); }); @@ -550,177 +569,20 @@ describe('Patch', function() { regions: [], }), ]; - const patch = new Patch({status: 'modified', hunks, buffer, layers}); + const marker = markRange(layers.patch, 0); + const patch = new Patch({status: 'modified', hunks, marker}); assert.deepEqual(patch.getFirstChangeRange(), [[0, 0], [0, 0]]); }); it('returns the origin if the patch is empty', function() { const buffer = new TextBuffer({text: ''}); const layers = buildLayers(buffer); - const patch = new Patch({status: 'modified', hunks: [], buffer, layers}); + const marker = markRange(layers.patch, 0); + const patch = new Patch({status: 'modified', hunks: [], marker}); 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(); - // Selected: - // deletions (1-2) and partial addition (4 from 3-5) from hunk 0 - // one deletion row (13 from 12-16) from the middle of hunk 1; - // nothing in hunks 2 or 3 - const lastSelectedRows = new Set([1, 2, 4, 5, 13]); - - const nBuffer = new TextBuffer({text: - // 0 1 2 3 4 - '0000\n0003\n0004\n0005\n0006\n' + - // 5 6 7 8 9 10 11 12 13 14 15 - '0007\n0008\n0009\n0010\n0011\n0012\n0014\n0015\n0016\n0017\n0018\n' + - // 16 17 18 19 20 - '0019\n0020\n0021\n0022\n0023\n' + - // 21 22 23 - '0024\n0025\n No newline at end of file\n', - }); - const nLayers = buildLayers(nBuffer); - const nHunks = [ - new Hunk({ - oldStartRow: 3, oldRowCount: 3, newStartRow: 3, newRowCount: 5, // next row drift = +2 - marker: markRange(nLayers.hunk, 0, 4), - regions: [ - new Unchanged(markRange(nLayers.unchanged, 0)), // 0 - new Addition(markRange(nLayers.addition, 1)), // + 1 - new Unchanged(markRange(nLayers.unchanged, 2)), // 2 - new Addition(markRange(nLayers.addition, 3)), // + 3 - new Unchanged(markRange(nLayers.unchanged, 4)), // 4 - ], - }), - new Hunk({ - oldStartRow: 12, oldRowCount: 9, newStartRow: 14, newRowCount: 7, // next row drift = +2 -2 = 0 - marker: markRange(nLayers.hunk, 5, 15), - regions: [ - new Unchanged(markRange(nLayers.unchanged, 5)), // 5 - new Addition(markRange(nLayers.addition, 6)), // +6 - new Unchanged(markRange(nLayers.unchanged, 7, 9)), // 7 8 9 - new Deletion(markRange(nLayers.deletion, 10, 13)), // -10 -11 -12 -13 - new Addition(markRange(nLayers.addition, 14)), // +14 - new Unchanged(markRange(nLayers.unchanged, 15)), // 15 - ], - }), - new Hunk({ - oldStartRow: 26, oldRowCount: 4, newStartRow: 26, newRowCount: 3, // next row drift = 0 -1 = -1 - marker: markRange(nLayers.hunk, 16, 20), - regions: [ - new Unchanged(markRange(nLayers.unchanged, 16)), // 16 - new Addition(markRange(nLayers.addition, 17)), // +17 - new Deletion(markRange(nLayers.deletion, 18, 19)), // -18 -19 - new Unchanged(markRange(nLayers.unchanged, 20)), // 20 - ], - }), - new Hunk({ - oldStartRow: 32, oldRowCount: 1, newStartRow: 31, newRowCount: 2, - marker: markRange(nLayers.hunk, 22, 24), - regions: [ - new Unchanged(markRange(nLayers.unchanged, 22)), // 22 - new Addition(markRange(nLayers.addition, 23)), // +23 - new NoNewline(markRange(nLayers.noNewline, 24)), - ], - }), - ]; - const nextPatch = new Patch({status: 'modified', hunks: nHunks, buffer: nBuffer, layers: nLayers}); - - const nextRange = nextPatch.getNextSelectionRange(lastPatch, lastSelectedRows); - // Original buffer row 14 = the next changed row = new buffer row 11 - assert.deepEqual(nextRange, [[11, 0], [11, Infinity]]); - }); - - it('offsets the chosen selection index by hunks that were completely selected', function() { - const buffer = buildBuffer(11); - const layers = buildLayers(buffer); - const lastPatch = new Patch({ - status: 'modified', - hunks: [ - new Hunk({ - oldStartRow: 1, oldRowCount: 3, newStartRow: 1, newRowCount: 3, - marker: markRange(layers.hunk, 0, 5), - regions: [ - new Unchanged(markRange(layers.unchanged, 0)), - new Addition(markRange(layers.addition, 1, 2)), - new Deletion(markRange(layers.deletion, 3, 4)), - new Unchanged(markRange(layers.unchanged, 5)), - ], - }), - new Hunk({ - oldStartRow: 5, oldRowCount: 4, newStartRow: 5, newRowCount: 4, - marker: markRange(layers.hunk, 6, 11), - regions: [ - new Unchanged(markRange(layers.unchanged, 6)), - new Addition(markRange(layers.addition, 7, 8)), - new Deletion(markRange(layers.deletion, 9, 10)), - new Unchanged(markRange(layers.unchanged, 11)), - ], - }), - ], - buffer, - layers, - }); - // 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 nextBuffer = new TextBuffer({text: '0006\n0007\n0008\n0009\n0010\n0011\n'}); - const nextLayers = buildLayers(nextBuffer); - const nextPatch = new Patch({ - status: 'modified', - hunks: [ - new Hunk({ - oldStartRow: 5, oldRowCount: 4, newStartRow: 5, newRowCount: 4, - marker: markRange(nextLayers.hunk, 0, 5), - regions: [ - new Unchanged(markRange(nextLayers.unchanged, 0)), - new Addition(markRange(nextLayers.addition, 1)), - new Deletion(markRange(nextLayers.deletion, 3, 4)), - new Unchanged(markRange(nextLayers.unchanged, 5)), - ], - }), - ], - buffer: nextBuffer, - layers: nextLayers, - }); - - 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 buffer = lastPatch.getBuffer(); - const layers = buildLayers(buffer); - const nextPatch = new Patch({ - status: 'modified', - hunks: [ - new Hunk({ - oldStartRow: 1, oldRowCount: 3, newStartRow: 1, newRowCount: 4, - marker: markRange(layers.hunk, 0, 4), - regions: [ - new Unchanged(markRange(layers.unchanged, 0)), - new Addition(markRange(layers.addition, 1, 2)), - new Deletion(markRange(layers.deletion, 3)), - new Unchanged(markRange(layers.unchanged, 4)), - ], - }), - ], - buffer, - layers, - }); - - const range = nextPatch.getNextSelectionRange(lastPatch, lastSelectedRows); - assert.deepEqual(range, [[1, 0], [1, Infinity]]); - }); - }); - it('prints itself as an apply-ready string', function() { const buffer = buildBuffer(10); const layers = buildLayers(buffer); From dc0c14dabd06a52365443f34b71cc6b53c5bb198 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 7 Nov 2018 12:03:10 -0500 Subject: [PATCH 167/284] Adapt callers to changed buildStagePatchForLines() return value --- lib/models/patch/file-patch.js | 14 +++----------- lib/models/patch/multi-file-patch.js | 2 +- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/lib/models/patch/file-patch.js b/lib/models/patch/file-patch.js index 97e3b75b560..3f0ca221c36 100644 --- a/lib/models/patch/file-patch.js +++ b/lib/models/patch/file-patch.js @@ -196,24 +196,16 @@ export default class FilePatch { } } - const {patch, buffer, layers} = this.patch.getStagePatchForLines( + const patch = this.patch.buildStagePatchForLines( originalBuffer, nextLayeredBuffer, selectedLineSet, ); if (this.getStatus() === 'deleted') { // Populate newFile - return { - filePatch: this.clone({newFile, patch}), - buffer, - layers, - }; + return this.clone({newFile, patch}); } else { - return { - filePatch: this.clone({patch}), - buffer, - layers, - }; + return this.clone({patch}); } } diff --git a/lib/models/patch/multi-file-patch.js b/lib/models/patch/multi-file-patch.js index 02007ea785c..e68725fc111 100644 --- a/lib/models/patch/multi-file-patch.js +++ b/lib/models/patch/multi-file-patch.js @@ -64,7 +64,7 @@ export default class MultiFilePatch { getStagePatchForLines(selectedLineSet) { const nextLayeredBuffer = this.buildLayeredBuffer(); - const nextFilePatches = this.getFilePatchesContaining(selectedLineSet).map(fp => { + const nextFilePatches = Array.from(this.getFilePatchesContaining(selectedLineSet), fp => { return fp.buildStagePatchForLines(this.getBuffer(), nextLayeredBuffer, selectedLineSet); }); From c148a8df3038d70ca90506ccd276959d6e56d6b5 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 7 Nov 2018 12:04:35 -0500 Subject: [PATCH 168/284] Catch that other "getFilePatchesContaining() returns a Set" call --- lib/models/patch/multi-file-patch.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/models/patch/multi-file-patch.js b/lib/models/patch/multi-file-patch.js index e68725fc111..ca7450add76 100644 --- a/lib/models/patch/multi-file-patch.js +++ b/lib/models/patch/multi-file-patch.js @@ -81,7 +81,7 @@ export default class MultiFilePatch { getUnstagePatchForLines(selectedLineSet) { const nextLayeredBuffer = this.buildLayeredBuffer(); - const nextFilePatches = this.getFilePatchesContaining(selectedLineSet).map(fp => { + const nextFilePatches = Array.from(this.getFilePatchesContaining(selectedLineSet), fp => { return fp.buildUnstagePatchForLines(this.getBuffer(), nextLayeredBuffer, selectedLineSet); }); From 188d2eaab1702714f7aff4cce6020cda9526444e Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 7 Nov 2018 13:03:21 -0500 Subject: [PATCH 169/284] Cover the last few Patch methods --- test/models/patch/patch.test.js | 40 ++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/test/models/patch/patch.test.js b/test/models/patch/patch.test.js index b6c923f1a87..e1dd627c6ba 100644 --- a/test/models/patch/patch.test.js +++ b/test/models/patch/patch.test.js @@ -147,6 +147,20 @@ describe('Patch', function() { assert.strictEqual(dup2.getMarker(), nMarker); }); + it('returns an empty Range at the beginning of its Marker', function() { + const {patch} = buildPatchFixture(); + assert.deepEqual(patch.getStartRange().serialize(), [[0, 0], [0, 0]]); + }); + + it('determines whether or not a buffer row belongs to this patch', function() { + const {patch} = buildPatchFixture(); + + assert.isTrue(patch.containsRow(0)); + assert.isTrue(patch.containsRow(5)); + assert.isTrue(patch.containsRow(26)); + assert.isFalse(patch.containsRow(27)); + }); + describe('stage patch generation', function() { let stageLayeredBuffer; @@ -547,6 +561,28 @@ describe('Patch', function() { assert.strictEqual(unstagePatch.getStatus(), 'deleted'); }); + it('returns an addition when unstaging a deletion', function() { + const buffer = new TextBuffer({text: '0000\n0001\n0002\n'}); + const layers = buildLayers(buffer); + const hunks = [ + new Hunk({ + oldStartRow: 1, + oldRowCount: 0, + newStartRow: 1, + newRowCount: 3, + marker: markRange(layers.hunk, 0, 2), + regions: [ + new Addition(markRange(layers.addition, 0, 2)), + ], + }), + ]; + const marker = markRange(layers.patch, 0, 2); + const patch = new Patch({status: 'deleted', hunks, marker}); + + const unstagePatch = patch.buildUnstagePatchForLines(buffer, unstageLayeredBuffer, new Set([0, 1, 2])); + assert.strictEqual(unstagePatch.getStatus(), 'added'); + }); + it('returns a nullPatch as a nullPatch', function() { const nullPatch = Patch.createNull(); assert.strictEqual(nullPatch.buildUnstagePatchForLines(new Set([1, 2, 3])), nullPatch); @@ -704,6 +740,8 @@ function markRange(buffer, start, end = start) { function buildPatchFixture() { const buffer = buildBuffer(26, true); + buffer.append('\n\n\n\n\n\n'); + const layers = buildLayers(buffer); const hunks = [ @@ -753,7 +791,7 @@ function buildPatchFixture() { ], }), ]; - const marker = markRange(layers.patch, 0, Infinity); + const marker = markRange(layers.patch, 0, 26); return { patch: new Patch({status: 'modified', hunks, marker}), From 2fff9721fbb734555f00fcba1ac757d9333ec522 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 7 Nov 2018 13:13:50 -0500 Subject: [PATCH 170/284] Bring NullPatch up to date --- lib/models/patch/patch.js | 32 ++++++++++++++++---------------- test/models/patch/patch.test.js | 11 ++++++----- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/lib/models/patch/patch.js b/lib/models/patch/patch.js index d7e7a53391d..20b0363bba1 100644 --- a/lib/models/patch/patch.js +++ b/lib/models/patch/patch.js @@ -265,7 +265,7 @@ export default class Patch { if (this.hunks.length !== other.hunks.length) { return false; } if (this.hunks.some((hunk, i) => !hunk.isEqual(other.hunks[i]))) { return false; } - if (this.buffer.getText() !== other.buffer.getText()) { return false; } + if (!this.marker.getRange().isEqual(other.marker.getRange())) { return false; } return true; } @@ -281,19 +281,27 @@ class NullPatch { return null; } + getMarker() { + return this.marker; + } + + getStartRange() { + return Range.fromObject([[0, 0], [0, 0]]); + } + getHunks() { return []; } - getMarker() { - return this.marker; + getChangedLineCount() { + return 0; } - getByteSize() { - return 0; + containsRow() { + return false; } - getChangedLineCount() { + getMaxLineNumberWidth() { return 0; } @@ -322,18 +330,10 @@ class NullPatch { } getFirstChangeRange() { - return [[0, 0], [0, 0]]; - } - - getNextSelectionRange() { - return [[0, 0], [0, 0]]; - } - - getMaxLineNumberWidth() { - return 0; + return Range.fromObject([[0, 0], [0, 0]]); } - toString() { + toStringIn() { return ''; } diff --git a/test/models/patch/patch.test.js b/test/models/patch/patch.test.js index e1dd627c6ba..2f7738eab78 100644 --- a/test/models/patch/patch.test.js +++ b/test/models/patch/patch.test.js @@ -694,14 +694,15 @@ describe('Patch', function() { it('has a stubbed nullPatch counterpart', function() { const nullPatch = Patch.createNull(); assert.isNull(nullPatch.getStatus()); - assert.deepEqual(nullPatch.getHunks(), []); assert.deepEqual(nullPatch.getMarker().getRange().serialize(), [[0, 0], [0, 0]]); - assert.isFalse(nullPatch.isPresent()); - assert.strictEqual(nullPatch.toString(), ''); + assert.deepEqual(nullPatch.getStartRange().serialize(), [[0, 0], [0, 0]]); + assert.deepEqual(nullPatch.getHunks(), []); assert.strictEqual(nullPatch.getChangedLineCount(), 0); + assert.isFalse(nullPatch.containsRow(0)); assert.strictEqual(nullPatch.getMaxLineNumberWidth(), 0); - assert.deepEqual(nullPatch.getFirstChangeRange(), [[0, 0], [0, 0]]); - assert.deepEqual(nullPatch.getNextSelectionRange(), [[0, 0], [0, 0]]); + assert.deepEqual(nullPatch.getFirstChangeRange().serialize(), [[0, 0], [0, 0]]); + assert.strictEqual(nullPatch.toStringIn(), ''); + assert.isFalse(nullPatch.isPresent()); }); }); From 462dffda3afbf708e5e8566c73de36b1237b4c2e Mon Sep 17 00:00:00 2001 From: Vanessa Yuen Date: Wed, 7 Nov 2018 20:23:36 +0100 Subject: [PATCH 171/284] missed a rename oops --- .../{file-patch-view.test.js => multi-file-patch-view.test.js} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename test/views/{file-patch-view.test.js => multi-file-patch-view.test.js} (99%) diff --git a/test/views/file-patch-view.test.js b/test/views/multi-file-patch-view.test.js similarity index 99% rename from test/views/file-patch-view.test.js rename to test/views/multi-file-patch-view.test.js index de957868a7e..05248898845 100644 --- a/test/views/file-patch-view.test.js +++ b/test/views/multi-file-patch-view.test.js @@ -8,7 +8,7 @@ import {nullFile} from '../../lib/models/patch/file'; import FilePatch from '../../lib/models/patch/file-patch'; import RefHolder from '../../lib/models/ref-holder'; -describe('MultiFilePatchView', function() { +describe.only('MultiFilePatchView', function() { let atomEnv, workspace, repository, filePatch; beforeEach(async function() { From 6b1744b019f52e482481f9222e4be1d79a2b0c74 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 7 Nov 2018 15:48:40 -0500 Subject: [PATCH 172/284] FilePatch tests :white_check_mark: + coverage --- lib/models/patch/file-patch.js | 33 +-- test/models/patch/file-patch.test.js | 332 +++++++-------------------- 2 files changed, 89 insertions(+), 276 deletions(-) diff --git a/lib/models/patch/file-patch.js b/lib/models/patch/file-patch.js index 3f0ca221c36..7f7ce899dc0 100644 --- a/lib/models/patch/file-patch.js +++ b/lib/models/patch/file-patch.js @@ -1,7 +1,6 @@ import {nullFile} from './file'; import Patch from './patch'; import {toGitPathSep} from '../../helpers'; -import {addEvent} from '../../reporter-proxy'; export default class FilePatch { static createNull() { @@ -12,12 +11,6 @@ export default class FilePatch { this.oldFile = oldFile; this.newFile = newFile; this.patch = patch; - // const metricsData = {package: 'github'}; - // if (this.getPatch()) { - // metricsData.sizeInBytes = this.getByteSize(); - // } - // - // addEvent('file-patch-constructed', metricsData); } isPresent() { @@ -183,16 +176,17 @@ export default class FilePatch { } buildStagePatchForLines(originalBuffer, nextLayeredBuffer, selectedLineSet) { - let newFile = this.getOldFile(); - - if (this.hasTypechange() && this.getStatus() === 'deleted') { - // Handle the special case when symlink is created where an entire file was deleted. In order to stage the file - // deletion, we must ensure that the created file patch has no new file. + let newFile = this.getNewFile(); + if (this.getStatus() === 'deleted') { if ( this.patch.getChangedLineCount() === selectedLineSet.size && Array.from(selectedLineSet, row => this.patch.containsRow(row)).every(Boolean) ) { + // Whole file deletion staged. newFile = nullFile; + } else { + // Partial file deletion, which becomes a modification. + newFile = this.getOldFile(); } } @@ -201,12 +195,7 @@ export default class FilePatch { nextLayeredBuffer, selectedLineSet, ); - if (this.getStatus() === 'deleted') { - // Populate newFile - return this.clone({newFile, patch}); - } else { - return this.clone({patch}); - } + return this.clone({newFile, patch}); } buildUnstagePatchForLines(originalBuffer, nextLayeredBuffer, selectedLineSet) { @@ -235,16 +224,12 @@ export default class FilePatch { } } - const {patch, buffer, layers} = this.patch.buildUnstagePatchForLines( + const patch = this.patch.buildUnstagePatchForLines( originalBuffer, nextLayeredBuffer, selectedLineSet, ); - return { - filePatch: this.clone({oldFile, newFile, patch}), - buffer, - layers, - }; + return this.clone({oldFile, newFile, patch}); } isEqual(other) { diff --git a/test/models/patch/file-patch.test.js b/test/models/patch/file-patch.test.js index 0726890900a..0fe38ffcff6 100644 --- a/test/models/patch/file-patch.test.js +++ b/test/models/patch/file-patch.test.js @@ -6,7 +6,6 @@ import Patch from '../../../lib/models/patch/patch'; import Hunk from '../../../lib/models/patch/hunk'; import {Unchanged, Addition, Deletion, NoNewline} from '../../../lib/models/patch/region'; import {assertInFilePatch} from '../../helpers'; -import * as reporterProxy from '../../../lib/reporter-proxy'; describe('FilePatch', function() { it('delegates methods to its files and patch', function() { @@ -23,15 +22,11 @@ describe('FilePatch', function() { }), ]; const marker = markRange(layers.patch); - const patch = new Patch({status: 'modified', hunks, buffer, layers, marker}); + const patch = new Patch({status: 'modified', hunks, marker}); const oldFile = new File({path: 'a.txt', mode: '120000', symlink: 'dest.txt'}); const newFile = new File({path: 'b.txt', mode: '100755'}); - sinon.stub(reporterProxy, 'addEvent'); - assert.isFalse(reporterProxy.addEvent.called); - const filePatch = new FilePatch(oldFile, newFile, patch); - assert.isTrue(reporterProxy.addEvent.calledOnceWithExactly('file-patch-constructed', {package: 'github', sizeInBytes: 15})); assert.isTrue(filePatch.isPresent()); @@ -43,28 +38,8 @@ describe('FilePatch', function() { assert.strictEqual(filePatch.getNewMode(), '100755'); assert.isUndefined(filePatch.getNewSymlink()); - assert.strictEqual(filePatch.getByteSize(), 15); - assert.strictEqual(filePatch.getBuffer().getText(), '0000\n0001\n0002\n'); assert.strictEqual(filePatch.getMarker(), marker); assert.strictEqual(filePatch.getMaxLineNumberWidth(), 1); - - const nBuffer = new TextBuffer({text: '0001\n0002\n'}); - const nLayers = buildLayers(nBuffer); - const nHunks = [ - new Hunk({ - oldStartRow: 3, oldRowCount: 1, newStartRow: 3, newRowCount: 2, - marker: markRange(nLayers.hunk, 0, 1), - regions: [ - new Unchanged(markRange(nLayers.unchanged, 0)), - new Addition(markRange(nLayers.addition, 1)), - ], - }), - ]; - const nPatch = new Patch({status: 'modified', hunks: nHunks, buffer: nBuffer, layers: nLayers}); - 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() { @@ -181,81 +156,6 @@ describe('FilePatch', function() { assert.deepEqual(filePatch.getStartRange().serialize(), [[1, 0], [1, 0]]); }); - it('adopts a buffer and layers from a prior FilePatch', function() { - const oldFile = new File({path: 'a.txt', mode: '100755'}); - const newFile = new File({path: 'b.txt', mode: '100755'}); - - const prevBuffer = new TextBuffer({text: '0000\n0001\n0002\n'}); - const prevLayers = buildLayers(prevBuffer); - const prevHunks = [ - new Hunk({ - oldStartRow: 2, oldRowCount: 2, newStartRow: 2, newRowCount: 3, - marker: markRange(prevLayers.hunk, 0, 2), - regions: [ - new Unchanged(markRange(prevLayers.unchanged, 0)), - new Addition(markRange(prevLayers.addition, 1)), - new Unchanged(markRange(prevLayers.unchanged, 2)), - ], - }), - ]; - const prevPatch = new Patch({status: 'modified', hunks: prevHunks, buffer: prevBuffer, layers: prevLayers}); - const prevFilePatch = new FilePatch(oldFile, newFile, prevPatch); - - const nextBuffer = new TextBuffer({text: '0000\n0001\n0002\n0003\n0004\n No newline at end of file'}); - const nextLayers = buildLayers(nextBuffer); - const nextHunks = [ - new Hunk({ - oldStartRow: 2, oldRowCount: 2, newStartRow: 2, newRowCount: 3, - marker: markRange(nextLayers.hunk, 0, 2), - regions: [ - new Unchanged(markRange(nextLayers.unchanged, 0)), - new Addition(markRange(nextLayers.addition, 1)), - new Unchanged(markRange(nextLayers.unchanged, 2)), - ], - }), - new Hunk({ - oldStartRow: 10, oldRowCount: 2, newStartRow: 11, newRowCount: 1, - marker: markRange(nextLayers.hunk, 3, 5), - regions: [ - new Unchanged(markRange(nextLayers.unchanged, 3)), - new Deletion(markRange(nextLayers.deletion, 4)), - new NoNewline(markRange(nextLayers.noNewline, 5)), - ], - }), - ]; - const nextPatch = new Patch({status: 'modified', hunks: nextHunks, buffer: nextBuffer, layers: nextLayers}); - const nextFilePatch = new FilePatch(oldFile, newFile, nextPatch); - - nextFilePatch.adoptBufferFrom(prevFilePatch); - - assert.strictEqual(nextFilePatch.getBuffer(), prevBuffer); - assert.strictEqual(nextFilePatch.getHunkLayer(), prevLayers.hunk); - assert.strictEqual(nextFilePatch.getUnchangedLayer(), prevLayers.unchanged); - assert.strictEqual(nextFilePatch.getAdditionLayer(), prevLayers.addition); - assert.strictEqual(nextFilePatch.getDeletionLayer(), prevLayers.deletion); - assert.strictEqual(nextFilePatch.getNoNewlineLayer(), prevLayers.noNewline); - - const rangesFrom = layer => layer.getMarkers().map(marker => marker.getRange().serialize()); - assert.deepEqual(rangesFrom(nextFilePatch.getHunkLayer()), [ - [[0, 0], [2, 4]], - [[3, 0], [5, 26]], - ]); - assert.deepEqual(rangesFrom(nextFilePatch.getUnchangedLayer()), [ - [[0, 0], [0, 4]], - [[2, 0], [2, 4]], - [[3, 0], [3, 4]], - ]); - assert.deepEqual(rangesFrom(nextFilePatch.getAdditionLayer()), [ - [[1, 0], [1, 4]], - ]); - assert.deepEqual(rangesFrom(nextFilePatch.getDeletionLayer()), [ - [[4, 0], [4, 4]], - ]); - assert.deepEqual(rangesFrom(nextFilePatch.getNoNewlineLayer()), [ - [[5, 0], [5, 26]], - ]); - }); - describe('file-level change detection', function() { let emptyPatch; @@ -347,7 +247,15 @@ describe('FilePatch', function() { assert.strictEqual(clone3.getPatch(), patch1); }); - describe('getStagePatchForLines()', function() { + describe('buildStagePatchForLines()', function() { + let stagedLayeredBuffer; + + beforeEach(function() { + const buffer = new TextBuffer(); + const layers = buildLayers(buffer); + stagedLayeredBuffer = {buffer, layers}; + }); + it('returns a new FilePatch that applies only the selected lines', function() { const buffer = new TextBuffer({text: '0000\n0001\n0002\n0003\n0004\n'}); const layers = buildLayers(buffer); @@ -363,17 +271,18 @@ describe('FilePatch', function() { ], }), ]; - const patch = new Patch({status: 'modified', hunks, buffer, layers}); + const marker = markRange(layers.patch, 0, 4); + const patch = new Patch({status: 'modified', hunks, marker}); 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])); + const stagedPatch = filePatch.buildStagePatchForLines(buffer, stagedLayeredBuffer, new Set([1, 3])); assert.strictEqual(stagedPatch.getStatus(), 'modified'); assert.strictEqual(stagedPatch.getOldFile(), oldFile); assert.strictEqual(stagedPatch.getNewFile(), newFile); - assert.strictEqual(stagedPatch.getBuffer().getText(), '0000\n0001\n0003\n0004\n'); - assertInFilePatch(stagedPatch).hunks( + assert.strictEqual(stagedLayeredBuffer.buffer.getText(), '0000\n0001\n0003\n0004\n'); + assertInFilePatch(stagedPatch, stagedLayeredBuffer.buffer).hunks( { startRow: 0, endRow: 3, @@ -389,10 +298,11 @@ describe('FilePatch', function() { }); describe('staging lines from deleted files', function() { + let buffer; let oldFile, deletionPatch; beforeEach(function() { - const buffer = new TextBuffer({text: '0000\n0001\n0002\n'}); + buffer = new TextBuffer({text: '0000\n0001\n0002\n'}); const layers = buildLayers(buffer); const hunks = [ new Hunk({ @@ -403,19 +313,20 @@ describe('FilePatch', function() { ], }), ]; - const patch = new Patch({status: 'deleted', hunks, buffer, layers}); + const marker = markRange(layers.patch, 0, 2); + const patch = new Patch({status: 'deleted', hunks, marker}); 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])); + const stagedPatch = deletionPatch.buildStagePatchForLines(buffer, stagedLayeredBuffer, new Set([1, 2])); assert.strictEqual(stagedPatch.getStatus(), 'modified'); assert.strictEqual(stagedPatch.getOldFile(), oldFile); assert.strictEqual(stagedPatch.getNewFile(), oldFile); - assert.strictEqual(stagedPatch.getBuffer().getText(), '0000\n0001\n0002\n'); - assertInFilePatch(stagedPatch).hunks( + assert.strictEqual(stagedLayeredBuffer.buffer.getText(), '0000\n0001\n0002\n'); + assertInFilePatch(stagedPatch, stagedLayeredBuffer.buffer).hunks( { startRow: 0, endRow: 2, @@ -429,12 +340,12 @@ describe('FilePatch', function() { }); it('handles staging all lines, leaving nothing unstaged', function() { - const stagedPatch = deletionPatch.getStagePatchForLines(new Set([1, 2, 3])); + const stagedPatch = deletionPatch.buildStagePatchForLines(buffer, stagedLayeredBuffer, new Set([0, 1, 2])); assert.strictEqual(stagedPatch.getStatus(), 'deleted'); assert.strictEqual(stagedPatch.getOldFile(), oldFile); assert.isFalse(stagedPatch.getNewFile().isPresent()); - assert.strictEqual(stagedPatch.getBuffer().getText(), '0000\n0001\n0002\n'); - assertInFilePatch(stagedPatch).hunks( + assert.strictEqual(stagedLayeredBuffer.buffer.getText(), '0000\n0001\n0002\n'); + assertInFilePatch(stagedPatch, stagedLayeredBuffer.buffer).hunks( { startRow: 0, endRow: 2, @@ -447,8 +358,8 @@ describe('FilePatch', function() { }); it('unsets the newFile when a symlink is created where a file was deleted', function() { - const buffer = new TextBuffer({text: '0000\n0001\n0002\n'}); - const layers = buildLayers(buffer); + const nBuffer = new TextBuffer({text: '0000\n0001\n0002\n'}); + const layers = buildLayers(nBuffer); const hunks = [ new Hunk({ oldStartRow: 1, oldRowCount: 3, newStartRow: 1, newRowCount: 0, @@ -458,65 +369,28 @@ describe('FilePatch', function() { ], }), ]; - const patch = new Patch({status: 'deleted', hunks, buffer, layers}); + const marker = markRange(layers.patch, 0, 2); + const patch = new Patch({status: 'deleted', hunks, marker}); 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])); + const stagedPatch = replacePatch.buildStagePatchForLines(nBuffer, stagedLayeredBuffer, new Set([0, 1, 2])); assert.strictEqual(stagedPatch.getOldFile(), oldFile); assert.isFalse(stagedPatch.getNewFile().isPresent()); }); }); }); - it('stages an entire hunk at once', function() { - const buffer = new TextBuffer({text: '0000\n0001\n0002\n0003\n0004\n0005\n'}); - const layers = buildLayers(buffer); - const hunks = [ - new Hunk({ - oldStartRow: 10, oldRowCount: 2, newStartRow: 10, newRowCount: 3, - marker: markRange(layers.hunk, 0, 2), - regions: [ - new Unchanged(markRange(layers.unchanged, 0)), - new Addition(markRange(layers.addition, 1)), - new Unchanged(markRange(layers.unchanged, 2)), - ], - }), - new Hunk({ - oldStartRow: 20, oldRowCount: 3, newStartRow: 19, newRowCount: 2, - marker: markRange(layers.hunk, 3, 5), - regions: [ - new Unchanged(markRange(layers.unchanged, 3)), - new Deletion(markRange(layers.deletion, 4)), - new Unchanged(markRange(layers.unchanged, 5)), - ], - }), - ]; - const patch = new Patch({status: 'modified', hunks, buffer, layers}); - 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); + describe('getUnstagePatchForLines()', function() { + let unstageLayeredBuffer; - const stagedPatch = filePatch.getStagePatchForHunk(hunks[1]); - assert.strictEqual(stagedPatch.getBuffer().getText(), '0003\n0004\n0005\n'); - assert.strictEqual(stagedPatch.getOldFile(), oldFile); - assert.strictEqual(stagedPatch.getNewFile(), newFile); - assertInFilePatch(stagedPatch).hunks( - { - startRow: 0, - endRow: 2, - header: '@@ -20,3 +18,2 @@', - regions: [ - {kind: 'unchanged', string: ' 0003\n', range: [[0, 0], [0, 4]]}, - {kind: 'deletion', string: '-0004\n', range: [[1, 0], [1, 4]]}, - {kind: 'unchanged', string: ' 0005\n', range: [[2, 0], [2, 4]]}, - ], - }, - ); - }); + beforeEach(function() { + const buffer = new TextBuffer(); + const layers = buildLayers(buffer); + unstageLayeredBuffer = {buffer, layers}; + }); - describe('getUnstagePatchForLines()', function() { it('returns a new FilePatch that unstages only the specified lines', function() { const buffer = new TextBuffer({text: '0000\n0001\n0002\n0003\n0004\n'}); const layers = buildLayers(buffer); @@ -532,17 +406,18 @@ describe('FilePatch', function() { ], }), ]; - const patch = new Patch({status: 'modified', hunks, buffer, layers}); + const marker = markRange(layers.patch, 0, 4); + const patch = new Patch({status: 'modified', hunks, marker}); 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])); + const unstagedPatch = filePatch.buildUnstagePatchForLines(buffer, unstageLayeredBuffer, new Set([1, 3])); assert.strictEqual(unstagedPatch.getStatus(), 'modified'); assert.strictEqual(unstagedPatch.getOldFile(), newFile); assert.strictEqual(unstagedPatch.getNewFile(), newFile); - assert.strictEqual(unstagedPatch.getBuffer().getText(), '0000\n0001\n0002\n0003\n0004\n'); - assertInFilePatch(unstagedPatch).hunks( + assert.strictEqual(unstageLayeredBuffer.buffer.getText(), '0000\n0001\n0002\n0003\n0004\n'); + assertInFilePatch(unstagedPatch, unstageLayeredBuffer.buffer).hunks( { startRow: 0, endRow: 4, @@ -559,10 +434,11 @@ describe('FilePatch', function() { }); describe('unstaging lines from an added file', function() { + let buffer; let newFile, addedPatch, addedFilePatch; beforeEach(function() { - const buffer = new TextBuffer({text: '0000\n0001\n0002\n'}); + buffer = new TextBuffer({text: '0000\n0001\n0002\n'}); const layers = buildLayers(buffer); const hunks = [ new Hunk({ @@ -573,17 +449,18 @@ describe('FilePatch', function() { ], }), ]; + const marker = markRange(layers.patch, 0, 2); newFile = new File({path: 'file.txt', mode: '100644'}); - addedPatch = new Patch({status: 'added', hunks, buffer, layers}); + addedPatch = new Patch({status: 'added', hunks, marker}); addedFilePatch = new FilePatch(nullFile, newFile, addedPatch); }); it('handles unstaging part of the file', function() { - const unstagePatch = addedFilePatch.getUnstagePatchForLines(new Set([2])); + const unstagePatch = addedFilePatch.buildUnstagePatchForLines(buffer, unstageLayeredBuffer, new Set([2])); assert.strictEqual(unstagePatch.getStatus(), 'modified'); assert.strictEqual(unstagePatch.getOldFile(), newFile); assert.strictEqual(unstagePatch.getNewFile(), newFile); - assertInFilePatch(unstagePatch).hunks( + assertInFilePatch(unstagePatch, unstageLayeredBuffer.buffer).hunks( { startRow: 0, endRow: 2, @@ -597,11 +474,11 @@ describe('FilePatch', function() { }); it('handles unstaging all lines, leaving nothing staged', function() { - const unstagePatch = addedFilePatch.getUnstagePatchForLines(new Set([0, 1, 2])); + const unstagePatch = addedFilePatch.buildUnstagePatchForLines(buffer, unstageLayeredBuffer, new Set([0, 1, 2])); assert.strictEqual(unstagePatch.getStatus(), 'deleted'); assert.strictEqual(unstagePatch.getOldFile(), newFile); assert.isFalse(unstagePatch.getNewFile().isPresent()); - assertInFilePatch(unstagePatch).hunks( + assertInFilePatch(unstagePatch, unstageLayeredBuffer.buffer).hunks( { startRow: 0, endRow: 2, @@ -616,10 +493,10 @@ describe('FilePatch', function() { it('unsets the newFile 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])); + const unstagePatch = patch.buildUnstagePatchForLines(buffer, unstageLayeredBuffer, new Set([0, 1, 2])); assert.strictEqual(unstagePatch.getOldFile(), newFile); assert.isFalse(unstagePatch.getNewFile().isPresent()); - assertInFilePatch(unstagePatch).hunks( + assertInFilePatch(unstagePatch, unstageLayeredBuffer.buffer).hunks( { startRow: 0, endRow: 2, @@ -633,10 +510,10 @@ describe('FilePatch', function() { }); describe('unstaging lines from a removed file', function() { - let oldFile, removedFilePatch; + let oldFile, removedFilePatch, buffer; beforeEach(function() { - const buffer = new TextBuffer({text: '0000\n0001\n0002\n'}); + buffer = new TextBuffer({text: '0000\n0001\n0002\n'}); const layers = buildLayers(buffer); const hunks = [ new Hunk({ @@ -648,16 +525,17 @@ describe('FilePatch', function() { }), ]; oldFile = new File({path: 'file.txt', mode: '100644'}); - const removedPatch = new Patch({status: 'deleted', hunks, buffer, layers}); + const marker = markRange(layers.patch, 0, 2); + const removedPatch = new Patch({status: 'deleted', hunks, marker}); removedFilePatch = new FilePatch(oldFile, nullFile, removedPatch); }); it('handles unstaging part of the file', function() { - const discardPatch = removedFilePatch.getUnstagePatchForLines(new Set([1])); + const discardPatch = removedFilePatch.buildUnstagePatchForLines(buffer, unstageLayeredBuffer, new Set([1])); assert.strictEqual(discardPatch.getStatus(), 'added'); assert.strictEqual(discardPatch.getOldFile(), nullFile); assert.strictEqual(discardPatch.getNewFile(), oldFile); - assertInFilePatch(discardPatch).hunks( + assertInFilePatch(discardPatch, unstageLayeredBuffer.buffer).hunks( { startRow: 0, endRow: 0, @@ -670,11 +548,15 @@ describe('FilePatch', function() { }); it('handles unstaging the entire file', function() { - const discardPatch = removedFilePatch.getUnstagePatchForLines(new Set([0, 1, 2])); + const discardPatch = removedFilePatch.buildUnstagePatchForLines( + buffer, + unstageLayeredBuffer, + new Set([0, 1, 2]), + ); assert.strictEqual(discardPatch.getStatus(), 'added'); assert.strictEqual(discardPatch.getOldFile(), nullFile); assert.strictEqual(discardPatch.getNewFile(), oldFile); - assertInFilePatch(discardPatch).hunks( + assertInFilePatch(discardPatch, unstageLayeredBuffer.buffer).hunks( { startRow: 0, endRow: 2, @@ -688,53 +570,7 @@ describe('FilePatch', function() { }); }); - it('unstages an entire hunk at once', function() { - const buffer = new TextBuffer({text: '0000\n0001\n0002\n0003\n0004\n0005\n'}); - const layers = buildLayers(buffer); - const hunks = [ - new Hunk({ - oldStartRow: 10, oldRowCount: 2, newStartRow: 10, newRowCount: 3, - marker: markRange(layers.hunk, 0, 2), - regions: [ - new Unchanged(markRange(layers.unchanged, 0)), - new Addition(markRange(layers.addition, 1)), - new Unchanged(markRange(layers.unchanged, 2)), - ], - }), - new Hunk({ - oldStartRow: 20, oldRowCount: 3, newStartRow: 19, newRowCount: 2, - marker: markRange(layers.hunk, 3, 5), - regions: [ - new Unchanged(markRange(layers.unchanged, 3)), - new Deletion(markRange(layers.deletion, 4)), - new Unchanged(markRange(layers.unchanged, 5)), - ], - }), - ]; - const patch = new Patch({status: 'modified', hunks, buffer, layers}); - 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.getBuffer().getText(), '0000\n0001\n0002\n'); - assert.strictEqual(unstagedPatch.getOldFile(), newFile); - assert.strictEqual(unstagedPatch.getNewFile(), newFile); - assertInFilePatch(unstagedPatch).hunks( - { - startRow: 0, - endRow: 2, - header: '@@ -10,3 +10,2 @@', - regions: [ - {kind: 'unchanged', string: ' 0000\n', range: [[0, 0], [0, 4]]}, - {kind: 'deletion', string: '-0001\n', range: [[1, 0], [1, 4]]}, - {kind: 'unchanged', string: ' 0002\n', range: [[2, 0], [2, 4]]}, - ], - }, - ); - }); - - describe('toString()', function() { + describe('toStringIn()', function() { it('converts the patch to the standard textual format', function() { const buffer = new TextBuffer({text: '0000\n0001\n0002\n0003\n0004\n0005\n0006\n0007\n'}); const layers = buildLayers(buffer); @@ -759,7 +595,8 @@ describe('FilePatch', function() { ], }), ]; - const patch = new Patch({status: 'modified', hunks, buffer, layers}); + const marker = markRange(layers.patch, 0, 7); + const patch = new Patch({status: 'modified', hunks, marker}); 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); @@ -778,7 +615,7 @@ describe('FilePatch', function() { ' 0005\n' + '+0006\n' + ' 0007\n'; - assert.strictEqual(filePatch.toString(), expectedString); + assert.strictEqual(filePatch.toStringIn(buffer), expectedString); }); it('correctly formats a file with no newline at the end', function() { @@ -795,7 +632,8 @@ describe('FilePatch', function() { ], }), ]; - const patch = new Patch({status: 'modified', hunks, buffer, layers}); + const marker = markRange(layers.patch, 0, 2); + const patch = new Patch({status: 'modified', hunks, marker}); 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); @@ -808,7 +646,7 @@ describe('FilePatch', function() { ' 0000\n' + '+0001\n' + '\\ No newline at end of file\n'; - assert.strictEqual(filePatch.toString(), expectedString); + assert.strictEqual(filePatch.toStringIn(buffer), expectedString); }); describe('typechange file patches', function() { @@ -824,7 +662,8 @@ describe('FilePatch', function() { ], }), ]; - const patch = new Patch({status: 'added', hunks, buffer, layers}); + const marker = markRange(layers.patch, 0, 1); + const patch = new Patch({status: 'added', hunks, marker}); 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); @@ -844,7 +683,7 @@ describe('FilePatch', function() { '@@ -1,0 +1,2 @@\n' + '+0000\n' + '+0001\n'; - assert.strictEqual(filePatch.toString(), expectedString); + assert.strictEqual(filePatch.toStringIn(buffer), expectedString); }); it('handles typechange patches for a file replaced with a symlink', function() { @@ -859,7 +698,8 @@ describe('FilePatch', function() { ], }), ]; - const patch = new Patch({status: 'deleted', hunks, buffer, layers}); + const marker = markRange(layers.patch, 0, 1); + const patch = new Patch({status: 'deleted', hunks, marker}); 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); @@ -879,15 +719,12 @@ describe('FilePatch', function() { '@@ -0,0 +1 @@\n' + '+dest.txt\n' + '\\ No newline at end of file\n'; - assert.strictEqual(filePatch.toString(), expectedString); + assert.strictEqual(filePatch.toStringIn(buffer), expectedString); }); }); }); it('has a nullFilePatch that stubs all FilePatch methods', function() { - const buffer = new TextBuffer({text: '0\n1\n2\n3\n'}); - const marker = markRange(buffer, 0, 1); - const nullFilePatch = FilePatch.createNull(); assert.isFalse(nullFilePatch.isPresent()); @@ -900,27 +737,18 @@ describe('FilePatch', function() { assert.isNull(nullFilePatch.getNewMode()); assert.isNull(nullFilePatch.getOldSymlink()); assert.isNull(nullFilePatch.getNewSymlink()); - assert.strictEqual(nullFilePatch.getByteSize(), 0); - assert.strictEqual(nullFilePatch.getBuffer().getText(), ''); assert.lengthOf(nullFilePatch.getAdditionRanges(), 0); assert.lengthOf(nullFilePatch.getDeletionRanges(), 0); assert.lengthOf(nullFilePatch.getNoNewlineRanges(), 0); - assert.lengthOf(nullFilePatch.getHunkLayer().getMarkers(), 0); - assert.lengthOf(nullFilePatch.getUnchangedLayer().getMarkers(), 0); - assert.lengthOf(nullFilePatch.getAdditionLayer().getMarkers(), 0); - assert.lengthOf(nullFilePatch.getDeletionLayer().getMarkers(), 0); - assert.lengthOf(nullFilePatch.getNoNewlineLayer().getMarkers(), 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({regions: [], marker})).isPresent()); - assert.isFalse(nullFilePatch.getUnstagePatchForLines(new Set([0])).isPresent()); - assert.isFalse(nullFilePatch.getUnstagePatchForHunk(new Hunk({regions: [], marker})).isPresent()); - assert.strictEqual(nullFilePatch.toString(), ''); + assert.isFalse(nullFilePatch.buildStagePatchForLines(new Set([0])).isPresent()); + assert.isFalse(nullFilePatch.buildUnstagePatchForLines(new Set([0])).isPresent()); + assert.strictEqual(nullFilePatch.toStringIn(new TextBuffer()), ''); }); }); From c8c0ef04286fb3c16be509b5908b4b2de4ee586e Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 7 Nov 2018 15:51:06 -0500 Subject: [PATCH 173/284] :fire: dot only --- test/controllers/multi-file-patch-controller.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/controllers/multi-file-patch-controller.test.js b/test/controllers/multi-file-patch-controller.test.js index 29b86f23cec..f33be04ea21 100644 --- a/test/controllers/multi-file-patch-controller.test.js +++ b/test/controllers/multi-file-patch-controller.test.js @@ -7,7 +7,7 @@ import MultiFilePatchController from '../../lib/controllers/multi-file-patch-con import * as reporterProxy from '../../lib/reporter-proxy'; import {cloneRepository, buildRepository} from '../helpers'; -describe.only('MultiFilePatchController', function() { +describe('MultiFilePatchController', function() { let atomEnv, repository, multiFilePatch; beforeEach(async function() { From 772c36d50da4d4dfd1a0f8c4dea538b8ab6e7d3a Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 7 Nov 2018 15:53:45 -0500 Subject: [PATCH 174/284] :fire: another dot only --- test/views/multi-file-patch-view.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/views/multi-file-patch-view.test.js b/test/views/multi-file-patch-view.test.js index 05248898845..de957868a7e 100644 --- a/test/views/multi-file-patch-view.test.js +++ b/test/views/multi-file-patch-view.test.js @@ -8,7 +8,7 @@ import {nullFile} from '../../lib/models/patch/file'; import FilePatch from '../../lib/models/patch/file-patch'; import RefHolder from '../../lib/models/ref-holder'; -describe.only('MultiFilePatchView', function() { +describe('MultiFilePatchView', function() { let atomEnv, workspace, repository, filePatch; beforeEach(async function() { From bbfd5100d1f79a46a0b72d962c09a55eaa52fa76 Mon Sep 17 00:00:00 2001 From: Vanessa Yuen Date: Wed, 7 Nov 2018 22:19:39 +0100 Subject: [PATCH 175/284] getpatchlayer --- lib/models/patch/multi-file-patch.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/models/patch/multi-file-patch.js b/lib/models/patch/multi-file-patch.js index ca7450add76..9230ea3a2a0 100644 --- a/lib/models/patch/multi-file-patch.js +++ b/lib/models/patch/multi-file-patch.js @@ -32,6 +32,10 @@ export default class MultiFilePatch { return this.hunkLayer; } + getPatchLayer() { + return this.patchLayer; + } + getUnchangedLayer() { return this.unchangedLayer; } From ae8a4fb7490b58d20a9507d25b43131834b595e9 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 7 Nov 2018 16:32:07 -0500 Subject: [PATCH 176/284] WIP MultiFilePatch tests --- lib/models/patch/multi-file-patch.js | 4 + test/models/patch/multi-file-patch.test.js | 228 ++++++++++++++++++++- 2 files changed, 231 insertions(+), 1 deletion(-) diff --git a/lib/models/patch/multi-file-patch.js b/lib/models/patch/multi-file-patch.js index ca7450add76..200e7575698 100644 --- a/lib/models/patch/multi-file-patch.js +++ b/lib/models/patch/multi-file-patch.js @@ -28,6 +28,10 @@ export default class MultiFilePatch { return this.buffer; } + getPatchLayer() { + return this.patchLayer; + } + getHunkLayer() { return this.hunkLayer; } diff --git a/test/models/patch/multi-file-patch.test.js b/test/models/patch/multi-file-patch.test.js index 7a64d746146..2e350cd5611 100644 --- a/test/models/patch/multi-file-patch.test.js +++ b/test/models/patch/multi-file-patch.test.js @@ -1,4 +1,5 @@ import {TextBuffer} from 'atom'; +import dedent from 'dedent-js'; import MultiFilePatch from '../../../lib/models/patch/multi-file-patch'; import FilePatch from '../../../lib/models/patch/file-patch'; @@ -6,6 +7,7 @@ import File from '../../../lib/models/patch/file'; import Patch from '../../../lib/models/patch/patch'; import Hunk from '../../../lib/models/patch/hunk'; import {Unchanged, Addition, Deletion} from '../../../lib/models/patch/region'; +import {assertInFilePatch} from '../../helpers'; describe('MultiFilePatch', function() { let buffer, layers; @@ -89,6 +91,7 @@ describe('MultiFilePatch', function() { nextPatch.adoptBufferFrom(lastPatch); assert.strictEqual(nextPatch.getBuffer(), lastBuffer); + assert.strictEqual(nextPatch.getPatchLayer(), lastLayers.patch); assert.strictEqual(nextPatch.getHunkLayer(), lastLayers.hunk); assert.strictEqual(nextPatch.getUnchangedLayer(), lastLayers.unchanged); assert.strictEqual(nextPatch.getAdditionLayer(), lastLayers.addition); @@ -98,6 +101,229 @@ describe('MultiFilePatch', function() { assert.lengthOf(nextPatch.getHunkLayer().getMarkers(), 8); }); + it('generates a stage patch for arbitrary buffer rows', function() { + const filePatches = [ + buildFilePatchFixture(0), + buildFilePatchFixture(1), + buildFilePatchFixture(2), + buildFilePatchFixture(3), + ]; + const original = new MultiFilePatch(buffer, layers, filePatches); + const stagePatch = original.getStagePatchForLines(new Set([18, 24, 44, 45])); + + assert.strictEqual(stagePatch.getBuffer().getText(), dedent` + file-1 line-0 + file-1 line-1 + file-1 line-2 + file-1 line-3 + file-1 line-4 + file-1 line-6 + file-1 line-7 + file-3 line-0 + file-3 line-1 + file-3 line-2 + file-3 line-3 + `); + + assert.lengthOf(stagePatch.getFilePatches(), 2); + const [fp0, fp1] = stagePatch.getFilePatches(); + assert.strictEqual(fp0.getOldPath(), 'file-1.txt'); + assertInFilePatch(fp0, buffer).hunks( + { + startRow: 0, endRow: 3, + header: '@@ -0,4 +0,3 @@', + regions: [ + {kind: 'unchanged', string: ' file-1 line-0\n', range: [[0, 0], [0, 13]]}, + {kind: 'addition', string: '+file-1 line-1\n', range: [[1, 0], [1, 13]]}, + {kind: 'unchanged', string: ' file-1 line-2\n file-1 line-3\n', range: [[2, 0], [3, 13]]}, + ], + }, + { + startRow: 4, endRow: 8, + header: '@@ -10,3 +9,3 @@', + regions: [ + {kind: 'unchanged', string: ' file-1 line-4\n', range: [[4, 0], [4, 13]]}, + {kind: 'deletion', string: '-file-1 line-6\n', range: [[5, 0], [5, 13]]}, + {kind: 'unchanged', string: ' file-1 line-7\n', range: [[6, 0], [6, 13]]}, + ], + }, + ); + + assert.strictEqual(fp1.getOldPath(), 'file-3.txt'); + assertInFilePatch(fp1, buffer).hunks( + { + startRow: 9, endRow: 12, + header: '@@ -0,3 +0.3 @@', + regions: [ + {kind: 'unchanged', string: ' file-3 line-0\n', range: [[7, 0], [7, 13]]}, + {kind: 'addition', string: '+file-3 line-1\n', range: [[8, 0], [8, 13]]}, + {kind: 'deletion', string: '-file-3 line-2\n', range: [[9, 0], [9, 13]]}, + {kind: 'unchanged', string: ' file-3 line-3\n', range: [[10, 0], [10, 13]]}, + ], + }, + ); + }); + + // FIXME adapt these to the lifted method. + // describe('next selection range derivation', function() { + // it('selects the first change region after the highest buffer row', function() { + // const lastPatch = buildPatchFixture(); + // // Selected: + // // deletions (1-2) and partial addition (4 from 3-5) from hunk 0 + // // one deletion row (13 from 12-16) from the middle of hunk 1; + // // nothing in hunks 2 or 3 + // const lastSelectedRows = new Set([1, 2, 4, 5, 13]); + // + // const nBuffer = new TextBuffer({text: + // // 0 1 2 3 4 + // '0000\n0003\n0004\n0005\n0006\n' + + // // 5 6 7 8 9 10 11 12 13 14 15 + // '0007\n0008\n0009\n0010\n0011\n0012\n0014\n0015\n0016\n0017\n0018\n' + + // // 16 17 18 19 20 + // '0019\n0020\n0021\n0022\n0023\n' + + // // 21 22 23 + // '0024\n0025\n No newline at end of file\n', + // }); + // const nLayers = buildLayers(nBuffer); + // const nHunks = [ + // new Hunk({ + // oldStartRow: 3, oldRowCount: 3, newStartRow: 3, newRowCount: 5, // next row drift = +2 + // marker: markRange(nLayers.hunk, 0, 4), + // regions: [ + // new Unchanged(markRange(nLayers.unchanged, 0)), // 0 + // new Addition(markRange(nLayers.addition, 1)), // + 1 + // new Unchanged(markRange(nLayers.unchanged, 2)), // 2 + // new Addition(markRange(nLayers.addition, 3)), // + 3 + // new Unchanged(markRange(nLayers.unchanged, 4)), // 4 + // ], + // }), + // new Hunk({ + // oldStartRow: 12, oldRowCount: 9, newStartRow: 14, newRowCount: 7, // next row drift = +2 -2 = 0 + // marker: markRange(nLayers.hunk, 5, 15), + // regions: [ + // new Unchanged(markRange(nLayers.unchanged, 5)), // 5 + // new Addition(markRange(nLayers.addition, 6)), // +6 + // new Unchanged(markRange(nLayers.unchanged, 7, 9)), // 7 8 9 + // new Deletion(markRange(nLayers.deletion, 10, 13)), // -10 -11 -12 -13 + // new Addition(markRange(nLayers.addition, 14)), // +14 + // new Unchanged(markRange(nLayers.unchanged, 15)), // 15 + // ], + // }), + // new Hunk({ + // oldStartRow: 26, oldRowCount: 4, newStartRow: 26, newRowCount: 3, // next row drift = 0 -1 = -1 + // marker: markRange(nLayers.hunk, 16, 20), + // regions: [ + // new Unchanged(markRange(nLayers.unchanged, 16)), // 16 + // new Addition(markRange(nLayers.addition, 17)), // +17 + // new Deletion(markRange(nLayers.deletion, 18, 19)), // -18 -19 + // new Unchanged(markRange(nLayers.unchanged, 20)), // 20 + // ], + // }), + // new Hunk({ + // oldStartRow: 32, oldRowCount: 1, newStartRow: 31, newRowCount: 2, + // marker: markRange(nLayers.hunk, 22, 24), + // regions: [ + // new Unchanged(markRange(nLayers.unchanged, 22)), // 22 + // new Addition(markRange(nLayers.addition, 23)), // +23 + // new NoNewline(markRange(nLayers.noNewline, 24)), + // ], + // }), + // ]; + // const nextPatch = new Patch({status: 'modified', hunks: nHunks, buffer: nBuffer, layers: nLayers}); + // + // const nextRange = nextPatch.getNextSelectionRange(lastPatch, lastSelectedRows); + // // Original buffer row 14 = the next changed row = new buffer row 11 + // assert.deepEqual(nextRange, [[11, 0], [11, Infinity]]); + // }); + // + // it('offsets the chosen selection index by hunks that were completely selected', function() { + // const buffer = buildBuffer(11); + // const layers = buildLayers(buffer); + // const lastPatch = new Patch({ + // status: 'modified', + // hunks: [ + // new Hunk({ + // oldStartRow: 1, oldRowCount: 3, newStartRow: 1, newRowCount: 3, + // marker: markRange(layers.hunk, 0, 5), + // regions: [ + // new Unchanged(markRange(layers.unchanged, 0)), + // new Addition(markRange(layers.addition, 1, 2)), + // new Deletion(markRange(layers.deletion, 3, 4)), + // new Unchanged(markRange(layers.unchanged, 5)), + // ], + // }), + // new Hunk({ + // oldStartRow: 5, oldRowCount: 4, newStartRow: 5, newRowCount: 4, + // marker: markRange(layers.hunk, 6, 11), + // regions: [ + // new Unchanged(markRange(layers.unchanged, 6)), + // new Addition(markRange(layers.addition, 7, 8)), + // new Deletion(markRange(layers.deletion, 9, 10)), + // new Unchanged(markRange(layers.unchanged, 11)), + // ], + // }), + // ], + // buffer, + // layers, + // }); + // // 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 nextBuffer = new TextBuffer({text: '0006\n0007\n0008\n0009\n0010\n0011\n'}); + // const nextLayers = buildLayers(nextBuffer); + // const nextPatch = new Patch({ + // status: 'modified', + // hunks: [ + // new Hunk({ + // oldStartRow: 5, oldRowCount: 4, newStartRow: 5, newRowCount: 4, + // marker: markRange(nextLayers.hunk, 0, 5), + // regions: [ + // new Unchanged(markRange(nextLayers.unchanged, 0)), + // new Addition(markRange(nextLayers.addition, 1)), + // new Deletion(markRange(nextLayers.deletion, 3, 4)), + // new Unchanged(markRange(nextLayers.unchanged, 5)), + // ], + // }), + // ], + // buffer: nextBuffer, + // layers: nextLayers, + // }); + // + // 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 buffer = lastPatch.getBuffer(); + // const layers = buildLayers(buffer); + // const nextPatch = new Patch({ + // status: 'modified', + // hunks: [ + // new Hunk({ + // oldStartRow: 1, oldRowCount: 3, newStartRow: 1, newRowCount: 4, + // marker: markRange(layers.hunk, 0, 4), + // regions: [ + // new Unchanged(markRange(layers.unchanged, 0)), + // new Addition(markRange(layers.addition, 1, 2)), + // new Deletion(markRange(layers.deletion, 3)), + // new Unchanged(markRange(layers.unchanged, 4)), + // ], + // }), + // ], + // buffer, + // layers, + // }); + // + // const range = nextPatch.getNextSelectionRange(lastPatch, lastSelectedRows); + // assert.deepEqual(range, [[1, 0], [1, Infinity]]); + // }); + // }); + function buildFilePatchFixture(index) { const rowOffset = buffer.getLastRow(); for (let i = 0; i < 8; i++) { @@ -132,7 +358,7 @@ describe('MultiFilePatch', function() { ]; const marker = mark(layers.patch, 0, 7); - const patch = new Patch({status: 'modified', hunks, buffer, layers, marker}); + const patch = new Patch({status: 'modified', hunks, marker}); const oldFile = new File({path: `file-${index}.txt`, mode: '100644'}); const newFile = new File({path: `file-${index}.txt`, mode: '100644'}); From 2b1b4a9b7133da9d8f06b6bff4a827c7b26b0764 Mon Sep 17 00:00:00 2001 From: Vanessa Yuen Date: Wed, 7 Nov 2018 22:51:07 +0100 Subject: [PATCH 177/284] probably should be for..in since we're interating through an array --- lib/models/patch/multi-file-patch.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/models/patch/multi-file-patch.js b/lib/models/patch/multi-file-patch.js index 9230ea3a2a0..ce38ccc42a0 100644 --- a/lib/models/patch/multi-file-patch.js +++ b/lib/models/patch/multi-file-patch.js @@ -231,7 +231,7 @@ export default class MultiFilePatch { const filePatches = new Set(); let lastFilePatch = null; - for (const row in sortedRowSet) { + for (const row of sortedRowSet) { // Because the rows are sorted, consecutive rows will almost certainly belong to the same patch, so we can save // many avoidable marker index lookups by comparing with the last. if (lastFilePatch && lastFilePatch.containsRow(row)) { From 14184980799a854e5ec31fe6fa5796ee8ef2d9ce Mon Sep 17 00:00:00 2001 From: Tilde Ann Thurium Date: Wed, 7 Nov 2018 13:53:16 -0800 Subject: [PATCH 178/284] fix ChangedFileContainer tests --- .../containers/changed-file-container.test.js | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/test/containers/changed-file-container.test.js b/test/containers/changed-file-container.test.js index 40d4c09b216..3e40987e79b 100644 --- a/test/containers/changed-file-container.test.js +++ b/test/containers/changed-file-container.test.js @@ -54,45 +54,45 @@ describe('ChangedFileContainer', function() { assert.isTrue(wrapper.find('LoadingView').exists()); }); - it('renders a FilePatchView', async function() { + it('renders a MultiFilePatchController', async function() { const wrapper = mount(buildApp({relPath: 'a.txt', stagingStatus: 'unstaged'})); - await assert.async.isTrue(wrapper.update().find('FilePatchView').exists()); + await assert.async.isTrue(wrapper.update().find('MultiFilePatchController').exists()); }); it('adopts the buffer from the previous FilePatch when a new one arrives', async function() { const wrapper = mount(buildApp({relPath: 'a.txt', stagingStatus: 'unstaged'})); - await assert.async.isTrue(wrapper.update().find('FilePatchController').exists()); + await assert.async.isTrue(wrapper.update().find('MultiFilePatchController').exists()); - const prevPatch = wrapper.find('FilePatchController').prop('filePatch'); + const prevPatch = wrapper.find('MultiFilePatchController').prop('multiFilePatch'); const prevBuffer = prevPatch.getBuffer(); await fs.writeFile(path.join(repository.getWorkingDirectoryPath(), 'a.txt'), 'changed\nagain\n'); repository.refresh(); - await assert.async.notStrictEqual(wrapper.update().find('FilePatchController').prop('filePatch'), prevPatch); + await assert.async.notStrictEqual(wrapper.update().find('MultiFilePatchController').prop('multiFilePatch'), prevPatch); - const nextBuffer = wrapper.find('FilePatchController').prop('filePatch').getBuffer(); + const nextBuffer = wrapper.find('MultiFilePatchController').prop('multiFilePatch').getBuffer(); assert.strictEqual(nextBuffer, prevBuffer); }); it('does not adopt a buffer from an unchanged patch', async function() { const wrapper = mount(buildApp({relPath: 'a.txt', stagingStatus: 'unstaged'})); - await assert.async.isTrue(wrapper.update().find('FilePatchController').exists()); + await assert.async.isTrue(wrapper.update().find('MultiFilePatchController').exists()); - const prevPatch = wrapper.find('FilePatchController').prop('filePatch'); + const prevPatch = wrapper.find('MultiFilePatchController').prop('multiFilePatch'); sinon.spy(prevPatch, 'adoptBufferFrom'); wrapper.setProps({}); assert.isFalse(prevPatch.adoptBufferFrom.called); - const nextPatch = wrapper.find('FilePatchController').prop('filePatch'); + const nextPatch = wrapper.find('MultiFilePatchController').prop('multiFilePatch'); assert.strictEqual(nextPatch, prevPatch); }); 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); + await assert.async.strictEqual(wrapper.update().find('MultiFilePatchView').prop('extra'), extra); }); }); From 8a49372279866da3fdb39c6873a841579de1af8b Mon Sep 17 00:00:00 2001 From: Tilde Ann Thurium Date: Wed, 7 Nov 2018 17:35:21 -0800 Subject: [PATCH 179/284] fix some of the RootController tests --- test/controllers/root-controller.test.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/controllers/root-controller.test.js b/test/controllers/root-controller.test.js index 5cd8fe293de..1c1071a31b8 100644 --- a/test/controllers/root-controller.test.js +++ b/test/controllers/root-controller.test.js @@ -495,6 +495,7 @@ describe('RootController', function() { fs.writeFileSync(path.join(workdirPath, 'a.txt'), 'modification\n'); const unstagedFilePatch = await repository.getFilePatchForPath('a.txt'); + const unstagedFilePatch = multiFilePatch.getFilePatches()[0]; const editor = await workspace.open(path.join(workdirPath, 'a.txt')); @@ -511,14 +512,14 @@ describe('RootController', function() { sinon.stub(notificationManager, 'addError'); // unmodified buffer const hunkLines = unstagedFilePatch.getHunks()[0].getBufferRows(); - await wrapper.instance().discardLines(unstagedFilePatch, new Set([hunkLines[0]])); + await wrapper.instance().discardLines(multiFilePatch, new Set([hunkLines[0]])); assert.isTrue(repository.applyPatchToWorkdir.calledOnce); assert.isFalse(notificationManager.addError.called); // modified buffer repository.applyPatchToWorkdir.reset(); editor.setText('modify contents'); - await wrapper.instance().discardLines(unstagedFilePatch, new Set(unstagedFilePatch.getHunks()[0].getBufferRows())); + await wrapper.instance().discardLines(multiFilePatch, new Set(unstagedFilePatch.getHunks()[0].getBufferRows())); assert.isFalse(repository.applyPatchToWorkdir.called); const notificationArgs = notificationManager.addError.args[0]; assert.equal(notificationArgs[0], 'Cannot discard lines.'); From 680f3731fc39bc4360f4d1307263bcf6623533d3 Mon Sep 17 00:00:00 2001 From: Tilde Ann Thurium Date: Wed, 7 Nov 2018 17:37:22 -0800 Subject: [PATCH 180/284] WIP - why `RootController` tests no worky --- test/controllers/root-controller.test.js | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/test/controllers/root-controller.test.js b/test/controllers/root-controller.test.js index 1c1071a31b8..9348ea779fc 100644 --- a/test/controllers/root-controller.test.js +++ b/test/controllers/root-controller.test.js @@ -20,7 +20,6 @@ import * as reporterProxy from '../../lib/reporter-proxy'; 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; @@ -494,7 +493,7 @@ describe('RootController', function() { const repository = await buildRepository(workdirPath); fs.writeFileSync(path.join(workdirPath, 'a.txt'), 'modification\n'); - const unstagedFilePatch = await repository.getFilePatchForPath('a.txt'); + const multiFilePatch = await repository.getFilePatchForPath('a.txt'); const unstagedFilePatch = multiFilePatch.getFilePatches()[0]; const editor = await workspace.open(path.join(workdirPath, 'a.txt')); @@ -561,14 +560,16 @@ describe('RootController', function() { describe('undoLastDiscard(partialDiscardFilePath)', () => { describe('when partialDiscardFilePath is not null', () => { - let unstagedFilePatch, repository, absFilePath, wrapper; + let unstagedFilePatch, multiFilePatch, 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'); + multiFilePatch = await repository.getFilePatchForPath('sample.js'); + + unstagedFilePatch = multiFilePatch.getFilePatches()[0]; app = React.cloneElement(app, {repository}); wrapper = shallow(app); @@ -581,14 +582,16 @@ 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].getBufferRows().slice(0, 2))); + await wrapper.instance().discardLines(multiFilePatch, new Set(unstagedFilePatch.getHunks()[0].getBufferRows().slice(0, 2)), repository); const contents2 = fs.readFileSync(absFilePath, 'utf8'); + assert.notEqual(contents1, contents2); await repository.refresh(); - unstagedFilePatch = await repository.getFilePatchForPath('sample.js'); + multiFilePatch = await repository.getFilePatchForPath('sample.js'); + wrapper.setState({filePatch: unstagedFilePatch}); - await wrapper.instance().discardLines(unstagedFilePatch, new Set(unstagedFilePatch.getHunks()[0].getBufferRows().slice(2, 4))); + await wrapper.instance().discardLines(multiFilePatch, new Set(unstagedFilePatch.getHunks()[0].getBufferRows().slice(2, 4))); const contents3 = fs.readFileSync(absFilePath, 'utf8'); assert.notEqual(contents2, contents3); @@ -600,7 +603,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].getBufferRows().slice(0, 2))); + await wrapper.instance().discardLines(multiFilePatch, new Set(unstagedFilePatch.getHunks()[0].getBufferRows().slice(0, 2))); const contents2 = fs.readFileSync(absFilePath, 'utf8'); assert.notEqual(contents1, contents2); From 1513921d647bbdc65993f8955861c89b96d499fd Mon Sep 17 00:00:00 2001 From: Tilde Ann Thurium Date: Wed, 7 Nov 2018 18:04:31 -0800 Subject: [PATCH 181/284] fix test syntax oops --- test/controllers/root-controller.test.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/controllers/root-controller.test.js b/test/controllers/root-controller.test.js index 9348ea779fc..31ff53cf6db 100644 --- a/test/controllers/root-controller.test.js +++ b/test/controllers/root-controller.test.js @@ -20,6 +20,7 @@ import * as reporterProxy from '../../lib/reporter-proxy'; 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; @@ -486,6 +487,7 @@ import RootController from '../../lib/controllers/root-controller'; }); }); + // these tests no worky describe('discarding and restoring changed lines', () => { describe('discardLines(filePatch, lines)', () => { it('only discards lines if buffer is unmodified, otherwise notifies user', async () => { From 43199eed86510ba0b599f3cf9e97e0269b279da9 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Wed, 7 Nov 2018 16:39:16 -0800 Subject: [PATCH 182/284] Correctly invalidate cache in `applyPatchToIndex` based on MFP file paths --- lib/models/repository-states/present.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js index 38118725cb4..b8592892a06 100644 --- a/lib/models/repository-states/present.js +++ b/lib/models/repository-states/present.js @@ -273,11 +273,16 @@ export default class Present extends State { ); } - applyPatchToIndex(filePatch) { + applyPatchToIndex(multiFilePatch) { + const filePathSet = multiFilePatch.getFilePatches().reduce((pathSet, filePatch) => { + pathSet.add(filePatch.getOldPath()); + pathSet.add(filePatch.getNewPath()); + return pathSet; + }, new Set()); return this.invalidate( - () => Keys.cacheOperationKeys([filePatch.getOldPath(), filePatch.getNewPath()]), + () => Keys.cacheOperationKeys(Array.from(filePathSet)), () => { - const patchStr = filePatch.toString(); + const patchStr = multiFilePatch.toString(); return this.git().applyPatch(patchStr, {index: true}); }, ); From 05dff689962469201bf21faa2356f355df0f18df Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Wed, 7 Nov 2018 16:53:44 -0800 Subject: [PATCH 183/284] :fire: duplicate `getPatchLayer` --- lib/models/patch/multi-file-patch.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/models/patch/multi-file-patch.js b/lib/models/patch/multi-file-patch.js index 93255307d19..f1035d6709c 100644 --- a/lib/models/patch/multi-file-patch.js +++ b/lib/models/patch/multi-file-patch.js @@ -36,10 +36,6 @@ export default class MultiFilePatch { return this.hunkLayer; } - getPatchLayer() { - return this.patchLayer; - } - getUnchangedLayer() { return this.unchangedLayer; } From 15039ba608f5f04b14f186da53e57184386681f8 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Wed, 7 Nov 2018 17:17:21 -0800 Subject: [PATCH 184/284] :fire: unused import --- lib/models/repository-states/present.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js index b8592892a06..eba9a852908 100644 --- a/lib/models/repository-states/present.js +++ b/lib/models/repository-states/present.js @@ -14,7 +14,6 @@ import BranchSet from '../branch-set'; import Remote from '../remote'; import RemoteSet from '../remote-set'; import Commit from '../commit'; -import MultiFilePatch from '../patch/multi-file-patch'; import OperationStates from '../operation-states'; import {addEvent} from '../../reporter-proxy'; import {filePathEndsWith} from '../../helpers'; From 344f4b983cd8e4248cc4d8c7216280454b353019 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Wed, 7 Nov 2018 18:27:06 -0800 Subject: [PATCH 185/284] Clear and remark patch layer in `adoptBufferFrom` Co-Authored-By: Ash Wilson --- lib/models/patch/multi-file-patch.js | 23 +++++++++++------------ lib/models/patch/patch.js | 8 ++++++++ 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/lib/models/patch/multi-file-patch.js b/lib/models/patch/multi-file-patch.js index f1035d6709c..87f3ebdde78 100644 --- a/lib/models/patch/multi-file-patch.js +++ b/lib/models/patch/multi-file-patch.js @@ -161,18 +161,27 @@ export default class MultiFilePatch { } adoptBufferFrom(lastMultiFilePatch) { + lastMultiFilePatch.getPatchLayer().clear(); lastMultiFilePatch.getHunkLayer().clear(); lastMultiFilePatch.getUnchangedLayer().clear(); lastMultiFilePatch.getAdditionLayer().clear(); lastMultiFilePatch.getDeletionLayer().clear(); lastMultiFilePatch.getNoNewlineLayer().clear(); + this.filePatchesByMarker.clear(); + this.hunksByMarker.clear(); + const nextBuffer = lastMultiFilePatch.getBuffer(); nextBuffer.setText(this.getBuffer().getText()); - for (const patch of this.getFilePatches()) { - for (const hunk of patch.getHunks()) { + for (const filePatch of this.getFilePatches()) { + filePatch.getPatch().reMarkOn(lastMultiFilePatch.getPatchLayer()); + this.filePatchesByMarker.set(filePatch.getMarker(), filePatch); + + for (const hunk of filePatch.getHunks()) { hunk.reMarkOn(lastMultiFilePatch.getHunkLayer()); + this.hunksByMarker.set(hunk.getMarker(), hunk); + for (const region of hunk.getRegions()) { const target = region.when({ unchanged: () => lastMultiFilePatch.getUnchangedLayer(), @@ -185,16 +194,6 @@ export default class MultiFilePatch { } } - this.filePatchesByMarker.clear(); - this.hunksByMarker.clear(); - - for (const filePatch of this.filePatches) { - this.filePatchesByMarker.set(filePatch.getMarker(), filePatch); - for (const hunk of filePatch.getHunks()) { - this.hunksByMarker.set(hunk.getMarker(), hunk); - } - } - this.patchLayer = lastMultiFilePatch.getPatchLayer(); this.hunkLayer = lastMultiFilePatch.getHunkLayer(); this.unchangedLayer = lastMultiFilePatch.getUnchangedLayer(); diff --git a/lib/models/patch/patch.js b/lib/models/patch/patch.js index 20b0363bba1..b2e4efd6ec3 100644 --- a/lib/models/patch/patch.js +++ b/lib/models/patch/patch.js @@ -24,6 +24,10 @@ export default class Patch { return this.marker; } + getRange() { + return this.getMarker().getRange(); + } + getStartRange() { const startPoint = this.getMarker().getRange().start; return Range.fromObject([startPoint, startPoint]); @@ -41,6 +45,10 @@ export default class Patch { return this.marker.getRange().intersectsRow(row); } + reMarkOn(markable) { + this.marker = markable.markRange(this.getRange(), {invalidate: 'never', exclusive: false}); + } + getMaxLineNumberWidth() { const lastHunk = this.hunks[this.hunks.length - 1]; return lastHunk ? lastHunk.getMaxLineNumberWidth() : 0; From eb714c545404edede5f474d483e0bfa633333d27 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Wed, 7 Nov 2018 18:58:35 -0800 Subject: [PATCH 186/284] Add missing methods to NullPatch --- lib/models/patch/patch.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/models/patch/patch.js b/lib/models/patch/patch.js index b2e4efd6ec3..d660309415b 100644 --- a/lib/models/patch/patch.js +++ b/lib/models/patch/patch.js @@ -293,6 +293,10 @@ class NullPatch { return this.marker; } + getRange() { + return this.getMarker().getRange(); + } + getStartRange() { return Range.fromObject([[0, 0], [0, 0]]); } @@ -309,6 +313,10 @@ class NullPatch { return false; } + reMarkOn(markable) { + this.marker = markable.markRange(this.getRange(), {invalidate: 'never', exclusive: false}); + } + getMaxLineNumberWidth() { return 0; } From 8d9aa251abd161fad254203de24dce7b17b37610 Mon Sep 17 00:00:00 2001 From: simurai Date: Thu, 8 Nov 2018 19:49:49 +0900 Subject: [PATCH 187/284] Restyle file and hunk headers --- styles/commit-preview-view.less | 20 -------------------- styles/file-patch-view.less | 21 +++++++++++++++++---- styles/hunk-header-view.less | 3 +++ 3 files changed, 20 insertions(+), 24 deletions(-) delete mode 100644 styles/commit-preview-view.less diff --git a/styles/commit-preview-view.less b/styles/commit-preview-view.less deleted file mode 100644 index aff47bd9a6c..00000000000 --- a/styles/commit-preview-view.less +++ /dev/null @@ -1,20 +0,0 @@ -@import "variables"; - -.github-CommitPreview-root { - overflow: auto; - z-index: 1; // Fixes scrollbar on macOS - - .github-FilePatchView { - border-bottom: 1px solid @base-border-color; - - &:last-child { - border-bottom: none; - } - - & + .github-FilePatchView { - margin-top: @component-padding; - border-top: 1px solid @base-border-color; - } - } - -} diff --git a/styles/file-patch-view.less b/styles/file-patch-view.less index 918c5e891c8..2e3f73e6db7 100644 --- a/styles/file-patch-view.less +++ b/styles/file-patch-view.less @@ -2,8 +2,7 @@ @import "octicon-utf-codes"; @import "octicon-mixins"; -@hunk-fg-color: @text-color-subtle; -@hunk-bg-color: @pane-item-background-color; +@header-bg-color: mix(@syntax-text-color, @syntax-background-color, 6%); .github-FilePatchView { display: flex; @@ -24,14 +23,28 @@ padding: @component-padding; } + // TODO: Use better selector + .react-atom-decoration { + padding: @component-padding; + padding-left: 0; + background-color: @syntax-background-color; + + & + .react-atom-decoration { + padding-top: 0; + } + } + &-header { display: flex; justify-content: space-between; align-items: center; padding: @component-padding/2; padding-left: @component-padding; - border-bottom: 1px solid @base-border-color; - background-color: @pane-item-background-color; + border: 1px solid @base-border-color; + border-radius: @component-border-radius; + font-family: system-ui; + background-color: @header-bg-color; + cursor: default; .btn { font-size: .9em; diff --git a/styles/hunk-header-view.less b/styles/hunk-header-view.less index a3f88c8ba0b..18e682fb87b 100644 --- a/styles/hunk-header-view.less +++ b/styles/hunk-header-view.less @@ -9,6 +9,8 @@ display: flex; align-items: stretch; font-size: .9em; + border: 1px solid @base-border-color; + border-radius: @component-border-radius; background-color: @hunk-bg-color; cursor: default; @@ -65,6 +67,7 @@ .github-HunkHeaderView--isSelected { color: contrast(@button-background-color-selected); background-color: @button-background-color-selected; + border-color: transparent; .github-HunkHeaderView-title { color: inherit; } From 1b1cbd233f5894be7d3dc539d01cf9f8a2e54095 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 8 Nov 2018 09:29:40 -0500 Subject: [PATCH 188/284] Remove long-obsolete .setState() calls in RootController tests Co-Authored-By: Tilde Ann Thurium --- test/controllers/root-controller.test.js | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/test/controllers/root-controller.test.js b/test/controllers/root-controller.test.js index 31ff53cf6db..cec56017955 100644 --- a/test/controllers/root-controller.test.js +++ b/test/controllers/root-controller.test.js @@ -575,11 +575,6 @@ describe('RootController', function() { 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 () => { @@ -592,7 +587,6 @@ describe('RootController', function() { multiFilePatch = await repository.getFilePatchForPath('sample.js'); - wrapper.setState({filePatch: unstagedFilePatch}); await wrapper.instance().discardLines(multiFilePatch, new Set(unstagedFilePatch.getHunks()[0].getBufferRows().slice(2, 4))); const contents3 = fs.readFileSync(absFilePath, 'utf8'); assert.notEqual(contents2, contents3); @@ -618,7 +612,6 @@ describe('RootController', function() { 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.'); @@ -638,8 +631,6 @@ describe('RootController', function() { fs.writeFileSync(absFilePath, contents2 + change); await repository.refresh(); - unstagedFilePatch = await repository.getFilePatchForPath('sample.js'); - wrapper.setState({filePatch: unstagedFilePatch}); await wrapper.instance().undoLastDiscard('sample.js'); await assert.async.equal(fs.readFileSync(absFilePath, 'utf8'), contents1 + change); @@ -658,8 +649,6 @@ describe('RootController', function() { fs.writeFileSync(absFilePath, change + contents2); await repository.refresh(); - unstagedFilePatch = await repository.getFilePatchForPath('sample.js'); - wrapper.setState({filePatch: unstagedFilePatch}); // click 'Cancel' confirm.returns(2); @@ -714,8 +703,6 @@ describe('RootController', function() { // this would occur in the case of garbage collection cleaning out the blob 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].getBufferRows().slice(2, 4))); // remove blob from git object store From 8c004aadee4055f9841dc389e994b502511e349e Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 8 Nov 2018 09:30:15 -0500 Subject: [PATCH 189/284] Update RootController discard and undo tests to use MultiFilePatches Co-Authored-By: Tilde Ann Thurium --- test/controllers/root-controller.test.js | 30 +++++++++++++++--------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/test/controllers/root-controller.test.js b/test/controllers/root-controller.test.js index cec56017955..e1721eb08e6 100644 --- a/test/controllers/root-controller.test.js +++ b/test/controllers/root-controller.test.js @@ -562,7 +562,8 @@ describe('RootController', function() { describe('undoLastDiscard(partialDiscardFilePath)', () => { describe('when partialDiscardFilePath is not null', () => { - let unstagedFilePatch, multiFilePatch, repository, absFilePath, wrapper; + let multiFilePatch, repository, absFilePath, wrapper; + beforeEach(async () => { const workdirPath = await cloneRepository('multi-line-file'); repository = await buildRepository(workdirPath); @@ -571,15 +572,15 @@ describe('RootController', function() { fs.writeFileSync(absFilePath, 'foo\nbar\nbaz\n'); multiFilePatch = await repository.getFilePatchForPath('sample.js'); - unstagedFilePatch = multiFilePatch.getFilePatches()[0]; - app = React.cloneElement(app, {repository}); wrapper = shallow(app); }); it('reverses last discard for file path', async () => { const contents1 = fs.readFileSync(absFilePath, 'utf8'); - await wrapper.instance().discardLines(multiFilePatch, new Set(unstagedFilePatch.getHunks()[0].getBufferRows().slice(0, 2)), repository); + + const rows0 = new Set(multiFilePatch.getFilePatches()[0].getHunks()[0].getBufferRows().slice(0, 2)); + await wrapper.instance().discardLines(multiFilePatch, rows0, repository); const contents2 = fs.readFileSync(absFilePath, 'utf8'); assert.notEqual(contents1, contents2); @@ -587,7 +588,8 @@ describe('RootController', function() { multiFilePatch = await repository.getFilePatchForPath('sample.js'); - await wrapper.instance().discardLines(multiFilePatch, new Set(unstagedFilePatch.getHunks()[0].getBufferRows().slice(2, 4))); + const rows1 = new Set(multiFilePatch.getFilePatches()[0].getHunks()[0].getBufferRows().slice(2, 4)); + await wrapper.instance().discardLines(multiFilePatch, rows1); const contents3 = fs.readFileSync(absFilePath, 'utf8'); assert.notEqual(contents2, contents3); @@ -599,7 +601,8 @@ describe('RootController', function() { it('does not undo if buffer is modified', async () => { const contents1 = fs.readFileSync(absFilePath, 'utf8'); - await wrapper.instance().discardLines(multiFilePatch, new Set(unstagedFilePatch.getHunks()[0].getBufferRows().slice(0, 2))); + const rows0 = new Set(multiFilePatch.getFilePatches()[0].getHunks()[0].getBufferRows().slice(0, 2)); + await wrapper.instance().discardLines(multiFilePatch, rows0); const contents2 = fs.readFileSync(absFilePath, 'utf8'); assert.notEqual(contents1, contents2); @@ -611,7 +614,6 @@ describe('RootController', function() { sinon.stub(notificationManager, 'addError'); await repository.refresh(); - unstagedFilePatch = await repository.getFilePatchForPath('sample.js'); await wrapper.instance().undoLastDiscard('sample.js'); const notificationArgs = notificationManager.addError.args[0]; assert.equal(notificationArgs[0], 'Cannot undo last discard.'); @@ -622,7 +624,8 @@ 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].getBufferRows().slice(0, 2))); + const rows0 = new Set(multiFilePatch.getFilePatches()[0].getHunks()[0].getBufferRows().slice(0, 2)); + await wrapper.instance().discardLines(multiFilePatch, rows0); const contents2 = fs.readFileSync(absFilePath, 'utf8'); assert.notEqual(contents1, contents2); @@ -640,7 +643,8 @@ 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].getBufferRows().slice(0, 2))); + const rows0 = new Set(multiFilePatch.getFilePatches()[0].getHunks()[0].getBufferRows().slice(0, 2)); + await wrapper.instance().discardLines(multiFilePatch, rows0); const contents2 = fs.readFileSync(absFilePath, 'utf8'); assert.notEqual(contents1, contents2); @@ -701,9 +705,13 @@ 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].getBufferRows().slice(0, 2))); + const rows0 = new Set(multiFilePatch.getFilePatches()[0].getHunks()[0].getBufferRows().slice(0, 2)); + await wrapper.instance().discardLines(multiFilePatch, rows0); await repository.refresh(); - const {beforeSha} = await wrapper.instance().discardLines(unstagedFilePatch, new Set(unstagedFilePatch.getHunks()[0].getBufferRows().slice(2, 4))); + + const multiFilePatch1 = await repository.getFilePatchForPath('sample.js'); + const rows1 = new Set(multiFilePatch1.getFilePatches()[0].getHunks()[0].getBufferRows().slice(2, 4)); + const {beforeSha} = await wrapper.instance().discardLines(multiFilePatch1, rows1); // remove blob from git object store fs.unlinkSync(path.join(repository.getGitDirectoryPath(), 'objects', beforeSha.slice(0, 2), beforeSha.slice(2))); From f3809cb395e4dcbeeddaa7ee05bd96304c52a2df Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 8 Nov 2018 09:30:41 -0500 Subject: [PATCH 190/284] Accept a MultiFilePatch in Present::applyPatchToWorkdir() Co-Authored-By: Tilde Ann Thurium --- lib/models/patch/multi-file-patch.js | 11 +++++++++++ lib/models/repository-states/present.js | 13 ++++--------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/lib/models/patch/multi-file-patch.js b/lib/models/patch/multi-file-patch.js index 87f3ebdde78..b56422b0989 100644 --- a/lib/models/patch/multi-file-patch.js +++ b/lib/models/patch/multi-file-patch.js @@ -56,6 +56,17 @@ export default class MultiFilePatch { return this.filePatches; } + getPathSet() { + return this.getFilePatches().reduce((pathSet, filePatch) => { + for (const file of [filePatch.getOldFile(), filePatch.getNewFile()]) { + if (file.isPresent()) { + pathSet.add(file.getPath()); + } + } + return pathSet; + }, new Set()); + } + getFilePatchAt(bufferRow) { const [marker] = this.patchLayer.findMarkers({intersectsRow: bufferRow}); return this.filePatchesByMarker.get(marker); diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js index eba9a852908..22a803c9e94 100644 --- a/lib/models/repository-states/present.js +++ b/lib/models/repository-states/present.js @@ -273,13 +273,8 @@ export default class Present extends State { } applyPatchToIndex(multiFilePatch) { - const filePathSet = multiFilePatch.getFilePatches().reduce((pathSet, filePatch) => { - pathSet.add(filePatch.getOldPath()); - pathSet.add(filePatch.getNewPath()); - return pathSet; - }, new Set()); return this.invalidate( - () => Keys.cacheOperationKeys(Array.from(filePathSet)), + () => Keys.cacheOperationKeys(Array.from(multiFilePatch.getPathSet())), () => { const patchStr = multiFilePatch.toString(); return this.git().applyPatch(patchStr, {index: true}); @@ -287,11 +282,11 @@ export default class Present extends State { ); } - applyPatchToWorkdir(filePatch) { + applyPatchToWorkdir(multiFilePatch) { return this.invalidate( - () => Keys.workdirOperationKeys([filePatch.getOldPath(), filePatch.getNewPath()]), + () => Keys.workdirOperationKeys(Array.from(multiFilePatch.getPathSet())), () => { - const patchStr = filePatch.toString(); + const patchStr = multiFilePatch.toString(); return this.git().applyPatch(patchStr); }, ); From 86a8da119b5c91670641aceafb3eb164c4d118e6 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 8 Nov 2018 10:54:27 -0500 Subject: [PATCH 191/284] Construct partial stage patches on an existing non-empty TextBuffer --- lib/models/patch/patch.js | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/lib/models/patch/patch.js b/lib/models/patch/patch.js index d660309415b..3d2ee1ea101 100644 --- a/lib/models/patch/patch.js +++ b/lib/models/patch/patch.js @@ -63,7 +63,8 @@ export default class Patch { } buildStagePatchForLines(originalBuffer, nextLayeredBuffer, rowSet) { - const builder = new BufferBuilder(originalBuffer, nextLayeredBuffer); + const originalBaseOffset = this.getMarker().getRange().start.row; + const builder = new BufferBuilder(originalBuffer, originalBaseOffset, nextLayeredBuffer); const hunks = []; let newRowDelta = 0; @@ -142,7 +143,7 @@ export default class Patch { const buffer = builder.getBuffer(); const layers = builder.getLayers(); - const marker = layers.patch.markRange([[0, 0], [buffer.getLastRow(), Infinity]]); + const marker = layers.patch.markRange([[0, 0], [buffer.getLastRow() - 1, Infinity]]); const wholeFile = rowSet.size === this.changedLineCount; const status = this.getStatus() === 'deleted' && !wholeFile ? 'modified' : this.getStatus(); @@ -150,7 +151,8 @@ export default class Patch { } buildUnstagePatchForLines(originalBuffer, nextLayeredBuffer, rowSet) { - const builder = new BufferBuilder(originalBuffer, nextLayeredBuffer); + const originalBaseOffset = this.getMarker().getRange().start.row; + const builder = new BufferBuilder(originalBuffer, originalBaseOffset, nextLayeredBuffer); const hunks = []; let newRowDelta = 0; @@ -363,7 +365,7 @@ class NullPatch { } class BufferBuilder { - constructor(original, nextLayeredBuffer) { + constructor(original, originalBaseOffset, nextLayeredBuffer) { this.originalBuffer = original; this.buffer = nextLayeredBuffer.buffer; @@ -376,11 +378,14 @@ class BufferBuilder { ['patch', nextLayeredBuffer.layers.patch], ]); - this.offset = 0; + // The ranges provided to builder methods are expected to be valid within the original buffer. Account for + // the position of the Patch within its original TextBuffer, and any existing content already on the next + // TextBuffer. + this.offset = this.buffer.getLastRow() - originalBaseOffset; this.hunkBufferText = ''; this.hunkRowCount = 0; - this.hunkStartOffset = 0; + this.hunkStartOffset = this.offset; this.hunkRegions = []; this.hunkRange = null; From 181adf8c8ed84ee749b1abd8d15347ed2d452ad9 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 8 Nov 2018 10:55:15 -0500 Subject: [PATCH 192/284] Passing test for cross-file partial stage patch generation --- test/models/patch/multi-file-patch.test.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/test/models/patch/multi-file-patch.test.js b/test/models/patch/multi-file-patch.test.js index 2e350cd5611..7915983964c 100644 --- a/test/models/patch/multi-file-patch.test.js +++ b/test/models/patch/multi-file-patch.test.js @@ -109,7 +109,7 @@ describe('MultiFilePatch', function() { buildFilePatchFixture(3), ]; const original = new MultiFilePatch(buffer, layers, filePatches); - const stagePatch = original.getStagePatchForLines(new Set([18, 24, 44, 45])); + const stagePatch = original.getStagePatchForLines(new Set([9, 14, 25, 26])); assert.strictEqual(stagePatch.getBuffer().getText(), dedent` file-1 line-0 @@ -123,15 +123,16 @@ describe('MultiFilePatch', function() { file-3 line-1 file-3 line-2 file-3 line-3 + `); assert.lengthOf(stagePatch.getFilePatches(), 2); const [fp0, fp1] = stagePatch.getFilePatches(); assert.strictEqual(fp0.getOldPath(), 'file-1.txt'); - assertInFilePatch(fp0, buffer).hunks( + assertInFilePatch(fp0, stagePatch.getBuffer()).hunks( { startRow: 0, endRow: 3, - header: '@@ -0,4 +0,3 @@', + header: '@@ -0,3 +0,4 @@', regions: [ {kind: 'unchanged', string: ' file-1 line-0\n', range: [[0, 0], [0, 13]]}, {kind: 'addition', string: '+file-1 line-1\n', range: [[1, 0], [1, 13]]}, @@ -139,8 +140,8 @@ describe('MultiFilePatch', function() { ], }, { - startRow: 4, endRow: 8, - header: '@@ -10,3 +9,3 @@', + startRow: 4, endRow: 6, + header: '@@ -10,3 +11,2 @@', regions: [ {kind: 'unchanged', string: ' file-1 line-4\n', range: [[4, 0], [4, 13]]}, {kind: 'deletion', string: '-file-1 line-6\n', range: [[5, 0], [5, 13]]}, @@ -150,10 +151,10 @@ describe('MultiFilePatch', function() { ); assert.strictEqual(fp1.getOldPath(), 'file-3.txt'); - assertInFilePatch(fp1, buffer).hunks( + assertInFilePatch(fp1, stagePatch.getBuffer()).hunks( { - startRow: 9, endRow: 12, - header: '@@ -0,3 +0.3 @@', + startRow: 7, endRow: 10, + header: '@@ -0,3 +0,3 @@', regions: [ {kind: 'unchanged', string: ' file-3 line-0\n', range: [[7, 0], [7, 13]]}, {kind: 'addition', string: '+file-3 line-1\n', range: [[8, 0], [8, 13]]}, From 972cab79a06ef6ac53d0f54d7ca5214969d8b0b0 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 8 Nov 2018 10:55:41 -0500 Subject: [PATCH 193/284] Preserve FilePatch order in getFilePatchesContaining() --- lib/models/patch/multi-file-patch.js | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/lib/models/patch/multi-file-patch.js b/lib/models/patch/multi-file-patch.js index b56422b0989..3d44201a167 100644 --- a/lib/models/patch/multi-file-patch.js +++ b/lib/models/patch/multi-file-patch.js @@ -79,7 +79,7 @@ export default class MultiFilePatch { getStagePatchForLines(selectedLineSet) { const nextLayeredBuffer = this.buildLayeredBuffer(); - const nextFilePatches = Array.from(this.getFilePatchesContaining(selectedLineSet), fp => { + const nextFilePatches = this.getFilePatchesContaining(selectedLineSet).map(fp => { return fp.buildStagePatchForLines(this.getBuffer(), nextLayeredBuffer, selectedLineSet); }); @@ -96,7 +96,7 @@ export default class MultiFilePatch { getUnstagePatchForLines(selectedLineSet) { const nextLayeredBuffer = this.buildLayeredBuffer(); - const nextFilePatches = Array.from(this.getFilePatchesContaining(selectedLineSet), fp => { + const nextFilePatches = this.getFilePatchesContaining(selectedLineSet).map(fp => { return fp.buildUnstagePatchForLines(this.getBuffer(), nextLayeredBuffer, selectedLineSet); }); @@ -237,9 +237,10 @@ export default class MultiFilePatch { */ getFilePatchesContaining(rowSet) { const sortedRowSet = Array.from(rowSet); - sortedRowSet.sort((a, b) => b - a); + sortedRowSet.sort((a, b) => a - b); - const filePatches = new Set(); + const filePatches = []; + const seen = new Set(); let lastFilePatch = null; for (const row of sortedRowSet) { // Because the rows are sorted, consecutive rows will almost certainly belong to the same patch, so we can save @@ -249,7 +250,12 @@ export default class MultiFilePatch { } lastFilePatch = this.getFilePatchAt(row); - filePatches.add(lastFilePatch); + if (seen.has(lastFilePatch)) { + continue; + } + + filePatches.push(lastFilePatch); + seen.add(lastFilePatch); } return filePatches; From 836207f209065ee54ee5ea9ef33c3b06cd434b63 Mon Sep 17 00:00:00 2001 From: Tilde Ann Thurium Date: Thu, 8 Nov 2018 09:33:27 -0800 Subject: [PATCH 194/284] make CommitPreviewContainer tests pass Co-Authored-By: Vanessa Yuen --- test/containers/commit-preview-container.test.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/containers/commit-preview-container.test.js b/test/containers/commit-preview-container.test.js index b33b11f169d..1feadd8e071 100644 --- a/test/containers/commit-preview-container.test.js +++ b/test/containers/commit-preview-container.test.js @@ -19,8 +19,11 @@ describe('CommitPreviewContainer', function() { }); function buildApp(override = {}) { + const props = { repository, + ...atomEnv, + destroy: () => {}, ...override, }; From 07cee58cc9cb5476ccb8ee6fe6a5772089b7197b Mon Sep 17 00:00:00 2001 From: Vanessa Yuen Date: Thu, 8 Nov 2018 17:06:59 +0100 Subject: [PATCH 195/284] fix nullpatch being displayed --- lib/models/patch/multi-file-patch.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/models/patch/multi-file-patch.js b/lib/models/patch/multi-file-patch.js index 3d44201a167..78003fd42f3 100644 --- a/lib/models/patch/multi-file-patch.js +++ b/lib/models/patch/multi-file-patch.js @@ -262,7 +262,7 @@ export default class MultiFilePatch { } anyPresent() { - return this.buffer !== null; + return this.buffer !== null && this.filePatches.some(fp => fp.isPresent()); } didAnyChangeExecutableMode() { From ddd0630b6afde612dbba70b681bd671e8271bb56 Mon Sep 17 00:00:00 2001 From: Vanessa Yuen Date: Thu, 8 Nov 2018 18:34:17 +0100 Subject: [PATCH 196/284] fix commitpreviewitem tests Co-Authored-By: Tilde Ann Thurium --- test/items/commit-preview-item.test.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/items/commit-preview-item.test.js b/test/items/commit-preview-item.test.js index e550c176a44..15af0dcadd5 100644 --- a/test/items/commit-preview-item.test.js +++ b/test/items/commit-preview-item.test.js @@ -28,6 +28,11 @@ describe('CommitPreviewItem', function() { function buildPaneApp(override = {}) { const props = { workdirContextPool: pool, + workspace: atomEnv.workspace, + commands: atomEnv.commands, + keymaps: atomEnv.keymaps, + tooltips: atomEnv.tooltips, + config: atomEnv.config, ...override, }; From 6bfe0255c9584714dd1336325669893c930efcb6 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 8 Nov 2018 12:36:12 -0500 Subject: [PATCH 197/284] Turns out we don't actually need that Set --- lib/models/patch/multi-file-patch.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/lib/models/patch/multi-file-patch.js b/lib/models/patch/multi-file-patch.js index 78003fd42f3..7cd739343e0 100644 --- a/lib/models/patch/multi-file-patch.js +++ b/lib/models/patch/multi-file-patch.js @@ -240,7 +240,6 @@ export default class MultiFilePatch { sortedRowSet.sort((a, b) => a - b); const filePatches = []; - const seen = new Set(); let lastFilePatch = null; for (const row of sortedRowSet) { // Because the rows are sorted, consecutive rows will almost certainly belong to the same patch, so we can save @@ -250,12 +249,7 @@ export default class MultiFilePatch { } lastFilePatch = this.getFilePatchAt(row); - if (seen.has(lastFilePatch)) { - continue; - } - filePatches.push(lastFilePatch); - seen.add(lastFilePatch); } return filePatches; From 1eff20fe79dacd47c238db3d43bb3fcbcbc29c25 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 8 Nov 2018 12:36:48 -0500 Subject: [PATCH 198/284] We actually care about typechanges, not just having a symlink Oops --- lib/models/patch/multi-file-patch.js | 9 ++------- lib/views/multi-file-patch-view.js | 2 +- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/lib/models/patch/multi-file-patch.js b/lib/models/patch/multi-file-patch.js index 7cd739343e0..c038edca706 100644 --- a/lib/models/patch/multi-file-patch.js +++ b/lib/models/patch/multi-file-patch.js @@ -268,13 +268,8 @@ export default class MultiFilePatch { return false; } - anyHaveSymlink() { - for (const filePatch of this.getFilePatches()) { - if (filePatch.hasSymlink()) { - return true; - } - } - return false; + anyHaveTypechange() { + return this.getFilePatches().some(fp => fp.hasTypechange()); } getMaxLineNumberWidth() { diff --git a/lib/views/multi-file-patch-view.js b/lib/views/multi-file-patch-view.js index d72d7982dec..ec16a26ae00 100644 --- a/lib/views/multi-file-patch-view.js +++ b/lib/views/multi-file-patch-view.js @@ -199,7 +199,7 @@ export default class MultiFilePatchView extends React.Component { stageModeCommand = ; } - if (this.props.multiFilePatch.anyHaveSymlink()) { + if (this.props.multiFilePatch.anyHaveTypechange()) { const command = this.props.stagingStatus === 'unstaged' ? 'github:stage-symlink-change' : 'github:unstage-symlink-change'; From ccd4eb6b0f603a73045bf69cd88489dd5e6bcc0c Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 8 Nov 2018 12:38:27 -0500 Subject: [PATCH 199/284] MultiFilePatch tests: (un)staged patch generation, predicates --- test/models/patch/multi-file-patch.test.js | 298 ++++++++++++++++++--- 1 file changed, 268 insertions(+), 30 deletions(-) diff --git a/test/models/patch/multi-file-patch.test.js b/test/models/patch/multi-file-patch.test.js index 7915983964c..93875df0feb 100644 --- a/test/models/patch/multi-file-patch.test.js +++ b/test/models/patch/multi-file-patch.test.js @@ -3,7 +3,7 @@ import dedent from 'dedent-js'; import MultiFilePatch from '../../../lib/models/patch/multi-file-patch'; 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 {Unchanged, Addition, Deletion} from '../../../lib/models/patch/region'; @@ -24,12 +24,59 @@ describe('MultiFilePatch', function() { }; }); + it('creates an empty patch when constructed with no arguments', function() { + const empty = new MultiFilePatch(); + assert.isFalse(empty.anyPresent()); + assert.lengthOf(empty.getFilePatches(), 0); + }); + + it('detects when it is not empty', function() { + const mp = new MultiFilePatch(buffer, layers, [buildFilePatchFixture(0)]); + assert.isTrue(mp.anyPresent()); + }); + it('has an accessor for its file patches', function() { const filePatches = [buildFilePatchFixture(0), buildFilePatchFixture(1)]; const mp = new MultiFilePatch(buffer, layers, filePatches); assert.strictEqual(mp.getFilePatches(), filePatches); }); + describe('didAnyChangeExecutableMode()', function() { + it('detects when at least one patch contains an executable mode change', function() { + const yes = new MultiFilePatch(buffer, layers, [ + buildFilePatchFixture(0, {oldFileMode: '100644', newFileMode: '100755'}), + buildFilePatchFixture(1), + ]); + assert.isTrue(yes.didAnyChangeExecutableMode()); + }); + + it('detects when none of the patches contain an executable mode change', function() { + const no = new MultiFilePatch(buffer, layers, [ + buildFilePatchFixture(0), + buildFilePatchFixture(1), + ]); + assert.isFalse(no.didAnyChangeExecutableMode()); + }); + }); + + describe('anyHaveTypechange()', function() { + it('detects when at least one patch contains a symlink change', function() { + const yes = new MultiFilePatch(buffer, layers, [ + buildFilePatchFixture(0, {oldFileMode: '100644', newFileMode: '120000', newFileSymlink: 'destination'}), + buildFilePatchFixture(1), + ]); + assert.isTrue(yes.anyHaveTypechange()); + }); + + it('detects when none of its patches contain a symlink change', function() { + const no = new MultiFilePatch(buffer, layers, [ + buildFilePatchFixture(0), + buildFilePatchFixture(1), + ]); + assert.isFalse(no.anyHaveTypechange()); + }); + }); + it('locates an individual FilePatch by marker lookup', function() { const filePatches = []; for (let i = 0; i < 10; i++) { @@ -43,6 +90,19 @@ describe('MultiFilePatch', function() { assert.strictEqual(mp.getFilePatchAt(79), filePatches[9]); }); + it('creates a set of all unique paths referenced by patches', function() { + const mp = new MultiFilePatch(buffer, layers, [ + buildFilePatchFixture(0, {oldFilePath: 'file-0-before.txt', newFilePath: 'file-0-after.txt'}), + buildFilePatchFixture(1, {status: 'added', newFilePath: 'file-1.txt'}), + buildFilePatchFixture(2, {oldFilePath: 'file-2.txt', newFilePath: 'file-2.txt'}), + ]); + + assert.sameMembers( + Array.from(mp.getPathSet()), + ['file-0-before.txt', 'file-0-after.txt', 'file-1.txt', 'file-2.txt'], + ); + }); + it('locates a Hunk by marker lookup', function() { const filePatches = [ buildFilePatchFixture(0), @@ -165,6 +225,157 @@ describe('MultiFilePatch', function() { ); }); + it('generates a stage patch from an arbitrary hunk', function() { + const filePatches = [ + buildFilePatchFixture(0), + buildFilePatchFixture(1), + ]; + const original = new MultiFilePatch(buffer, layers, filePatches); + const hunk = original.getFilePatches()[0].getHunks()[1]; + const stagePatch = original.getStagePatchForHunk(hunk); + + assert.strictEqual(stagePatch.getBuffer().getText(), dedent` + file-0 line-4 + file-0 line-5 + file-0 line-6 + file-0 line-7 + + `); + assert.lengthOf(stagePatch.getFilePatches(), 1); + const [fp0] = stagePatch.getFilePatches(); + assert.strictEqual(fp0.getOldPath(), 'file-0.txt'); + assert.strictEqual(fp0.getNewPath(), 'file-0.txt'); + assertInFilePatch(fp0, stagePatch.getBuffer()).hunks( + { + startRow: 0, endRow: 3, + header: '@@ -10,3 +10,3 @@', + regions: [ + {kind: 'unchanged', string: ' file-0 line-4\n', range: [[0, 0], [0, 13]]}, + {kind: 'addition', string: '+file-0 line-5\n', range: [[1, 0], [1, 13]]}, + {kind: 'deletion', string: '-file-0 line-6\n', range: [[2, 0], [2, 13]]}, + {kind: 'unchanged', string: ' file-0 line-7\n', range: [[3, 0], [3, 13]]}, + ], + }, + ); + }); + + it('generates an unstage patch for arbitrary buffer rows', function() { + const filePatches = [ + buildFilePatchFixture(0), + buildFilePatchFixture(1), + buildFilePatchFixture(2), + buildFilePatchFixture(3), + ]; + const original = new MultiFilePatch(buffer, layers, filePatches); + + const unstagePatch = original.getUnstagePatchForLines(new Set([1, 2, 21, 26, 29, 30])); + + assert.strictEqual(unstagePatch.getBuffer().getText(), dedent` + file-0 line-0 + file-0 line-1 + file-0 line-2 + file-0 line-3 + file-2 line-4 + file-2 line-5 + file-2 line-7 + file-3 line-0 + file-3 line-1 + file-3 line-2 + file-3 line-3 + file-3 line-4 + file-3 line-5 + file-3 line-6 + file-3 line-7 + + `); + + assert.lengthOf(unstagePatch.getFilePatches(), 3); + const [fp0, fp1, fp2] = unstagePatch.getFilePatches(); + assert.strictEqual(fp0.getOldPath(), 'file-0.txt'); + assertInFilePatch(fp0, unstagePatch.getBuffer()).hunks( + { + startRow: 0, endRow: 3, + header: '@@ -0,3 +0,3 @@', + regions: [ + {kind: 'unchanged', string: ' file-0 line-0\n', range: [[0, 0], [0, 13]]}, + {kind: 'deletion', string: '-file-0 line-1\n', range: [[1, 0], [1, 13]]}, + {kind: 'addition', string: '+file-0 line-2\n', range: [[2, 0], [2, 13]]}, + {kind: 'unchanged', string: ' file-0 line-3\n', range: [[3, 0], [3, 13]]}, + ], + }, + ); + + assert.strictEqual(fp1.getOldPath(), 'file-2.txt'); + assertInFilePatch(fp1, unstagePatch.getBuffer()).hunks( + { + startRow: 4, endRow: 6, + header: '@@ -10,3 +10,2 @@', + regions: [ + {kind: 'unchanged', string: ' file-2 line-4\n', range: [[4, 0], [4, 13]]}, + {kind: 'deletion', string: '-file-2 line-5\n', range: [[5, 0], [5, 13]]}, + {kind: 'unchanged', string: ' file-2 line-7\n', range: [[6, 0], [6, 13]]}, + ], + }, + ); + + assert.strictEqual(fp2.getOldPath(), 'file-3.txt'); + assertInFilePatch(fp2, unstagePatch.getBuffer()).hunks( + { + startRow: 7, endRow: 10, + header: '@@ -0,3 +0,4 @@', + regions: [ + {kind: 'unchanged', string: ' file-3 line-0\n file-3 line-1\n', range: [[7, 0], [8, 13]]}, + {kind: 'addition', string: '+file-3 line-2\n', range: [[9, 0], [9, 13]]}, + {kind: 'unchanged', string: ' file-3 line-3\n', range: [[10, 0], [10, 13]]}, + ], + }, + { + startRow: 11, endRow: 14, + header: '@@ -10,3 +11,3 @@', + regions: [ + {kind: 'unchanged', string: ' file-3 line-4\n', range: [[11, 0], [11, 13]]}, + {kind: 'deletion', string: '-file-3 line-5\n', range: [[12, 0], [12, 13]]}, + {kind: 'addition', string: '+file-3 line-6\n', range: [[13, 0], [13, 13]]}, + {kind: 'unchanged', string: ' file-3 line-7\n', range: [[14, 0], [14, 13]]}, + ], + }, + ); + }); + + it('generates an unstaged patch for an arbitrary hunk', function() { + const filePatches = [ + buildFilePatchFixture(0), + buildFilePatchFixture(1), + ]; + const original = new MultiFilePatch(buffer, layers, filePatches); + const hunk = original.getFilePatches()[1].getHunks()[0]; + const unstagePatch = original.getUnstagePatchForHunk(hunk); + + assert.strictEqual(unstagePatch.getBuffer().getText(), dedent` + file-1 line-0 + file-1 line-1 + file-1 line-2 + file-1 line-3 + + `); + assert.lengthOf(unstagePatch.getFilePatches(), 1); + const [fp0] = unstagePatch.getFilePatches(); + assert.strictEqual(fp0.getOldPath(), 'file-1.txt'); + assert.strictEqual(fp0.getNewPath(), 'file-1.txt'); + assertInFilePatch(fp0, unstagePatch.getBuffer()).hunks( + { + startRow: 0, endRow: 3, + header: '@@ -0,3 +0,3 @@', + regions: [ + {kind: 'unchanged', string: ' file-1 line-0\n', range: [[0, 0], [0, 13]]}, + {kind: 'deletion', string: '-file-1 line-1\n', range: [[1, 0], [1, 13]]}, + {kind: 'addition', string: '+file-1 line-2\n', range: [[2, 0], [2, 13]]}, + {kind: 'unchanged', string: ' file-1 line-3\n', range: [[3, 0], [3, 13]]}, + ], + }, + ); + }); + // FIXME adapt these to the lifted method. // describe('next selection range derivation', function() { // it('selects the first change region after the highest buffer row', function() { @@ -325,44 +536,71 @@ describe('MultiFilePatch', function() { // }); // }); - function buildFilePatchFixture(index) { + function buildFilePatchFixture(index, options = {}) { + const opts = { + oldFilePath: `file-${index}.txt`, + oldFileMode: '100644', + oldFileSymlink: null, + newFilePath: `file-${index}.txt`, + newFileMode: '100644', + newFileSymlink: null, + status: 'modified', + ...options, + }; + const rowOffset = buffer.getLastRow(); for (let i = 0; i < 8; i++) { buffer.append(`file-${index} line-${i}\n`); } + let oldFile = new File({path: opts.oldFilePath, mode: opts.oldFileMode, symlink: opts.oldFileSymlink}); + const newFile = new File({path: opts.newFilePath, mode: opts.newFileMode, symlink: opts.newFileSymlink}); + const mark = (layer, start, end = start) => layer.markRange([[rowOffset + start, 0], [rowOffset + end, Infinity]]); - const hunks = [ - new Hunk({ - oldStartRow: 0, newStartRow: 0, oldRowCount: 3, newRowCount: 3, - sectionHeading: `file-${index} hunk-0`, - marker: mark(layers.hunk, 0, 3), - regions: [ - new Unchanged(mark(layers.unchanged, 0)), - new Addition(mark(layers.addition, 1)), - new Deletion(mark(layers.deletion, 2)), - new Unchanged(mark(layers.unchanged, 3)), - ], - }), - new Hunk({ - oldStartRow: 10, newStartRow: 10, oldRowCount: 3, newRowCount: 3, - sectionHeading: `file-${index} hunk-1`, - marker: mark(layers.hunk, 4, 7), - regions: [ - new Unchanged(mark(layers.unchanged, 4)), - new Addition(mark(layers.addition, 5)), - new Deletion(mark(layers.deletion, 6)), - new Unchanged(mark(layers.unchanged, 7)), - ], - }), - ]; + let hunks = []; + if (opts.status === 'modified') { + hunks = [ + new Hunk({ + oldStartRow: 0, newStartRow: 0, oldRowCount: 3, newRowCount: 3, + sectionHeading: `file-${index} hunk-0`, + marker: mark(layers.hunk, 0, 3), + regions: [ + new Unchanged(mark(layers.unchanged, 0)), + new Addition(mark(layers.addition, 1)), + new Deletion(mark(layers.deletion, 2)), + new Unchanged(mark(layers.unchanged, 3)), + ], + }), + new Hunk({ + oldStartRow: 10, newStartRow: 10, oldRowCount: 3, newRowCount: 3, + sectionHeading: `file-${index} hunk-1`, + marker: mark(layers.hunk, 4, 7), + regions: [ + new Unchanged(mark(layers.unchanged, 4)), + new Addition(mark(layers.addition, 5)), + new Deletion(mark(layers.deletion, 6)), + new Unchanged(mark(layers.unchanged, 7)), + ], + }), + ]; + } else if (opts.status === 'added') { + hunks = [ + new Hunk({ + oldStartRow: 0, newStartRow: 0, oldRowCount: 8, newRowCount: 8, + sectionHeading: `file-${index} hunk-0`, + marker: mark(layers.hunk, 0, 7), + regions: [ + new Addition(mark(layers.addition, 0, 7)), + ], + }), + ]; - const marker = mark(layers.patch, 0, 7); - const patch = new Patch({status: 'modified', hunks, marker}); + oldFile = nullFile; + } - const oldFile = new File({path: `file-${index}.txt`, mode: '100644'}); - const newFile = new File({path: `file-${index}.txt`, mode: '100644'}); + const marker = mark(layers.patch, 0, 7); + const patch = new Patch({status: opts.status, hunks, marker}); return new FilePatch(oldFile, newFile, patch); } From d52b987b509031e5c3c9fcead9c651d70e26b5fa Mon Sep 17 00:00:00 2001 From: Vanessa Yuen Date: Thu, 8 Nov 2018 19:30:36 +0100 Subject: [PATCH 200/284] =?UTF-8?q?filePatch=20is=20soooo=20last=20week;?= =?UTF-8?q?=20we=20are=20on=20multiFilePatch=20now.=20=F0=9F=86=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/views/multi-file-patch-view.test.js | 33 +++++++++++++++++++----- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/test/views/multi-file-patch-view.test.js b/test/views/multi-file-patch-view.test.js index de957868a7e..9caa9fd6ca3 100644 --- a/test/views/multi-file-patch-view.test.js +++ b/test/views/multi-file-patch-view.test.js @@ -3,13 +3,13 @@ import {shallow, mount} from 'enzyme'; import {cloneRepository, buildRepository} from '../helpers'; import MultiFilePatchView from '../../lib/views/multi-file-patch-view'; -import {buildFilePatch} from '../../lib/models/patch'; +import {buildFilePatch, buildMultiFilePatch} from '../../lib/models/patch'; import {nullFile} from '../../lib/models/patch/file'; import FilePatch from '../../lib/models/patch/file-patch'; import RefHolder from '../../lib/models/ref-holder'; -describe('MultiFilePatchView', function() { - let atomEnv, workspace, repository, filePatch; +describe.only('MultiFilePatchView', function() { + let atomEnv, workspace, repository, filePatches; beforeEach(async function() { atomEnv = global.buildAtomEnvironment(); @@ -17,9 +17,10 @@ describe('MultiFilePatchView', function() { const workdirPath = await cloneRepository(); repository = await buildRepository(workdirPath); + // filePatches = repository.getStagedChangesPatch(); // path.txt: unstaged changes - filePatch = buildFilePatch([{ + filePatches = buildMultiFilePatch([{ oldPath: 'path.txt', oldMode: '100644', newPath: 'path.txt', @@ -37,6 +38,24 @@ describe('MultiFilePatchView', function() { lines: [' 0005', '+0006', '-0007', ' 0008'], }, ], + }, { + oldPath: 'path2.txt', + oldMode: '100644', + newPath: 'path2.txt', + newMode: '100644', + status: 'modified', + hunks: [ + { + 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'], + }, + ], }]); }); @@ -49,7 +68,7 @@ describe('MultiFilePatchView', function() { relPath: 'path.txt', stagingStatus: 'unstaged', isPartiallyStaged: false, - filePatch, + multiFilePatch: filePatches, hasUndoHistory: false, selectionMode: 'line', selectedRows: new Set(), @@ -89,7 +108,7 @@ describe('MultiFilePatchView', function() { const undoLastDiscard = sinon.spy(); const wrapper = shallow(buildApp({undoLastDiscard})); - wrapper.find('FilePatchHeaderView').prop('undoLastDiscard')(); + wrapper.find('FilePatchHeaderView').first().prop('undoLastDiscard')(); assert.isTrue(undoLastDiscard.calledWith({eventSource: 'button'})); }); @@ -98,7 +117,7 @@ describe('MultiFilePatchView', function() { const wrapper = mount(buildApp()); const editor = wrapper.find('AtomTextEditor'); - assert.strictEqual(editor.instance().getModel().getText(), filePatch.getBuffer().getText()); + assert.strictEqual(editor.instance().getModel().getText(), filePatches.getBuffer().getText()); }); it('enables autoHeight on the editor when requested', function() { From d03c37c8d0c3a23d5e70ed4130b839013e94e0c3 Mon Sep 17 00:00:00 2001 From: Vanessa Yuen Date: Thu, 8 Nov 2018 19:32:44 +0100 Subject: [PATCH 201/284] we don't care about active/inactive anymore... i think. --- test/views/multi-file-patch-view.test.js | 9 --------- 1 file changed, 9 deletions(-) diff --git a/test/views/multi-file-patch-view.test.js b/test/views/multi-file-patch-view.test.js index 9caa9fd6ca3..aed5aa1c90d 100644 --- a/test/views/multi-file-patch-view.test.js +++ b/test/views/multi-file-patch-view.test.js @@ -136,15 +136,6 @@ describe.only('MultiFilePatchView', function() { assert.isTrue(wrapper.find('.github-FilePatchView--hunkMode').exists()); }); - it('sets the root class when active or inactive', function() { - const wrapper = shallow(buildApp({isActive: true})); - assert.isTrue(wrapper.find('.github-FilePatchView--active').exists()); - assert.isFalse(wrapper.find('.github-FilePatchView--inactive').exists()); - wrapper.setProps({isActive: false}); - assert.isFalse(wrapper.find('.github-FilePatchView--active').exists()); - assert.isTrue(wrapper.find('.github-FilePatchView--inactive').exists()); - }); - it('preserves the selection index when a new file patch arrives in line selection mode', function() { const selectedRowsChanged = sinon.spy(); const wrapper = mount(buildApp({ From 467a33eb69e989e10b50501503d6c5eb9e3d3f44 Mon Sep 17 00:00:00 2001 From: Vanessa Yuen Date: Thu, 8 Nov 2018 19:35:09 +0100 Subject: [PATCH 202/284] no `.only` --- test/views/multi-file-patch-view.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/views/multi-file-patch-view.test.js b/test/views/multi-file-patch-view.test.js index aed5aa1c90d..9b6e0e62fdf 100644 --- a/test/views/multi-file-patch-view.test.js +++ b/test/views/multi-file-patch-view.test.js @@ -8,7 +8,7 @@ import {nullFile} from '../../lib/models/patch/file'; import FilePatch from '../../lib/models/patch/file-patch'; import RefHolder from '../../lib/models/ref-holder'; -describe.only('MultiFilePatchView', function() { +describe('MultiFilePatchView', function() { let atomEnv, workspace, repository, filePatches; beforeEach(async function() { From ea76f598e9a631fbf05801ad4a8a9a792d86461e Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 8 Nov 2018 14:08:59 -0500 Subject: [PATCH 203/284] Full MultiFilePatch coverage for everything but getNextSelectionRange() --- test/models/patch/multi-file-patch.test.js | 74 +++++++++++++++++++--- 1 file changed, 65 insertions(+), 9 deletions(-) diff --git a/test/models/patch/multi-file-patch.test.js b/test/models/patch/multi-file-patch.test.js index 93875df0feb..41ad2b920a8 100644 --- a/test/models/patch/multi-file-patch.test.js +++ b/test/models/patch/multi-file-patch.test.js @@ -6,7 +6,7 @@ import FilePatch 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'; -import {Unchanged, Addition, Deletion} from '../../../lib/models/patch/region'; +import {Unchanged, Addition, Deletion, NoNewline} from '../../../lib/models/patch/region'; import {assertInFilePatch} from '../../helpers'; describe('MultiFilePatch', function() { @@ -77,6 +77,14 @@ describe('MultiFilePatch', function() { }); }); + it('computes the maximum line number width of any hunk in any patch', function() { + const mp = new MultiFilePatch(buffer, layers, [ + buildFilePatchFixture(0), + buildFilePatchFixture(1), + ]); + assert.strictEqual(mp.getMaxLineNumberWidth(), 2); + }); + it('locates an individual FilePatch by marker lookup', function() { const filePatches = []; for (let i = 0; i < 10; i++) { @@ -121,13 +129,50 @@ describe('MultiFilePatch', function() { assert.strictEqual(mp.getHunkAt(23), filePatches[2].getHunks()[1]); }); + it('represents itself as an apply-ready string', function() { + const mp = new MultiFilePatch(buffer, layers, [ + buildFilePatchFixture(0), + buildFilePatchFixture(1), + ]); + + assert.strictEqual(mp.toString(), dedent` + diff --git a/file-0.txt b/file-0.txt + --- a/file-0.txt + +++ b/file-0.txt + @@ -0,3 +0,3 @@ + file-0 line-0 + +file-0 line-1 + -file-0 line-2 + file-0 line-3 + @@ -10,3 +10,3 @@ + file-0 line-4 + +file-0 line-5 + -file-0 line-6 + file-0 line-7 + diff --git a/file-1.txt b/file-1.txt + --- a/file-1.txt + +++ b/file-1.txt + @@ -0,3 +0,3 @@ + file-1 line-0 + +file-1 line-1 + -file-1 line-2 + file-1 line-3 + @@ -10,3 +10,3 @@ + file-1 line-4 + +file-1 line-5 + -file-1 line-6 + file-1 line-7 + + `); + }); + it('adopts a buffer from a previous patch', function() { const lastBuffer = buffer; const lastLayers = layers; const lastFilePatches = [ buildFilePatchFixture(0), buildFilePatchFixture(1), - buildFilePatchFixture(2), + buildFilePatchFixture(2, {noNewline: true}), ]; const lastPatch = new MultiFilePatch(lastBuffer, layers, lastFilePatches); @@ -144,7 +189,7 @@ describe('MultiFilePatch', function() { buildFilePatchFixture(0), buildFilePatchFixture(1), buildFilePatchFixture(2), - buildFilePatchFixture(3), + buildFilePatchFixture(3, {noNewline: true}), ]; const nextPatch = new MultiFilePatch(buffer, layers, nextFilePatches); @@ -545,6 +590,7 @@ describe('MultiFilePatch', function() { newFileMode: '100644', newFileSymlink: null, status: 'modified', + noNewline: false, ...options, }; @@ -552,12 +598,22 @@ describe('MultiFilePatch', function() { for (let i = 0; i < 8; i++) { buffer.append(`file-${index} line-${i}\n`); } + if (opts.noNewline) { + buffer.append(' No newline at end of file\n'); + } let oldFile = new File({path: opts.oldFilePath, mode: opts.oldFileMode, symlink: opts.oldFileSymlink}); const newFile = new File({path: opts.newFilePath, mode: opts.newFileMode, symlink: opts.newFileSymlink}); const mark = (layer, start, end = start) => layer.markRange([[rowOffset + start, 0], [rowOffset + end, Infinity]]); + const withNoNewlineRegion = regions => { + if (opts.noNewline) { + regions.push(new NoNewline(mark(layers.noNewline, 8))); + } + return regions; + }; + let hunks = []; if (opts.status === 'modified') { hunks = [ @@ -575,13 +631,13 @@ describe('MultiFilePatch', function() { new Hunk({ oldStartRow: 10, newStartRow: 10, oldRowCount: 3, newRowCount: 3, sectionHeading: `file-${index} hunk-1`, - marker: mark(layers.hunk, 4, 7), - regions: [ + marker: mark(layers.hunk, 4, opts.noNewline ? 8 : 7), + regions: withNoNewlineRegion([ new Unchanged(mark(layers.unchanged, 4)), new Addition(mark(layers.addition, 5)), new Deletion(mark(layers.deletion, 6)), new Unchanged(mark(layers.unchanged, 7)), - ], + ]), }), ]; } else if (opts.status === 'added') { @@ -589,10 +645,10 @@ describe('MultiFilePatch', function() { new Hunk({ oldStartRow: 0, newStartRow: 0, oldRowCount: 8, newRowCount: 8, sectionHeading: `file-${index} hunk-0`, - marker: mark(layers.hunk, 0, 7), - regions: [ + marker: mark(layers.hunk, 0, opts.noNewline ? 8 : 7), + regions: withNoNewlineRegion([ new Addition(mark(layers.addition, 0, 7)), - ], + ]), }), ]; From c6a8aa052571a04c32a4a072a996bc54d3a87156 Mon Sep 17 00:00:00 2001 From: Vanessa Yuen Date: Thu, 8 Nov 2018 20:10:24 +0100 Subject: [PATCH 204/284] new clone method for MFP --- lib/models/patch/multi-file-patch.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/lib/models/patch/multi-file-patch.js b/lib/models/patch/multi-file-patch.js index c038edca706..06a80953b5e 100644 --- a/lib/models/patch/multi-file-patch.js +++ b/lib/models/patch/multi-file-patch.js @@ -24,6 +24,21 @@ export default class MultiFilePatch { } } + clone(opts = {}) { + return new this.constructor({ + buffer: opts.buffer !== undefined ? opts.buffer : this.getBuffer(), + layers: opts.layers !== undefined ? opts.layers : { + patch: this.getPatchLayer(), + hunk: this.getHunkLayer(), + unchanged: this.getUnchangedLayer(), + addition: this.getAdditionLayer(), + deletion: this.getDeletionLayer(), + noNewline: this.getNoNewlineLayer(), + }, + filePatches: opts.filePatches !== undefined ? opts.filePatches : this.getFilePatches(), + }); + } + getBuffer() { return this.buffer; } From 4ac8af809687b343c42403eaa186a22d25f6d357 Mon Sep 17 00:00:00 2001 From: Vanessa Yuen Date: Thu, 8 Nov 2018 20:12:17 +0100 Subject: [PATCH 205/284] use the new clone method in MFP view test --- test/views/multi-file-patch-view.test.js | 30 +++++++----------------- 1 file changed, 8 insertions(+), 22 deletions(-) diff --git a/test/views/multi-file-patch-view.test.js b/test/views/multi-file-patch-view.test.js index 9b6e0e62fdf..d9a408d5c98 100644 --- a/test/views/multi-file-patch-view.test.js +++ b/test/views/multi-file-patch-view.test.js @@ -38,24 +38,6 @@ describe('MultiFilePatchView', function() { lines: [' 0005', '+0006', '-0007', ' 0008'], }, ], - }, { - oldPath: 'path2.txt', - oldMode: '100644', - newPath: 'path2.txt', - newMode: '100644', - status: 'modified', - hunks: [ - { - 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'], - }, - ], }]); }); @@ -302,12 +284,16 @@ describe('MultiFilePatchView', function() { 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 [fp] = filePatches.getFilePatches(); + + const mfp = filePatches.clone({ + filePatches: fp.clone({ + oldFile: fp.getOldFile().clone({mode: '100644'}), + newFile: fp.getNewFile().clone({mode: '100644'}), + }), }); - const wrapper = shallow(buildApp({filePatch: fp})); + const wrapper = shallow(buildApp({multiFilePatch: mfp})); assert.isFalse(wrapper.find('FilePatchMetaView[title="Mode change"]').exists()); }); From 3908ffc9dc8467116c7077955f3d69f0fa7f598e Mon Sep 17 00:00:00 2001 From: Vanessa Yuen Date: Thu, 8 Nov 2018 20:12:47 +0100 Subject: [PATCH 206/284] import the right thing --- test/views/multi-file-patch-view.test.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/views/multi-file-patch-view.test.js b/test/views/multi-file-patch-view.test.js index d9a408d5c98..a4d8c73afd8 100644 --- a/test/views/multi-file-patch-view.test.js +++ b/test/views/multi-file-patch-view.test.js @@ -6,6 +6,7 @@ import MultiFilePatchView from '../../lib/views/multi-file-patch-view'; import {buildFilePatch, buildMultiFilePatch} from '../../lib/models/patch'; import {nullFile} from '../../lib/models/patch/file'; import FilePatch from '../../lib/models/patch/file-patch'; +import MultiFilePatch from '../../lib/models/patch/multi-file-patch'; import RefHolder from '../../lib/models/ref-holder'; describe('MultiFilePatchView', function() { From 44bc9b767cd3f397a9c159b68827ac0e289155aa Mon Sep 17 00:00:00 2001 From: Tilde Ann Thurium Date: Thu, 8 Nov 2018 11:19:24 -0800 Subject: [PATCH 207/284] use this.constructor.name for metrics --- lib/controllers/multi-file-patch-controller.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/controllers/multi-file-patch-controller.js b/lib/controllers/multi-file-patch-controller.js index ebcd8171abf..7ffeb2b38f8 100644 --- a/lib/controllers/multi-file-patch-controller.js +++ b/lib/controllers/multi-file-patch-controller.js @@ -87,10 +87,11 @@ export default class MultiFilePatchController extends React.Component { undoLastDiscard(filePatch, {eventSource} = {}) { addEvent('undo-last-discard', { package: 'github', - component: 'FilePatchController', + component: this.constructor.name, eventSource, }); + return this.props.undoLastDiscard(filePatch.getPath(), this.props.repository); } @@ -191,7 +192,7 @@ export default class MultiFilePatchController extends React.Component { addEvent('discard-unstaged-changes', { package: 'github', - component: 'FilePatchController', + component: this.constructor.name, lineCount: chosenRows.size, eventSource, }); From bc86c23f9627435d3adcdce9dc69556f4abebaba Mon Sep 17 00:00:00 2001 From: Vanessa Yuen Date: Thu, 8 Nov 2018 20:49:47 +0100 Subject: [PATCH 208/284] replace usages of old clone method with those of the new one --- test/views/multi-file-patch-view.test.js | 81 +++++++++++++++--------- 1 file changed, 50 insertions(+), 31 deletions(-) diff --git a/test/views/multi-file-patch-view.test.js b/test/views/multi-file-patch-view.test.js index a4d8c73afd8..da471fa3c25 100644 --- a/test/views/multi-file-patch-view.test.js +++ b/test/views/multi-file-patch-view.test.js @@ -6,7 +6,6 @@ import MultiFilePatchView from '../../lib/views/multi-file-patch-view'; import {buildFilePatch, buildMultiFilePatch} from '../../lib/models/patch'; import {nullFile} from '../../lib/models/patch/file'; import FilePatch from '../../lib/models/patch/file-patch'; -import MultiFilePatch from '../../lib/models/patch/multi-file-patch'; import RefHolder from '../../lib/models/ref-holder'; describe('MultiFilePatchView', function() { @@ -286,12 +285,11 @@ describe('MultiFilePatchView', function() { describe('executable mode changes', function() { it('does not render if the mode has not changed', function() { const [fp] = filePatches.getFilePatches(); - const mfp = filePatches.clone({ - filePatches: fp.clone({ + filePatches: [fp.clone({ oldFile: fp.getOldFile().clone({mode: '100644'}), newFile: fp.getNewFile().clone({mode: '100644'}), - }), + })], }); const wrapper = shallow(buildApp({multiFilePatch: mfp})); @@ -299,12 +297,15 @@ describe('MultiFilePatchView', function() { }); 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 [fp] = filePatches.getFilePatches(); + const mfp = filePatches.clone({ + filePatches: [fp.clone({ + oldFile: fp.getOldFile().clone({mode: '100644'}), + newFile: fp.getNewFile().clone({mode: '100755'}), + })], }); - const wrapper = mount(buildApp({filePatch: fp, stagingStatus: 'unstaged'})); + const wrapper = mount(buildApp({multiFilePatch: mfp, stagingStatus: 'unstaged'})); const meta = wrapper.find('FilePatchMetaView[title="Mode change"]'); assert.strictEqual(meta.prop('actionIcon'), 'icon-move-down'); @@ -315,13 +316,16 @@ describe('MultiFilePatchView', function() { }); 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 [fp] = filePatches.getFilePatches(); + const mfp = filePatches.clone({ + filePatches: fp.clone({ + oldFile: fp.getOldFile().clone({mode: '100644'}), + newFile: fp.getNewFile().clone({mode: '100755'}), + }), }); const toggleModeChange = sinon.stub(); - const wrapper = shallow(buildApp({filePatch: fp, stagingStatus: 'staged', toggleModeChange})); + const wrapper = mount(buildApp({multiFilePatch: mfp, stagingStatus: 'unstaged', toggleModeChange})); const meta = wrapper.find('FilePatchMetaView[title="Mode change"]'); assert.isTrue(meta.exists()); @@ -335,22 +339,28 @@ describe('MultiFilePatchView', function() { 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 [fp] = filePatches.getFilePatches(); + const mfp = filePatches.clone({ + filePatches: fp.clone({ + oldFile: fp.getOldFile().clone({mode: '100644'}), + newFile: fp.getNewFile().clone({mode: '100755'}), + }), }); - const wrapper = mount(buildApp({filePatch: fp})); + const wrapper = mount(buildApp({multiFilePatch: mfp})); 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 [fp] = filePatches.getFilePatches(); + const mfp = filePatches.clone({ + filePatches: fp.clone({ + oldFile: fp.getOldFile().clone({mode: '120000', symlink: '/old.txt'}), + newFile: fp.getNewFile().clone({mode: '120000', symlink: '/new.txt'}), + }), }); - const wrapper = mount(buildApp({filePatch: fp, stagingStatus: 'unstaged'})); + const wrapper = mount(buildApp({multiFilePatch: mfp, stagingStatus: 'unstaged'})); const meta = wrapper.find('FilePatchMetaView[title="Symlink changed"]'); assert.isTrue(meta.exists()); assert.strictEqual(meta.prop('actionIcon'), 'icon-move-down'); @@ -363,12 +373,15 @@ describe('MultiFilePatchView', function() { 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 [fp] = filePatches.getFilePatches(); + const mfp = filePatches.clone({ + filePatches: fp.clone({ + oldFile: fp.getOldFile().clone({mode: '120000', symlink: '/old.txt'}), + newFile: fp.getNewFile().clone({mode: '120000', symlink: '/new.txt'}), + }), }); - const wrapper = mount(buildApp({filePatch: fp, stagingStatus: 'staged', toggleSymlinkChange})); + const wrapper = mount(buildApp({multiFilePatch: mfp, stagingStatus: 'staged', toggleSymlinkChange})); const meta = wrapper.find('FilePatchMetaView[title="Symlink changed"]'); assert.isTrue(meta.exists()); assert.strictEqual(meta.prop('actionIcon'), 'icon-move-up'); @@ -379,12 +392,15 @@ describe('MultiFilePatchView', function() { }); it('renders details for a symlink deletion', function() { - const fp = filePatch.clone({ - oldFile: filePatch.getOldFile().clone({mode: '120000', symlink: '/old.txt'}), - newFile: nullFile, + const [fp] = filePatches.getFilePatches(); + const mfp = filePatches.clone({ + filePatches: fp.clone({ + oldFile: fp.getOldFile().clone({mode: '120000', symlink: '/old.txt'}), + newFile: nullFile, + }), }); - const wrapper = mount(buildApp({filePatch: fp})); + const wrapper = mount(buildApp({multiFilePatch: mfp})); const meta = wrapper.find('FilePatchMetaView[title="Symlink deleted"]'); assert.isTrue(meta.exists()); assert.strictEqual( @@ -394,9 +410,12 @@ describe('MultiFilePatchView', function() { }); it('renders details for a symlink creation', function() { - const fp = filePatch.clone({ - oldFile: nullFile, - newFile: filePatch.getOldFile().clone({mode: '120000', symlink: '/new.txt'}), + const [fp] = filePatches.getFilePatches(); + const mfp = filePatches.clone({ + filePatches: fp.clone({ + oldFile: nullFile, + newFile: fp.getOldFile().clone({mode: '120000', symlink: '/new.txt'}), + }), }); const wrapper = mount(buildApp({filePatch: fp})); From 634a3914d455e8465fb013c380467c3d3fe453d1 Mon Sep 17 00:00:00 2001 From: Vanessa Yuen Date: Thu, 8 Nov 2018 20:59:51 +0100 Subject: [PATCH 209/284] One more thing... --- test/views/multi-file-patch-view.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/views/multi-file-patch-view.test.js b/test/views/multi-file-patch-view.test.js index da471fa3c25..b09df3b557f 100644 --- a/test/views/multi-file-patch-view.test.js +++ b/test/views/multi-file-patch-view.test.js @@ -418,7 +418,7 @@ describe('MultiFilePatchView', function() { }), }); - const wrapper = mount(buildApp({filePatch: fp})); + const wrapper = mount(buildApp({multiFilePatch: mfp})); const meta = wrapper.find('FilePatchMetaView[title="Symlink created"]'); assert.isTrue(meta.exists()); assert.strictEqual( From 973566d6421220c1af508e02a397ce268e8819aa Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 8 Nov 2018 15:00:54 -0500 Subject: [PATCH 210/284] Make the MultiFilePatch constructor like the others --- lib/models/patch/builder.js | 4 +- lib/models/patch/multi-file-patch.js | 18 ++++---- lib/models/repository-states/state.js | 4 +- test/models/patch/multi-file-patch.test.js | 48 +++++++++++----------- 4 files changed, 37 insertions(+), 37 deletions(-) diff --git a/lib/models/patch/builder.js b/lib/models/patch/builder.js index 7e3c9c0e699..35070215af0 100644 --- a/lib/models/patch/builder.js +++ b/lib/models/patch/builder.js @@ -21,7 +21,7 @@ export function buildFilePatch(diffs) { throw new Error(`Unexpected number of diffs: ${diffs.length}`); } - return new MultiFilePatch(layeredBuffer.buffer, layeredBuffer.layers, [filePatch]); + return new MultiFilePatch({filePatches: [filePatch], ...layeredBuffer}); } export function buildMultiFilePatch(diffs) { @@ -60,7 +60,7 @@ export function buildMultiFilePatch(diffs) { const filePatches = actions.map(action => action()); - return new MultiFilePatch(layeredBuffer.buffer, layeredBuffer.layers, filePatches); + return new MultiFilePatch({filePatches, ...layeredBuffer}); } function emptyDiffFilePatch() { diff --git a/lib/models/patch/multi-file-patch.js b/lib/models/patch/multi-file-patch.js index 06a80953b5e..736dbd8fa40 100644 --- a/lib/models/patch/multi-file-patch.js +++ b/lib/models/patch/multi-file-patch.js @@ -1,17 +1,17 @@ import {TextBuffer} from 'atom'; export default class MultiFilePatch { - constructor(buffer = null, layers = {}, filePatches = []) { - this.buffer = buffer; + constructor({buffer, layers, filePatches}) { + this.buffer = buffer || null; - this.patchLayer = layers.patch; - this.hunkLayer = layers.hunk; - this.unchangedLayer = layers.unchanged; - this.additionLayer = layers.addition; - this.deletionLayer = layers.deletion; - this.noNewlineLayer = layers.noNewline; + this.patchLayer = layers && layers.patch; + this.hunkLayer = layers && layers.hunk; + this.unchangedLayer = layers && layers.unchanged; + this.additionLayer = layers && layers.addition; + this.deletionLayer = layers && layers.deletion; + this.noNewlineLayer = layers && layers.noNewline; - this.filePatches = filePatches; + this.filePatches = filePatches || []; this.filePatchesByMarker = new Map(); this.hunksByMarker = new Map(); diff --git a/lib/models/repository-states/state.js b/lib/models/repository-states/state.js index c31af1bbfac..9cb953ee489 100644 --- a/lib/models/repository-states/state.js +++ b/lib/models/repository-states/state.js @@ -276,11 +276,11 @@ export default class State { } getFilePatchForPath(filePath, options = {}) { - return Promise.resolve(new MultiFilePatch()); + return Promise.resolve(new MultiFilePatch({})); } getStagedChangesPatch() { - return Promise.resolve(new MultiFilePatch()); + return Promise.resolve(new MultiFilePatch({})); } readFileFromIndex(filePath) { diff --git a/test/models/patch/multi-file-patch.test.js b/test/models/patch/multi-file-patch.test.js index 41ad2b920a8..1ea45887aa3 100644 --- a/test/models/patch/multi-file-patch.test.js +++ b/test/models/patch/multi-file-patch.test.js @@ -25,63 +25,63 @@ describe('MultiFilePatch', function() { }); it('creates an empty patch when constructed with no arguments', function() { - const empty = new MultiFilePatch(); + const empty = new MultiFilePatch({}); assert.isFalse(empty.anyPresent()); assert.lengthOf(empty.getFilePatches(), 0); }); it('detects when it is not empty', function() { - const mp = new MultiFilePatch(buffer, layers, [buildFilePatchFixture(0)]); + const mp = new MultiFilePatch({buffer, layers, filePatches: [buildFilePatchFixture(0)]}); assert.isTrue(mp.anyPresent()); }); it('has an accessor for its file patches', function() { const filePatches = [buildFilePatchFixture(0), buildFilePatchFixture(1)]; - const mp = new MultiFilePatch(buffer, layers, filePatches); + const mp = new MultiFilePatch({buffer, layers, filePatches}); assert.strictEqual(mp.getFilePatches(), filePatches); }); describe('didAnyChangeExecutableMode()', function() { it('detects when at least one patch contains an executable mode change', function() { - const yes = new MultiFilePatch(buffer, layers, [ + const yes = new MultiFilePatch({buffer, layers, filePatches: [ buildFilePatchFixture(0, {oldFileMode: '100644', newFileMode: '100755'}), buildFilePatchFixture(1), - ]); + ]}); assert.isTrue(yes.didAnyChangeExecutableMode()); }); it('detects when none of the patches contain an executable mode change', function() { - const no = new MultiFilePatch(buffer, layers, [ + const no = new MultiFilePatch({buffer, layers, filePatches: [ buildFilePatchFixture(0), buildFilePatchFixture(1), - ]); + ]}); assert.isFalse(no.didAnyChangeExecutableMode()); }); }); describe('anyHaveTypechange()', function() { it('detects when at least one patch contains a symlink change', function() { - const yes = new MultiFilePatch(buffer, layers, [ + const yes = new MultiFilePatch({buffer, layers, filePatches: [ buildFilePatchFixture(0, {oldFileMode: '100644', newFileMode: '120000', newFileSymlink: 'destination'}), buildFilePatchFixture(1), - ]); + ]}); assert.isTrue(yes.anyHaveTypechange()); }); it('detects when none of its patches contain a symlink change', function() { - const no = new MultiFilePatch(buffer, layers, [ + const no = new MultiFilePatch({buffer, layers, filePatches: [ buildFilePatchFixture(0), buildFilePatchFixture(1), - ]); + ]}); assert.isFalse(no.anyHaveTypechange()); }); }); it('computes the maximum line number width of any hunk in any patch', function() { - const mp = new MultiFilePatch(buffer, layers, [ + const mp = new MultiFilePatch({buffer, layers, filePatches: [ buildFilePatchFixture(0), buildFilePatchFixture(1), - ]); + ]}); assert.strictEqual(mp.getMaxLineNumberWidth(), 2); }); @@ -90,7 +90,7 @@ describe('MultiFilePatch', function() { for (let i = 0; i < 10; i++) { filePatches.push(buildFilePatchFixture(i)); } - const mp = new MultiFilePatch(buffer, layers, filePatches); + const mp = new MultiFilePatch({buffer, layers, filePatches}); assert.strictEqual(mp.getFilePatchAt(0), filePatches[0]); assert.strictEqual(mp.getFilePatchAt(7), filePatches[0]); @@ -99,11 +99,11 @@ describe('MultiFilePatch', function() { }); it('creates a set of all unique paths referenced by patches', function() { - const mp = new MultiFilePatch(buffer, layers, [ + const mp = new MultiFilePatch({buffer, layers, filePatches: [ buildFilePatchFixture(0, {oldFilePath: 'file-0-before.txt', newFilePath: 'file-0-after.txt'}), buildFilePatchFixture(1, {status: 'added', newFilePath: 'file-1.txt'}), buildFilePatchFixture(2, {oldFilePath: 'file-2.txt', newFilePath: 'file-2.txt'}), - ]); + ]}); assert.sameMembers( Array.from(mp.getPathSet()), @@ -117,7 +117,7 @@ describe('MultiFilePatch', function() { buildFilePatchFixture(1), buildFilePatchFixture(2), ]; - const mp = new MultiFilePatch(buffer, layers, filePatches); + const mp = new MultiFilePatch({buffer, layers, filePatches}); assert.strictEqual(mp.getHunkAt(0), filePatches[0].getHunks()[0]); assert.strictEqual(mp.getHunkAt(3), filePatches[0].getHunks()[0]); @@ -130,10 +130,10 @@ describe('MultiFilePatch', function() { }); it('represents itself as an apply-ready string', function() { - const mp = new MultiFilePatch(buffer, layers, [ + const mp = new MultiFilePatch({buffer, layers, filePatches: [ buildFilePatchFixture(0), buildFilePatchFixture(1), - ]); + ]}); assert.strictEqual(mp.toString(), dedent` diff --git a/file-0.txt b/file-0.txt @@ -174,7 +174,7 @@ describe('MultiFilePatch', function() { buildFilePatchFixture(1), buildFilePatchFixture(2, {noNewline: true}), ]; - const lastPatch = new MultiFilePatch(lastBuffer, layers, lastFilePatches); + const lastPatch = new MultiFilePatch({buffer: lastBuffer, layers, filePatches: lastFilePatches}); buffer = new TextBuffer(); layers = { @@ -213,7 +213,7 @@ describe('MultiFilePatch', function() { buildFilePatchFixture(2), buildFilePatchFixture(3), ]; - const original = new MultiFilePatch(buffer, layers, filePatches); + const original = new MultiFilePatch({buffer, layers, filePatches}); const stagePatch = original.getStagePatchForLines(new Set([9, 14, 25, 26])); assert.strictEqual(stagePatch.getBuffer().getText(), dedent` @@ -275,7 +275,7 @@ describe('MultiFilePatch', function() { buildFilePatchFixture(0), buildFilePatchFixture(1), ]; - const original = new MultiFilePatch(buffer, layers, filePatches); + const original = new MultiFilePatch({buffer, layers, filePatches}); const hunk = original.getFilePatches()[0].getHunks()[1]; const stagePatch = original.getStagePatchForHunk(hunk); @@ -311,7 +311,7 @@ describe('MultiFilePatch', function() { buildFilePatchFixture(2), buildFilePatchFixture(3), ]; - const original = new MultiFilePatch(buffer, layers, filePatches); + const original = new MultiFilePatch({buffer, layers, filePatches}); const unstagePatch = original.getUnstagePatchForLines(new Set([1, 2, 21, 26, 29, 30])); @@ -392,7 +392,7 @@ describe('MultiFilePatch', function() { buildFilePatchFixture(0), buildFilePatchFixture(1), ]; - const original = new MultiFilePatch(buffer, layers, filePatches); + const original = new MultiFilePatch({buffer, layers, filePatches}); const hunk = original.getFilePatches()[1].getHunks()[0]; const unstagePatch = original.getUnstagePatchForHunk(hunk); From a3ef82ac7e2763bb8ab4de69ca5cfce0851770b3 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 8 Nov 2018 15:01:23 -0500 Subject: [PATCH 211/284] Return real Ranges --- lib/models/patch/multi-file-patch.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/models/patch/multi-file-patch.js b/lib/models/patch/multi-file-patch.js index 736dbd8fa40..1dd87a169a6 100644 --- a/lib/models/patch/multi-file-patch.js +++ b/lib/models/patch/multi-file-patch.js @@ -1,4 +1,4 @@ -import {TextBuffer} from 'atom'; +import {TextBuffer, Range} from 'atom'; export default class MultiFilePatch { constructor({buffer, layers, filePatches}) { @@ -183,7 +183,7 @@ export default class MultiFilePatch { } } - return [[newSelectionRow, 0], [newSelectionRow, Infinity]]; + return Range.fromObject([[newSelectionRow, 0], [newSelectionRow, Infinity]]); } adoptBufferFrom(lastMultiFilePatch) { From 37de91aa9e1ce9e6d4b9535412ece66399d33557 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 8 Nov 2018 15:01:51 -0500 Subject: [PATCH 212/284] Use .clone() within MultiFilePatch --- lib/models/patch/multi-file-patch.js | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/lib/models/patch/multi-file-patch.js b/lib/models/patch/multi-file-patch.js index 1dd87a169a6..d44d44c33cf 100644 --- a/lib/models/patch/multi-file-patch.js +++ b/lib/models/patch/multi-file-patch.js @@ -97,12 +97,7 @@ export default class MultiFilePatch { const nextFilePatches = this.getFilePatchesContaining(selectedLineSet).map(fp => { return fp.buildStagePatchForLines(this.getBuffer(), nextLayeredBuffer, selectedLineSet); }); - - return new MultiFilePatch( - nextLayeredBuffer.buffer, - nextLayeredBuffer.layers, - nextFilePatches, - ); + return this.clone({...nextLayeredBuffer, filePatches: nextFilePatches}); } getStagePatchForHunk(hunk) { @@ -114,12 +109,7 @@ export default class MultiFilePatch { const nextFilePatches = this.getFilePatchesContaining(selectedLineSet).map(fp => { return fp.buildUnstagePatchForLines(this.getBuffer(), nextLayeredBuffer, selectedLineSet); }); - - return new MultiFilePatch( - nextLayeredBuffer.buffer, - nextLayeredBuffer.layers, - nextFilePatches, - ); + return this.clone({...nextLayeredBuffer, filePatches: nextFilePatches}); } getUnstagePatchForHunk(hunk) { @@ -130,7 +120,7 @@ export default class MultiFilePatch { if (lastSelectedRows.size === 0) { const [firstPatch] = this.getFilePatches(); if (!firstPatch) { - return [[0, 0], [0, 0]]; + return Range.fromObject([[0, 0], [0, 0]]); } return firstPatch.getFirstChangeRange(); @@ -146,12 +136,12 @@ export default class MultiFilePatch { changeLoop: for (const change of hunk.getChanges()) { for (const {intersection, gap} of change.intersectRows(lastSelectedRows, true)) { - // Only include a partial range if this intersection includes the last selected buffer row. + // Only include a partial range if this intersection includes the last selected buffer row. includesMax = intersection.intersectsRow(lastMax); const delta = includesMax ? lastMax - intersection.start.row + 1 : intersection.getRowCount(); if (gap) { - // Range of unselected changes. + // Range of unselected changes. hunkSelectionOffset += delta; } From ccced0b8343d1b89e413513f68b2669ea120a6d5 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 8 Nov 2018 16:20:02 -0500 Subject: [PATCH 213/284] Test fixture builders for the MultiFilePatch models --- test/builder/patch.js | 275 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 275 insertions(+) create mode 100644 test/builder/patch.js diff --git a/test/builder/patch.js b/test/builder/patch.js new file mode 100644 index 00000000000..ea0efcf900b --- /dev/null +++ b/test/builder/patch.js @@ -0,0 +1,275 @@ +// Builders for classes related to MultiFilePatches. + +import {TextBuffer} from 'atom'; +import MultiFilePatch from '../../lib/models/patch/multi-file-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 {Unchanged, Addition, Deletion, NoNewline} from '../../lib/models/patch/region'; + +class LayeredBuffer { + constructor() { + this.buffer = new TextBuffer(); + this.layers = ['patch', 'hunk', 'unchanged', 'addition', 'deletion', 'noNewline'].reduce((layers, name) => { + layers[name] = this.buffer.addMarkerLayer(); + return layers; + }, {}); + } + + getInsertionPoint() { + return this.buffer.getEndPosition(); + } + + getLayer(markerLayerName) { + const layer = this.layers[markerLayerName]; + if (!layer) { + throw new Error(`invalid marker layer name: ${markerLayerName}`); + } + return layer; + } + + appendMarked(markerLayerName, lines) { + const startPosition = this.buffer.getEndPosition(); + const layer = this.getLayer(markerLayerName); + this.buffer.append(lines.join('\n')); + const marker = layer.markRange([startPosition, this.buffer.getEndPosition()]); + this.buffer.append('\n'); + return marker; + } + + markFrom(markerLayerName, startPosition) { + const endPosition = this.buffer.getEndPosition(); + const layer = this.getLayer(markerLayerName); + return layer.markRange([startPosition, endPosition]); + } + + wrapReturn(object) { + return { + buffer: this.buffer, + layers: this.layers, + ...object, + }; + } +} + +class MultiFilePatchBuilder { + constructor(layeredBuffer = null) { + this.layeredBuffer = layeredBuffer; + + this.filePatches = []; + } + + addFilePatch(block) { + const filePatch = new FilePatchBuilder(this.layeredBuffer); + block(filePatch); + this.filePatches.push(filePatch.build().filePatch); + return this; + } + + build() { + return this.layeredBuffer.wrapReturn({ + multiFilePatch: new MultiFilePatch({ + buffer: this.layeredBuffer.buffer, + layers: this.layeredBuffer.layers, + filePatches: this.filePatches, + }), + }); + } +} + +class FilePatchBuilder { + constructor(layeredBuffer = null) { + this.layeredBuffer = layeredBuffer; + + this.oldFile = new File({path: 'file', mode: '100644'}); + this.newFile = new File({path: 'file', mode: '100644'}); + + this.patchBuilder = new PatchBuilder(this.layeredBuffer); + } + + setOldFile(block) { + const file = new FileBuilder(); + block(file); + this.oldFile = file.build().file; + return this; + } + + setNewFile(block) { + const file = new FileBuilder(); + block(file); + this.newFile = file.build().file; + return this; + } + + build() { + const {patch} = this.patchBuilder.build(); + + return this.layeredBuffer.wrapReturn({ + filePatch: new FilePatch(this.oldFile, this.newFile, patch), + }); + } +} + +class FileBuilder { + constructor() { + this.path = 'file.txt'; + this.mode = '100644'; + this.symlink = null; + } + + path(thePath) { + this.path = thePath; + return this; + } + + mode(theMode) { + this.mode = theMode; + return this; + } + + executable() { + return this.mode('100755'); + } + + symlinkTo(destinationPath) { + this.symlink = destinationPath; + return this.mode('120000'); + } + + build() { + return {file: new File({path: this.path, mode: this.mode, symlink: this.symlink})}; + } +} + +class PatchBuilder { + constructor(layeredBuffer = null) { + this.layeredBuffer = layeredBuffer; + + this.status = 'modified'; + this.hunks = []; + + this.patchStart = this.layeredBuffer.getInsertionPoint(); + } + + status(st) { + if (['modified', 'added', 'deleted'].indexOf(st) === -1) { + throw new Error(`Unrecognized status: ${st} (must be 'modified', 'added' or 'deleted')`); + } + + this.status = st; + return this; + } + + addHunk(block) { + const hunk = new HunkBuilder(this.layeredBuffer); + this.hunks.push(hunk.build().hunk); + return this; + } + + build() { + if (this.hunks.length === 0) { + this.addHunk(hunk => hunk.unchanged('0000').added('0001').deleted('0002').unchanged('0003')); + this.addHunk(hunk => hunk.startRow(10).unchanged('0004').added('0005').deleted('0006').unchanged('0007')); + } + + const marker = this.layeredBuffer.markFrom(this.patchStart()); + + return this.layeredBuffer.wrapReturn({ + patch: new Patch({status: this.status, hunks: this.hunks, marker}), + }); + } +} + +class HunkBuilder { + constructor(layeredBuffer = null) { + this.layeredBuffer = layeredBuffer; + + this.oldStartRow = 0; + this.oldRowCount = null; + this.newStartRow = 0; + this.newRowCount = null; + + this.sectionHeading = "don't care"; + + this.hunkStartPoint = this.layeredBuffer.getInsertionPoint(); + this.regions = []; + } + + oldRow(rowNumber) { + this.oldStartRow = rowNumber; + return this; + } + + unchanged(...lines) { + this.regions.push(new Unchanged(this.layeredBuffer.appendMarked(lines))); + return this; + } + + added(...lines) { + this.regions.push(new Addition(this.layeredBuffer.appendMarked(lines))); + return this; + } + + deleted(...lines) { + this.regions.push(new Deletion(this.layeredBuffer.appendMarked(lines))); + return this; + } + + noNewline() { + this.regions.push(new NoNewline(this.layeredBuffer.appendMarked(' No newline at end of file'))); + return this; + } + + build() { + if (this.oldRowCount === null) { + this.oldRowCount = this.regions.reduce((count, region) => region.when({ + unchanged: () => count + region.bufferRowCount(), + deletion: () => count + region.bufferRowCount(), + default: () => count, + }), 0); + } + + if (this.newRowCount === null) { + this.newRowCount = this.regions.reduce((count, region) => region.when({ + unchanged: () => count + region.bufferRowCount(), + addition: () => count + region.bufferRowCount(), + default: () => count, + }), 0); + } + + if (this.regions.length === 0) { + this.unchanged('0000').added('0001').deleted('0002').unchanged('0003'); + } + + const marker = this.layeredBuffer.markFrom('hunk', this.hunkStartPoint); + + return this.layeredBuffer.wrapReturn({ + hunk: new Hunk({ + oldStartRow: this.oldStartRow, + oldRowCount: this.oldRowCount, + newStartRow: this.newStartRow, + newRowCount: this.newRowCount, + sectionHeading: this.sectionHeading, + marker, + regions: this.regions, + }), + }); + } +} + +export function buildMultiFilePatch() { + return new MultiFilePatchBuilder(new LayeredBuffer()); +} + +export function buildFilePatch() { + return new FilePatchBuilder(new LayeredBuffer()); +} + +export function buildPatch() { + return new PatchBuilder(new LayeredBuffer()); +} + +export function buildHunk() { + return new HunkBuilder(new LayeredBuffer()); +} From 52945456ed63bde78bc3e57fa7763ed9acddc232 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 8 Nov 2018 16:29:55 -0500 Subject: [PATCH 214/284] Default newFile to oldFile because they're almost always the same --- test/builder/patch.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/builder/patch.js b/test/builder/patch.js index ea0efcf900b..2ba04ccd0c7 100644 --- a/test/builder/patch.js +++ b/test/builder/patch.js @@ -83,7 +83,7 @@ class FilePatchBuilder { this.layeredBuffer = layeredBuffer; this.oldFile = new File({path: 'file', mode: '100644'}); - this.newFile = new File({path: 'file', mode: '100644'}); + this.newFile = null; this.patchBuilder = new PatchBuilder(this.layeredBuffer); } @@ -105,6 +105,10 @@ class FilePatchBuilder { build() { const {patch} = this.patchBuilder.build(); + if (this.newFile === null) { + this.newFile = this.oldFile.clone(); + } + return this.layeredBuffer.wrapReturn({ filePatch: new FilePatch(this.oldFile, this.newFile, patch), }); From 8fc07fa57bc5a89f6435fa2a6036a1fd2e8d5889 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 8 Nov 2018 16:37:48 -0500 Subject: [PATCH 215/284] Can't use the same names for properties and methods --- test/builder/patch.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/test/builder/patch.js b/test/builder/patch.js index 2ba04ccd0c7..e0e03dfef96 100644 --- a/test/builder/patch.js +++ b/test/builder/patch.js @@ -117,18 +117,18 @@ class FilePatchBuilder { class FileBuilder { constructor() { - this.path = 'file.txt'; - this.mode = '100644'; - this.symlink = null; + this._path = 'file.txt'; + this._mode = '100644'; + this._symlink = null; } path(thePath) { - this.path = thePath; + this._path = thePath; return this; } mode(theMode) { - this.mode = theMode; + this._mode = theMode; return this; } @@ -137,12 +137,12 @@ class FileBuilder { } symlinkTo(destinationPath) { - this.symlink = destinationPath; + this._symlink = destinationPath; return this.mode('120000'); } build() { - return {file: new File({path: this.path, mode: this.mode, symlink: this.symlink})}; + return {file: new File({path: this._path, mode: this._mode, symlink: this._symlink})}; } } From 483c69fd8d9c69f22f1533080634f1de1ccb08ed Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 8 Nov 2018 16:38:00 -0500 Subject: [PATCH 216/284] Forgot the first argument to .appendMarked() --- test/builder/patch.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/builder/patch.js b/test/builder/patch.js index e0e03dfef96..5add33f6515 100644 --- a/test/builder/patch.js +++ b/test/builder/patch.js @@ -177,7 +177,7 @@ class PatchBuilder { this.addHunk(hunk => hunk.startRow(10).unchanged('0004').added('0005').deleted('0006').unchanged('0007')); } - const marker = this.layeredBuffer.markFrom(this.patchStart()); + const marker = this.layeredBuffer.markFrom('patch', this.patchStart); return this.layeredBuffer.wrapReturn({ patch: new Patch({status: this.status, hunks: this.hunks, marker}), @@ -206,22 +206,22 @@ class HunkBuilder { } unchanged(...lines) { - this.regions.push(new Unchanged(this.layeredBuffer.appendMarked(lines))); + this.regions.push(new Unchanged(this.layeredBuffer.appendMarked('unchanged', lines))); return this; } added(...lines) { - this.regions.push(new Addition(this.layeredBuffer.appendMarked(lines))); + this.regions.push(new Addition(this.layeredBuffer.appendMarked('addition', lines))); return this; } deleted(...lines) { - this.regions.push(new Deletion(this.layeredBuffer.appendMarked(lines))); + this.regions.push(new Deletion(this.layeredBuffer.appendMarked('deletion', lines))); return this; } noNewline() { - this.regions.push(new NoNewline(this.layeredBuffer.appendMarked(' No newline at end of file'))); + this.regions.push(new NoNewline(this.layeredBuffer.appendMarked('noNewline', ' No newline at end of file'))); return this; } From 85475053f51c76f947145f5832b8e5c119bc69d9 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 8 Nov 2018 16:39:08 -0500 Subject: [PATCH 217/284] First batch of examples that use the builder API --- test/models/patch/multi-file-patch.test.js | 62 ++++++++++++++-------- 1 file changed, 41 insertions(+), 21 deletions(-) diff --git a/test/models/patch/multi-file-patch.test.js b/test/models/patch/multi-file-patch.test.js index 1ea45887aa3..8633e7d79bf 100644 --- a/test/models/patch/multi-file-patch.test.js +++ b/test/models/patch/multi-file-patch.test.js @@ -1,6 +1,8 @@ import {TextBuffer} from 'atom'; import dedent from 'dedent-js'; +import {buildMultiFilePatch} from '../../builder/patch'; + import MultiFilePatch from '../../../lib/models/patch/multi-file-patch'; import FilePatch from '../../../lib/models/patch/file-patch'; import File, {nullFile} from '../../../lib/models/patch/file'; @@ -31,48 +33,66 @@ describe('MultiFilePatch', function() { }); it('detects when it is not empty', function() { - const mp = new MultiFilePatch({buffer, layers, filePatches: [buildFilePatchFixture(0)]}); - assert.isTrue(mp.anyPresent()); + const {multiFilePatch} = buildMultiFilePatch() + .addFilePatch(filePatch => { + filePatch + .setOldFile(file => file.path('file-0.txt')) + .setNewFile(file => file.path('file-0.txt')); + }) + .build(); + + assert.isTrue(multiFilePatch.anyPresent()); }); it('has an accessor for its file patches', function() { - const filePatches = [buildFilePatchFixture(0), buildFilePatchFixture(1)]; - const mp = new MultiFilePatch({buffer, layers, filePatches}); - assert.strictEqual(mp.getFilePatches(), filePatches); + const {multiFilePatch} = buildMultiFilePatch() + .addFilePatch(filePatch => filePatch.setOldFile(file => file.path('file-0.txt'))) + .addFilePatch(filePatch => filePatch.setOldFile(file => file.path('file-1.txt'))) + .build(); + + assert.lengthOf(multiFilePatch.getFilePatches(), 2); + const [fp0, fp1] = multiFilePatch.getFilePatches(); + assert.strictEqual(fp0.getOldPath(), 'file-0.txt'); + assert.strictEqual(fp1.getOldPath(), 'file-1.txt'); }); describe('didAnyChangeExecutableMode()', function() { it('detects when at least one patch contains an executable mode change', function() { - const yes = new MultiFilePatch({buffer, layers, filePatches: [ - buildFilePatchFixture(0, {oldFileMode: '100644', newFileMode: '100755'}), - buildFilePatchFixture(1), - ]}); + const {multiFilePatch: yes} = buildMultiFilePatch() + .addFilePatch(filePatch => { + filePatch.setOldFile(file => file.path('file-0.txt')); + filePatch.setNewFile(file => file.path('file-0.txt').executable()); + }) + .build(); assert.isTrue(yes.didAnyChangeExecutableMode()); }); it('detects when none of the patches contain an executable mode change', function() { - const no = new MultiFilePatch({buffer, layers, filePatches: [ - buildFilePatchFixture(0), - buildFilePatchFixture(1), - ]}); + const {multiFilePatch: no} = buildMultiFilePatch() + .addFilePatch(filePatch => filePatch.setOldFile(file => file.path('file-0.txt'))) + .addFilePatch(filePatch => filePatch.setOldFile(file => file.path('file-1.txt'))) + .build(); assert.isFalse(no.didAnyChangeExecutableMode()); }); }); describe('anyHaveTypechange()', function() { it('detects when at least one patch contains a symlink change', function() { - const yes = new MultiFilePatch({buffer, layers, filePatches: [ - buildFilePatchFixture(0, {oldFileMode: '100644', newFileMode: '120000', newFileSymlink: 'destination'}), - buildFilePatchFixture(1), - ]}); + const {multiFilePatch: yes} = buildMultiFilePatch() + .addFilePatch(filePatch => filePatch.setOldFile(file => file.path('file-0.txt'))) + .addFilePatch(filePatch => { + filePatch.setOldFile(file => file.path('file-0.txt')); + filePatch.setNewFile(file => file.path('file-0.txt').symlink('somewhere.txt')); + }) + .build(); assert.isTrue(yes.anyHaveTypechange()); }); it('detects when none of its patches contain a symlink change', function() { - const no = new MultiFilePatch({buffer, layers, filePatches: [ - buildFilePatchFixture(0), - buildFilePatchFixture(1), - ]}); + const {multiFilePatch: no} = buildMultiFilePatch() + .addFilePatch(filePatch => filePatch.setOldFile(file => file.path('file-0.txt'))) + .addFilePatch(filePatch => filePatch.setOldFile(file => file.path('file-1.txt'))) + .build(); assert.isFalse(no.anyHaveTypechange()); }); }); From f3a8b1f964021700bd310f173cbac7f63754f1bd Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 8 Nov 2018 16:41:28 -0500 Subject: [PATCH 218/284] Build real Ranges in Patch methods --- lib/models/patch/patch.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/models/patch/patch.js b/lib/models/patch/patch.js index 3d2ee1ea101..cc5cd9ef5f3 100644 --- a/lib/models/patch/patch.js +++ b/lib/models/patch/patch.js @@ -246,16 +246,16 @@ export default class Patch { getFirstChangeRange() { const firstHunk = this.getHunks()[0]; if (!firstHunk) { - return [[0, 0], [0, 0]]; + return Range.fromObject([[0, 0], [0, 0]]); } const firstChange = firstHunk.getChanges()[0]; if (!firstChange) { - return [[0, 0], [0, 0]]; + return Range.fromObject([[0, 0], [0, 0]]); } const firstRow = firstChange.getStartBufferRow(); - return [[firstRow, 0], [firstRow, Infinity]]; + return Range.fromObject([[firstRow, 0], [firstRow, Infinity]]); } toStringIn(buffer) { From b6176ec38e18eb958ea1d543919ec2c825edb147 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 8 Nov 2018 16:41:40 -0500 Subject: [PATCH 219/284] Delegate getFirstChangeRange() to the Patch --- lib/models/patch/file-patch.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/models/patch/file-patch.js b/lib/models/patch/file-patch.js index 7f7ce899dc0..2fa95ba2aff 100644 --- a/lib/models/patch/file-patch.js +++ b/lib/models/patch/file-patch.js @@ -73,6 +73,10 @@ export default class FilePatch { return this.getPatch().getBuffer(); } + getFirstChangeRange() { + return this.getPatch().getFirstChangeRange(); + } + getMaxLineNumberWidth() { return this.getPatch().getMaxLineNumberWidth(); } From 566b9e9e09c6bb192011091ff15c6cf88fe5843b Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 8 Nov 2018 16:45:50 -0500 Subject: [PATCH 220/284] Forward .status() and .addHunk() to PatchBuilder --- test/builder/patch.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/builder/patch.js b/test/builder/patch.js index 5add33f6515..3d23862b3e3 100644 --- a/test/builder/patch.js +++ b/test/builder/patch.js @@ -102,6 +102,16 @@ class FilePatchBuilder { return this; } + status(...args) { + this.patchBuilder.status(...args); + return this; + } + + addHunk(...args) { + this.patchBuilder.addHunk(...args); + return this; + } + build() { const {patch} = this.patchBuilder.build(); From 73d88c0bb0b1cb15bfd99e75fc159ff178dbcdfc Mon Sep 17 00:00:00 2001 From: Tilde Ann Thurium Date: Thu, 8 Nov 2018 16:24:47 -0800 Subject: [PATCH 221/284] fix MultiFilePatchController tests --- .../multi-file-patch-controller.test.js | 114 ++++++++++-------- 1 file changed, 61 insertions(+), 53 deletions(-) diff --git a/test/controllers/multi-file-patch-controller.test.js b/test/controllers/multi-file-patch-controller.test.js index f33be04ea21..6987f1e567c 100644 --- a/test/controllers/multi-file-patch-controller.test.js +++ b/test/controllers/multi-file-patch-controller.test.js @@ -1,14 +1,14 @@ import path from 'path'; import fs from 'fs-extra'; import React from 'react'; -import {shallow} from 'enzyme'; +import {mount, shallow} from 'enzyme'; import MultiFilePatchController from '../../lib/controllers/multi-file-patch-controller'; import * as reporterProxy from '../../lib/reporter-proxy'; import {cloneRepository, buildRepository} from '../helpers'; describe('MultiFilePatchController', function() { - let atomEnv, repository, multiFilePatch; + let atomEnv, repository, multiFilePatch, filePatch; beforeEach(async function() { atomEnv = global.buildAtomEnvironment(); @@ -17,9 +17,11 @@ describe('MultiFilePatchController', function() { repository = await buildRepository(workdirPath); // a.txt: unstaged changes - await fs.writeFile(path.join(workdirPath, 'a.txt'), '00\n01\n02\n03\n04\n05\n06'); + const filePath = 'a.txt'; + await fs.writeFile(path.join(workdirPath, filePath), '00\n01\n02\n03\n04\n05\n06'); - multiFilePatch = await repository.getStagedChangesPatch(); + multiFilePatch = await repository.getFilePatchForPath(filePath); + [filePatch] = multiFilePatch.getFilePatches(); }); afterEach(function() { @@ -30,8 +32,6 @@ describe('MultiFilePatchController', function() { const props = { repository, stagingStatus: 'unstaged', - relPath: 'a.txt', - isPartiallyStaged: false, multiFilePatch, hasUndoHistory: false, workspace: atomEnv.workspace, @@ -58,31 +58,32 @@ describe('MultiFilePatchController', function() { it('calls undoLastDiscard through with set arguments', function() { const undoLastDiscard = sinon.spy(); - const wrapper = shallow(buildApp({relPath: 'b.txt', undoLastDiscard})); - wrapper.find('MultiFilePatchView').prop('undoLastDiscard')(); + const wrapper = mount(buildApp({undoLastDiscard, stagingStatus: 'staged'})); - assert.isTrue(undoLastDiscard.calledWith('b.txt', repository)); + wrapper.find('MultiFilePatchView').prop('undoLastDiscard')(filePatch); + + assert.isTrue(undoLastDiscard.calledWith(filePatch.getPath(), repository)); }); it('calls surfaceFileAtPath with set arguments', function() { const surfaceFileAtPath = sinon.spy(); const wrapper = shallow(buildApp({relPath: 'c.txt', surfaceFileAtPath})); - wrapper.find('MultiFilePatchView').prop('surfaceFile')(); + wrapper.find('MultiFilePatchView').prop('surfaceFile')(filePatch); - assert.isTrue(surfaceFileAtPath.calledWith('c.txt', 'unstaged')); + assert.isTrue(surfaceFileAtPath.calledWith(filePatch.getPath(), 'unstaged')); }); 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})); + const wrapper = shallow(buildApp({stagingStatus: 'unstaged', destroy})); - await wrapper.find('MultiFilePatchView').prop('diveIntoMirrorPatch')(); + await wrapper.find('MultiFilePatchView').prop('diveIntoMirrorPatch')(filePatch); assert.isTrue(destroy.called); assert.isTrue(atomEnv.workspace.open.calledWith( - 'atom-github://file-patch/c.txt' + + `atom-github://file-patch/${filePatch.getPath()}` + `?workdir=${encodeURIComponent(repository.getWorkingDirectoryPath())}&stagingStatus=staged`, )); }); @@ -90,13 +91,14 @@ describe('MultiFilePatchController', function() { 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})); + const wrapper = shallow(buildApp({stagingStatus: 'staged', destroy})); + - await wrapper.find('MultiFilePatchView').prop('diveIntoMirrorPatch')(); + await wrapper.find('MultiFilePatchView').prop('diveIntoMirrorPatch')(filePatch); assert.isTrue(destroy.called); assert.isTrue(atomEnv.workspace.open.calledWith( - 'atom-github://file-patch/d.txt' + + `atom-github://file-patch/${filePatch.getPath()}` + `?workdir=${encodeURIComponent(repository.getWorkingDirectoryPath())}&stagingStatus=unstaged`, )); }); @@ -104,22 +106,22 @@ describe('MultiFilePatchController', function() { 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('MultiFilePatchView').prop('openFile')([]); + const wrapper = shallow(buildApp({stagingStatus: 'unstaged'})); + const editor = await wrapper.find('MultiFilePatchView').prop('openFile')(filePatch, []); - assert.strictEqual(editor.getPath(), path.join(repository.getWorkingDirectoryPath(), 'a.txt')); + assert.strictEqual(editor.getPath(), path.join(repository.getWorkingDirectoryPath(), filePatch.getPath())); }); it('sets the cursor to a single position', async function() { const wrapper = shallow(buildApp({relPath: 'a.txt', stagingStatus: 'unstaged'})); - const editor = await wrapper.find('MultiFilePatchView').prop('openFile')([[1, 1]]); + const editor = await wrapper.find('MultiFilePatchView').prop('openFile')(filePatch, [[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('MultiFilePatchView').prop('openFile')([[1, 1], [3, 1], [5, 0]]); + const wrapper = shallow(buildApp({stagingStatus: 'unstaged'})); + const editor = await wrapper.find('MultiFilePatchView').prop('openFile')(filePatch, [[1, 1], [3, 1], [5, 0]]); assert.deepEqual(editor.getCursorBufferPositions().map(p => p.serialize()), [[1, 1], [3, 1], [5, 0]]); }); @@ -128,37 +130,37 @@ describe('MultiFilePatchController', function() { 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'})); + const wrapper = shallow(buildApp({stagingStatus: 'unstaged'})); - await wrapper.find('MultiFilePatchView').prop('toggleFile')(); + await wrapper.find('MultiFilePatchView').prop('toggleFile')(filePatch); - assert.isTrue(repository.stageFiles.calledWith(['a.txt'])); + assert.isTrue(repository.stageFiles.calledWith([filePatch.getPath()])); }); it('unstages the current file if staged', async function() { sinon.spy(repository, 'unstageFiles'); - const wrapper = shallow(buildApp({relPath: 'a.txt', stagingStatus: 'staged'})); + const wrapper = shallow(buildApp({stagingStatus: 'staged'})); - await wrapper.find('MultiFilePatchView').prop('toggleFile')(); + await wrapper.find('MultiFilePatchView').prop('toggleFile')(filePatch); - assert.isTrue(repository.unstageFiles.calledWith(['a.txt'])); + assert.isTrue(repository.unstageFiles.calledWith([filePatch.getPath()])); }); 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('MultiFilePatchView').prop('toggleFile')(), 'staged'); + const wrapper = shallow(buildApp({stagingStatus: 'unstaged'})); + assert.strictEqual(await wrapper.find('MultiFilePatchView').prop('toggleFile')(filePatch), 'staged'); wrapper.setProps({stagingStatus: 'staged'}); - assert.isNull(await wrapper.find('MultiFilePatchView').prop('toggleFile')()); + assert.isNull(await wrapper.find('MultiFilePatchView').prop('toggleFile')(filePatch)); const promise = wrapper.instance().patchChangePromise; wrapper.setProps({multiFilePatch: multiFilePatch.clone()}); await promise; - assert.strictEqual(await wrapper.find('MultiFilePatchView').prop('toggleFile')(), 'unstaged'); + assert.strictEqual(await wrapper.find('MultiFilePatchView').prop('toggleFile')(filePatch), 'unstaged'); }); }); @@ -219,7 +221,7 @@ describe('MultiFilePatchController', function() { it('records an event', function() { const wrapper = shallow(buildApp()); sinon.stub(reporterProxy, 'addEvent'); - wrapper.find('MultiFilePatchView').prop('undoLastDiscard')(); + wrapper.find('MultiFilePatchView').prop('undoLastDiscard')(filePatch); assert.isTrue(reporterProxy.addEvent.calledWith('undo-last-discard', { package: 'github', component: 'MultiFilePatchController', @@ -277,7 +279,7 @@ describe('MultiFilePatchController', function() { sinon.spy(otherPatch, 'getUnstagePatchForLines'); sinon.spy(repository, 'applyPatchToIndex'); - await wrapper.find('MultiFilePatchView').prop('toggleRows')(); + await wrapper.find('MultiFilePatchView').prop('toggleRows')(new Set([2]), 'hunk'); assert.sameMembers(Array.from(otherPatch.getUnstagePatchForLines.lastCall.args[0]), [2]); assert.isTrue(repository.applyPatchToIndex.calledWith(otherPatch.getUnstagePatchForLines.returnValues[0])); @@ -290,12 +292,13 @@ describe('MultiFilePatchController', 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 newMultiFilePatch = await repository.getFilePatchForPath('a.txt', {staged: false}); - const wrapper = shallow(buildApp({filePatch: newFilePatch, stagingStatus: 'unstaged'})); + const wrapper = shallow(buildApp({filePatch: newMultiFilePatch, stagingStatus: 'unstaged'})); + const [newFilePatch] = newMultiFilePatch.getFilePatches(); sinon.spy(repository, 'stageFileModeChange'); - await wrapper.find('MultiFilePatchView').prop('toggleModeChange')(); + await wrapper.find('MultiFilePatchView').prop('toggleModeChange')(newFilePatch); assert.isTrue(repository.stageFileModeChange.calledWith('a.txt', '100755')); }); @@ -305,12 +308,13 @@ describe('MultiFilePatchController', function() { await fs.chmod(p, 0o755); await repository.stageFiles(['a.txt']); repository.refresh(); - const newFilePatch = await repository.getFilePatchForPath('a.txt', {staged: true}); + const newMultiFilePatch = await repository.getFilePatchForPath('a.txt', {staged: true}); + const [newFilePatch] = newMultiFilePatch.getFilePatches(); - const wrapper = shallow(buildApp({filePatch: newFilePatch, stagingStatus: 'staged'})); + const wrapper = shallow(buildApp({filePatch: newMultiFilePatch, stagingStatus: 'staged'})); sinon.spy(repository, 'stageFileModeChange'); - await wrapper.find('MultiFilePatchView').prop('toggleModeChange')(); + await wrapper.find('MultiFilePatchView').prop('toggleModeChange')(newFilePatch); assert.isTrue(repository.stageFileModeChange.calledWith('a.txt', '100644')); }); @@ -330,12 +334,13 @@ describe('MultiFilePatchController', function() { 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'})); + const symlinkMultiPatch = await repository.getFilePatchForPath('waslink.txt', {staged: false}); + const wrapper = shallow(buildApp({filePatch: symlinkMultiPatch, relPath: 'waslink.txt', stagingStatus: 'unstaged'})); + const [symlinkPatch] = symlinkMultiPatch.getFilePatches() sinon.spy(repository, 'stageFileSymlinkChange'); - await wrapper.find('MultiFilePatchView').prop('toggleSymlinkChange')(); + await wrapper.find('MultiFilePatchView').prop('toggleSymlinkChange')(symlinkPatch); assert.isTrue(repository.stageFileSymlinkChange.calledWith('waslink.txt')); }); @@ -352,12 +357,13 @@ describe('MultiFilePatchController', function() { 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'})); + const symlinkMultiPatch = await repository.getFilePatchForPath('waslink.txt', {staged: false}); + const wrapper = shallow(buildApp({filePatch: symlinkMultiPatch, relPath: 'waslink.txt', stagingStatus: 'unstaged'})); sinon.spy(repository, 'stageFiles'); - await wrapper.find('MultiFilePatchView').prop('toggleSymlinkChange')(); + const [symlinkPatch] = symlinkMultiPatch.getFilePatches() + await wrapper.find('MultiFilePatchView').prop('toggleSymlinkChange')(symlinkPatch); assert.isTrue(repository.stageFiles.calledWith(['waslink.txt'])); }); @@ -376,12 +382,13 @@ describe('MultiFilePatchController', function() { 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'})); + const symlinkMultiPatch = await repository.getFilePatchForPath('waslink.txt', {staged: true}); + const wrapper = shallow(buildApp({filePatch: symlinkMultiPatch, relPath: 'waslink.txt', stagingStatus: 'staged'})); sinon.spy(repository, 'stageFileSymlinkChange'); - await wrapper.find('MultiFilePatchView').prop('toggleSymlinkChange')(); + const [symlinkPatch] = symlinkMultiPatch.getFilePatches() + await wrapper.find('MultiFilePatchView').prop('toggleSymlinkChange')(symlinkPatch); assert.isTrue(repository.stageFileSymlinkChange.calledWith('waslink.txt')); }); @@ -398,12 +405,13 @@ describe('MultiFilePatchController', function() { 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'})); + const symlinkMultiPatch = await repository.getFilePatchForPath('waslink.txt', {staged: true}); + const wrapper = shallow(buildApp({filePatch: symlinkMultiPatch, relPath: 'waslink.txt', stagingStatus: 'staged'})); sinon.spy(repository, 'unstageFiles'); - await wrapper.find('MultiFilePatchView').prop('toggleSymlinkChange')(); + const [symlinkPatch] = symlinkMultiPatch.getFilePatches(); + await wrapper.find('MultiFilePatchView').prop('toggleSymlinkChange')(symlinkPatch); assert.isTrue(repository.unstageFiles.calledWith(['waslink.txt'])); }); From e62f508bfdcdf038dcfb99fac5d60ca65f21144d Mon Sep 17 00:00:00 2001 From: Tilde Ann Thurium Date: Thu, 8 Nov 2018 16:28:53 -0800 Subject: [PATCH 222/284] :fire: mount --- test/controllers/multi-file-patch-controller.test.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/controllers/multi-file-patch-controller.test.js b/test/controllers/multi-file-patch-controller.test.js index 6987f1e567c..6273b04cec3 100644 --- a/test/controllers/multi-file-patch-controller.test.js +++ b/test/controllers/multi-file-patch-controller.test.js @@ -2,6 +2,7 @@ import path from 'path'; import fs from 'fs-extra'; import React from 'react'; import {mount, shallow} from 'enzyme'; +import {shallow} from 'enzyme'; import MultiFilePatchController from '../../lib/controllers/multi-file-patch-controller'; import * as reporterProxy from '../../lib/reporter-proxy'; @@ -59,6 +60,7 @@ describe('MultiFilePatchController', function() { it('calls undoLastDiscard through with set arguments', function() { const undoLastDiscard = sinon.spy(); const wrapper = mount(buildApp({undoLastDiscard, stagingStatus: 'staged'})); + const wrapper = shallow(buildApp({undoLastDiscard, stagingStatus: 'staged'})); wrapper.find('MultiFilePatchView').prop('undoLastDiscard')(filePatch); From e6561aaea7895885ec1972eacfa34270f262c874 Mon Sep 17 00:00:00 2001 From: Tilde Ann Thurium Date: Thu, 8 Nov 2018 16:30:25 -0800 Subject: [PATCH 223/284] I fuck up partial staging more often than not. --- test/controllers/multi-file-patch-controller.test.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/controllers/multi-file-patch-controller.test.js b/test/controllers/multi-file-patch-controller.test.js index 6273b04cec3..8669c1d6e93 100644 --- a/test/controllers/multi-file-patch-controller.test.js +++ b/test/controllers/multi-file-patch-controller.test.js @@ -1,7 +1,6 @@ import path from 'path'; import fs from 'fs-extra'; import React from 'react'; -import {mount, shallow} from 'enzyme'; import {shallow} from 'enzyme'; import MultiFilePatchController from '../../lib/controllers/multi-file-patch-controller'; @@ -59,7 +58,6 @@ describe('MultiFilePatchController', function() { it('calls undoLastDiscard through with set arguments', function() { const undoLastDiscard = sinon.spy(); - const wrapper = mount(buildApp({undoLastDiscard, stagingStatus: 'staged'})); const wrapper = shallow(buildApp({undoLastDiscard, stagingStatus: 'staged'})); wrapper.find('MultiFilePatchView').prop('undoLastDiscard')(filePatch); From 1ece7ebcb19f3ec74569a17ee215e7400bf2f31a Mon Sep 17 00:00:00 2001 From: Tilde Ann Thurium Date: Thu, 8 Nov 2018 16:40:19 -0800 Subject: [PATCH 224/284] :shirt: --- lib/models/repository-states/state.js | 1 - test/controllers/multi-file-patch-controller.test.js | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/models/repository-states/state.js b/lib/models/repository-states/state.js index 9cb953ee489..3b1775ec3e2 100644 --- a/lib/models/repository-states/state.js +++ b/lib/models/repository-states/state.js @@ -2,7 +2,6 @@ import {nullCommit} from '../commit'; import BranchSet from '../branch-set'; import RemoteSet from '../remote-set'; import {nullOperationStates} from '../operation-states'; -import FilePatch from '../patch/file-patch'; import MultiFilePatch from '../patch/multi-file-patch'; /** diff --git a/test/controllers/multi-file-patch-controller.test.js b/test/controllers/multi-file-patch-controller.test.js index 8669c1d6e93..88086e3ccbe 100644 --- a/test/controllers/multi-file-patch-controller.test.js +++ b/test/controllers/multi-file-patch-controller.test.js @@ -336,7 +336,7 @@ describe('MultiFilePatchController', function() { repository.refresh(); const symlinkMultiPatch = await repository.getFilePatchForPath('waslink.txt', {staged: false}); const wrapper = shallow(buildApp({filePatch: symlinkMultiPatch, relPath: 'waslink.txt', stagingStatus: 'unstaged'})); - const [symlinkPatch] = symlinkMultiPatch.getFilePatches() + const [symlinkPatch] = symlinkMultiPatch.getFilePatches(); sinon.spy(repository, 'stageFileSymlinkChange'); @@ -362,7 +362,7 @@ describe('MultiFilePatchController', function() { sinon.spy(repository, 'stageFiles'); - const [symlinkPatch] = symlinkMultiPatch.getFilePatches() + const [symlinkPatch] = symlinkMultiPatch.getFilePatches(); await wrapper.find('MultiFilePatchView').prop('toggleSymlinkChange')(symlinkPatch); assert.isTrue(repository.stageFiles.calledWith(['waslink.txt'])); @@ -387,7 +387,7 @@ describe('MultiFilePatchController', function() { sinon.spy(repository, 'stageFileSymlinkChange'); - const [symlinkPatch] = symlinkMultiPatch.getFilePatches() + const [symlinkPatch] = symlinkMultiPatch.getFilePatches(); await wrapper.find('MultiFilePatchView').prop('toggleSymlinkChange')(symlinkPatch); assert.isTrue(repository.stageFileSymlinkChange.calledWith('waslink.txt')); From 513e83ce0d641fd3fd150775bef88e4a3da67071 Mon Sep 17 00:00:00 2001 From: Tilde Ann Thurium Date: Thu, 8 Nov 2018 18:13:18 -0800 Subject: [PATCH 225/284] it was the missing brackets in the parlor with the lead pipe. --- test/views/multi-file-patch-view.test.js | 25 ++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/test/views/multi-file-patch-view.test.js b/test/views/multi-file-patch-view.test.js index b09df3b557f..362cd7f337e 100644 --- a/test/views/multi-file-patch-view.test.js +++ b/test/views/multi-file-patch-view.test.js @@ -317,11 +317,12 @@ describe('MultiFilePatchView', function() { it("stages or unstages the mode change when the meta container's action is triggered", function() { const [fp] = filePatches.getFilePatches(); + const mfp = filePatches.clone({ - filePatches: fp.clone({ + filePatches: [fp.clone({ oldFile: fp.getOldFile().clone({mode: '100644'}), newFile: fp.getNewFile().clone({mode: '100755'}), - }), + })], }); const toggleModeChange = sinon.stub(); @@ -341,10 +342,10 @@ describe('MultiFilePatchView', function() { it('does not render if the symlink status is unchanged', function() { const [fp] = filePatches.getFilePatches(); const mfp = filePatches.clone({ - filePatches: fp.clone({ + filePatches: [fp.clone({ oldFile: fp.getOldFile().clone({mode: '100644'}), newFile: fp.getNewFile().clone({mode: '100755'}), - }), + })], }); const wrapper = mount(buildApp({multiFilePatch: mfp})); @@ -354,10 +355,10 @@ describe('MultiFilePatchView', function() { it('renders symlink change information within a meta container', function() { const [fp] = filePatches.getFilePatches(); const mfp = filePatches.clone({ - filePatches: fp.clone({ + filePatches: [fp.clone({ oldFile: fp.getOldFile().clone({mode: '120000', symlink: '/old.txt'}), newFile: fp.getNewFile().clone({mode: '120000', symlink: '/new.txt'}), - }), + })], }); const wrapper = mount(buildApp({multiFilePatch: mfp, stagingStatus: 'unstaged'})); @@ -375,10 +376,10 @@ describe('MultiFilePatchView', function() { const toggleSymlinkChange = sinon.stub(); const [fp] = filePatches.getFilePatches(); const mfp = filePatches.clone({ - filePatches: fp.clone({ + filePatches: [fp.clone({ oldFile: fp.getOldFile().clone({mode: '120000', symlink: '/old.txt'}), newFile: fp.getNewFile().clone({mode: '120000', symlink: '/new.txt'}), - }), + })], }); const wrapper = mount(buildApp({multiFilePatch: mfp, stagingStatus: 'staged', toggleSymlinkChange})); @@ -394,10 +395,10 @@ describe('MultiFilePatchView', function() { it('renders details for a symlink deletion', function() { const [fp] = filePatches.getFilePatches(); const mfp = filePatches.clone({ - filePatches: fp.clone({ + filePatches: [fp.clone({ oldFile: fp.getOldFile().clone({mode: '120000', symlink: '/old.txt'}), newFile: nullFile, - }), + })], }); const wrapper = mount(buildApp({multiFilePatch: mfp})); @@ -412,10 +413,10 @@ describe('MultiFilePatchView', function() { it('renders details for a symlink creation', function() { const [fp] = filePatches.getFilePatches(); const mfp = filePatches.clone({ - filePatches: fp.clone({ + filePatches: [fp.clone({ oldFile: nullFile, newFile: fp.getOldFile().clone({mode: '120000', symlink: '/new.txt'}), - }), + })], }); const wrapper = mount(buildApp({multiFilePatch: mfp})); From 4f80490c19559d2b30a1d1967e714f622a89e41f Mon Sep 17 00:00:00 2001 From: Tilde Ann Thurium Date: Thu, 8 Nov 2018 18:42:22 -0800 Subject: [PATCH 226/284] that's not how you get the hunks --- test/views/multi-file-patch-view.test.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/views/multi-file-patch-view.test.js b/test/views/multi-file-patch-view.test.js index 362cd7f337e..642b0d27174 100644 --- a/test/views/multi-file-patch-view.test.js +++ b/test/views/multi-file-patch-view.test.js @@ -450,9 +450,10 @@ describe('MultiFilePatchView', function() { }, ], }]); - const hunks = fp.getHunks(); + const hunks = fp.getFilePatches()[0].patch.hunks; 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])); }); From f9930e54c287502ad73428dea478cf985e7ec26d Mon Sep 17 00:00:00 2001 From: simurai Date: Fri, 9 Nov 2018 11:57:28 +0900 Subject: [PATCH 227/284] Add more spacing between files --- styles/file-patch-view.less | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/styles/file-patch-view.less b/styles/file-patch-view.less index 2e3f73e6db7..89f7255c6e3 100644 --- a/styles/file-patch-view.less +++ b/styles/file-patch-view.less @@ -25,8 +25,7 @@ // TODO: Use better selector .react-atom-decoration { - padding: @component-padding; - padding-left: 0; + padding: @component-padding*2 @component-padding @component-padding 0; background-color: @syntax-background-color; & + .react-atom-decoration { @@ -38,6 +37,7 @@ display: flex; justify-content: space-between; align-items: center; + margin-top: @component-padding*2; padding: @component-padding/2; padding-left: @component-padding; border: 1px solid @base-border-color; From dfdbd477c1f5d20ef60a9fa3285185af2d84ac19 Mon Sep 17 00:00:00 2001 From: simurai Date: Fri, 9 Nov 2018 12:57:43 +0900 Subject: [PATCH 228/284] Make text selections blue To match hunk selection --- styles/file-patch-view.less | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/styles/file-patch-view.less b/styles/file-patch-view.less index 89f7255c6e3..629d573f938 100644 --- a/styles/file-patch-view.less +++ b/styles/file-patch-view.less @@ -33,6 +33,14 @@ } } + // Editor overrides + + atom-text-editor { + .selection .region { + background-color: mix(@button-background-color-selected, @syntax-background-color, 24%); + } + } + &-header { display: flex; justify-content: space-between; From 8da27e6667fa1a435cc8d863cee4187e3ee9a466 Mon Sep 17 00:00:00 2001 From: simurai Date: Fri, 9 Nov 2018 12:58:23 +0900 Subject: [PATCH 229/284] Remove background color from hunks To differentiate them more from file headers --- styles/hunk-header-view.less | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/styles/hunk-header-view.less b/styles/hunk-header-view.less index 18e682fb87b..64737bfa181 100644 --- a/styles/hunk-header-view.less +++ b/styles/hunk-header-view.less @@ -1,7 +1,9 @@ @import "variables"; @hunk-fg-color: @text-color-subtle; -@hunk-bg-color: mix(@syntax-text-color, @syntax-background-color, 4%); +@hunk-bg-color: mix(@syntax-text-color, @syntax-background-color, 0%); +@hunk-bg-color-hover: mix(@syntax-text-color, @syntax-background-color, 4%); +@hunk-bg-color-active: mix(@syntax-text-color, @syntax-background-color, 2%); .github-HunkHeaderView { font-family: Menlo, Consolas, 'DejaVu Sans Mono', monospace; @@ -23,8 +25,8 @@ white-space: nowrap; text-overflow: ellipsis; -webkit-font-smoothing: antialiased; - &:hover { background-color: mix(@syntax-text-color, @syntax-background-color, 8%); } - &:active { background-color: mix(@syntax-text-color, @syntax-background-color, 2%); } + &:hover { background-color: @hunk-bg-color-hover; } + &:active { background-color: @hunk-bg-color-active; } } &-stageButton, @@ -36,8 +38,8 @@ border: none; background-color: transparent; cursor: default; - &:hover { background-color: mix(@syntax-text-color, @syntax-background-color, 8%); } - &:active { background-color: mix(@syntax-text-color, @syntax-background-color, 2%); } + &:hover { background-color: @hunk-bg-color-hover; } + &:active { background-color: @hunk-bg-color-active; } .keystroke { margin-right: 1em; @@ -59,8 +61,8 @@ &-title, &-stageButton, &-discardButton { - &:hover { background-color: mix(@syntax-text-color, @syntax-background-color, 8%); } - &:active { background-color: mix(@syntax-text-color, @syntax-background-color, 2%); } + &:hover { background-color: @hunk-bg-color-hover; } + &:active { background-color: @hunk-bg-color-active; } } } From 70b4dee54475df563b10f47c12654ec11051db79 Mon Sep 17 00:00:00 2001 From: simurai Date: Fri, 9 Nov 2018 13:10:15 +0900 Subject: [PATCH 230/284] Fix cursor line on diffs --- styles/file-patch-view.less | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/styles/file-patch-view.less b/styles/file-patch-view.less index 629d573f938..63a42c5ef15 100644 --- a/styles/file-patch-view.less +++ b/styles/file-patch-view.less @@ -182,10 +182,10 @@ &-line { // mixin .hunk-line-mixin(@bg;) { - background-color: fade(@bg, 18%); + background-color: fade(@bg, 16%); - .github-FilePatchView--active &.line.cursor-line { - background-color: fade(@bg, 28%); + &.line.cursor-line { + background-color: fade(@bg, 22%); } } From 4280a307b3a662d69440e68e4e3f5b15286aa166 Mon Sep 17 00:00:00 2001 From: simurai Date: Fri, 9 Nov 2018 14:26:09 +0900 Subject: [PATCH 231/284] Add borders to hunk buttons --- styles/hunk-header-view.less | 2 ++ 1 file changed, 2 insertions(+) diff --git a/styles/hunk-header-view.less b/styles/hunk-header-view.less index 64737bfa181..7d6f3ccd514 100644 --- a/styles/hunk-header-view.less +++ b/styles/hunk-header-view.less @@ -36,6 +36,7 @@ padding-right: @component-padding; font-family: @font-family; border: none; + border-left: inherit; background-color: transparent; cursor: default; &:hover { background-color: @hunk-bg-color-hover; } @@ -50,6 +51,7 @@ &-discardButton:before { text-align: left; width: auto; + vertical-align: 2px; } } From c898ad64da6e21408f713c594b113951937367ae Mon Sep 17 00:00:00 2001 From: simurai Date: Fri, 9 Nov 2018 15:25:47 +0900 Subject: [PATCH 232/284] :fire: Remove gutter background It's currently broken anyways --- styles/hunk-header-view.less | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/styles/hunk-header-view.less b/styles/hunk-header-view.less index 7d6f3ccd514..11cffdec766 100644 --- a/styles/hunk-header-view.less +++ b/styles/hunk-header-view.less @@ -82,19 +82,3 @@ &:active { background-color: darken(@button-background-color-selected, 4%); } } } - - -// Hacks ----------------------- -// Please unhack (one day TM) - -// Make the gap in the gutter also use the same background as .github-HunkHeaderView -// Note: This only works with the default font-size -.github-FilePatchView .line-number[style="margin-top: 30px;"]:before { - content: ""; - position: absolute; - left: 0; - right: 0; - top: -30px; - height: 30px; - background-color: @hunk-bg-color; -} From 53177a8b806f7d09ba58f8802a1c97025968f56d Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 9 Nov 2018 08:15:07 -0500 Subject: [PATCH 233/284] Mark regions with exclusive: true to avoid stretching markers --- test/builder/patch.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/builder/patch.js b/test/builder/patch.js index 3d23862b3e3..10fe455c40c 100644 --- a/test/builder/patch.js +++ b/test/builder/patch.js @@ -33,7 +33,7 @@ class LayeredBuffer { const startPosition = this.buffer.getEndPosition(); const layer = this.getLayer(markerLayerName); this.buffer.append(lines.join('\n')); - const marker = layer.markRange([startPosition, this.buffer.getEndPosition()]); + const marker = layer.markRange([startPosition, this.buffer.getEndPosition()], {exclusive: true}); this.buffer.append('\n'); return marker; } @@ -41,7 +41,7 @@ class LayeredBuffer { markFrom(markerLayerName, startPosition) { const endPosition = this.buffer.getEndPosition(); const layer = this.getLayer(markerLayerName); - return layer.markRange([startPosition, endPosition]); + return layer.markRange([startPosition, endPosition], {exclusive: true}); } wrapReturn(object) { From 5bebe03d3d098fc2a26b4b796e8ae4a777cb80bf Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 9 Nov 2018 08:15:18 -0500 Subject: [PATCH 234/284] It helps if you actually call that block --- test/builder/patch.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/builder/patch.js b/test/builder/patch.js index 10fe455c40c..d36bdf69d15 100644 --- a/test/builder/patch.js +++ b/test/builder/patch.js @@ -177,6 +177,7 @@ class PatchBuilder { addHunk(block) { const hunk = new HunkBuilder(this.layeredBuffer); + block(hunk); this.hunks.push(hunk.build().hunk); return this; } From 9b69f0ecfc0cf5a195b019cf7d97280707fbb26e Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 9 Nov 2018 08:20:51 -0500 Subject: [PATCH 235/284] Initialize default regions before computing hunk row counts --- test/builder/patch.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/builder/patch.js b/test/builder/patch.js index d36bdf69d15..6c0b44cf612 100644 --- a/test/builder/patch.js +++ b/test/builder/patch.js @@ -237,6 +237,10 @@ class HunkBuilder { } build() { + if (this.regions.length === 0) { + this.unchanged('0000').added('0001').deleted('0002').unchanged('0003'); + } + if (this.oldRowCount === null) { this.oldRowCount = this.regions.reduce((count, region) => region.when({ unchanged: () => count + region.bufferRowCount(), @@ -253,10 +257,6 @@ class HunkBuilder { }), 0); } - if (this.regions.length === 0) { - this.unchanged('0000').added('0001').deleted('0002').unchanged('0003'); - } - const marker = this.layeredBuffer.markFrom('hunk', this.hunkStartPoint); return this.layeredBuffer.wrapReturn({ From e7feca77bef2e91b558dd67487299ad203d52ea0 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 9 Nov 2018 08:38:10 -0500 Subject: [PATCH 236/284] Compute valid hunk start rows --- test/builder/patch.js | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/test/builder/patch.js b/test/builder/patch.js index 6c0b44cf612..70380fec0b4 100644 --- a/test/builder/patch.js +++ b/test/builder/patch.js @@ -164,6 +164,7 @@ class PatchBuilder { this.hunks = []; this.patchStart = this.layeredBuffer.getInsertionPoint(); + this.drift = 0; } status(st) { @@ -176,9 +177,11 @@ class PatchBuilder { } addHunk(block) { - const hunk = new HunkBuilder(this.layeredBuffer); - block(hunk); - this.hunks.push(hunk.build().hunk); + const builder = new HunkBuilder(this.layeredBuffer, this.drift); + block(builder); + const {hunk, drift} = builder.build(); + this.hunks.push(hunk); + this.drift = drift; return this; } @@ -197,12 +200,13 @@ class PatchBuilder { } class HunkBuilder { - constructor(layeredBuffer = null) { + constructor(layeredBuffer = null, drift = 0) { this.layeredBuffer = layeredBuffer; + this.drift = drift; this.oldStartRow = 0; this.oldRowCount = null; - this.newStartRow = 0; + this.newStartRow = null; this.newRowCount = null; this.sectionHeading = "don't care"; @@ -249,6 +253,10 @@ class HunkBuilder { }), 0); } + if (this.newStartRow === null) { + this.newStartRow = this.oldStartRow + this.drift; + } + if (this.newRowCount === null) { this.newRowCount = this.regions.reduce((count, region) => region.when({ unchanged: () => count + region.bufferRowCount(), @@ -269,6 +277,7 @@ class HunkBuilder { marker, regions: this.regions, }), + drift: this.drift + this.newRowCount - this.oldRowCount, }); } } From 1475fce26bbb5b7ee7150e011c2eb8598420c8ab Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 9 Nov 2018 08:44:07 -0500 Subject: [PATCH 237/284] It helps if you call methods that actually exist --- test/builder/patch.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/builder/patch.js b/test/builder/patch.js index 70380fec0b4..e4264ee003b 100644 --- a/test/builder/patch.js +++ b/test/builder/patch.js @@ -187,8 +187,8 @@ class PatchBuilder { build() { if (this.hunks.length === 0) { - this.addHunk(hunk => hunk.unchanged('0000').added('0001').deleted('0002').unchanged('0003')); - this.addHunk(hunk => hunk.startRow(10).unchanged('0004').added('0005').deleted('0006').unchanged('0007')); + this.addHunk(hunk => hunk.oldRow(1).unchanged('0000').added('0001').deleted('0002').unchanged('0003')); + this.addHunk(hunk => hunk.oldRow(10).unchanged('0004').added('0005').deleted('0006').unchanged('0007')); } const marker = this.layeredBuffer.markFrom('patch', this.patchStart); From c51417079a5688466d92c3e3a2bdff943e5f2cb6 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 9 Nov 2018 08:59:43 -0500 Subject: [PATCH 238/284] Mark to the end of the previous line --- test/builder/patch.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/builder/patch.js b/test/builder/patch.js index e4264ee003b..11b29eeb51c 100644 --- a/test/builder/patch.js +++ b/test/builder/patch.js @@ -39,7 +39,7 @@ class LayeredBuffer { } markFrom(markerLayerName, startPosition) { - const endPosition = this.buffer.getEndPosition(); + const endPosition = this.buffer.getEndPosition().translate([-1, Infinity]); const layer = this.getLayer(markerLayerName); return layer.markRange([startPosition, endPosition], {exclusive: true}); } From d81a9cc4bb047246533b422802b9fa49b80f2c58 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 9 Nov 2018 09:02:51 -0500 Subject: [PATCH 239/284] Allow nullFiles in FilePatches --- test/builder/patch.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/test/builder/patch.js b/test/builder/patch.js index 11b29eeb51c..116b249c0b1 100644 --- a/test/builder/patch.js +++ b/test/builder/patch.js @@ -3,7 +3,7 @@ import {TextBuffer} from 'atom'; import MultiFilePatch from '../../lib/models/patch/multi-file-patch'; 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 {Unchanged, Addition, Deletion, NoNewline} from '../../lib/models/patch/region'; @@ -95,6 +95,11 @@ class FilePatchBuilder { return this; } + nullOldFile() { + this.oldFile = nullFile; + return this; + } + setNewFile(block) { const file = new FileBuilder(); block(file); @@ -102,6 +107,11 @@ class FilePatchBuilder { return this; } + nullNewFile() { + this.newFile = nullFile; + return this; + } + status(...args) { this.patchBuilder.status(...args); return this; From e5d0b95b99b5bd7b7ed2bf1134a7caed41ebb653 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 9 Nov 2018 09:06:12 -0500 Subject: [PATCH 240/284] Another method/property name collision --- test/builder/patch.js | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/test/builder/patch.js b/test/builder/patch.js index 116b249c0b1..cfc78b20edd 100644 --- a/test/builder/patch.js +++ b/test/builder/patch.js @@ -170,7 +170,7 @@ class PatchBuilder { constructor(layeredBuffer = null) { this.layeredBuffer = layeredBuffer; - this.status = 'modified'; + this._status = 'modified'; this.hunks = []; this.patchStart = this.layeredBuffer.getInsertionPoint(); @@ -182,7 +182,7 @@ class PatchBuilder { throw new Error(`Unrecognized status: ${st} (must be 'modified', 'added' or 'deleted')`); } - this.status = st; + this._status = st; return this; } @@ -197,14 +197,20 @@ class PatchBuilder { build() { if (this.hunks.length === 0) { - this.addHunk(hunk => hunk.oldRow(1).unchanged('0000').added('0001').deleted('0002').unchanged('0003')); - this.addHunk(hunk => hunk.oldRow(10).unchanged('0004').added('0005').deleted('0006').unchanged('0007')); + if (this._status === 'modified') { + this.addHunk(hunk => hunk.oldRow(1).unchanged('0000').added('0001').deleted('0002').unchanged('0003')); + this.addHunk(hunk => hunk.oldRow(10).unchanged('0004').added('0005').deleted('0006').unchanged('0007')); + } else if (this._status === 'added') { + this.addHunk(hunk => hunk.oldRow(1).added('0000', '0001', '0002', '0003')); + } else if (this._status === 'deleted') { + this.addHunk(hunk => hunk.oldRow(1).deleted('0000', '0001', '0002', '0003')); + } } const marker = this.layeredBuffer.markFrom('patch', this.patchStart); return this.layeredBuffer.wrapReturn({ - patch: new Patch({status: this.status, hunks: this.hunks, marker}), + patch: new Patch({status: this._status, hunks: this.hunks, marker}), }); } } From b7a4dc758293546e9c23c2856001f312768dfec9 Mon Sep 17 00:00:00 2001 From: Vanessa Yuen Date: Fri, 9 Nov 2018 15:33:23 +0100 Subject: [PATCH 241/284] fix exec mode change tests --- test/views/multi-file-patch-view.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/views/multi-file-patch-view.test.js b/test/views/multi-file-patch-view.test.js index 642b0d27174..b1fc26d8b5d 100644 --- a/test/views/multi-file-patch-view.test.js +++ b/test/views/multi-file-patch-view.test.js @@ -326,7 +326,7 @@ describe('MultiFilePatchView', function() { }); const toggleModeChange = sinon.stub(); - const wrapper = mount(buildApp({multiFilePatch: mfp, stagingStatus: 'unstaged', toggleModeChange})); + const wrapper = mount(buildApp({multiFilePatch: mfp, stagingStatus: 'staged', toggleModeChange})); const meta = wrapper.find('FilePatchMetaView[title="Mode change"]'); assert.isTrue(meta.exists()); From ec70e50394571f0acf3c29daf7f742d819479ab7 Mon Sep 17 00:00:00 2001 From: Vanessa Yuen Date: Fri, 9 Nov 2018 15:33:46 +0100 Subject: [PATCH 242/284] fix hunk headers test --- test/views/multi-file-patch-view.test.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/test/views/multi-file-patch-view.test.js b/test/views/multi-file-patch-view.test.js index b1fc26d8b5d..1f1931f82e9 100644 --- a/test/views/multi-file-patch-view.test.js +++ b/test/views/multi-file-patch-view.test.js @@ -431,7 +431,7 @@ describe('MultiFilePatchView', function() { describe('hunk headers', function() { it('renders one for each hunk', function() { - const fp = buildFilePatch([{ + const mfp = buildMultiFilePatch([{ oldPath: 'path.txt', oldMode: '100644', newPath: 'path.txt', @@ -451,8 +451,8 @@ describe('MultiFilePatchView', function() { ], }]); - const hunks = fp.getFilePatches()[0].patch.hunks; - const wrapper = mount(buildApp({filePatch: fp})); + const hunks = mfp.getFilePatches()[0].getHunks(); + const wrapper = mount(buildApp({multiFilePatch: mfp})); assert.isTrue(wrapper.find('HunkHeaderView').someWhere(h => h.prop('hunk') === hunks[0])); assert.isTrue(wrapper.find('HunkHeaderView').someWhere(h => h.prop('hunk') === hunks[1])); @@ -515,7 +515,7 @@ describe('MultiFilePatchView', function() { }); it('handles mousedown as a selection event', function() { - const fp = buildFilePatch([{ + const mfp = buildMultiFilePatch([{ oldPath: 'path.txt', oldMode: '100644', newPath: 'path.txt', @@ -536,9 +536,9 @@ describe('MultiFilePatchView', function() { }]); const selectedRowsChanged = sinon.spy(); - const wrapper = mount(buildApp({filePatch: fp, selectedRowsChanged, selectionMode: 'line'})); + const wrapper = mount(buildApp({multiFilePatch: mfp, selectedRowsChanged, selectionMode: 'line'})); - wrapper.find('HunkHeaderView').at(1).prop('mouseDown')({button: 0}, fp.getHunks()[1]); + wrapper.find('HunkHeaderView').at(1).prop('mouseDown')({button: 0}, mfp.getFilePatches()[0].getHunks()[1]); assert.sameMembers(Array.from(selectedRowsChanged.lastCall.args[0]), [4]); assert.strictEqual(selectedRowsChanged.lastCall.args[1], 'hunk'); @@ -576,8 +576,8 @@ describe('MultiFilePatchView', function() { const wrapper = mount(buildApp({selectedRows: new Set([2]), discardRows, selectionMode: 'line'})); wrapper.find('HunkHeaderView').at(1).prop('discardSelection')(); - assert.sameMembers(Array.from(discardRows.lastCall.args[0]), [6, 7]); - assert.strictEqual(discardRows.lastCall.args[1], 'hunk'); + assert.sameMembers(Array.from(discardRows.lastCall.args[1]), [6, 7]); + assert.strictEqual(discardRows.lastCall.args[2], 'hunk'); }); }); From e0855f694e64264a2955b32d37f845277c0a774a Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 9 Nov 2018 09:40:41 -0500 Subject: [PATCH 243/284] Allow empty blocks for addHunk() and addFilePatch() to accept defaults --- test/builder/patch.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/builder/patch.js b/test/builder/patch.js index cfc78b20edd..fa66f6486fe 100644 --- a/test/builder/patch.js +++ b/test/builder/patch.js @@ -60,7 +60,7 @@ class MultiFilePatchBuilder { this.filePatches = []; } - addFilePatch(block) { + addFilePatch(block = () => {}) { const filePatch = new FilePatchBuilder(this.layeredBuffer); block(filePatch); this.filePatches.push(filePatch.build().filePatch); @@ -186,7 +186,7 @@ class PatchBuilder { return this; } - addHunk(block) { + addHunk(block = () => {}) { const builder = new HunkBuilder(this.layeredBuffer, this.drift); block(builder); const {hunk, drift} = builder.build(); From 1fd0b0d38c9afe9807b74e232056ed6074cc59d6 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 9 Nov 2018 09:40:49 -0500 Subject: [PATCH 244/284] Right right that expects an Array --- test/builder/patch.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/builder/patch.js b/test/builder/patch.js index fa66f6486fe..70a25cd89d6 100644 --- a/test/builder/patch.js +++ b/test/builder/patch.js @@ -252,7 +252,7 @@ class HunkBuilder { } noNewline() { - this.regions.push(new NoNewline(this.layeredBuffer.appendMarked('noNewline', ' No newline at end of file'))); + this.regions.push(new NoNewline(this.layeredBuffer.appendMarked('noNewline', [' No newline at end of file']))); return this; } From cd4ec3776b9f394d20ff8a82a1c35d285b80d4e9 Mon Sep 17 00:00:00 2001 From: Vanessa Yuen Date: Fri, 9 Nov 2018 15:44:25 +0100 Subject: [PATCH 245/284] fix hunk lines tests --- test/views/multi-file-patch-view.test.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/test/views/multi-file-patch-view.test.js b/test/views/multi-file-patch-view.test.js index 1f1931f82e9..de0be901639 100644 --- a/test/views/multi-file-patch-view.test.js +++ b/test/views/multi-file-patch-view.test.js @@ -17,7 +17,6 @@ describe('MultiFilePatchView', function() { const workdirPath = await cloneRepository(); repository = await buildRepository(workdirPath); - // filePatches = repository.getStagedChangesPatch(); // path.txt: unstaged changes filePatches = buildMultiFilePatch([{ @@ -624,8 +623,9 @@ describe('MultiFilePatchView', function() { const decorations = layerWrapper.find('Decoration[type="line-number"][gutterName="diff-icons"]'); assert.isTrue(decorations.exists()); }; - assertLayerDecorated(filePatch.getAdditionLayer()); - assertLayerDecorated(filePatch.getDeletionLayer()); + + assertLayerDecorated(filePatches.getAdditionLayer()); + assertLayerDecorated(filePatches.getDeletionLayer()); atomEnv.config.set('github.showDiffIconGutter', false); wrapper.update(); @@ -826,7 +826,7 @@ describe('MultiFilePatchView', function() { let linesPatch; beforeEach(function() { - linesPatch = buildFilePatch([{ + linesPatch = buildMultiFilePatch([{ oldPath: 'file.txt', oldMode: '100644', newPath: 'file.txt', @@ -851,7 +851,7 @@ describe('MultiFilePatchView', function() { }); it('decorates added lines', function() { - const wrapper = mount(buildApp({filePatch: linesPatch})); + const wrapper = mount(buildApp({multiFilePatch: linesPatch})); const decorationSelector = 'Decoration[type="line"][className="github-FilePatchView-line--added"]'; const decoration = wrapper.find(decorationSelector); @@ -862,7 +862,7 @@ describe('MultiFilePatchView', function() { }); it('decorates deleted lines', function() { - const wrapper = mount(buildApp({filePatch: linesPatch})); + const wrapper = mount(buildApp({multiFilePatch: linesPatch})); const decorationSelector = 'Decoration[type="line"][className="github-FilePatchView-line--deleted"]'; const decoration = wrapper.find(decorationSelector); @@ -873,7 +873,7 @@ describe('MultiFilePatchView', function() { }); it('decorates the nonewline line', function() { - const wrapper = mount(buildApp({filePatch: linesPatch})); + const wrapper = mount(buildApp({multiFilePatch: linesPatch})); const decorationSelector = 'Decoration[type="line"][className="github-FilePatchView-line--nonewline"]'; const decoration = wrapper.find(decorationSelector); @@ -1004,7 +1004,7 @@ describe('MultiFilePatchView', function() { assert.isTrue(surfaceFile.called); }); - describe('hunk mode navigation', function() { + describe.only('hunk mode navigation', function() { beforeEach(function() { filePatch = buildFilePatch([{ oldPath: 'path.txt', From 13ad6487dd8843ad100bafe201e7e33d71384eb3 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 9 Nov 2018 10:06:41 -0500 Subject: [PATCH 246/284] That rename we were just talking about --- test/builder/patch.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/builder/patch.js b/test/builder/patch.js index 70a25cd89d6..828250ea730 100644 --- a/test/builder/patch.js +++ b/test/builder/patch.js @@ -298,18 +298,18 @@ class HunkBuilder { } } -export function buildMultiFilePatch() { +export function multiFilePatchBuilder() { return new MultiFilePatchBuilder(new LayeredBuffer()); } -export function buildFilePatch() { +export function filePatchBuilder() { return new FilePatchBuilder(new LayeredBuffer()); } -export function buildPatch() { +export function patchBuilder() { return new PatchBuilder(new LayeredBuffer()); } -export function buildHunk() { +export function hunkBuilder() { return new HunkBuilder(new LayeredBuffer()); } From cca116e84679cc57c2f9878831fde45eb2810903 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 9 Nov 2018 12:33:48 -0500 Subject: [PATCH 247/284] MultiFilePatch tests so far --- test/models/patch/multi-file-patch.test.js | 934 +++++++++------------ 1 file changed, 395 insertions(+), 539 deletions(-) diff --git a/test/models/patch/multi-file-patch.test.js b/test/models/patch/multi-file-patch.test.js index 8633e7d79bf..e3aa99513cb 100644 --- a/test/models/patch/multi-file-patch.test.js +++ b/test/models/patch/multi-file-patch.test.js @@ -1,7 +1,7 @@ import {TextBuffer} from 'atom'; import dedent from 'dedent-js'; -import {buildMultiFilePatch} from '../../builder/patch'; +import {multiFilePatchBuilder} from '../../builder/patch'; import MultiFilePatch from '../../../lib/models/patch/multi-file-patch'; import FilePatch from '../../../lib/models/patch/file-patch'; @@ -33,7 +33,7 @@ describe('MultiFilePatch', function() { }); it('detects when it is not empty', function() { - const {multiFilePatch} = buildMultiFilePatch() + const {multiFilePatch} = multiFilePatchBuilder() .addFilePatch(filePatch => { filePatch .setOldFile(file => file.path('file-0.txt')) @@ -45,7 +45,7 @@ describe('MultiFilePatch', function() { }); it('has an accessor for its file patches', function() { - const {multiFilePatch} = buildMultiFilePatch() + const {multiFilePatch} = multiFilePatchBuilder() .addFilePatch(filePatch => filePatch.setOldFile(file => file.path('file-0.txt'))) .addFilePatch(filePatch => filePatch.setOldFile(file => file.path('file-1.txt'))) .build(); @@ -58,7 +58,7 @@ describe('MultiFilePatch', function() { describe('didAnyChangeExecutableMode()', function() { it('detects when at least one patch contains an executable mode change', function() { - const {multiFilePatch: yes} = buildMultiFilePatch() + const {multiFilePatch: yes} = multiFilePatchBuilder() .addFilePatch(filePatch => { filePatch.setOldFile(file => file.path('file-0.txt')); filePatch.setNewFile(file => file.path('file-0.txt').executable()); @@ -68,7 +68,7 @@ describe('MultiFilePatch', function() { }); it('detects when none of the patches contain an executable mode change', function() { - const {multiFilePatch: no} = buildMultiFilePatch() + const {multiFilePatch: no} = multiFilePatchBuilder() .addFilePatch(filePatch => filePatch.setOldFile(file => file.path('file-0.txt'))) .addFilePatch(filePatch => filePatch.setOldFile(file => file.path('file-1.txt'))) .build(); @@ -78,18 +78,18 @@ describe('MultiFilePatch', function() { describe('anyHaveTypechange()', function() { it('detects when at least one patch contains a symlink change', function() { - const {multiFilePatch: yes} = buildMultiFilePatch() + const {multiFilePatch: yes} = multiFilePatchBuilder() .addFilePatch(filePatch => filePatch.setOldFile(file => file.path('file-0.txt'))) .addFilePatch(filePatch => { filePatch.setOldFile(file => file.path('file-0.txt')); - filePatch.setNewFile(file => file.path('file-0.txt').symlink('somewhere.txt')); + filePatch.setNewFile(file => file.path('file-0.txt').symlinkTo('somewhere.txt')); }) .build(); assert.isTrue(yes.anyHaveTypechange()); }); it('detects when none of its patches contain a symlink change', function() { - const {multiFilePatch: no} = buildMultiFilePatch() + const {multiFilePatch: no} = multiFilePatchBuilder() .addFilePatch(filePatch => filePatch.setOldFile(file => file.path('file-0.txt'))) .addFilePatch(filePatch => filePatch.setOldFile(file => file.path('file-1.txt'))) .build(); @@ -98,586 +98,442 @@ describe('MultiFilePatch', function() { }); it('computes the maximum line number width of any hunk in any patch', function() { - const mp = new MultiFilePatch({buffer, layers, filePatches: [ - buildFilePatchFixture(0), - buildFilePatchFixture(1), - ]}); - assert.strictEqual(mp.getMaxLineNumberWidth(), 2); + const {multiFilePatch} = multiFilePatchBuilder() + .addFilePatch(fp => { + fp.setOldFile(f => f.path('file-0.txt')); + fp.addHunk(h => h.oldRow(10)); + fp.addHunk(h => h.oldRow(99)); + }) + .addFilePatch(fp => { + fp.setOldFile(f => f.path('file-1.txt')); + fp.addHunk(h => h.oldRow(5)); + fp.addHunk(h => h.oldRow(15)); + }) + .build(); + + assert.strictEqual(multiFilePatch.getMaxLineNumberWidth(), 3); }); it('locates an individual FilePatch by marker lookup', function() { - const filePatches = []; + const builder = multiFilePatchBuilder(); for (let i = 0; i < 10; i++) { - filePatches.push(buildFilePatchFixture(i)); + builder.addFilePatch(fp => { + fp.setOldFile(f => f.path(`file-${i}.txt`)); + fp.addHunk(h => { + h.oldRow(1).unchanged('a', 'b').added('c').deleted('d').unchanged('e'); + }); + fp.addHunk(h => { + h.oldRow(10).unchanged('f').deleted('g', 'h', 'i').unchanged('j'); + }); + }); } - const mp = new MultiFilePatch({buffer, layers, filePatches}); + const {multiFilePatch} = builder.build(); + const fps = multiFilePatch.getFilePatches(); - assert.strictEqual(mp.getFilePatchAt(0), filePatches[0]); - assert.strictEqual(mp.getFilePatchAt(7), filePatches[0]); - assert.strictEqual(mp.getFilePatchAt(8), filePatches[1]); - assert.strictEqual(mp.getFilePatchAt(79), filePatches[9]); + assert.strictEqual(multiFilePatch.getFilePatchAt(0), fps[0]); + assert.strictEqual(multiFilePatch.getFilePatchAt(9), fps[0]); + assert.strictEqual(multiFilePatch.getFilePatchAt(10), fps[1]); + assert.strictEqual(multiFilePatch.getFilePatchAt(99), fps[9]); }); it('creates a set of all unique paths referenced by patches', function() { - const mp = new MultiFilePatch({buffer, layers, filePatches: [ - buildFilePatchFixture(0, {oldFilePath: 'file-0-before.txt', newFilePath: 'file-0-after.txt'}), - buildFilePatchFixture(1, {status: 'added', newFilePath: 'file-1.txt'}), - buildFilePatchFixture(2, {oldFilePath: 'file-2.txt', newFilePath: 'file-2.txt'}), - ]}); + const {multiFilePatch} = multiFilePatchBuilder() + .addFilePatch(fp => { + fp.setOldFile(f => f.path('file-0-before.txt')); + fp.setNewFile(f => f.path('file-0-after.txt')); + }) + .addFilePatch(fp => { + fp.status('added'); + fp.nullOldFile(); + fp.setNewFile(f => f.path('file-1.txt')); + }) + .addFilePatch(fp => { + fp.setOldFile(f => f.path('file-2.txt')); + fp.setNewFile(f => f.path('file-2.txt')); + }) + .build(); assert.sameMembers( - Array.from(mp.getPathSet()), + Array.from(multiFilePatch.getPathSet()), ['file-0-before.txt', 'file-0-after.txt', 'file-1.txt', 'file-2.txt'], ); }); it('locates a Hunk by marker lookup', function() { - const filePatches = [ - buildFilePatchFixture(0), - buildFilePatchFixture(1), - buildFilePatchFixture(2), - ]; - const mp = new MultiFilePatch({buffer, layers, filePatches}); - - assert.strictEqual(mp.getHunkAt(0), filePatches[0].getHunks()[0]); - assert.strictEqual(mp.getHunkAt(3), filePatches[0].getHunks()[0]); - assert.strictEqual(mp.getHunkAt(4), filePatches[0].getHunks()[1]); - assert.strictEqual(mp.getHunkAt(7), filePatches[0].getHunks()[1]); - assert.strictEqual(mp.getHunkAt(8), filePatches[1].getHunks()[0]); - assert.strictEqual(mp.getHunkAt(15), filePatches[1].getHunks()[1]); - assert.strictEqual(mp.getHunkAt(16), filePatches[2].getHunks()[0]); - assert.strictEqual(mp.getHunkAt(23), filePatches[2].getHunks()[1]); + const {multiFilePatch} = multiFilePatchBuilder() + .addFilePatch(fp => { + fp.addHunk(h => h.oldRow(1).added('0', '1', '2', '3', '4')); + fp.addHunk(h => h.oldRow(10).deleted('5', '6', '7', '8', '9')); + }) + .addFilePatch(fp => { + fp.addHunk(h => h.oldRow(5).unchanged('10', '11').added('12').deleted('13')); + fp.addHunk(h => h.oldRow(20).unchanged('14').deleted('15')); + }) + .addFilePatch(fp => { + fp.status('deleted'); + fp.addHunk(h => h.oldRow(4).deleted('16', '17', '18', '19')); + }) + .build(); + + const [fp0, fp1, fp2] = multiFilePatch.getFilePatches(); + + assert.strictEqual(multiFilePatch.getHunkAt(0), fp0.getHunks()[0]); + assert.strictEqual(multiFilePatch.getHunkAt(4), fp0.getHunks()[0]); + assert.strictEqual(multiFilePatch.getHunkAt(5), fp0.getHunks()[1]); + assert.strictEqual(multiFilePatch.getHunkAt(9), fp0.getHunks()[1]); + assert.strictEqual(multiFilePatch.getHunkAt(10), fp1.getHunks()[0]); + assert.strictEqual(multiFilePatch.getHunkAt(15), fp1.getHunks()[1]); + assert.strictEqual(multiFilePatch.getHunkAt(16), fp2.getHunks()[0]); + assert.strictEqual(multiFilePatch.getHunkAt(19), fp2.getHunks()[0]); }); it('represents itself as an apply-ready string', function() { - const mp = new MultiFilePatch({buffer, layers, filePatches: [ - buildFilePatchFixture(0), - buildFilePatchFixture(1), - ]}); + const {multiFilePatch} = multiFilePatchBuilder() + .addFilePatch(fp => { + fp.setOldFile(f => f.path('file-0.txt')); + fp.addHunk(h => h.oldRow(1).unchanged('0;0;0').added('0;0;1').deleted('0;0;2').unchanged('0;0;3')); + fp.addHunk(h => h.oldRow(10).unchanged('0;1;0').added('0;1;1').deleted('0;1;2').unchanged('0;1;3')); + }) + .addFilePatch(fp => { + fp.setOldFile(f => f.path('file-1.txt')); + fp.addHunk(h => h.oldRow(1).unchanged('1;0;0').added('1;0;1').deleted('1;0;2').unchanged('1;0;3')); + fp.addHunk(h => h.oldRow(10).unchanged('1;1;0').added('1;1;1').deleted('1;1;2').unchanged('1;1;3')); + }) + .build(); - assert.strictEqual(mp.toString(), dedent` + assert.strictEqual(multiFilePatch.toString(), dedent` diff --git a/file-0.txt b/file-0.txt --- a/file-0.txt +++ b/file-0.txt - @@ -0,3 +0,3 @@ - file-0 line-0 - +file-0 line-1 - -file-0 line-2 - file-0 line-3 + @@ -1,3 +1,3 @@ + 0;0;0 + +0;0;1 + -0;0;2 + 0;0;3 @@ -10,3 +10,3 @@ - file-0 line-4 - +file-0 line-5 - -file-0 line-6 - file-0 line-7 + 0;1;0 + +0;1;1 + -0;1;2 + 0;1;3 diff --git a/file-1.txt b/file-1.txt --- a/file-1.txt +++ b/file-1.txt - @@ -0,3 +0,3 @@ - file-1 line-0 - +file-1 line-1 - -file-1 line-2 - file-1 line-3 + @@ -1,3 +1,3 @@ + 1;0;0 + +1;0;1 + -1;0;2 + 1;0;3 @@ -10,3 +10,3 @@ - file-1 line-4 - +file-1 line-5 - -file-1 line-6 - file-1 line-7 + 1;1;0 + +1;1;1 + -1;1;2 + 1;1;3 `); }); it('adopts a buffer from a previous patch', function() { - const lastBuffer = buffer; - const lastLayers = layers; - const lastFilePatches = [ - buildFilePatchFixture(0), - buildFilePatchFixture(1), - buildFilePatchFixture(2, {noNewline: true}), - ]; - const lastPatch = new MultiFilePatch({buffer: lastBuffer, layers, filePatches: lastFilePatches}); - - buffer = new TextBuffer(); - layers = { - patch: buffer.addMarkerLayer(), - hunk: buffer.addMarkerLayer(), - unchanged: buffer.addMarkerLayer(), - addition: buffer.addMarkerLayer(), - deletion: buffer.addMarkerLayer(), - noNewline: buffer.addMarkerLayer(), - }; - const nextFilePatches = [ - buildFilePatchFixture(0), - buildFilePatchFixture(1), - buildFilePatchFixture(2), - buildFilePatchFixture(3, {noNewline: true}), - ]; - const nextPatch = new MultiFilePatch(buffer, layers, nextFilePatches); - - nextPatch.adoptBufferFrom(lastPatch); - - assert.strictEqual(nextPatch.getBuffer(), lastBuffer); - assert.strictEqual(nextPatch.getPatchLayer(), lastLayers.patch); - assert.strictEqual(nextPatch.getHunkLayer(), lastLayers.hunk); - assert.strictEqual(nextPatch.getUnchangedLayer(), lastLayers.unchanged); - assert.strictEqual(nextPatch.getAdditionLayer(), lastLayers.addition); - assert.strictEqual(nextPatch.getDeletionLayer(), lastLayers.deletion); - assert.strictEqual(nextPatch.getNoNewlineLayer(), lastLayers.noNewline); - - assert.lengthOf(nextPatch.getHunkLayer().getMarkers(), 8); - }); - - it('generates a stage patch for arbitrary buffer rows', function() { - const filePatches = [ - buildFilePatchFixture(0), - buildFilePatchFixture(1), - buildFilePatchFixture(2), - buildFilePatchFixture(3), - ]; - const original = new MultiFilePatch({buffer, layers, filePatches}); - const stagePatch = original.getStagePatchForLines(new Set([9, 14, 25, 26])); - - assert.strictEqual(stagePatch.getBuffer().getText(), dedent` - file-1 line-0 - file-1 line-1 - file-1 line-2 - file-1 line-3 - file-1 line-4 - file-1 line-6 - file-1 line-7 - file-3 line-0 - file-3 line-1 - file-3 line-2 - file-3 line-3 - - `); - - assert.lengthOf(stagePatch.getFilePatches(), 2); - const [fp0, fp1] = stagePatch.getFilePatches(); - assert.strictEqual(fp0.getOldPath(), 'file-1.txt'); - assertInFilePatch(fp0, stagePatch.getBuffer()).hunks( - { - startRow: 0, endRow: 3, - header: '@@ -0,3 +0,4 @@', - regions: [ - {kind: 'unchanged', string: ' file-1 line-0\n', range: [[0, 0], [0, 13]]}, - {kind: 'addition', string: '+file-1 line-1\n', range: [[1, 0], [1, 13]]}, - {kind: 'unchanged', string: ' file-1 line-2\n file-1 line-3\n', range: [[2, 0], [3, 13]]}, - ], - }, - { - startRow: 4, endRow: 6, - header: '@@ -10,3 +11,2 @@', - regions: [ - {kind: 'unchanged', string: ' file-1 line-4\n', range: [[4, 0], [4, 13]]}, - {kind: 'deletion', string: '-file-1 line-6\n', range: [[5, 0], [5, 13]]}, - {kind: 'unchanged', string: ' file-1 line-7\n', range: [[6, 0], [6, 13]]}, - ], - }, - ); - - assert.strictEqual(fp1.getOldPath(), 'file-3.txt'); - assertInFilePatch(fp1, stagePatch.getBuffer()).hunks( - { - startRow: 7, endRow: 10, - header: '@@ -0,3 +0,3 @@', - regions: [ - {kind: 'unchanged', string: ' file-3 line-0\n', range: [[7, 0], [7, 13]]}, - {kind: 'addition', string: '+file-3 line-1\n', range: [[8, 0], [8, 13]]}, - {kind: 'deletion', string: '-file-3 line-2\n', range: [[9, 0], [9, 13]]}, - {kind: 'unchanged', string: ' file-3 line-3\n', range: [[10, 0], [10, 13]]}, - ], - }, - ); - }); - - it('generates a stage patch from an arbitrary hunk', function() { - const filePatches = [ - buildFilePatchFixture(0), - buildFilePatchFixture(1), - ]; - const original = new MultiFilePatch({buffer, layers, filePatches}); - const hunk = original.getFilePatches()[0].getHunks()[1]; - const stagePatch = original.getStagePatchForHunk(hunk); - - assert.strictEqual(stagePatch.getBuffer().getText(), dedent` - file-0 line-4 - file-0 line-5 - file-0 line-6 - file-0 line-7 + const {multiFilePatch: lastMultiPatch, buffer: lastBuffer, layers: lastLayers} = multiFilePatchBuilder() + .addFilePatch(fp => { + fp.addHunk(h => h.unchanged('a0').added('a1').deleted('a2').unchanged('a3')); + }) + .addFilePatch(fp => { + fp.addHunk(h => h.unchanged('a4').deleted('a5').unchanged('a6')); + fp.addHunk(h => h.unchanged('a7').added('a8').unchanged('a9')); + }) + .addFilePatch(fp => { + fp.addHunk(h => h.oldRow(99).deleted('7').noNewline()); + }) + .build(); - `); - assert.lengthOf(stagePatch.getFilePatches(), 1); - const [fp0] = stagePatch.getFilePatches(); - assert.strictEqual(fp0.getOldPath(), 'file-0.txt'); - assert.strictEqual(fp0.getNewPath(), 'file-0.txt'); - assertInFilePatch(fp0, stagePatch.getBuffer()).hunks( - { - startRow: 0, endRow: 3, - header: '@@ -10,3 +10,3 @@', - regions: [ - {kind: 'unchanged', string: ' file-0 line-4\n', range: [[0, 0], [0, 13]]}, - {kind: 'addition', string: '+file-0 line-5\n', range: [[1, 0], [1, 13]]}, - {kind: 'deletion', string: '-file-0 line-6\n', range: [[2, 0], [2, 13]]}, - {kind: 'unchanged', string: ' file-0 line-7\n', range: [[3, 0], [3, 13]]}, - ], - }, - ); - }); + const {multiFilePatch: nextMultiPatch, buffer: nextBuffer, layers: nextLayers} = multiFilePatchBuilder() + .addFilePatch(fp => { + fp.addHunk(h => h.unchanged('b0', 'b1').added('b2').unchanged('b3', 'b4')); + }) + .addFilePatch(fp => { + fp.addHunk(h => h.unchanged('b5', 'b6').added('b7')); + }) + .addFilePatch(fp => { + fp.addHunk(h => h.unchanged('b8', 'b9').deleted('b10').unchanged('b11')); + fp.addHunk(h => h.oldRow(99).deleted('b12').noNewline()); + }) + .build(); - it('generates an unstage patch for arbitrary buffer rows', function() { - const filePatches = [ - buildFilePatchFixture(0), - buildFilePatchFixture(1), - buildFilePatchFixture(2), - buildFilePatchFixture(3), - ]; - const original = new MultiFilePatch({buffer, layers, filePatches}); - - const unstagePatch = original.getUnstagePatchForLines(new Set([1, 2, 21, 26, 29, 30])); - - assert.strictEqual(unstagePatch.getBuffer().getText(), dedent` - file-0 line-0 - file-0 line-1 - file-0 line-2 - file-0 line-3 - file-2 line-4 - file-2 line-5 - file-2 line-7 - file-3 line-0 - file-3 line-1 - file-3 line-2 - file-3 line-3 - file-3 line-4 - file-3 line-5 - file-3 line-6 - file-3 line-7 + assert.notStrictEqual(nextBuffer, lastBuffer); + assert.notStrictEqual(nextLayers, lastLayers); + + nextMultiPatch.adoptBufferFrom(lastMultiPatch); + + assert.strictEqual(nextMultiPatch.getBuffer(), lastBuffer); + assert.strictEqual(nextMultiPatch.getPatchLayer(), lastLayers.patch); + assert.strictEqual(nextMultiPatch.getHunkLayer(), lastLayers.hunk); + assert.strictEqual(nextMultiPatch.getUnchangedLayer(), lastLayers.unchanged); + assert.strictEqual(nextMultiPatch.getAdditionLayer(), lastLayers.addition); + assert.strictEqual(nextMultiPatch.getDeletionLayer(), lastLayers.deletion); + assert.strictEqual(nextMultiPatch.getNoNewlineLayer(), lastLayers.noNewline); + + assert.deepEqual(lastBuffer.getText(), dedent` + b0 + b1 + b2 + b3 + b4 + b5 + b6 + b7 + b8 + b9 + b10 + b11 + b12 + No newline at end of file `); - assert.lengthOf(unstagePatch.getFilePatches(), 3); - const [fp0, fp1, fp2] = unstagePatch.getFilePatches(); - assert.strictEqual(fp0.getOldPath(), 'file-0.txt'); - assertInFilePatch(fp0, unstagePatch.getBuffer()).hunks( - { - startRow: 0, endRow: 3, - header: '@@ -0,3 +0,3 @@', - regions: [ - {kind: 'unchanged', string: ' file-0 line-0\n', range: [[0, 0], [0, 13]]}, - {kind: 'deletion', string: '-file-0 line-1\n', range: [[1, 0], [1, 13]]}, - {kind: 'addition', string: '+file-0 line-2\n', range: [[2, 0], [2, 13]]}, - {kind: 'unchanged', string: ' file-0 line-3\n', range: [[3, 0], [3, 13]]}, - ], - }, - ); - - assert.strictEqual(fp1.getOldPath(), 'file-2.txt'); - assertInFilePatch(fp1, unstagePatch.getBuffer()).hunks( - { - startRow: 4, endRow: 6, - header: '@@ -10,3 +10,2 @@', - regions: [ - {kind: 'unchanged', string: ' file-2 line-4\n', range: [[4, 0], [4, 13]]}, - {kind: 'deletion', string: '-file-2 line-5\n', range: [[5, 0], [5, 13]]}, - {kind: 'unchanged', string: ' file-2 line-7\n', range: [[6, 0], [6, 13]]}, - ], - }, - ); + const assertMarkedLayerRanges = (layer, ranges) => { + assert.deepEqual(layer.getMarkers().map(m => m.getRange().serialize()), ranges); + }; - assert.strictEqual(fp2.getOldPath(), 'file-3.txt'); - assertInFilePatch(fp2, unstagePatch.getBuffer()).hunks( - { - startRow: 7, endRow: 10, - header: '@@ -0,3 +0,4 @@', - regions: [ - {kind: 'unchanged', string: ' file-3 line-0\n file-3 line-1\n', range: [[7, 0], [8, 13]]}, - {kind: 'addition', string: '+file-3 line-2\n', range: [[9, 0], [9, 13]]}, - {kind: 'unchanged', string: ' file-3 line-3\n', range: [[10, 0], [10, 13]]}, - ], - }, - { - startRow: 11, endRow: 14, - header: '@@ -10,3 +11,3 @@', - regions: [ - {kind: 'unchanged', string: ' file-3 line-4\n', range: [[11, 0], [11, 13]]}, - {kind: 'deletion', string: '-file-3 line-5\n', range: [[12, 0], [12, 13]]}, - {kind: 'addition', string: '+file-3 line-6\n', range: [[13, 0], [13, 13]]}, - {kind: 'unchanged', string: ' file-3 line-7\n', range: [[14, 0], [14, 13]]}, - ], - }, - ); + assertMarkedLayerRanges(lastLayers.patch, [ + [[0, 0], [4, 2]], [[5, 0], [7, 2]], [[8, 0], [13, 26]], + ]); + assertMarkedLayerRanges(lastLayers.hunk, [ + [[0, 0], [4, 2]], [[5, 0], [7, 2]], [[8, 0], [11, 3]], [[12, 0], [13, 26]], + ]); + assertMarkedLayerRanges(lastLayers.unchanged, [ + [[0, 0], [1, 2]], [[3, 0], [4, 2]], [[5, 0], [6, 2]], [[8, 0], [9, 2]], [[11, 0], [11, 3]], + ]); + assertMarkedLayerRanges(lastLayers.addition, [ + [[2, 0], [2, 2]], [[7, 0], [7, 2]], + ]); + assertMarkedLayerRanges(lastLayers.deletion, [ + [[10, 0], [10, 3]], [[12, 0], [12, 3]], + ]); + assertMarkedLayerRanges(lastLayers.noNewline, [ + [[13, 0], [13, 26]], + ]); }); - it('generates an unstaged patch for an arbitrary hunk', function() { - const filePatches = [ - buildFilePatchFixture(0), - buildFilePatchFixture(1), - ]; - const original = new MultiFilePatch({buffer, layers, filePatches}); - const hunk = original.getFilePatches()[1].getHunks()[0]; - const unstagePatch = original.getUnstagePatchForHunk(hunk); - - assert.strictEqual(unstagePatch.getBuffer().getText(), dedent` - file-1 line-0 - file-1 line-1 - file-1 line-2 - file-1 line-3 + describe('derived patch generation', function() { + let multiFilePatch, rowSet; - `); - assert.lengthOf(unstagePatch.getFilePatches(), 1); - const [fp0] = unstagePatch.getFilePatches(); - assert.strictEqual(fp0.getOldPath(), 'file-1.txt'); - assert.strictEqual(fp0.getNewPath(), 'file-1.txt'); - assertInFilePatch(fp0, unstagePatch.getBuffer()).hunks( - { - startRow: 0, endRow: 3, - header: '@@ -0,3 +0,3 @@', - regions: [ - {kind: 'unchanged', string: ' file-1 line-0\n', range: [[0, 0], [0, 13]]}, - {kind: 'deletion', string: '-file-1 line-1\n', range: [[1, 0], [1, 13]]}, - {kind: 'addition', string: '+file-1 line-2\n', range: [[2, 0], [2, 13]]}, - {kind: 'unchanged', string: ' file-1 line-3\n', range: [[3, 0], [3, 13]]}, - ], - }, - ); - }); - - // FIXME adapt these to the lifted method. - // describe('next selection range derivation', function() { - // it('selects the first change region after the highest buffer row', function() { - // const lastPatch = buildPatchFixture(); - // // Selected: - // // deletions (1-2) and partial addition (4 from 3-5) from hunk 0 - // // one deletion row (13 from 12-16) from the middle of hunk 1; - // // nothing in hunks 2 or 3 - // const lastSelectedRows = new Set([1, 2, 4, 5, 13]); - // - // const nBuffer = new TextBuffer({text: - // // 0 1 2 3 4 - // '0000\n0003\n0004\n0005\n0006\n' + - // // 5 6 7 8 9 10 11 12 13 14 15 - // '0007\n0008\n0009\n0010\n0011\n0012\n0014\n0015\n0016\n0017\n0018\n' + - // // 16 17 18 19 20 - // '0019\n0020\n0021\n0022\n0023\n' + - // // 21 22 23 - // '0024\n0025\n No newline at end of file\n', - // }); - // const nLayers = buildLayers(nBuffer); - // const nHunks = [ - // new Hunk({ - // oldStartRow: 3, oldRowCount: 3, newStartRow: 3, newRowCount: 5, // next row drift = +2 - // marker: markRange(nLayers.hunk, 0, 4), - // regions: [ - // new Unchanged(markRange(nLayers.unchanged, 0)), // 0 - // new Addition(markRange(nLayers.addition, 1)), // + 1 - // new Unchanged(markRange(nLayers.unchanged, 2)), // 2 - // new Addition(markRange(nLayers.addition, 3)), // + 3 - // new Unchanged(markRange(nLayers.unchanged, 4)), // 4 - // ], - // }), - // new Hunk({ - // oldStartRow: 12, oldRowCount: 9, newStartRow: 14, newRowCount: 7, // next row drift = +2 -2 = 0 - // marker: markRange(nLayers.hunk, 5, 15), - // regions: [ - // new Unchanged(markRange(nLayers.unchanged, 5)), // 5 - // new Addition(markRange(nLayers.addition, 6)), // +6 - // new Unchanged(markRange(nLayers.unchanged, 7, 9)), // 7 8 9 - // new Deletion(markRange(nLayers.deletion, 10, 13)), // -10 -11 -12 -13 - // new Addition(markRange(nLayers.addition, 14)), // +14 - // new Unchanged(markRange(nLayers.unchanged, 15)), // 15 - // ], - // }), - // new Hunk({ - // oldStartRow: 26, oldRowCount: 4, newStartRow: 26, newRowCount: 3, // next row drift = 0 -1 = -1 - // marker: markRange(nLayers.hunk, 16, 20), - // regions: [ - // new Unchanged(markRange(nLayers.unchanged, 16)), // 16 - // new Addition(markRange(nLayers.addition, 17)), // +17 - // new Deletion(markRange(nLayers.deletion, 18, 19)), // -18 -19 - // new Unchanged(markRange(nLayers.unchanged, 20)), // 20 - // ], - // }), - // new Hunk({ - // oldStartRow: 32, oldRowCount: 1, newStartRow: 31, newRowCount: 2, - // marker: markRange(nLayers.hunk, 22, 24), - // regions: [ - // new Unchanged(markRange(nLayers.unchanged, 22)), // 22 - // new Addition(markRange(nLayers.addition, 23)), // +23 - // new NoNewline(markRange(nLayers.noNewline, 24)), - // ], - // }), - // ]; - // const nextPatch = new Patch({status: 'modified', hunks: nHunks, buffer: nBuffer, layers: nLayers}); - // - // const nextRange = nextPatch.getNextSelectionRange(lastPatch, lastSelectedRows); - // // Original buffer row 14 = the next changed row = new buffer row 11 - // assert.deepEqual(nextRange, [[11, 0], [11, Infinity]]); - // }); - // - // it('offsets the chosen selection index by hunks that were completely selected', function() { - // const buffer = buildBuffer(11); - // const layers = buildLayers(buffer); - // const lastPatch = new Patch({ - // status: 'modified', - // hunks: [ - // new Hunk({ - // oldStartRow: 1, oldRowCount: 3, newStartRow: 1, newRowCount: 3, - // marker: markRange(layers.hunk, 0, 5), - // regions: [ - // new Unchanged(markRange(layers.unchanged, 0)), - // new Addition(markRange(layers.addition, 1, 2)), - // new Deletion(markRange(layers.deletion, 3, 4)), - // new Unchanged(markRange(layers.unchanged, 5)), - // ], - // }), - // new Hunk({ - // oldStartRow: 5, oldRowCount: 4, newStartRow: 5, newRowCount: 4, - // marker: markRange(layers.hunk, 6, 11), - // regions: [ - // new Unchanged(markRange(layers.unchanged, 6)), - // new Addition(markRange(layers.addition, 7, 8)), - // new Deletion(markRange(layers.deletion, 9, 10)), - // new Unchanged(markRange(layers.unchanged, 11)), - // ], - // }), - // ], - // buffer, - // layers, - // }); - // // 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 nextBuffer = new TextBuffer({text: '0006\n0007\n0008\n0009\n0010\n0011\n'}); - // const nextLayers = buildLayers(nextBuffer); - // const nextPatch = new Patch({ - // status: 'modified', - // hunks: [ - // new Hunk({ - // oldStartRow: 5, oldRowCount: 4, newStartRow: 5, newRowCount: 4, - // marker: markRange(nextLayers.hunk, 0, 5), - // regions: [ - // new Unchanged(markRange(nextLayers.unchanged, 0)), - // new Addition(markRange(nextLayers.addition, 1)), - // new Deletion(markRange(nextLayers.deletion, 3, 4)), - // new Unchanged(markRange(nextLayers.unchanged, 5)), - // ], - // }), - // ], - // buffer: nextBuffer, - // layers: nextLayers, - // }); - // - // 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 buffer = lastPatch.getBuffer(); - // const layers = buildLayers(buffer); - // const nextPatch = new Patch({ - // status: 'modified', - // hunks: [ - // new Hunk({ - // oldStartRow: 1, oldRowCount: 3, newStartRow: 1, newRowCount: 4, - // marker: markRange(layers.hunk, 0, 4), - // regions: [ - // new Unchanged(markRange(layers.unchanged, 0)), - // new Addition(markRange(layers.addition, 1, 2)), - // new Deletion(markRange(layers.deletion, 3)), - // new Unchanged(markRange(layers.unchanged, 4)), - // ], - // }), - // ], - // buffer, - // layers, - // }); - // - // const range = nextPatch.getNextSelectionRange(lastPatch, lastSelectedRows); - // assert.deepEqual(range, [[1, 0], [1, Infinity]]); - // }); - // }); - - function buildFilePatchFixture(index, options = {}) { - const opts = { - oldFilePath: `file-${index}.txt`, - oldFileMode: '100644', - oldFileSymlink: null, - newFilePath: `file-${index}.txt`, - newFileMode: '100644', - newFileSymlink: null, - status: 'modified', - noNewline: false, - ...options, - }; + beforeEach(function() { + // The row content pattern here is: ${fileno};${hunkno};${lineno}, with a (**) if it's selected + multiFilePatch = multiFilePatchBuilder() + .addFilePatch(fp => { + fp.setOldFile(f => f.path('file-0.txt')); + fp.addHunk(h => h.oldRow(1).unchanged('0;0;0').added('0;0;1').deleted('0;0;2').unchanged('0;0;3')); + fp.addHunk(h => h.oldRow(10).unchanged('0;1;0').added('0;1;1').deleted('0;1;2').unchanged('0;1;3')); + }) + .addFilePatch(fp => { + fp.setOldFile(f => f.path('file-1.txt')); + fp.addHunk(h => h.oldRow(1).unchanged('1;0;0').added('1;0;1 (**)').deleted('1;0;2').unchanged('1;0;3')); + fp.addHunk(h => h.oldRow(10).unchanged('1;1;0').added('1;1;1').deleted('1;1;2 (**)').unchanged('1;1;3')); + }) + .addFilePatch(fp => { + fp.setOldFile(f => f.path('file-2.txt')); + fp.addHunk(h => h.oldRow(1).unchanged('2;0;0').added('2;0;1').deleted('2;0;2').unchanged('2;0;3')); + fp.addHunk(h => h.oldRow(10).unchanged('2;1;0').added('2;1;1').deleted('2;2;2').unchanged('2;1;3')); + }) + .addFilePatch(fp => { + fp.setOldFile(f => f.path('file-3.txt')); + fp.addHunk(h => h.oldRow(1).unchanged('3;0;0').added('3;0;1 (**)').deleted('3;0;2 (**)').unchanged('3;0;3')); + fp.addHunk(h => h.oldRow(10).unchanged('3;1;0').added('3;1;1').deleted('3;2;2').unchanged('3;1;3')); + }) + .build() + .multiFilePatch; - const rowOffset = buffer.getLastRow(); - for (let i = 0; i < 8; i++) { - buffer.append(`file-${index} line-${i}\n`); - } - if (opts.noNewline) { - buffer.append(' No newline at end of file\n'); - } + // Buffer rows corresponding to the rows marked with (**) above + rowSet = new Set([9, 14, 25, 26]); + }); - let oldFile = new File({path: opts.oldFilePath, mode: opts.oldFileMode, symlink: opts.oldFileSymlink}); - const newFile = new File({path: opts.newFilePath, mode: opts.newFileMode, symlink: opts.newFileSymlink}); + it('generates a stage patch for arbitrary buffer rows', function() { + const stagePatch = multiFilePatch.getStagePatchForLines(rowSet); + + assert.strictEqual(stagePatch.getBuffer().getText(), dedent` + 1;0;0 + 1;0;1 (**) + 1;0;2 + 1;0;3 + 1;1;0 + 1;1;2 (**) + 1;1;3 + 3;0;0 + 3;0;1 (**) + 3;0;2 (**) + 3;0;3 + + `); + + assert.lengthOf(stagePatch.getFilePatches(), 2); + const [fp0, fp1] = stagePatch.getFilePatches(); + assert.strictEqual(fp0.getOldPath(), 'file-1.txt'); + assertInFilePatch(fp0, stagePatch.getBuffer()).hunks( + { + startRow: 0, endRow: 3, + header: '@@ -1,3 +1,4 @@', + regions: [ + {kind: 'unchanged', string: ' 1;0;0\n', range: [[0, 0], [0, 5]]}, + {kind: 'addition', string: '+1;0;1 (**)\n', range: [[1, 0], [1, 10]]}, + {kind: 'unchanged', string: ' 1;0;2\n 1;0;3\n', range: [[2, 0], [3, 5]]}, + ], + }, + { + startRow: 4, endRow: 6, + header: '@@ -10,3 +11,2 @@', + regions: [ + {kind: 'unchanged', string: ' 1;1;0\n', range: [[4, 0], [4, 5]]}, + {kind: 'deletion', string: '-1;1;2 (**)\n', range: [[5, 0], [5, 10]]}, + {kind: 'unchanged', string: ' 1;1;3\n', range: [[6, 0], [6, 5]]}, + ], + }, + ); + + assert.strictEqual(fp1.getOldPath(), 'file-3.txt'); + assertInFilePatch(fp1, stagePatch.getBuffer()).hunks( + { + startRow: 7, endRow: 10, + header: '@@ -1,3 +1,3 @@', + regions: [ + {kind: 'unchanged', string: ' 3;0;0\n', range: [[7, 0], [7, 5]]}, + {kind: 'addition', string: '+3;0;1 (**)\n', range: [[8, 0], [8, 10]]}, + {kind: 'deletion', string: '-3;0;2 (**)\n', range: [[9, 0], [9, 10]]}, + {kind: 'unchanged', string: ' 3;0;3\n', range: [[10, 0], [10, 5]]}, + ], + }, + ); + }); - const mark = (layer, start, end = start) => layer.markRange([[rowOffset + start, 0], [rowOffset + end, Infinity]]); + it('generates a stage patch from an arbitrary hunk', function() { + const hunk = multiFilePatch.getFilePatches()[0].getHunks()[1]; + const stagePatch = multiFilePatch.getStagePatchForHunk(hunk); + + assert.strictEqual(stagePatch.getBuffer().getText(), dedent` + 0;1;0 + 0;1;1 + 0;1;2 + 0;1;3 + + `); + assert.lengthOf(stagePatch.getFilePatches(), 1); + const [fp0] = stagePatch.getFilePatches(); + assert.strictEqual(fp0.getOldPath(), 'file-0.txt'); + assert.strictEqual(fp0.getNewPath(), 'file-0.txt'); + assertInFilePatch(fp0, stagePatch.getBuffer()).hunks( + { + startRow: 0, endRow: 3, + header: '@@ -10,3 +10,3 @@', + regions: [ + {kind: 'unchanged', string: ' 0;1;0\n', range: [[0, 0], [0, 5]]}, + {kind: 'addition', string: '+0;1;1\n', range: [[1, 0], [1, 5]]}, + {kind: 'deletion', string: '-0;1;2\n', range: [[2, 0], [2, 5]]}, + {kind: 'unchanged', string: ' 0;1;3\n', range: [[3, 0], [3, 5]]}, + ], + }, + ); + }); - const withNoNewlineRegion = regions => { - if (opts.noNewline) { - regions.push(new NoNewline(mark(layers.noNewline, 8))); - } - return regions; - }; + it('generates an unstage patch for arbitrary buffer rows', function() { + const unstagePatch = multiFilePatch.getUnstagePatchForLines(rowSet); + + assert.strictEqual(unstagePatch.getBuffer().getText(), dedent` + 1;0;0 + 1;0;1 (**) + 1;0;3 + 1;1;0 + 1;1;1 + 1;1;2 (**) + 1;1;3 + 3;0;0 + 3;0;1 (**) + 3;0;2 (**) + 3;0;3 + + `); + + assert.lengthOf(unstagePatch.getFilePatches(), 2); + const [fp0, fp1] = unstagePatch.getFilePatches(); + assert.strictEqual(fp0.getOldPath(), 'file-1.txt'); + assertInFilePatch(fp0, unstagePatch.getBuffer()).hunks( + { + startRow: 0, endRow: 2, + header: '@@ -1,3 +1,2 @@', + regions: [ + {kind: 'unchanged', string: ' 1;0;0\n', range: [[0, 0], [0, 5]]}, + {kind: 'deletion', string: '-1;0;1 (**)\n', range: [[1, 0], [1, 10]]}, + {kind: 'unchanged', string: ' 1;0;3\n', range: [[2, 0], [2, 5]]}, + ], + }, + { + startRow: 3, endRow: 6, + header: '@@ -10,3 +9,4 @@', + regions: [ + {kind: 'unchanged', string: ' 1;1;0\n 1;1;1\n', range: [[3, 0], [4, 5]]}, + {kind: 'addition', string: '+1;1;2 (**)\n', range: [[5, 0], [5, 10]]}, + {kind: 'unchanged', string: ' 1;1;3\n', range: [[6, 0], [6, 5]]}, + ], + }, + ); + + assert.strictEqual(fp1.getOldPath(), 'file-3.txt'); + assertInFilePatch(fp1, unstagePatch.getBuffer()).hunks( + { + startRow: 7, endRow: 10, + header: '@@ -1,3 +1,3 @@', + regions: [ + {kind: 'unchanged', string: ' 3;0;0\n', range: [[7, 0], [7, 5]]}, + {kind: 'deletion', string: '-3;0;1 (**)\n', range: [[8, 0], [8, 10]]}, + {kind: 'addition', string: '+3;0;2 (**)\n', range: [[9, 0], [9, 10]]}, + {kind: 'unchanged', string: ' 3;0;3\n', range: [[10, 0], [10, 5]]}, + ], + }, + ); + }); - let hunks = []; - if (opts.status === 'modified') { - hunks = [ - new Hunk({ - oldStartRow: 0, newStartRow: 0, oldRowCount: 3, newRowCount: 3, - sectionHeading: `file-${index} hunk-0`, - marker: mark(layers.hunk, 0, 3), + it('generates an unstage patch for an arbitrary hunk', function() { + const hunk = multiFilePatch.getFilePatches()[1].getHunks()[0]; + const unstagePatch = multiFilePatch.getUnstagePatchForHunk(hunk); + + assert.strictEqual(unstagePatch.getBuffer().getText(), dedent` + 1;0;0 + 1;0;1 (**) + 1;0;2 + 1;0;3 + + `); + assert.lengthOf(unstagePatch.getFilePatches(), 1); + const [fp0] = unstagePatch.getFilePatches(); + assert.strictEqual(fp0.getOldPath(), 'file-1.txt'); + assert.strictEqual(fp0.getNewPath(), 'file-1.txt'); + assertInFilePatch(fp0, unstagePatch.getBuffer()).hunks( + { + startRow: 0, endRow: 3, + header: '@@ -1,3 +1,3 @@', regions: [ - new Unchanged(mark(layers.unchanged, 0)), - new Addition(mark(layers.addition, 1)), - new Deletion(mark(layers.deletion, 2)), - new Unchanged(mark(layers.unchanged, 3)), + {kind: 'unchanged', string: ' 1;0;0\n', range: [[0, 0], [0, 5]]}, + {kind: 'deletion', string: '-1;0;1 (**)\n', range: [[1, 0], [1, 10]]}, + {kind: 'addition', string: '+1;0;2\n', range: [[2, 0], [2, 5]]}, + {kind: 'unchanged', string: ' 1;0;3\n', range: [[3, 0], [3, 5]]}, ], - }), - new Hunk({ - oldStartRow: 10, newStartRow: 10, oldRowCount: 3, newRowCount: 3, - sectionHeading: `file-${index} hunk-1`, - marker: mark(layers.hunk, 4, opts.noNewline ? 8 : 7), - regions: withNoNewlineRegion([ - new Unchanged(mark(layers.unchanged, 4)), - new Addition(mark(layers.addition, 5)), - new Deletion(mark(layers.deletion, 6)), - new Unchanged(mark(layers.unchanged, 7)), - ]), - }), - ]; - } else if (opts.status === 'added') { - hunks = [ - new Hunk({ - oldStartRow: 0, newStartRow: 0, oldRowCount: 8, newRowCount: 8, - sectionHeading: `file-${index} hunk-0`, - marker: mark(layers.hunk, 0, opts.noNewline ? 8 : 7), - regions: withNoNewlineRegion([ - new Addition(mark(layers.addition, 0, 7)), - ]), - }), - ]; - - oldFile = nullFile; - } + }, + ); + }); + }); + + describe('next selection range derivation', function() { + it('selects the origin if the new patch is empty', function() { + const {multiFilePatch: lastMultiPatch} = multiFilePatchBuilder().addFilePatch().build(); + const {multiFilePatch: nextMultiPatch} = multiFilePatchBuilder().build(); - const marker = mark(layers.patch, 0, 7); - const patch = new Patch({status: opts.status, hunks, marker}); + const nextSelectionRange = nextMultiPatch.getNextSelectionRange(lastMultiPatch, new Set()); + assert.deepEqual(nextSelectionRange.serialize(), [[0, 0], [0, 0]]); + }); - return new FilePatch(oldFile, newFile, patch); - } + it('selects the first change row if there was no prior selection', function() { + const {multiFilePatch: lastMultiPatch} = multiFilePatchBuilder().build(); + const {multiFilePatch: nextMultiPatch} = multiFilePatchBuilder().addFilePatch().build(); + const nextSelectionRange = nextMultiPatch.getNextSelectionRange(lastMultiPatch, new Set()); + assert.deepEqual(nextSelectionRange.serialize(), [[1, 0], [1, Infinity]]); + }); + }); }); From 3c3d8ba62b0c59c8beef1fdf38e4b8f00cf16639 Mon Sep 17 00:00:00 2001 From: Vanessa Yuen Date: Fri, 9 Nov 2018 17:29:34 +0100 Subject: [PATCH 248/284] =?UTF-8?q?use=20the=20new=20shiny=20`multiFilePat?= =?UTF-8?q?chBuilder`=20=E2=9C=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/views/multi-file-patch-view.test.js | 52 +++++++++++------------- 1 file changed, 24 insertions(+), 28 deletions(-) diff --git a/test/views/multi-file-patch-view.test.js b/test/views/multi-file-patch-view.test.js index de0be901639..e5678d278f4 100644 --- a/test/views/multi-file-patch-view.test.js +++ b/test/views/multi-file-patch-view.test.js @@ -4,11 +4,12 @@ import {shallow, mount} from 'enzyme'; import {cloneRepository, buildRepository} from '../helpers'; import MultiFilePatchView from '../../lib/views/multi-file-patch-view'; import {buildFilePatch, buildMultiFilePatch} from '../../lib/models/patch'; +import {multiFilePatchBuilder} from '../builder/patch'; import {nullFile} from '../../lib/models/patch/file'; import FilePatch from '../../lib/models/patch/file-patch'; import RefHolder from '../../lib/models/ref-holder'; -describe('MultiFilePatchView', function() { +describe.only('MultiFilePatchView', function() { let atomEnv, workspace, repository, filePatches; beforeEach(async function() { @@ -18,26 +19,20 @@ describe('MultiFilePatchView', function() { const workdirPath = await cloneRepository(); repository = await buildRepository(workdirPath); - // path.txt: unstaged changes - filePatches = buildMultiFilePatch([{ - oldPath: 'path.txt', - oldMode: '100644', - newPath: 'path.txt', - newMode: '100644', - status: 'modified', - hunks: [ - { - 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'], - }, - ], - }]); + const {multiFilePatch} = multiFilePatchBuilder() + .addFilePatch(fp => { + fp.setOldFile(f => f.path('path.txt')); + fp.addHunk(h => { + h.oldRow(4); + h.unchanged('0000').added('0001', '0002').deleted('0003').unchanged('0004'); + }); + fp.addHunk(h => { + h.oldRow(8); + h.unchanged('0005').added('0006').deleted('0007').unchanged('0008'); + }); + }).build(); + + filePatches = multiFilePatch; }); afterEach(function() { @@ -899,7 +894,8 @@ describe('MultiFilePatchView', function() { describe('when viewing an empty patch', function() { it('renders an empty patch message', function() { - const wrapper = shallow(buildApp({filePatch: FilePatch.createNull()})); + const {multiFilePatch: emptyMfp} = multiFilePatchBuilder().build(); + const wrapper = shallow(buildApp({multiFilePatch: emptyMfp})); assert.isTrue(wrapper.find('.github-FilePatchView').hasClass('github-FilePatchView--blank')); assert.isTrue(wrapper.find('.github-FilePatchView-message').exists()); }); @@ -1004,9 +1000,9 @@ describe('MultiFilePatchView', function() { assert.isTrue(surfaceFile.called); }); - describe.only('hunk mode navigation', function() { + describe('hunk mode navigation', function() { beforeEach(function() { - filePatch = buildFilePatch([{ + filePatches = buildFilePatch([{ oldPath: 'path.txt', oldMode: '100644', newPath: 'path.txt', @@ -1045,7 +1041,7 @@ describe('MultiFilePatchView', function() { it('advances the selection to the next hunks', function() { const selectedRowsChanged = sinon.spy(); const selectedRows = new Set([1, 7, 10]); - const wrapper = mount(buildApp({filePatch, selectedRowsChanged, selectedRows, selectionMode: 'hunk'})); + const wrapper = mount(buildApp({multiFilePatch: filePatches, selectedRowsChanged, selectedRows, selectionMode: 'hunk'})); const editor = wrapper.find('AtomTextEditor').instance().getModel(); editor.setSelectedBufferRanges([ [[0, 0], [2, 4]], // hunk 0 @@ -1069,7 +1065,7 @@ describe('MultiFilePatchView', function() { it('does not advance a selected hunk at the end of the patch', function() { const selectedRowsChanged = sinon.spy(); const selectedRows = new Set([4, 13, 14]); - const wrapper = mount(buildApp({filePatch, selectedRowsChanged, selectedRows, selectionMode: 'hunk'})); + const wrapper = mount(buildApp({multiFilePatch: filePatches, selectedRowsChanged, selectedRows, selectionMode: 'hunk'})); const editor = wrapper.find('AtomTextEditor').instance().getModel(); editor.setSelectedBufferRanges([ [[3, 0], [5, 4]], // hunk 1 @@ -1091,7 +1087,7 @@ describe('MultiFilePatchView', function() { it('retreats the selection to the previous hunks', function() { const selectedRowsChanged = sinon.spy(); const selectedRows = new Set([4, 10, 13, 14]); - const wrapper = mount(buildApp({filePatch, selectedRowsChanged, selectedRows, selectionMode: 'hunk'})); + const wrapper = mount(buildApp({multiFilePatch: filePatches, selectedRowsChanged, selectedRows, selectionMode: 'hunk'})); const editor = wrapper.find('AtomTextEditor').instance().getModel(); editor.setSelectedBufferRanges([ [[3, 0], [5, 4]], // hunk 1 @@ -1115,7 +1111,7 @@ describe('MultiFilePatchView', function() { it('does not retreat a selected hunk at the beginning of the patch', function() { const selectedRowsChanged = sinon.spy(); const selectedRows = new Set([4, 10, 13, 14]); - const wrapper = mount(buildApp({filePatch, selectedRowsChanged, selectedRows, selectionMode: 'hunk'})); + const wrapper = mount(buildApp({multiFilePatch: filePatches, selectedRowsChanged, selectedRows, selectionMode: 'hunk'})); const editor = wrapper.find('AtomTextEditor').instance().getModel(); editor.setSelectedBufferRanges([ [[0, 0], [2, 4]], // hunk 0 From 5a16641c044b9fcc38e608142148b544481a8374 Mon Sep 17 00:00:00 2001 From: Vanessa Yuen Date: Fri, 9 Nov 2018 17:49:48 +0100 Subject: [PATCH 249/284] replace old buildMultiPatch method with multiFilePatchBuilder --- test/views/multi-file-patch-view.test.js | 220 +++++++++-------------- 1 file changed, 87 insertions(+), 133 deletions(-) diff --git a/test/views/multi-file-patch-view.test.js b/test/views/multi-file-patch-view.test.js index e5678d278f4..5b9948d59db 100644 --- a/test/views/multi-file-patch-view.test.js +++ b/test/views/multi-file-patch-view.test.js @@ -120,22 +120,16 @@ describe.only('MultiFilePatchView', function() { 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}); + const {multiFilePatch} = multiFilePatchBuilder() + .addFilePatch(fp => { + fp.setOldFile(f => f.path('path.txt')); + fp.addHunk(h => { + h.oldRow(5); + h.unchanged('0000').added('0001').unchanged('0002').deleted('0003').unchanged('0004'); + }); + }).build(); + + wrapper.setProps({multiFilePatch}); assert.sameMembers(Array.from(selectedRowsChanged.lastCall.args[0]), [3]); assert.strictEqual(selectedRowsChanged.lastCall.args[1], 'line'); @@ -150,71 +144,53 @@ describe.only('MultiFilePatchView', function() { }); it('selects the next full hunk when a new file patch arrives in hunk selection mode', function() { - const multiHunkPatch = buildFilePatch([{ - oldPath: 'path.txt', - oldMode: '100644', - newPath: 'path.txt', - newMode: '100644', - status: 'modified', - hunks: [ - { - oldStartLine: 10, oldLineCount: 4, newStartLine: 10, newLineCount: 4, - heading: '0', - lines: [' 0000', '+0001', ' 0002', '-0003', ' 0004'], - }, - { - oldStartLine: 20, oldLineCount: 3, newStartLine: 20, newLineCount: 4, - heading: '1', - lines: [' 0005', '+0006', '+0007', '-0008', ' 0009'], - }, - { - oldStartLine: 30, oldLineCount: 3, newStartLine: 31, newLineCount: 3, - heading: '2', - lines: [' 0010', '+0011', '-0012', ' 0013'], - }, - { - oldStartLine: 40, oldLineCount: 4, newStartLine: 41, newLineCount: 4, - heading: '3', - lines: [' 0014', '-0015', ' 0016', '+0017', ' 0018'], - }, - ], - }]); + const {multiFilePatch} = multiFilePatchBuilder() + .addFilePatch(fp => { + fp.setOldFile(f => f.path('path.txt')); + fp.addHunk(h => { + h.oldRow(10); + h.unchanged('0000').added('0001').unchanged('0002').deleted('0003').unchanged('0004'); + }); + fp.addHunk(h => { + h.oldRow(20); + h.unchanged('0005').added('0006').added('0007').deleted('0008').unchanged('0009'); + }); + fp.addHunk(h => { + h.oldRow(30); + h.unchanged('0010').added('0011').deleted('0012').unchanged('0013'); + }); + fp.addHunk(h => { + h.oldRow(40); + h.unchanged('0014').deleted('0015').unchanged('0016').added('0017').unchanged('0018'); + }); + }).build(); const selectedRowsChanged = sinon.spy(); const wrapper = mount(buildApp({ - filePatch: multiHunkPatch, + multiFilePatch, selectedRows: new Set([6, 7, 8]), selectionMode: 'hunk', selectedRowsChanged, })); - const nextPatch = buildFilePatch([{ - oldPath: 'path.txt', - oldMode: '100644', - newPath: 'path.txt', - newMode: '100644', - status: 'modified', - hunks: [ - { - oldStartLine: 10, oldLineCount: 4, newStartLine: 10, newLineCount: 4, - heading: '0', - lines: [' 0000', '+0001', ' 0002', '-0003', ' 0004'], - }, - { - oldStartLine: 30, oldLineCount: 3, newStartLine: 30, newLineCount: 3, - heading: '2', - // 5 6 7 8 - lines: [' 0010', '+0011', '-0012', ' 0013'], - }, - { - oldStartLine: 40, oldLineCount: 4, newStartLine: 40, newLineCount: 4, - heading: '3', - lines: [' 0014', '-0015', ' 0016', '+0017', ' 0018'], - }, - ], - }]); - - wrapper.setProps({filePatch: nextPatch}); + const {multiFilePatch: nextMfp} = multiFilePatchBuilder() + .addFilePatch(fp => { + fp.setOldFile(f => f.path('path.txt')); + fp.addHunk(h => { + h.oldRow(10); + h.unchanged('0000').added('0001').unchanged('0002').deleted('0003').unchanged('0004'); + }); + fp.addHunk(h => { + h.oldRow(30); + h.unchanged('0010').added('0011').deleted('0012').unchanged('0013'); + }); + fp.addHunk(h => { + h.oldRow(40); + h.unchanged('0014').deleted('0015').unchanged('0016').added('0017').unchanged('0018'); + }); + }).build(); + + wrapper.setProps({multiFilePatch: nextMfp}); assert.sameMembers(Array.from(selectedRowsChanged.lastCall.args[0]), [6, 7]); assert.strictEqual(selectedRowsChanged.lastCall.args[1], 'hunk'); @@ -425,25 +401,18 @@ describe.only('MultiFilePatchView', function() { describe('hunk headers', function() { it('renders one for each hunk', function() { - const mfp = buildMultiFilePatch([{ - 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 {multiFilePatch: mfp} = multiFilePatchBuilder() + .addFilePatch(fp => { + fp.setOldFile(f => f.path('path.txt')); + fp.addHunk(h => { + h.oldRow(1); + h.unchanged('0000').added('0001').unchanged('0002'); + }); + fp.addHunk(h => { + h.oldRow(10); + h.unchanged('0003').deleted('0004').unchanged('0005'); + }); + }).build(); const hunks = mfp.getFilePatches()[0].getHunks(); const wrapper = mount(buildApp({multiFilePatch: mfp})); @@ -509,25 +478,18 @@ describe.only('MultiFilePatchView', function() { }); it('handles mousedown as a selection event', function() { - const mfp = buildMultiFilePatch([{ - 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 {multiFilePatch: mfp} = multiFilePatchBuilder() + .addFilePatch(fp => { + fp.setOldFile(f => f.path('path.txt')); + fp.addHunk(h => { + h.oldRow(1); + h.unchanged('0000').added('0001').unchanged('0002'); + }); + fp.addHunk(h => { + h.oldRow(10); + h.unchanged('0003').deleted('0004').unchanged('0005'); + }); + }).build(); const selectedRowsChanged = sinon.spy(); const wrapper = mount(buildApp({multiFilePatch: mfp, selectedRowsChanged, selectionMode: 'line'})); @@ -821,28 +783,20 @@ describe.only('MultiFilePatchView', function() { let linesPatch; beforeEach(function() { - linesPatch = buildMultiFilePatch([{ - 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', - ], - }, - ], - }]); + + const {multiFilePatch} = multiFilePatchBuilder() + .addFilePatch(fp => { + fp.setOldFile(f => f.path('path.txt')); + fp.addHunk(h => { + h.oldRow(1); + h.unchanged('0000').added('0001', '0002').deleted('0003').added('0004').added('0005').unchanged('0006'); + }); + fp.addHunk(h => { + h.oldRow(10); + h.unchanged('0007').deleted('0008', '0009', '0010').unchanged('0011').added('0012', '0013', '0014').deleted('0015').unchanged('0016').added('\\ No newline at end of file'); + }); + }).build(); + linesPatch = multiFilePatch; }); it('decorates added lines', function() { From 6679c9c37d46bd51c3e10ec0eac1b29ee8d2e5ce Mon Sep 17 00:00:00 2001 From: Vanessa Yuen Date: Fri, 9 Nov 2018 18:53:01 +0100 Subject: [PATCH 250/284] nonewline is its own special ting --- test/views/multi-file-patch-view.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/views/multi-file-patch-view.test.js b/test/views/multi-file-patch-view.test.js index 5b9948d59db..00a76762420 100644 --- a/test/views/multi-file-patch-view.test.js +++ b/test/views/multi-file-patch-view.test.js @@ -793,7 +793,7 @@ describe.only('MultiFilePatchView', function() { }); fp.addHunk(h => { h.oldRow(10); - h.unchanged('0007').deleted('0008', '0009', '0010').unchanged('0011').added('0012', '0013', '0014').deleted('0015').unchanged('0016').added('\\ No newline at end of file'); + h.unchanged('0007').deleted('0008', '0009', '0010').unchanged('0011').added('0012', '0013', '0014').deleted('0015').unchanged('0016').noNewline(); }); }).build(); linesPatch = multiFilePatch; From fc1faa73d3c8afcfc340416dd1390d67a66c4950 Mon Sep 17 00:00:00 2001 From: Vanessa Yuen Date: Fri, 9 Nov 2018 18:53:26 +0100 Subject: [PATCH 251/284] greenify open file tests --- test/views/multi-file-patch-view.test.js | 45 ++++++++++-------------- 1 file changed, 18 insertions(+), 27 deletions(-) diff --git a/test/views/multi-file-patch-view.test.js b/test/views/multi-file-patch-view.test.js index 00a76762420..ba6a328e716 100644 --- a/test/views/multi-file-patch-view.test.js +++ b/test/views/multi-file-patch-view.test.js @@ -1086,47 +1086,38 @@ describe.only('MultiFilePatchView', function() { }); describe('opening the file', function() { - let fp; + let mfp; beforeEach(function() { - 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', - // 2 3 4 - lines: [' 0000', '+0001', ' 0002'], - }, - { - oldStartLine: 10, oldLineCount: 5, newStartLine: 11, newLineCount: 6, - heading: 'second hunk', - // 11 12 13 14 15 16 - lines: [' 0003', '+0004', '+0005', '-0006', ' 0007', '+0008', '-0009', ' 0010'], - }, - ], - }]); + const {multiFilePatch} = multiFilePatchBuilder().addFilePatch(fp => { + fp.setOldFile(f => f.path('path.txt')); + fp.addHunk(h => { + h.oldRow(1); + h.unchanged('0000').added('0001').unchanged('0002'); + }); + fp.addHunk(h => { + h.oldRow(10); + h.unchanged('0003').added('0004', '0005').deleted('0006').unchanged('0007').added('0008').deleted('0009').unchanged('0010'); + }); + }).build(); + + mfp = multiFilePatch; }); it('opens the file at the current unchanged row', function() { const openFile = sinon.spy(); - const wrapper = mount(buildApp({filePatch: fp, openFile})); + const wrapper = mount(buildApp({multiFilePatch: mfp, openFile})); const editor = wrapper.find('AtomTextEditor').instance().getModel(); editor.setCursorBufferPosition([7, 2]); atomEnv.commands.dispatch(wrapper.getDOMNode(), 'github:open-file'); - assert.isTrue(openFile.calledWith([[14, 2]])); }); it('opens the file at a current added row', function() { const openFile = sinon.spy(); - const wrapper = mount(buildApp({filePatch: fp, openFile})); + const wrapper = mount(buildApp({multiFilePatch: mfp, openFile})); const editor = wrapper.find('AtomTextEditor').instance().getModel(); editor.setCursorBufferPosition([8, 3]); @@ -1138,7 +1129,7 @@ describe.only('MultiFilePatchView', function() { it('opens the file at the beginning of the previous added or unchanged row', function() { const openFile = sinon.spy(); - const wrapper = mount(buildApp({filePatch: fp, openFile})); + const wrapper = mount(buildApp({multiFilePatch: mfp, openFile})); const editor = wrapper.find('AtomTextEditor').instance().getModel(); editor.setCursorBufferPosition([9, 2]); @@ -1150,7 +1141,7 @@ describe.only('MultiFilePatchView', function() { it('preserves multiple cursors', function() { const openFile = sinon.spy(); - const wrapper = mount(buildApp({filePatch: fp, openFile})); + const wrapper = mount(buildApp({multiFilePatch: mfp, openFile})); const editor = wrapper.find('AtomTextEditor').instance().getModel(); editor.setCursorBufferPosition([3, 2]); From a142e524172cff27b021fbf91e2175bbe511898a Mon Sep 17 00:00:00 2001 From: Vanessa Yuen Date: Fri, 9 Nov 2018 18:53:41 +0100 Subject: [PATCH 252/284] remove old builder methods from mfp view test suite --- test/views/multi-file-patch-view.test.js | 1 - 1 file changed, 1 deletion(-) diff --git a/test/views/multi-file-patch-view.test.js b/test/views/multi-file-patch-view.test.js index ba6a328e716..c5facd9bf91 100644 --- a/test/views/multi-file-patch-view.test.js +++ b/test/views/multi-file-patch-view.test.js @@ -3,7 +3,6 @@ import {shallow, mount} from 'enzyme'; import {cloneRepository, buildRepository} from '../helpers'; import MultiFilePatchView from '../../lib/views/multi-file-patch-view'; -import {buildFilePatch, buildMultiFilePatch} from '../../lib/models/patch'; import {multiFilePatchBuilder} from '../builder/patch'; import {nullFile} from '../../lib/models/patch/file'; import FilePatch from '../../lib/models/patch/file-patch'; From c0a296e3af9378ddadb67ddb4e17f95e271444f7 Mon Sep 17 00:00:00 2001 From: Vanessa Yuen Date: Fri, 9 Nov 2018 19:19:01 +0100 Subject: [PATCH 253/284] fix hunk navigation tests Co-Authored-By: Tilde Ann Thurium --- test/views/multi-file-patch-view.test.js | 68 +++++++++++------------- 1 file changed, 30 insertions(+), 38 deletions(-) diff --git a/test/views/multi-file-patch-view.test.js b/test/views/multi-file-patch-view.test.js index c5facd9bf91..f14dd56cec9 100644 --- a/test/views/multi-file-patch-view.test.js +++ b/test/views/multi-file-patch-view.test.js @@ -954,47 +954,39 @@ describe.only('MultiFilePatchView', function() { }); describe('hunk mode navigation', function() { + let mfp; + beforeEach(function() { - filePatches = buildFilePatch([{ - oldPath: 'path.txt', - oldMode: '100644', - newPath: 'path.txt', - newMode: '100644', - status: 'modified', - hunks: [ - { - oldStartLine: 4, oldLineCount: 2, newStartLine: 4, newLineCount: 3, - heading: 'zero', - lines: [' 0000', '+0001', ' 0002'], - }, - { - oldStartLine: 10, oldLineCount: 3, newStartLine: 11, newLineCount: 2, - heading: 'one', - lines: [' 0003', '-0004', ' 0005'], - }, - { - oldStartLine: 20, oldLineCount: 2, newStartLine: 20, newLineCount: 3, - heading: 'two', - lines: [' 0006', '+0007', ' 0008'], - }, - { - oldStartLine: 30, oldLineCount: 2, newStartLine: 31, newLineCount: 3, - heading: 'three', - lines: [' 0009', '+0010', ' 0011'], - }, - { - oldStartLine: 40, oldLineCount: 4, newStartLine: 42, newLineCount: 2, - heading: 'four', - lines: [' 0012', '-0013', '-0014', ' 0015'], - }, - ], - }]); + const {multiFilePatch} = multiFilePatchBuilder().addFilePatch(fp => { + fp.setOldFile(f => f.path('path.txt')); + fp.addHunk(h => { + h.oldRow(4); + h.unchanged('0000').added('0001').unchanged('0002'); + }); + fp.addHunk(h => { + h.oldRow(10); + h.unchanged('0003').deleted('0004').unchanged('0005'); + }); + fp.addHunk(h => { + h.oldRow(20); + h.unchanged('0006').added('0007').unchanged('0008'); + }); + fp.addHunk(h => { + h.oldRow(30); + h.unchanged('0009').added('0010').unchanged('0011'); + }); + fp.addHunk(h => { + h.oldRow(40); + h.unchanged('0012').deleted('0013', '0014').unchanged('0015'); + }); + }).build(); + mfp = multiFilePatch; }); it('advances the selection to the next hunks', function() { const selectedRowsChanged = sinon.spy(); const selectedRows = new Set([1, 7, 10]); - const wrapper = mount(buildApp({multiFilePatch: filePatches, selectedRowsChanged, selectedRows, selectionMode: 'hunk'})); + const wrapper = mount(buildApp({multiFilePatch: mfp, selectedRowsChanged, selectedRows, selectionMode: 'hunk'})); const editor = wrapper.find('AtomTextEditor').instance().getModel(); editor.setSelectedBufferRanges([ [[0, 0], [2, 4]], // hunk 0 @@ -1018,7 +1010,7 @@ describe.only('MultiFilePatchView', function() { it('does not advance a selected hunk at the end of the patch', function() { const selectedRowsChanged = sinon.spy(); const selectedRows = new Set([4, 13, 14]); - const wrapper = mount(buildApp({multiFilePatch: filePatches, selectedRowsChanged, selectedRows, selectionMode: 'hunk'})); + const wrapper = mount(buildApp({multiFilePatch: mfp, selectedRowsChanged, selectedRows, selectionMode: 'hunk'})); const editor = wrapper.find('AtomTextEditor').instance().getModel(); editor.setSelectedBufferRanges([ [[3, 0], [5, 4]], // hunk 1 @@ -1040,7 +1032,7 @@ describe.only('MultiFilePatchView', function() { it('retreats the selection to the previous hunks', function() { const selectedRowsChanged = sinon.spy(); const selectedRows = new Set([4, 10, 13, 14]); - const wrapper = mount(buildApp({multiFilePatch: filePatches, selectedRowsChanged, selectedRows, selectionMode: 'hunk'})); + const wrapper = mount(buildApp({multiFilePatch: mfp, selectedRowsChanged, selectedRows, selectionMode: 'hunk'})); const editor = wrapper.find('AtomTextEditor').instance().getModel(); editor.setSelectedBufferRanges([ [[3, 0], [5, 4]], // hunk 1 @@ -1064,7 +1056,7 @@ describe.only('MultiFilePatchView', function() { it('does not retreat a selected hunk at the beginning of the patch', function() { const selectedRowsChanged = sinon.spy(); const selectedRows = new Set([4, 10, 13, 14]); - const wrapper = mount(buildApp({multiFilePatch: filePatches, selectedRowsChanged, selectedRows, selectionMode: 'hunk'})); + const wrapper = mount(buildApp({multiFilePatch: mfp, selectedRowsChanged, selectedRows, selectionMode: 'hunk'})); const editor = wrapper.find('AtomTextEditor').instance().getModel(); editor.setSelectedBufferRanges([ [[0, 0], [2, 4]], // hunk 0 From 2bb78b0fe654bdc0244fbe1abe49ad39aa51b231 Mon Sep 17 00:00:00 2001 From: Tilde Ann Thurium Date: Fri, 9 Nov 2018 11:06:15 -0800 Subject: [PATCH 254/284] fix typo in patch building for mfp file opening tests --- test/views/multi-file-patch-view.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/views/multi-file-patch-view.test.js b/test/views/multi-file-patch-view.test.js index f14dd56cec9..776f3ebd78a 100644 --- a/test/views/multi-file-patch-view.test.js +++ b/test/views/multi-file-patch-view.test.js @@ -8,7 +8,7 @@ import {nullFile} from '../../lib/models/patch/file'; import FilePatch from '../../lib/models/patch/file-patch'; import RefHolder from '../../lib/models/ref-holder'; -describe.only('MultiFilePatchView', function() { +describe('MultiFilePatchView', function() { let atomEnv, workspace, repository, filePatches; beforeEach(async function() { @@ -1083,7 +1083,7 @@ describe.only('MultiFilePatchView', function() { const {multiFilePatch} = multiFilePatchBuilder().addFilePatch(fp => { fp.setOldFile(f => f.path('path.txt')); fp.addHunk(h => { - h.oldRow(1); + h.oldRow(2); h.unchanged('0000').added('0001').unchanged('0002'); }); fp.addHunk(h => { From 1fc0d85762bf5e979d8a4b6734f7fdc0bd51a5df Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 9 Nov 2018 14:06:09 -0500 Subject: [PATCH 255/284] Break out of the correct loop --- lib/models/patch/multi-file-patch.js | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/lib/models/patch/multi-file-patch.js b/lib/models/patch/multi-file-patch.js index d44d44c33cf..a44860af837 100644 --- a/lib/models/patch/multi-file-patch.js +++ b/lib/models/patch/multi-file-patch.js @@ -129,12 +129,11 @@ export default class MultiFilePatch { const lastMax = Math.max(...lastSelectedRows); let lastSelectionIndex = 0; - for (const lastFilePatch of lastMultiFilePatch.getFilePatches()) { + patchLoop: for (const lastFilePatch of lastMultiFilePatch.getFilePatches()) { for (const hunk of lastFilePatch.getHunks()) { let includesMax = false; - let hunkSelectionOffset = 0; - changeLoop: for (const change of hunk.getChanges()) { + for (const change of hunk.getChanges()) { for (const {intersection, gap} of change.intersectRows(lastSelectedRows, true)) { // Only include a partial range if this intersection includes the last selected buffer row. includesMax = intersection.intersectsRow(lastMax); @@ -142,20 +141,14 @@ export default class MultiFilePatch { if (gap) { // Range of unselected changes. - hunkSelectionOffset += delta; + lastSelectionIndex += delta; } if (includesMax) { - break changeLoop; + break patchLoop; } } } - - lastSelectionIndex += hunkSelectionOffset; - - if (includesMax) { - break; - } } } From 3603a7650116e8f90c5597cffe1d266098992937 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 9 Nov 2018 14:06:42 -0500 Subject: [PATCH 256/284] getNextSelectionRange() tests :tada: --- test/models/patch/multi-file-patch.test.js | 62 ++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/test/models/patch/multi-file-patch.test.js b/test/models/patch/multi-file-patch.test.js index e3aa99513cb..31a0a6b72fe 100644 --- a/test/models/patch/multi-file-patch.test.js +++ b/test/models/patch/multi-file-patch.test.js @@ -535,5 +535,67 @@ describe('MultiFilePatch', function() { const nextSelectionRange = nextMultiPatch.getNextSelectionRange(lastMultiPatch, new Set()); assert.deepEqual(nextSelectionRange.serialize(), [[1, 0], [1, Infinity]]); }); + + it('preserves the numeric index of the highest selected change row', function() { + const {multiFilePatch: lastMultiPatch} = multiFilePatchBuilder() + .addFilePatch(fp => { + fp.addHunk(h => h.unchanged('.').added('0', '1', 'x *').unchanged('.')); + fp.addHunk(h => h.unchanged('.').deleted('2').added('3').unchanged('.')); + }) + .addFilePatch(fp => { + fp.addHunk(h => h.unchanged('.').deleted('4', '5 *', '6').unchanged('.')); + fp.addHunk(h => h.unchanged('.').added('7').unchanged('.')); + }) + .build(); + + const {multiFilePatch: nextMultiPatch} = multiFilePatchBuilder() + .addFilePatch(fp => { + fp.addHunk(h => h.unchanged('.').added('0', '1').unchanged('x', '.')); + fp.addHunk(h => h.unchanged('.').deleted('2').added('3').unchanged('.')); + }) + .addFilePatch(fp => { + fp.addHunk(h => h.unchanged('.').deleted('4', '6 *').unchanged('.')); + fp.addHunk(h => h.unchanged('.').added('7').unchanged('.')); + }) + .build(); + + const nextSelectionRange = nextMultiPatch.getNextSelectionRange(lastMultiPatch, new Set([3, 11])); + assert.deepEqual(nextSelectionRange.serialize(), [[11, 0], [11, Infinity]]); + }); + + it('skips hunks that were completely selected', function() { + const {multiFilePatch: lastMultiPatch} = multiFilePatchBuilder() + .addFilePatch(fp => { + fp.addHunk(h => h.unchanged('.').added('0').unchanged('.')); + fp.addHunk(h => h.unchanged('.').added('x *', 'x *').unchanged('.')); + }) + .addFilePatch(fp => { + fp.addHunk(h => h.unchanged('.').deleted('x *').unchanged('.')); + }) + .addFilePatch(fp => { + fp.addHunk(h => h.unchanged('.').added('x *', '1').deleted('2').unchanged('.')); + fp.addHunk(h => h.unchanged('.').deleted('x *').unchanged('.')); + fp.addHunk(h => h.unchanged('.', '.').deleted('4', '5 *', '6').unchanged('.')); + fp.addHunk(h => h.unchanged('.').deleted('7', '8').unchanged('.', '.')); + }) + .build(); + + const {multiFilePatch: nextMultiPatch} = multiFilePatchBuilder() + .addFilePatch(fp => { + fp.addHunk(h => h.unchanged('.').added('0').unchanged('.')); + }) + .addFilePatch(fp => { + fp.addHunk(h => h.unchanged('.', 'x').added('1').deleted('2').unchanged('.')); + fp.addHunk(h => h.unchanged('.', '.').deleted('4', '6 +').unchanged('.')); + fp.addHunk(h => h.unchanged('.').deleted('7', '8').unchanged('.', '.')); + }) + .build(); + + const nextSelectionRange = nextMultiPatch.getNextSelectionRange( + lastMultiPatch, + new Set([4, 5, 8, 11, 16, 21]), + ); + assert.deepEqual(nextSelectionRange.serialize(), [[11, 0], [11, Infinity]]); + }); }); }); From 201033e506ba0fc60f10fd69fc4ee439877efd7b Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 9 Nov 2018 14:08:26 -0500 Subject: [PATCH 257/284] :fire: unused imports, beforeEach, and lets --- test/models/patch/multi-file-patch.test.js | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/test/models/patch/multi-file-patch.test.js b/test/models/patch/multi-file-patch.test.js index 31a0a6b72fe..110decc1c7e 100644 --- a/test/models/patch/multi-file-patch.test.js +++ b/test/models/patch/multi-file-patch.test.js @@ -1,31 +1,11 @@ -import {TextBuffer} from 'atom'; import dedent from 'dedent-js'; import {multiFilePatchBuilder} from '../../builder/patch'; import MultiFilePatch from '../../../lib/models/patch/multi-file-patch'; -import FilePatch 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'; -import {Unchanged, Addition, Deletion, NoNewline} from '../../../lib/models/patch/region'; import {assertInFilePatch} from '../../helpers'; describe('MultiFilePatch', function() { - let buffer, layers; - - beforeEach(function() { - buffer = new TextBuffer(); - layers = { - patch: buffer.addMarkerLayer(), - hunk: buffer.addMarkerLayer(), - unchanged: buffer.addMarkerLayer(), - addition: buffer.addMarkerLayer(), - deletion: buffer.addMarkerLayer(), - noNewline: buffer.addMarkerLayer(), - }; - }); - it('creates an empty patch when constructed with no arguments', function() { const empty = new MultiFilePatch({}); assert.isFalse(empty.anyPresent()); From 02407d38c4c4d941d184f5060dac39c9c7cb975a Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 9 Nov 2018 11:13:11 -0800 Subject: [PATCH 258/284] Fix MultiFilePatchController tests --- test/controllers/multi-file-patch-controller.test.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/controllers/multi-file-patch-controller.test.js b/test/controllers/multi-file-patch-controller.test.js index 88086e3ccbe..8d6ab9ad956 100644 --- a/test/controllers/multi-file-patch-controller.test.js +++ b/test/controllers/multi-file-patch-controller.test.js @@ -273,7 +273,7 @@ describe('MultiFilePatchController', function() { 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'})); + const wrapper = shallow(buildApp({multiFilePatch: otherPatch, stagingStatus: 'staged'})); wrapper.find('MultiFilePatchView').prop('selectedRowsChanged')(new Set([2])); sinon.spy(otherPatch, 'getUnstagePatchForLines'); @@ -404,9 +404,11 @@ describe('MultiFilePatchController', function() { await fs.unlink(p); + await repository.stageFiles(['waslink.txt']); + repository.refresh(); const symlinkMultiPatch = await repository.getFilePatchForPath('waslink.txt', {staged: true}); - const wrapper = shallow(buildApp({filePatch: symlinkMultiPatch, relPath: 'waslink.txt', stagingStatus: 'staged'})); + const wrapper = shallow(buildApp({multiFilePatch: symlinkMultiPatch, relPath: 'waslink.txt', stagingStatus: 'staged'})); sinon.spy(repository, 'unstageFiles'); From 11e78f464cd746a4b55c90a51ed52d04c24c22bb Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 9 Nov 2018 14:21:00 -0500 Subject: [PATCH 259/284] Test coverage for .clone() --- test/models/patch/multi-file-patch.test.js | 58 +++++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/test/models/patch/multi-file-patch.test.js b/test/models/patch/multi-file-patch.test.js index 110decc1c7e..e23bc01c983 100644 --- a/test/models/patch/multi-file-patch.test.js +++ b/test/models/patch/multi-file-patch.test.js @@ -1,6 +1,6 @@ import dedent from 'dedent-js'; -import {multiFilePatchBuilder} from '../../builder/patch'; +import {multiFilePatchBuilder, filePatchBuilder} from '../../builder/patch'; import MultiFilePatch from '../../../lib/models/patch/multi-file-patch'; import {assertInFilePatch} from '../../helpers'; @@ -24,6 +24,62 @@ describe('MultiFilePatch', function() { assert.isTrue(multiFilePatch.anyPresent()); }); + describe('clone', function() { + let original; + + beforeEach(function() { + original = multiFilePatchBuilder() + .addFilePatch() + .addFilePatch() + .build() + .multiFilePatch; + }); + + it('defaults to creating an exact copy', function() { + const dup = original.clone(); + + assert.strictEqual(dup.getBuffer(), original.getBuffer()); + assert.strictEqual(dup.getPatchLayer(), original.getPatchLayer()); + assert.strictEqual(dup.getHunkLayer(), original.getHunkLayer()); + assert.strictEqual(dup.getUnchangedLayer(), original.getUnchangedLayer()); + assert.strictEqual(dup.getAdditionLayer(), original.getAdditionLayer()); + assert.strictEqual(dup.getDeletionLayer(), original.getDeletionLayer()); + assert.strictEqual(dup.getNoNewlineLayer(), original.getNoNewlineLayer()); + assert.strictEqual(dup.getFilePatches(), original.getFilePatches()); + }); + + it('creates a copy with a new buffer and layer set', function() { + const {buffer, layers} = multiFilePatchBuilder().build(); + const dup = original.clone({buffer, layers}); + + assert.strictEqual(dup.getBuffer(), buffer); + assert.strictEqual(dup.getPatchLayer(), layers.patch); + assert.strictEqual(dup.getHunkLayer(), layers.hunk); + assert.strictEqual(dup.getUnchangedLayer(), layers.unchanged); + assert.strictEqual(dup.getAdditionLayer(), layers.addition); + assert.strictEqual(dup.getDeletionLayer(), layers.deletion); + assert.strictEqual(dup.getNoNewlineLayer(), layers.noNewline); + assert.strictEqual(dup.getFilePatches(), original.getFilePatches()); + }); + + it('creates a copy with a new set of file patches', function() { + const nfp = [ + filePatchBuilder().build().filePatch, + filePatchBuilder().build().filePatch, + ]; + + const dup = original.clone({filePatches: nfp}); + assert.strictEqual(dup.getBuffer(), original.getBuffer()); + assert.strictEqual(dup.getPatchLayer(), original.getPatchLayer()); + assert.strictEqual(dup.getHunkLayer(), original.getHunkLayer()); + assert.strictEqual(dup.getUnchangedLayer(), original.getUnchangedLayer()); + assert.strictEqual(dup.getAdditionLayer(), original.getAdditionLayer()); + assert.strictEqual(dup.getDeletionLayer(), original.getDeletionLayer()); + assert.strictEqual(dup.getNoNewlineLayer(), original.getNoNewlineLayer()); + assert.strictEqual(dup.getFilePatches(), nfp); + }); + }); + it('has an accessor for its file patches', function() { const {multiFilePatch} = multiFilePatchBuilder() .addFilePatch(filePatch => filePatch.setOldFile(file => file.path('file-0.txt'))) From c667d3ede4e87d87cd5178568549a25345597c5d Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 9 Nov 2018 14:24:15 -0500 Subject: [PATCH 260/284] getFirstChangeRange() returns a real Range object --- test/models/patch/patch.test.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/models/patch/patch.test.js b/test/models/patch/patch.test.js index 2f7738eab78..b5989ab3fe9 100644 --- a/test/models/patch/patch.test.js +++ b/test/models/patch/patch.test.js @@ -592,7 +592,7 @@ 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]]); + assert.deepEqual(patch.getFirstChangeRange().serialize(), [[1, 0], [1, Infinity]]); }); it('returns the origin if the first hunk is empty', function() { @@ -607,7 +607,7 @@ describe('Patch', function() { ]; const marker = markRange(layers.patch, 0); const patch = new Patch({status: 'modified', hunks, marker}); - assert.deepEqual(patch.getFirstChangeRange(), [[0, 0], [0, 0]]); + assert.deepEqual(patch.getFirstChangeRange().serialize(), [[0, 0], [0, 0]]); }); it('returns the origin if the patch is empty', function() { @@ -615,7 +615,7 @@ describe('Patch', function() { const layers = buildLayers(buffer); const marker = markRange(layers.patch, 0); const patch = new Patch({status: 'modified', hunks: [], marker}); - assert.deepEqual(patch.getFirstChangeRange(), [[0, 0], [0, 0]]); + assert.deepEqual(patch.getFirstChangeRange().serialize(), [[0, 0], [0, 0]]); }); }); From aa1e8bff933e59aa76e459694e94beb9ec2238fe Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 9 Nov 2018 14:30:45 -0500 Subject: [PATCH 261/284] Implement .isEqual() on MultiFilePatch the dumbest possible way --- lib/models/patch/multi-file-patch.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/models/patch/multi-file-patch.js b/lib/models/patch/multi-file-patch.js index a44860af837..6d731db651d 100644 --- a/lib/models/patch/multi-file-patch.js +++ b/lib/models/patch/multi-file-patch.js @@ -283,4 +283,8 @@ export default class MultiFilePatch { toString() { return this.filePatches.map(fp => fp.toStringIn(this.buffer)).join(''); } + + isEqual(other) { + return this.toString() === other.toString(); + } } From e686a1f2fc87b37e3f8cbe872bf9bfd6e9bd1312 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 9 Nov 2018 14:34:27 -0500 Subject: [PATCH 262/284] The method is called .anyPresent() on a MultiFilePatch --- test/models/repository.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/models/repository.test.js b/test/models/repository.test.js index 993c5aa02bf..90a9e9c58c3 100644 --- a/test/models/repository.test.js +++ b/test/models/repository.test.js @@ -434,7 +434,7 @@ describe('Repository', function() { await repo.getLoadPromise(); const patch = await repo.getFilePatchForPath('no.txt'); - assert.isFalse(patch.isPresent()); + assert.isFalse(patch.anyPresent()); }); }); From ce8363b1bc5055bc7faa4bc397632a65b924d923 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 9 Nov 2018 14:34:54 -0500 Subject: [PATCH 263/284] Reword spec names to reflect MultiFilePatches being returned --- test/models/repository.test.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/models/repository.test.js b/test/models/repository.test.js index 90a9e9c58c3..d6cd353dcb2 100644 --- a/test/models/repository.test.js +++ b/test/models/repository.test.js @@ -398,7 +398,7 @@ describe('Repository', function() { }); describe('getFilePatchForPath', function() { - it('returns cached FilePatch objects if they exist', async function() { + it('returns cached MultiFilePatch objects if they exist', async function() { const workingDirPath = await cloneRepository('multiple-commits'); const repo = new Repository(workingDirPath); await repo.getLoadPromise(); @@ -413,7 +413,7 @@ describe('Repository', function() { assert.equal(await repo.getFilePatchForPath('file.txt', {staged: true}), stagedFilePatch); }); - it('returns new FilePatch object after repository refresh', async function() { + it('returns new MultiFilePatch object after repository refresh', async function() { const workingDirPath = await cloneRepository('three-files'); const repo = new Repository(workingDirPath); await repo.getLoadPromise(); @@ -428,7 +428,7 @@ describe('Repository', function() { assert.isTrue((await repo.getFilePatchForPath('a.txt')).isEqual(filePatchA)); }); - it('returns a nullFilePatch for unknown paths', async function() { + it('returns an empty MultiFilePatch for unknown paths', async function() { const workingDirPath = await cloneRepository('multiple-commits'); const repo = new Repository(workingDirPath); await repo.getLoadPromise(); From d12e3fdf71e466e6c5639d104c518ea818c9f026 Mon Sep 17 00:00:00 2001 From: Tilde Ann Thurium Date: Fri, 9 Nov 2018 11:37:49 -0800 Subject: [PATCH 264/284] fix buildFilePatch null patch test --- test/models/patch/builder.test.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/test/models/patch/builder.test.js b/test/models/patch/builder.test.js index c4e2385e723..ae11008366e 100644 --- a/test/models/patch/builder.test.js +++ b/test/models/patch/builder.test.js @@ -3,10 +3,12 @@ import {assertInPatch, assertInFilePatch} from '../../helpers'; describe('buildFilePatch', function() { 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()); + const multiFilePatch = buildFilePatch([]); + const [filePatch] = multiFilePatch.getFilePatches(); + + assert.isFalse(filePatch.getOldFile().isPresent()); + assert.isFalse(filePatch.getNewFile().isPresent()); + assert.isFalse(filePatch.getPatch().isPresent()); }); describe('with a single diff', function() { From 47aeebee685574cf93b506ce2d396a522d659b3a Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 9 Nov 2018 14:59:00 -0500 Subject: [PATCH 265/284] watchWorkspaceItem tests :white_check_mark: + :100: --- lib/watch-workspace-item.js | 97 ++---------------------------- test/watch-workspace-item.test.js | 98 ++++++++++++------------------- 2 files changed, 41 insertions(+), 154 deletions(-) diff --git a/lib/watch-workspace-item.js b/lib/watch-workspace-item.js index dc2fcd1077a..fafe8028a86 100644 --- a/lib/watch-workspace-item.js +++ b/lib/watch-workspace-item.js @@ -2,85 +2,6 @@ import {CompositeDisposable} from 'atom'; import URIPattern from './atom/uri-pattern'; -class ItemWatcher { - constructor(workspace, pattern, component, stateKey) { - this.workspace = workspace; - this.pattern = pattern instanceof URIPattern ? pattern : new URIPattern(pattern); - this.component = component; - this.stateKey = stateKey; - - this.itemCount = this.getItemCount(); - this.subs = new CompositeDisposable(); - } - - setInitialState() { - if (!this.component.state) { - this.component.state = {}; - } - this.component.state[this.stateKey] = this.itemCount > 0; - return this; - } - - subscribeToWorkspace() { - this.subs.dispose(); - this.subs = new CompositeDisposable( - this.workspace.onDidAddPaneItem(this.itemAdded), - this.workspace.onDidDestroyPaneItem(this.itemDestroyed), - ); - return this; - } - - setPattern(pattern) { - const wasTrue = this.itemCount > 0; - - this.pattern = pattern instanceof URIPattern ? pattern : new URIPattern(pattern); - - // Update the item count to match the new pattern - this.itemCount = this.getItemCount(); - - // Update the component's state if it's changed as a result - if (wasTrue && this.itemCount <= 0) { - return new Promise(resolve => this.component.setState({[this.stateKey]: false}, resolve)); - } else if (!wasTrue && this.itemCount > 0) { - return new Promise(resolve => this.component.setState({[this.stateKey]: true}, resolve)); - } else { - return Promise.resolve(); - } - } - - itemMatches = item => item && item.getURI && this.pattern.matches(item.getURI()).ok() - - getItemCount() { - return this.workspace.getPaneItems().filter(this.itemMatches).length; - } - - itemAdded = ({item}) => { - const hadOpen = this.itemCount > 0; - if (this.itemMatches(item)) { - this.itemCount++; - - if (this.itemCount > 0 && !hadOpen) { - this.component.setState({[this.stateKey]: true}); - } - } - } - - itemDestroyed = ({item}) => { - const hadOpen = this.itemCount > 0; - if (this.itemMatches(item)) { - this.itemCount--; - - if (this.itemCount <= 0 && hadOpen) { - this.component.setState({[this.stateKey]: false}); - } - } - } - - dispose() { - this.subs.dispose(); - } -} - class ActiveItemWatcher { constructor(workspace, pattern, component, stateKey, opts) { this.workspace = workspace; @@ -145,18 +66,8 @@ class ActiveItemWatcher { } } -export function watchWorkspaceItem(workspace, pattern, component, stateKey, options = {}) { - if (options.active) { - // I implemented this as a separate class because the logic differs enough - // and I suspect we can replace `ItemWatcher` with this. I don't see a clear use case for the `ItemWatcher` class - return new ActiveItemWatcher(workspace, pattern, component, stateKey, options) - .setInitialState() - .subscribeToWorkspace(); - } else { - // TODO: would we ever actually use this? If not, clean it up, along with tests - return new ItemWatcher(workspace, pattern, component, stateKey, options) - .setInitialState() - .subscribeToWorkspace(); - } - +export function watchWorkspaceItem(workspace, pattern, component, stateKey) { + return new ActiveItemWatcher(workspace, pattern, component, stateKey) + .setInitialState() + .subscribeToWorkspace(); } diff --git a/test/watch-workspace-item.test.js b/test/watch-workspace-item.test.js index 672ae15dee5..f233f0ca405 100644 --- a/test/watch-workspace-item.test.js +++ b/test/watch-workspace-item.test.js @@ -17,6 +17,13 @@ describe('watchWorkspaceItem', function() { if (uri.startsWith('atom-github://')) { return { getURI() { return uri; }, + + getElement() { + if (!this.element) { + this.element = document.createElement('div'); + } + return this.element; + }, }; } else { return undefined; @@ -44,23 +51,31 @@ describe('watchWorkspaceItem', function() { assert.isFalse(component.state.someKey); }); - it('is true when the pane is already open', async function() { + it('is false when the pane is open but not active', async function() { await workspace.open('atom-github://item/one'); await workspace.open('atom-github://item/two'); sub = watchWorkspaceItem(workspace, 'atom-github://item/one', component, 'theKey'); + assert.isFalse(component.state.theKey); + }); + + it('is true when the pane is already open and active', async function() { + await workspace.open('atom-github://item/two'); + await workspace.open('atom-github://item/one'); + sub = watchWorkspaceItem(workspace, 'atom-github://item/one', component, 'theKey'); assert.isTrue(component.state.theKey); }); - it('is true when multiple panes matching the URI pattern are open', async function() { - await workspace.open('atom-github://item/one'); - await workspace.open('atom-github://item/two'); - await workspace.open('atom-github://nonmatch'); + it('is true when the pane is open and active in any pane', async function() { + await workspace.open('atom-github://some-item', {location: 'right'}); + await workspace.open('atom-github://nonmatching'); - sub = watchWorkspaceItem(workspace, 'atom-github://item/{pattern}', component, 'theKey'); + assert.strictEqual(workspace.getRightDock().getActivePaneItem().getURI(), 'atom-github://some-item'); + assert.strictEqual(workspace.getActivePaneItem().getURI(), 'atom-github://nonmatching'); - assert.isTrue(component.state.theKey); + sub = watchWorkspaceItem(workspace, 'atom-github://some-item', component, 'someKey', {active: true}); + assert.isTrue(component.state.someKey); }); it('accepts a preconstructed URIPattern', async function() { @@ -70,49 +85,11 @@ describe('watchWorkspaceItem', function() { sub = watchWorkspaceItem(workspace, u, component, 'theKey'); assert.isTrue(component.state.theKey); }); - - describe('{active: true}', function() { - it('is false when the pane is not open', async function() { - await workspace.open('atom-github://nonmatching'); - - sub = watchWorkspaceItem(workspace, 'atom-github://item', component, 'someKey', {active: true}); - assert.isFalse(component.state.someKey); - }); - - it('is false when the pane is open, but not active', async function() { - // TODO: fix this test suite so that 'atom-github://item' works - await workspace.open('atom-github://item'); - await workspace.open('atom-github://nonmatching'); - - sub = watchWorkspaceItem(workspace, 'atom-github://item', component, 'someKey', {active: true}); - assert.isFalse(component.state.someKey); - }); - - it('is true when the pane is open and active in the workspace', async function() { - await workspace.open('atom-github://nonmatching'); - await workspace.open('atom-github://item'); - - sub = watchWorkspaceItem(workspace, 'atom-github://item', component, 'someKey', {active: true}); - assert.isTrue(component.state.someKey); - }); - - it('is true when the pane is open and active in any pane', async function() { - await workspace.open('atom-github://some-item', {location: 'right'}); - await workspace.open('atom-github://nonmatching'); - - assert.strictEqual(workspace.getRightDock().getActivePaneItem().getURI(), 'atom-github://some-item'); - assert.strictEqual(workspace.getActivePaneItem().getURI(), 'atom-github://nonmatching'); - - sub = watchWorkspaceItem(workspace, 'atom-github://some-item', component, 'someKey', {active: true}); - assert.isTrue(component.state.someKey); - }); - }); }); describe('workspace events', function() { it('becomes true when the pane is opened', async function() { sub = watchWorkspaceItem(workspace, 'atom-github://item/{pattern}', component, 'theKey'); - assert.isFalse(component.state.theKey); await workspace.open('atom-github://item/match'); @@ -123,24 +100,19 @@ describe('watchWorkspaceItem', function() { it('remains true if another matching pane is opened', async function() { await workspace.open('atom-github://item/match0'); sub = watchWorkspaceItem(workspace, 'atom-github://item/{pattern}', component, 'theKey'); - assert.isTrue(component.state.theKey); await workspace.open('atom-github://item/match1'); - assert.isFalse(component.setState.called); }); - it('remains true if a matching pane is closed but another remains open', async function() { + it('becomes false if a nonmatching pane is opened', async function() { await workspace.open('atom-github://item/match0'); - await workspace.open('atom-github://item/match1'); - sub = watchWorkspaceItem(workspace, 'atom-github://item/{pattern}', component, 'theKey'); assert.isTrue(component.state.theKey); - assert.isTrue(workspace.hide('atom-github://item/match1')); - - assert.isFalse(component.setState.called); + await workspace.open('atom-github://other-item/match1'); + assert.isTrue(component.setState.calledWith({theKey: false})); }); it('becomes false if the last matching pane is closed', async function() { @@ -151,18 +123,14 @@ describe('watchWorkspaceItem', function() { assert.isTrue(component.state.theKey); assert.isTrue(workspace.hide('atom-github://item/match1')); - assert.isTrue(workspace.hide('atom-github://item/match0')); + assert.isFalse(component.setState.called); + assert.isTrue(workspace.hide('atom-github://item/match0')); assert.isTrue(component.setState.calledWith({theKey: false})); }); - - describe('{active: true}', function() { - // - }); }); it('stops updating when disposed', async function() { - // TODO: fix this test suite so that 'atom-github://item' works sub = watchWorkspaceItem(workspace, 'atom-github://item', component, 'theKey'); assert.isFalse(component.state.theKey); @@ -198,8 +166,16 @@ describe('watchWorkspaceItem', function() { assert.isTrue(component.setState.calledWith({theKey: true})); }); - describe('{active: true}', function() { - // + it('accepts a preconstructed URIPattern', async function() { + sub = watchWorkspaceItem(workspace, 'atom-github://item0/{pattern}', component, 'theKey'); + assert.isFalse(component.state.theKey); + + await workspace.open('atom-github://item1/match'); + assert.isFalse(component.setState.called); + + await sub.setPattern(new URIPattern('atom-github://item1/{pattern}')); + assert.isFalse(component.state.theKey); + assert.isTrue(component.setState.calledWith({theKey: true})); }); }); }); From a7a345b29db5e6c42eaff9134519c9fc21b2b736 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 9 Nov 2018 11:48:25 -0800 Subject: [PATCH 266/284] Use `anyPresent` instead of `isPresent` --- test/models/repository.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/models/repository.test.js b/test/models/repository.test.js index d6cd353dcb2..f784d3da287 100644 --- a/test/models/repository.test.js +++ b/test/models/repository.test.js @@ -110,7 +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()); + assert.isFalse((await repository.getFilePatchForPath('anything.txt')).anyPresent()); }); it('returns a rejecting promise', async function() { From 0c8f000eec6cf330765f54b15d0ee917e075f996 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 9 Nov 2018 11:48:48 -0800 Subject: [PATCH 267/284] Check status on individual filePatch instead of MFP --- test/models/repository.test.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/test/models/repository.test.js b/test/models/repository.test.js index f784d3da287..c713ad845b8 100644 --- a/test/models/repository.test.js +++ b/test/models/repository.test.js @@ -338,7 +338,9 @@ describe('Repository', function() { await repo.stageFileSymlinkChange(deletedSymlinkAddedFilePath); assert.isNull(await indexModeAndOid(deletedSymlinkAddedFilePath)); const unstagedFilePatch = await repo.getFilePatchForPath(deletedSymlinkAddedFilePath, {staged: false}); - assert.equal(unstagedFilePatch.getStatus(), 'added'); + assert.lengthOf(unstagedFilePatch.getFilePatches(), 1); + const [uFilePatch] = unstagedFilePatch.getFilePatches(); + assert.equal(uFilePatch.getStatus(), 'added'); assert.equal(unstagedFilePatch.toString(), dedent` diff --git a/symlink.txt b/symlink.txt new file mode 100644 @@ -357,7 +359,9 @@ describe('Repository', function() { await repo.stageFileSymlinkChange(deletedFileAddedSymlinkPath); assert.isNull(await indexModeAndOid(deletedFileAddedSymlinkPath)); const stagedFilePatch = await repo.getFilePatchForPath(deletedFileAddedSymlinkPath, {staged: true}); - assert.equal(stagedFilePatch.getStatus(), 'deleted'); + assert.lengthOf(stagedFilePatch.getFilePatches(), 1); + const [sFilePatch] = stagedFilePatch.getFilePatches(); + assert.equal(sFilePatch.getStatus(), 'deleted'); assert.equal(stagedFilePatch.toString(), dedent` diff --git a/a.txt b/a.txt deleted file mode 100644 From 7cd5cfd3cb49cd0f8c974edc7707ad678b69aa27 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 9 Nov 2018 12:10:40 -0800 Subject: [PATCH 268/284] Set pull.rebase config to false in `setUpLocalAndRemoteRepositories` This was causing the `only performs a fast-forward merge with ffOnly` test to fail if the system's global config had this set to `true` --- test/helpers.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/helpers.js b/test/helpers.js index 274f85c0132..4733eb2476b 100644 --- a/test/helpers.js +++ b/test/helpers.js @@ -99,6 +99,7 @@ export async function setUpLocalAndRemoteRepositories(repoName = 'multiple-commi await localGit.exec(['config', '--local', 'commit.gpgsign', 'false']); await localGit.exec(['config', '--local', 'user.email', FAKE_USER.email]); await localGit.exec(['config', '--local', 'user.name', FAKE_USER.name]); + await localGit.exec(['config', '--local', 'pull.rebase', false]); return {baseRepoPath, remoteRepoPath, localRepoPath}; } From 488612948122becd3b615d318610f58d04ddf901 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 9 Nov 2018 15:38:02 -0500 Subject: [PATCH 269/284] Move coveralls report to after_script --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 821983cbf55..d5718814fa0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -61,5 +61,5 @@ before_script: script: - ./script/cibuild -after_success: +after_script: - npm run coveralls From 7b4078f3e6f1b8f0cf2e6e67757ddd00239b4d4a Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 9 Nov 2018 12:44:32 -0800 Subject: [PATCH 270/284] Only call `discardLines` if MFP has a single file patch --- lib/controllers/multi-file-patch-controller.js | 8 ++++++++ lib/controllers/root-controller.js | 16 ++++++++++------ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/lib/controllers/multi-file-patch-controller.js b/lib/controllers/multi-file-patch-controller.js index 7ffeb2b38f8..6b7a19c6e01 100644 --- a/lib/controllers/multi-file-patch-controller.js +++ b/lib/controllers/multi-file-patch-controller.js @@ -183,6 +183,14 @@ export default class MultiFilePatchController extends React.Component { } async discardRows(rowSet, nextSelectionMode, {eventSource} = {}) { + // (kuychaco) For now we only support discarding rows for MultiFilePatches that contain a single file patch + // The only way to access this method from the UI is to be in a ChangedFileItem, which only has a single file patch + // This check is duplicated in RootController#discardLines. We also want it here to prevent us from sending metrics + // unnecessarily + if (this.props.multiFilePatch.getFilePatches().length !== 1) { + return Promise.resolve(null); + } + let chosenRows = rowSet; if (chosenRows) { await this.selectedRowsChanged(chosenRows, nextSelectionMode); diff --git a/lib/controllers/root-controller.js b/lib/controllers/root-controller.js index db41139e7b6..4ade80eaf68 100644 --- a/lib/controllers/root-controller.js +++ b/lib/controllers/root-controller.js @@ -678,18 +678,22 @@ export default class RootController extends React.Component { } async discardLines(multiFilePatch, lines, repository = this.props.repository) { - const filePaths = multiFilePatch.getFilePatches().map(fp => fp.getPath()); + // (kuychaco) For now we only support discarding rows for MultiFilePatches that contain a single file patch + // The only way to access this method from the UI is to be in a ChangedFileItem, which only has a single file patch + if (multiFilePatch.getFilePatches().length !== 1) { + return Promise.resolve(null); + } + + const filePath = multiFilePatch.getFilePatches()[0].getPath(); const destructiveAction = async () => { const discardFilePatch = multiFilePatch.getUnstagePatchForLines(lines); await repository.applyPatchToWorkdir(discardFilePatch); }; return await repository.storeBeforeAndAfterBlobs( - [filePaths], - () => this.ensureNoUnsavedFiles(filePaths, 'Cannot discard lines.', repository.getWorkingDirectoryPath()), + [filePath], + () => this.ensureNoUnsavedFiles([filePath], 'Cannot discard lines.', repository.getWorkingDirectoryPath()), destructiveAction, - // FIXME: Present::storeBeforeAndAfterBlobs() and DiscardHistory::storeBeforeAndAfterBlobs() need a way to store - // multiple partial paths - filePaths[0], + filePath, ); } From 3908db1d09baabe0eac8b2df043df1771abc3209 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 9 Nov 2018 14:19:10 -0800 Subject: [PATCH 271/284] Fix `buildFilePatch` --- test/models/patch/builder.test.js | 87 +++++++++++++++++++++---------- 1 file changed, 60 insertions(+), 27 deletions(-) diff --git a/test/models/patch/builder.test.js b/test/models/patch/builder.test.js index ae11008366e..2933f218bec 100644 --- a/test/models/patch/builder.test.js +++ b/test/models/patch/builder.test.js @@ -13,7 +13,7 @@ describe('buildFilePatch', function() { describe('with a single diff', function() { it('assembles a patch from non-symlink sides', function() { - const p = buildFilePatch([{ + const multiFilePatch = buildFilePatch([{ oldPath: 'old/path', oldMode: '100644', newPath: 'new/path', @@ -66,18 +66,22 @@ describe('buildFilePatch', function() { ], }]); + assert.lengthOf(multiFilePatch.getFilePatches(), 1); + const [p] = multiFilePatch.getFilePatches(); + const buffer = multiFilePatch.getBuffer(); + 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'); - const buffer = + const bufferText = '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.getBuffer().getText(), buffer); + assert.strictEqual(buffer.getText(), bufferText); - assertInPatch(p).hunks( + assertInPatch(p, buffer).hunks( { startRow: 0, endRow: 8, @@ -115,7 +119,7 @@ describe('buildFilePatch', function() { }); it("sets the old file's symlink destination", function() { - const p = buildFilePatch([{ + const multiFilePatch = buildFilePatch([{ oldPath: 'old/path', oldMode: '120000', newPath: 'new/path', @@ -132,12 +136,14 @@ describe('buildFilePatch', function() { ], }]); + assert.lengthOf(multiFilePatch.getFilePatches(), 1); + const [p] = multiFilePatch.getFilePatches(); assert.strictEqual(p.getOldSymlink(), 'old/destination'); assert.isNull(p.getNewSymlink()); }); it("sets the new file's symlink destination", function() { - const p = buildFilePatch([{ + const multiFilePatch = buildFilePatch([{ oldPath: 'old/path', oldMode: '100644', newPath: 'new/path', @@ -154,12 +160,14 @@ describe('buildFilePatch', function() { ], }]); + assert.lengthOf(multiFilePatch.getFilePatches(), 1); + const [p] = multiFilePatch.getFilePatches(); assert.isNull(p.getOldSymlink()); assert.strictEqual(p.getNewSymlink(), 'new/destination'); }); it("sets both files' symlink destinations", function() { - const p = buildFilePatch([{ + const multiFilePatch = buildFilePatch([{ oldPath: 'old/path', oldMode: '120000', newPath: 'new/path', @@ -180,12 +188,14 @@ describe('buildFilePatch', function() { ], }]); + assert.lengthOf(multiFilePatch.getFilePatches(), 1); + const [p] = multiFilePatch.getFilePatches(); assert.strictEqual(p.getOldSymlink(), 'old/destination'); assert.strictEqual(p.getNewSymlink(), 'new/destination'); }); it('assembles a patch from a file deletion', function() { - const p = buildFilePatch([{ + const multiFilePatch = buildFilePatch([{ oldPath: 'old/path', oldMode: '100644', newPath: null, @@ -208,16 +218,20 @@ describe('buildFilePatch', function() { ], }]); + assert.lengthOf(multiFilePatch.getFilePatches(), 1); + const [p] = multiFilePatch.getFilePatches(); + const buffer = multiFilePatch.getBuffer(); + 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\n'; - assert.strictEqual(p.getBuffer().getText(), buffer); + const bufferText = 'line-0\nline-1\nline-2\nline-3\n\n'; + assert.strictEqual(buffer.getText(), bufferText); - assertInPatch(p).hunks( + assertInPatch(p, buffer).hunks( { startRow: 0, endRow: 4, @@ -230,7 +244,7 @@ describe('buildFilePatch', function() { }); it('assembles a patch from a file addition', function() { - const p = buildFilePatch([{ + const multiFilePatch = buildFilePatch([{ oldPath: null, oldMode: null, newPath: 'new/path', @@ -251,16 +265,20 @@ describe('buildFilePatch', function() { ], }]); + assert.lengthOf(multiFilePatch.getFilePatches(), 1); + const [p] = multiFilePatch.getFilePatches(); + const buffer = multiFilePatch.getBuffer(); + 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.getBuffer().getText(), buffer); + const bufferText = 'line-0\nline-1\nline-2\n'; + assert.strictEqual(buffer.getText(), bufferText); - assertInPatch(p).hunks( + assertInPatch(p, buffer).hunks( { startRow: 0, endRow: 2, @@ -286,7 +304,7 @@ describe('buildFilePatch', function() { }); it('parses a no-newline marker', function() { - const p = buildFilePatch([{ + const multiFilePatch = buildFilePatch([{ oldPath: 'old/path', oldMode: '100644', newPath: 'new/path', @@ -297,9 +315,12 @@ describe('buildFilePatch', function() { ]}], }]); - assert.strictEqual(p.getBuffer().getText(), 'line-0\nline-1\n No newline at end of file\n'); + assert.lengthOf(multiFilePatch.getFilePatches(), 1); + const [p] = multiFilePatch.getFilePatches(); + const buffer = multiFilePatch.getBuffer(); + assert.strictEqual(buffer.getText(), 'line-0\nline-1\n No newline at end of file\n'); - assertInPatch(p).hunks({ + assertInPatch(p, buffer).hunks({ startRow: 0, endRow: 2, header: '@@ -0,1 +0,1 @@', @@ -314,7 +335,7 @@ describe('buildFilePatch', function() { 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([ + const multiFilePatch = buildFilePatch([ { oldPath: 'the-path', oldMode: '000000', @@ -349,6 +370,10 @@ describe('buildFilePatch', function() { }, ]); + assert.lengthOf(multiFilePatch.getFilePatches(), 1); + const [p] = multiFilePatch.getFilePatches(); + const buffer = multiFilePatch.getBuffer(); + assert.strictEqual(p.getOldPath(), 'the-path'); assert.strictEqual(p.getOldMode(), '100644'); assert.isNull(p.getOldSymlink()); @@ -357,8 +382,8 @@ describe('buildFilePatch', function() { assert.strictEqual(p.getNewSymlink(), 'the-destination'); assert.strictEqual(p.getStatus(), 'deleted'); - assert.strictEqual(p.getBuffer().getText(), 'line-0\nline-1\n'); - assertInPatch(p).hunks({ + assert.strictEqual(buffer.getText(), 'line-0\nline-1\n'); + assertInPatch(p, buffer).hunks({ startRow: 0, endRow: 1, header: '@@ -0,0 +0,2 @@', @@ -369,7 +394,7 @@ describe('buildFilePatch', function() { }); it('identifies a symlink that was deleted and replaced by a file', function() { - const p = buildFilePatch([ + const multiFilePatch = buildFilePatch([ { oldPath: 'the-path', oldMode: '120000', @@ -404,6 +429,10 @@ describe('buildFilePatch', function() { }, ]); + assert.lengthOf(multiFilePatch.getFilePatches(), 1); + const [p] = multiFilePatch.getFilePatches(); + const buffer = multiFilePatch.getBuffer(); + assert.strictEqual(p.getOldPath(), 'the-path'); assert.strictEqual(p.getOldMode(), '120000'); assert.strictEqual(p.getOldSymlink(), 'the-destination'); @@ -412,8 +441,8 @@ describe('buildFilePatch', function() { assert.isNull(p.getNewSymlink()); assert.strictEqual(p.getStatus(), 'added'); - assert.strictEqual(p.getBuffer().getText(), 'line-0\nline-1\n'); - assertInPatch(p).hunks({ + assert.strictEqual(buffer.getText(), 'line-0\nline-1\n'); + assertInPatch(p, buffer).hunks({ startRow: 0, endRow: 1, header: '@@ -0,2 +0,0 @@', @@ -424,7 +453,7 @@ describe('buildFilePatch', function() { }); it('is indifferent to the order of the diffs', function() { - const p = buildFilePatch([ + const multiFilePatch = buildFilePatch([ { oldMode: '100644', newPath: 'the-path', @@ -458,6 +487,10 @@ describe('buildFilePatch', function() { }, ]); + assert.lengthOf(multiFilePatch.getFilePatches(), 1); + const [p] = multiFilePatch.getFilePatches(); + const buffer = multiFilePatch.getBuffer(); + assert.strictEqual(p.getOldPath(), 'the-path'); assert.strictEqual(p.getOldMode(), '100644'); assert.isNull(p.getOldSymlink()); @@ -466,8 +499,8 @@ describe('buildFilePatch', function() { assert.strictEqual(p.getNewSymlink(), 'the-destination'); assert.strictEqual(p.getStatus(), 'deleted'); - assert.strictEqual(p.getBuffer().getText(), 'line-0\nline-1\n'); - assertInPatch(p).hunks({ + assert.strictEqual(buffer.getText(), 'line-0\nline-1\n'); + assertInPatch(p, buffer).hunks({ startRow: 0, endRow: 1, header: '@@ -0,0 +0,2 @@', From 37967eefe3b75b1b4b5bc48b31fcf3958ae0d7ab Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 9 Nov 2018 14:28:12 -0800 Subject: [PATCH 272/284] Fix `buildMultiFilePatch` tests --- test/models/patch/builder.test.js | 29 +++++++++++------------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/test/models/patch/builder.test.js b/test/models/patch/builder.test.js index 2933f218bec..d1bc6e1161e 100644 --- a/test/models/patch/builder.test.js +++ b/test/models/patch/builder.test.js @@ -590,6 +590,8 @@ describe('buildFilePatch', function() { }, ]); + const buffer = mp.getBuffer(); + assert.lengthOf(mp.getFilePatches(), 3); assert.strictEqual( @@ -599,20 +601,9 @@ describe('buildFilePatch', function() { 'line-0\nline-1\nline-2\n', ); - const assertAllSame = getter => { - assert.lengthOf( - Array.from(new Set(mp.getFilePatches().map(p => p[getter]()))), - 1, - `FilePatches have different results from ${getter}`, - ); - }; - for (const getter of ['getUnchangedLayer', 'getAdditionLayer', 'getDeletionLayer', 'getNoNewlineLayer']) { - assertAllSame(getter); - } - assert.strictEqual(mp.getFilePatches()[0].getOldPath(), 'first'); assert.deepEqual(mp.getFilePatches()[0].getMarker().getRange().serialize(), [[0, 0], [6, 6]]); - assertInFilePatch(mp.getFilePatches()[0]).hunks( + assertInFilePatch(mp.getFilePatches()[0], buffer).hunks( { startRow: 0, endRow: 3, header: '@@ -1,2 +1,4 @@', regions: [ {kind: 'unchanged', string: ' line-0\n', range: [[0, 0], [0, 6]]}, @@ -630,7 +621,7 @@ describe('buildFilePatch', function() { ); assert.strictEqual(mp.getFilePatches()[1].getOldPath(), 'second'); assert.deepEqual(mp.getFilePatches()[1].getMarker().getRange().serialize(), [[7, 0], [10, 6]]); - assertInFilePatch(mp.getFilePatches()[1]).hunks( + assertInFilePatch(mp.getFilePatches()[1], buffer).hunks( { startRow: 7, endRow: 10, header: '@@ -5,3 +5,3 @@', regions: [ {kind: 'unchanged', string: ' line-5\n', range: [[7, 0], [7, 6]]}, @@ -642,7 +633,7 @@ describe('buildFilePatch', function() { ); assert.strictEqual(mp.getFilePatches()[2].getOldPath(), 'third'); assert.deepEqual(mp.getFilePatches()[2].getMarker().getRange().serialize(), [[11, 0], [13, 6]]); - assertInFilePatch(mp.getFilePatches()[2]).hunks( + assertInFilePatch(mp.getFilePatches()[2], buffer).hunks( { startRow: 11, endRow: 13, header: '@@ -1,0 +1,3 @@', regions: [ {kind: 'addition', string: '+line-0\n+line-1\n+line-2\n', range: [[11, 0], [13, 6]]}, @@ -713,11 +704,13 @@ describe('buildFilePatch', function() { }, ]); + const buffer = mp.getBuffer(); + assert.lengthOf(mp.getFilePatches(), 4); const [fp0, fp1, fp2, fp3] = mp.getFilePatches(); assert.strictEqual(fp0.getOldPath(), 'first'); - assertInFilePatch(fp0).hunks({ + assertInFilePatch(fp0, buffer).hunks({ startRow: 0, endRow: 2, header: '@@ -1,2 +1,3 @@', regions: [ {kind: 'unchanged', string: ' line-0\n', range: [[0, 0], [0, 6]]}, {kind: 'addition', string: '+line-1\n', range: [[1, 0], [1, 6]]}, @@ -728,7 +721,7 @@ describe('buildFilePatch', function() { assert.strictEqual(fp1.getOldPath(), 'was-non-symlink'); assert.isTrue(fp1.hasTypechange()); assert.strictEqual(fp1.getNewSymlink(), 'was-non-symlink-destination'); - assertInFilePatch(fp1).hunks({ + assertInFilePatch(fp1, buffer).hunks({ startRow: 3, endRow: 4, header: '@@ -1,2 +1,0 @@', regions: [ {kind: 'deletion', string: '-line-0\n-line-1\n', range: [[3, 0], [4, 6]]}, ], @@ -737,14 +730,14 @@ describe('buildFilePatch', function() { assert.strictEqual(fp2.getOldPath(), 'was-symlink'); assert.isTrue(fp2.hasTypechange()); assert.strictEqual(fp2.getOldSymlink(), 'was-symlink-destination'); - assertInFilePatch(fp2).hunks({ + assertInFilePatch(fp2, buffer).hunks({ startRow: 5, endRow: 6, header: '@@ -1,0 +1,2 @@', regions: [ {kind: 'addition', string: '+line-0\n+line-1\n', range: [[5, 0], [6, 6]]}, ], }); assert.strictEqual(fp3.getNewPath(), 'third'); - assertInFilePatch(fp3).hunks({ + assertInFilePatch(fp3, buffer).hunks({ startRow: 7, endRow: 9, header: '@@ -1,3 +1,0 @@', regions: [ {kind: 'deletion', string: '-line-0\n-line-1\n-line-2\n', range: [[7, 0], [9, 6]]}, ], From 3a3c68a08e87255e16e059ab1d56de05ddd41739 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 9 Nov 2018 16:16:41 -0800 Subject: [PATCH 273/284] Fix `Open in File` and make it work for multiple files --- lib/views/multi-file-patch-view.js | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/lib/views/multi-file-patch-view.js b/lib/views/multi-file-patch-view.js index ec16a26ae00..dbc0f8f4403 100644 --- a/lib/views/multi-file-patch-view.js +++ b/lib/views/multi-file-patch-view.js @@ -850,7 +850,7 @@ export default class MultiFilePatchView extends React.Component { } didOpenFile() { - const cursors = []; + const cursorsByFilePatch = new Map(); this.refEditor.map(editor => { const placedRows = new Set(); @@ -858,6 +858,7 @@ export default class MultiFilePatchView extends React.Component { for (const cursor of editor.getCursors()) { const cursorRow = cursor.getBufferPosition().row; const hunk = this.props.multiFilePatch.getHunkAt(cursorRow); + const filePatch = this.props.multiFilePatch.getFilePatchAt(cursorRow); /* istanbul ignore next */ if (!hunk) { continue; @@ -866,7 +867,7 @@ export default class MultiFilePatchView extends React.Component { let newRow = hunk.getNewRowAt(cursorRow); let newColumn = cursor.getBufferPosition().column; if (newRow === null) { - let nearestRow = hunk.getNewStartRow() - 1; + let nearestRow = hunk.getNewStartRow(); for (const region of hunk.getRegions()) { if (!region.includesBufferRow(cursorRow)) { region.when({ @@ -890,14 +891,24 @@ export default class MultiFilePatchView extends React.Component { } if (newRow !== null) { - cursors.push([newRow, newColumn]); + newRow -= 1; // Why is this needed? What's not being + const cursors = cursorsByFilePatch.get(filePatch); + if (!cursors) { + cursorsByFilePatch.set(filePatch, [[newRow, newColumn]]); + } else { + cursors.push([newRow, newColumn]); + } } } return null; }); - this.props.openFile(cursors); + return Promise.all(Array.from(cursorsByFilePatch).map(value => { + const [filePatch, cursors] = value; + return this.props.openFile(filePatch, cursors); + })); + } getSelectedRows() { From 52634b9f1b3e4a73fd771e660046eaf5005af1f6 Mon Sep 17 00:00:00 2001 From: Tilde Ann Thurium Date: Fri, 9 Nov 2018 16:23:29 -0800 Subject: [PATCH 274/284] workshopping button text --- lib/views/commit-view.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/views/commit-view.js b/lib/views/commit-view.js index d5aa3514836..4aedd511598 100644 --- a/lib/views/commit-view.js +++ b/lib/views/commit-view.js @@ -164,10 +164,10 @@ export default class CommitView extends React.Component {
From 73f5d69ed3280f22281c82b43d8cd724b54e41d5 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 9 Nov 2018 16:41:03 -0800 Subject: [PATCH 275/284] Finish up that question/comment... --- lib/views/multi-file-patch-view.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/views/multi-file-patch-view.js b/lib/views/multi-file-patch-view.js index dbc0f8f4403..538825d7b59 100644 --- a/lib/views/multi-file-patch-view.js +++ b/lib/views/multi-file-patch-view.js @@ -891,7 +891,9 @@ export default class MultiFilePatchView extends React.Component { } if (newRow !== null) { - newRow -= 1; // Why is this needed? What's not being + // Why is this needed? I _think_ everything is in terms of buffer position + // so there shouldn't be an off-by-one issue + newRow -= 1; const cursors = cursorsByFilePatch.get(filePatch); if (!cursors) { cursorsByFilePatch.set(filePatch, [[newRow, newColumn]]); From dc1c00993a369d06a0c4814dc314c6222d33a133 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 9 Nov 2018 17:00:01 -0800 Subject: [PATCH 276/284] Fix `undoLastDiscard` --- lib/controllers/multi-file-patch-controller.js | 1 - lib/views/multi-file-patch-view.js | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/controllers/multi-file-patch-controller.js b/lib/controllers/multi-file-patch-controller.js index 6b7a19c6e01..6c9b8b0707a 100644 --- a/lib/controllers/multi-file-patch-controller.js +++ b/lib/controllers/multi-file-patch-controller.js @@ -91,7 +91,6 @@ export default class MultiFilePatchController extends React.Component { eventSource, }); - return this.props.undoLastDiscard(filePatch.getPath(), this.props.repository); } diff --git a/lib/views/multi-file-patch-view.js b/lib/views/multi-file-patch-view.js index 538825d7b59..91a5e1d3008 100644 --- a/lib/views/multi-file-patch-view.js +++ b/lib/views/multi-file-patch-view.js @@ -585,8 +585,8 @@ export default class MultiFilePatchView extends React.Component { } } - undoLastDiscardFromButton = () => { - this.props.undoLastDiscard({eventSource: 'button'}); + undoLastDiscardFromButton = filePatch => { + this.props.undoLastDiscard(filePatch, {eventSource: 'button'}); } discardSelectionFromCommand = () => { From 6ab88e844e6c20e64d5b2bfff5a5c73c6f5b9b3d Mon Sep 17 00:00:00 2001 From: Tilde Ann Thurium Date: Fri, 9 Nov 2018 17:04:29 -0800 Subject: [PATCH 277/284] :memo: add new components to React atlas --- docs/react-component-atlas.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/docs/react-component-atlas.md b/docs/react-component-atlas.md index 35e55f883fe..81c80e0a436 100644 --- a/docs/react-component-atlas.md +++ b/docs/react-component-atlas.md @@ -34,6 +34,11 @@ This is a high-level overview of the structure of the React component tree that > > > > The "GitHub" tab that appears in the right dock (by default). > > +> > > [``](/lig/items/commit-preview-item.js) +> > > [``](/lib/containers/commit-preview-container.js) +> > > +> > > Allows users to view all unstaged commits in one pane. +> > > > > > [``](/lib/views/remote-selector-view.js) > > > > > > Shown if the current repository has more than one remote that's identified as a github.com remote. @@ -66,12 +71,13 @@ This is a high-level overview of the structure of the React component tree that > > > > > > > > > > > > Render a list of issueish results as rows within the result list of a specific search. > -> > [``](/lib/controllers/file-patch-controller.js) -> > [``](/lib/views/file-patch-view.js) +> > [ ``](/lib/containers/changed-file-container.js) +> > [``](/lib/controllers/multi-file-patch-controller.js) +> > [``](/lib/views/multi-file-patch-view.js) +> > +> > The workspace-center pane that appears when looking at the staged or unstaged changes associated with one or more files. > > -> > The workspace-center pane that appears when looking at the staged or unstaged changes associated with a file. > > -> > :construction: Being rewritten in [#1712](https://github.com/atom/github/pull/1512) :construction: > > > [``](/lib/items/issueish-detail-item.js) > > [``](/lib/containers/issueish-detail-container.js) From a92f21c85ecc9a280522d642cf82a45e8a53ba01 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 9 Nov 2018 17:19:32 -0800 Subject: [PATCH 278/284] Fix `undoLastDiscard` test for MultiFilePatchView --- test/views/multi-file-patch-view.test.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/views/multi-file-patch-view.test.js b/test/views/multi-file-patch-view.test.js index 776f3ebd78a..7b71f591ddd 100644 --- a/test/views/multi-file-patch-view.test.js +++ b/test/views/multi-file-patch-view.test.js @@ -85,7 +85,9 @@ describe('MultiFilePatchView', function() { wrapper.find('FilePatchHeaderView').first().prop('undoLastDiscard')(); - assert.isTrue(undoLastDiscard.calledWith({eventSource: 'button'})); + assert.lengthOf(filePatches.getFilePatches(), 1); + const [filePatch] = filePatches.getFilePatches(); + assert.isTrue(undoLastDiscard.calledWith(filePatch, {eventSource: 'button'})); }); it('renders the file patch within an editor', function() { From a2d5c99242aa571c37a20b9e4dc8bf6850a68f4f Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 9 Nov 2018 17:51:00 -0800 Subject: [PATCH 279/284] Update button text in tests --- test/views/commit-view.test.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/views/commit-view.test.js b/test/views/commit-view.test.js index 14aef600800..b4213f8217a 100644 --- a/test/views/commit-view.test.js +++ b/test/views/commit-view.test.js @@ -656,13 +656,13 @@ describe('CommitView', function() { it('displays correct button text depending on prop value', function() { const wrapper = shallow(app); - assert.strictEqual(wrapper.find('.github-CommitView-commitPreview').text(), 'Preview Commit'); + assert.strictEqual(wrapper.find('.github-CommitView-commitPreview').text(), 'See All Staged Changes'); wrapper.setProps({commitPreviewActive: true}); - assert.strictEqual(wrapper.find('.github-CommitView-commitPreview').text(), 'Close Commit Preview'); + assert.strictEqual(wrapper.find('.github-CommitView-commitPreview').text(), 'Hide All Staged Changes'); wrapper.setProps({commitPreviewActive: false}); - assert.strictEqual(wrapper.find('.github-CommitView-commitPreview').text(), 'Preview Commit'); + assert.strictEqual(wrapper.find('.github-CommitView-commitPreview').text(), 'See All Staged Changes'); }); }); }); From a60cfc5bdbbd425e8a1588a47dece84f5fec56db Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 9 Nov 2018 18:00:10 -0800 Subject: [PATCH 280/284] Fix tests for opening file when there is only a single file patch --- test/views/multi-file-patch-view.test.js | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/test/views/multi-file-patch-view.test.js b/test/views/multi-file-patch-view.test.js index 7b71f591ddd..e16769805fb 100644 --- a/test/views/multi-file-patch-view.test.js +++ b/test/views/multi-file-patch-view.test.js @@ -8,7 +8,7 @@ import {nullFile} from '../../lib/models/patch/file'; import FilePatch from '../../lib/models/patch/file-patch'; import RefHolder from '../../lib/models/ref-holder'; -describe('MultiFilePatchView', function() { +describe.only('MultiFilePatchView', function() { let atomEnv, workspace, repository, filePatches; beforeEach(async function() { @@ -1078,8 +1078,8 @@ describe('MultiFilePatchView', function() { }); }); - describe('opening the file', function() { - let mfp; + describe('opening the file when there is only one file patch', function() { + let mfp, fp; beforeEach(function() { const {multiFilePatch} = multiFilePatchBuilder().addFilePatch(fp => { @@ -1095,6 +1095,8 @@ describe('MultiFilePatchView', function() { }).build(); mfp = multiFilePatch; + assert.lengthOf(mfp.getFilePatches(), 1); + fp = mfp.getFilePatches()[0]; }); it('opens the file at the current unchanged row', function() { @@ -1105,7 +1107,9 @@ describe('MultiFilePatchView', function() { editor.setCursorBufferPosition([7, 2]); atomEnv.commands.dispatch(wrapper.getDOMNode(), 'github:open-file'); - assert.isTrue(openFile.calledWith([[14, 2]])); + console.log(openFile.args); + console.log(fp); + assert.isTrue(openFile.calledWith(fp, [[13, 2]])); }); it('opens the file at a current added row', function() { @@ -1117,7 +1121,7 @@ describe('MultiFilePatchView', function() { atomEnv.commands.dispatch(wrapper.getDOMNode(), 'github:open-file'); - assert.isTrue(openFile.calledWith([[15, 3]])); + assert.isTrue(openFile.calledWith(fp, [[14, 3]])); }); it('opens the file at the beginning of the previous added or unchanged row', function() { @@ -1129,7 +1133,7 @@ describe('MultiFilePatchView', function() { atomEnv.commands.dispatch(wrapper.getDOMNode(), 'github:open-file'); - assert.isTrue(openFile.calledWith([[15, 0]])); + assert.isTrue(openFile.calledWith(fp, [[15, 0]])); }); it('preserves multiple cursors', function() { @@ -1147,10 +1151,10 @@ describe('MultiFilePatchView', function() { atomEnv.commands.dispatch(wrapper.getDOMNode(), 'github:open-file'); - assert.isTrue(openFile.calledWith([ + assert.isTrue(openFile.calledWith(fp, [ + [10, 2], [11, 2], - [12, 2], - [3, 3], + [2, 3], [15, 0], ])); }); From 1c711f00dbe09760a406dd91323af13d9cbef592 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 9 Nov 2018 18:08:03 -0800 Subject: [PATCH 281/284] :fire: console.logs --- test/views/multi-file-patch-view.test.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/views/multi-file-patch-view.test.js b/test/views/multi-file-patch-view.test.js index e16769805fb..5653d947fe4 100644 --- a/test/views/multi-file-patch-view.test.js +++ b/test/views/multi-file-patch-view.test.js @@ -1107,8 +1107,6 @@ describe.only('MultiFilePatchView', function() { editor.setCursorBufferPosition([7, 2]); atomEnv.commands.dispatch(wrapper.getDOMNode(), 'github:open-file'); - console.log(openFile.args); - console.log(fp); assert.isTrue(openFile.calledWith(fp, [[13, 2]])); }); From 36e1e5f9b397bea55d1bb9b276bf478c42329596 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 9 Nov 2018 18:09:44 -0800 Subject: [PATCH 282/284] :shirt: don't shadow `fp` --- test/views/multi-file-patch-view.test.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/views/multi-file-patch-view.test.js b/test/views/multi-file-patch-view.test.js index 5653d947fe4..382c4e41bd1 100644 --- a/test/views/multi-file-patch-view.test.js +++ b/test/views/multi-file-patch-view.test.js @@ -1082,13 +1082,13 @@ describe.only('MultiFilePatchView', function() { let mfp, fp; beforeEach(function() { - const {multiFilePatch} = multiFilePatchBuilder().addFilePatch(fp => { - fp.setOldFile(f => f.path('path.txt')); - fp.addHunk(h => { + const {multiFilePatch} = multiFilePatchBuilder().addFilePatch(filePatch => { + filePatch.setOldFile(f => f.path('path.txt')); + filePatch.addHunk(h => { h.oldRow(2); h.unchanged('0000').added('0001').unchanged('0002'); }); - fp.addHunk(h => { + filePatch.addHunk(h => { h.oldRow(10); h.unchanged('0003').added('0004', '0005').deleted('0006').unchanged('0007').added('0008').deleted('0009').unchanged('0010'); }); From 54b23c99fb5c4186bdcd07df5cf1289d6c987954 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 9 Nov 2018 18:29:43 -0800 Subject: [PATCH 283/284] Make commit preview button styled as `secondary` rather than `primary` UXR with @mattmattmatt -- he says if it's primary it competes too much with the commit button Co-Authored-By: Matt --- 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 4aedd511598..fdfa143ef45 100644 --- a/lib/views/commit-view.js +++ b/lib/views/commit-view.js @@ -164,7 +164,7 @@ export default class CommitView extends React.Component {