From cca6f09c9ed130c8b8442d23b1e8c91160236cf1 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 30 Oct 2018 09:34:51 -0400 Subject: [PATCH 001/409] 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 0000000000..1b6165203b --- /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 0000000000..28d2b42a81 --- /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/409] 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 0000000000..46022cca3f --- /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 0000000000..49a91323e2 --- /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/409] 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 0000000000..c3d34b6261 --- /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 0000000000..44a697831f --- /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/409] 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 49a91323e2..5b05ddb50c 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/409] 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 5b05ddb50c..ffbc020239 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 44a697831f..e86834e3db 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/409] 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 1b6165203b..2574abac0c 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 28d2b42a81..ce5c7df0bf 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/409] 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 2574abac0c..33d093a3f1 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/409] 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 33d093a3f1..32dd2c4019 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 ce5c7df0bf..c6fb420a07 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/409] 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 b5b65be47e..64b9b0bcf4 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 3504ad6e5f..3617fd9f9d 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/409] 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 32dd2c4019..b03aaa793d 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 c6fb420a07..79bdce5a20 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/409] 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 46022cca3f..ef6fd11170 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 ffbc020239..5953d2d1ab 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/409] 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 0000000000..b47b90f545 --- /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 0000000000..3c0a38f525 --- /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/409] 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 ef6fd11170..d8af3a424d 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 c3d34b6261..8642647b26 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 13d0b4d394..f00c709f8f 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/409] 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 a773e3d976..0d44ee5d0d 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 596fcfde50..525043dbc4 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 6b2ce62401..60eab52a71 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/409] 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 0d44ee5d0d..c2bef5fe72 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 60eab52a71..987194ef82 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/409] 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 e6a4e6b697..a2b5693dfb 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 635b09dafd..2c3bdcb7cc 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/409] 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 ed25c56e49..6bef2053b0 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 dc3b8e9bb4..ccefa2cd1b 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 ab50db5f52..6801b95262 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 d1ad3a84c4..8fc548625d 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/409] 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 6bef2053b0..a226ef11e4 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 8fc548625d..f0fb42c4e0 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/409] 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 d8af3a424d..c7165e7e33 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/409] 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 3b4a6ad9f0..b88534d4c1 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/409] 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 b88534d4c1..b609d42bf2 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/409] 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 8642647b26..6016272d47 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 b609d42bf2..b7285655f9 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 b03aaa793d..9634d3a4c5 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 0000000000..60f02c79ce --- /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 c04a3445b0..418de093ad 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/409] 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 9634d3a4c5..ebd3a42c68 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/409] 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 4580547704..3bf6cc7635 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 14dc10db17..56491549da 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 b7285655f9..6ceef27e1e 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 7341d3fcfb..d91aaf82d4 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 00467f1d2f..466cce067d 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/409] 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 3b3865f08c..b6de5d3b2a 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 8ba0359b83..57ddc3302b 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 466cce067d..59f8d13101 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/409] 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 c7165e7e33..c1c5d9a70f 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 6016272d47..f10b7ddfaa 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 e86834e3db..78864d23ed 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/409] 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 b6de5d3b2a..22d73e70a2 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 a226ef11e4..863c59a1e3 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 ccefa2cd1b..791b52174d 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 6801b95262..08970a981d 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 f14f59aa01..0000000000 --- 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/409] 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 56491549da..6fffce65e1 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/409] 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 f8eeb45b51..153b723d28 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 cac4e26b2c..4c8bc73a4b 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/409] 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 6fffce65e1..f28277ec58 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/409] 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 0000000000..36a66c3cbd --- /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 418de093ad..c04a3445b0 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/409] 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 36a66c3cbd..20e9b7c368 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/409] 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 20e9b7c368..6589a1ba00 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/409] 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 6589a1ba00..087ef76832 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/409] 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 8736496e21..071c14157d 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/409] 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 7119f2e893..a35f673b45 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/409] 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 6ceef27e1e..a0a3a0385e 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/409] 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 071c14157d..8a0d1492b9 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 f0c70afd89..8964aac090 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/409] 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 22d73e70a2..f2cc89db52 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 c1c5d9a70f..877fa76cfb 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/409] 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 7690da29d9..6338ab489a 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/409] 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 c4d637c56b..176138220a 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 3e716ea10d..24ccd2b7a5 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 6d8fb94b84..d2e8eb58a3 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/409] 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 8a0d1492b9..93fab72a36 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/409] 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 176138220a..ca162bd587 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 6338ab489a..22e544d404 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/409] :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 27838dd427..2a0d62dd37 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/409] 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 6531ea3714..0932fbf4e1 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 ac9f6174d3..1b705c5828 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 6b8c106e77..7510cf84a5 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/409] 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 1b705c5828..516d1fa388 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 7510cf84a5..f7567a4e62 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/409] 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 6ee00e7df3..2c76b1349f 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 c2bef5fe72..4a8ba43fb1 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 863c59a1e3..97ebccd24e 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/409] 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 2c76b1349f..6ee00e7df3 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 4a8ba43fb1..c2bef5fe72 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 97ebccd24e..863c59a1e3 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/409] 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 63a07f36a2..a0fa33e21b 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/409] :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 69c2d2099f..92b9b08e4b 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/409] :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 ebf83c698b..400c96c4d0 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 f7567a4e62..ad542a6837 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/409] 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 ad542a6837..fd182e4e50 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/409] 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 516d1fa388..ffd4c1c9be 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/409] 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 0000000000..0dd844bc46 --- /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 0000000000..9ea6826a5b --- /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/409] 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 9ea6826a5b..25c21753c3 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/409] 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 0a213f50e9..558a54f95d 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 4cb77a52ad..a36f332401 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/409] 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 099ebfe810..e5301ce839 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/409] 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 ffd4c1c9be..9abb689022 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/409] 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 0dd844bc46..0f231d7ba5 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/409] 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 0f231d7ba5..e55fdae9ba 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 25c21753c3..f592685546 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/409] 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 e5301ce839..f4515eec8e 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 9abb689022..8e450e80dc 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/409] :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 92b9b08e4b..b3d9e348bd 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/409] 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 b3d9e348bd..e40a8cc021 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 8964aac090..da8b386b8c 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/409] 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 e40a8cc021..4a52d2ed85 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 da8b386b8c..ed9ebfdf95 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/409] 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 4a52d2ed85..a8e0f4f008 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 ed9ebfdf95..7c7073bf1f 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/409] 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 c04a3445b0..a2694cc9b2 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/409] 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 a8e0f4f008..4cbf23538a 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 7c7073bf1f..05a919011a 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/409] 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 a2694cc9b2..8fe131c6c0 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/409] :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 f4515eec8e..059a7282bb 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/409] 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 368b2d5759..2075d94559 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/409] 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 400c96c4d0..6901706915 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/409] 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 f592685546..533d74fb99 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/409] 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 67e8654a66..6142c3d2b5 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/409] 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 4ddf9c3728..601c084fa8 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/409] 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 e55fdae9ba..f047648334 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/409] 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 f047648334..99f46c723d 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/409] 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 99f46c723d..17c315542c 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/409] 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 17c315542c..05da51cf13 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/409] 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 533d74fb99..96a857158f 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/409] 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 96a857158f..1a392132b7 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/409] 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 05da51cf13..4d225ee49e 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/409] 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 601c084fa8..e097b2b41b 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 358934d6bc..af82d6da96 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 4d225ee49e..319568e6d4 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 fd182e4e50..14aef60080 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/409] 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 319568e6d4..dc2fcd1077 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/409] 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 e097b2b41b..4ddab2d384 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/409] 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 1a392132b7..9448f74beb 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/409] 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 6142c3d2b5..8e4a868573 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 9448f74beb..672ae15dee 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/409] =?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 8e4a868573..5cf9dd6b29 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/409] 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 5cf9dd6b29..bbb187bfa6 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/409] 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 987194ef82..2f295fdd4c 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/409] 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 c2bef5fe72..6f700e1c6c 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 b47b90f545..5c3356ece8 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 2f295fdd4c..3fa8b748c0 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/409] 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 ebda46ca87..acf3d354b5 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/409] 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 6cbba58939..c67a96e394 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/409] 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 6f700e1c6c..3226963b7d 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 76c98bccbc..d657dedb72 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 2a924f85f2..11b02e7533 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 3fa8b748c0..c4e2385e72 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/409] 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 d657dedb72..16f9165469 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 11b02e7533..585b1bf7e6 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 4df0625843..09414abb02 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/409] 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 3226963b7d..f11ffb4a51 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 5c3356ece8..620448b311 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 3c0a38f525..f866020cc3 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/409] 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 620448b311..00fb595011 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 585b1bf7e6..fdfa3de092 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 c67a96e394..6c992721ea 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 09414abb02..0726890900 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 f866020cc3..8719758fcf 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 0ee6dccbef..2de64df4df 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/409] 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 f11ffb4a51..7ec3e0a1bd 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/409] 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 00fb595011..b4c275d089 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 fdfa3de092..7e137843dc 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 8719758fcf..608eac6d16 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 2de64df4df..e20ebcc512 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/409] 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 b4c275d089..25f1deae90 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/409] 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 7ec3e0a1bd..083ac2b657 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 608eac6d16..7a64d74614 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/409] 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 7e137843dc..38d8e3a0eb 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/409] 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 083ac2b657..9dbd146c57 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/409] 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 16f9165469..494d663982 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 25f1deae90..eb2b022e17 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 38d8e3a0eb..1c3f380b25 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/409] 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 eb2b022e17..d406b35e44 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/409] 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 acf3d354b5..d6092f9a33 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 6c992721ea..38e4f9887d 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/409] 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 5622941a05..db41139e7b 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/409] 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 1c3f380b25..7416215839 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 e20ebcc512..b412df6c45 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/409] 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 6ee00e7df3..da4d819382 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 877fa76cfb..abc1739070 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 494d663982..7d7199f0bc 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/409] 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 d6092f9a33..7625ed485d 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 38e4f9887d..3f6bdc9885 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/409] 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 da4d819382..30c9a245e4 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 a1ad50a251..ebcd8171ab 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 37f1854de1..5acc2f6cd5 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 3f6bdc9885..670acf49c8 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 b5177297e0..0000000000 --- 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 858932cab7..7079614ddf 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 4f8ca64ad4..de957868a7 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/409] 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 7079614ddf..29b86f23ce 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/409] 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 d406b35e44..61045efab1 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 791b52174d..39f3bf598a 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/409] 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 61045efab1..c89eed6f00 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/409] 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 9dbd146c57..e35b31da2e 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/409] :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 30c9a245e4..eb6a8a7f8a 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 863c59a1e3..97ebccd24e 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 39f3bf598a..fe0ff5a4a9 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 08970a981d..6801b95262 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/409] 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 c89eed6f00..e4d30fda23 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/409] 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 fe0ff5a4a9..72c451e53d 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/409] 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 e35b31da2e..458aa9dbc4 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/409] 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 670acf49c8..600067d149 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/409] 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 e4d30fda23..730f0662d6 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/409] 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 458aa9dbc4..9ab2eca8c1 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/409] :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 a0fa33e21b..aff47bd9a6 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/409] 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 9ab2eca8c1..7e3c9c0e69 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 730f0662d6..de6dd978af 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/409] 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 600067d149..2a1df7781c 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/409] 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 7d7199f0bc..f6aee78c62 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 de6dd978af..e2640424fb 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 7416215839..223e549302 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/409] 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 2a1df7781c..d306baeb17 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/409] 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 d306baeb17..d72d7982de 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/409] 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 f6aee78c62..97e3b75b56 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 e2640424fb..02007ea785 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 223e549302..697381747e 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 b412df6c45..b3d0c857f1 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/409] 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 4c58f0475a..86c39ab3e2 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/409] 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 697381747e..d7e7a53391 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 b3d0c857f1..b6c923f1a8 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/409] 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 97e3b75b56..3f0ca221c3 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 02007ea785..e68725fc11 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/409] 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 e68725fc11..ca7450add7 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/409] 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 b6c923f1a8..e1dd627c6b 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/409] 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 d7e7a53391..20b0363bba 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 e1dd627c6b..2f7738eab7 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/409] 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 de957868a7..0524889884 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/409] 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 3f0ca221c3..7f7ce899dc 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 0726890900..0fe38ffcff 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/409] :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 29b86f23ce..f33be04ea2 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/409] :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 0524889884..de957868a7 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/409] 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 ca7450add7..9230ea3a2a 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/409] 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 ca7450add7..200e757569 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 7a64d74614..2e350cd561 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/409] 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 9230ea3a2a..ce38ccc42a 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/409] 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 40d4c09b21..3e40987e79 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/409] 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 5cd8fe293d..1c1071a31b 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/409] 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 1c1071a31b..9348ea779f 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/409] 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 9348ea779f..31ff53cf6d 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/409] 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 38118725cb..b8592892a0 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/409] :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 93255307d1..f1035d6709 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/409] :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 b8592892a0..eba9a85290 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/409] 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 f1035d6709..87f3ebdde7 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 20b0363bba..b2e4efd6ec 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/409] 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 b2e4efd6ec..d660309415 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/409] 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 aff47bd9a6..0000000000 --- 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 918c5e891c..2e3f73e6db 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 a3f88c8ba0..18e682fb87 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/409] 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 31ff53cf6d..cec5601795 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/409] 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 cec5601795..e1721eb08e 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/409] 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 87f3ebdde7..b56422b098 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 eba9a85290..22a803c9e9 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/409] 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 d660309415..3d2ee1ea10 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/409] 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 2e350cd561..7915983964 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/409] 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 b56422b098..3d44201a16 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/409] 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 b33b11f169..1feadd8e07 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/409] 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 3d44201a16..78003fd42f 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/409] 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 e550c176a4..15af0dcadd 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/409] 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 78003fd42f..7cd739343e 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/409] 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 7cd739343e..c038edca70 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 d72d7982de..ec16a26ae0 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/409] 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 7915983964..93875df0fe 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/409] =?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 de957868a7..9caa9fd6ca 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/409] 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 9caa9fd6ca..aed5aa1c90 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/409] 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 aed5aa1c90..9b6e0e62fd 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/409] 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 93875df0fe..41ad2b920a 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/409] 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 c038edca70..06a80953b5 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/409] 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 9b6e0e62fd..d9a408d5c9 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/409] 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 d9a408d5c9..a4d8c73afd 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/409] 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 ebcd8171ab..7ffeb2b38f 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/409] 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 a4d8c73afd..da471fa3c2 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/409] 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 da471fa3c2..b09df3b557 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/409] 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 7e3c9c0e69..35070215af 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 06a80953b5..736dbd8fa4 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 c31af1bbfa..9cb953ee48 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 41ad2b920a..1ea45887aa 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/409] 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 736dbd8fa4..1dd87a169a 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/409] 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 1dd87a169a..d44d44c33c 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/409] 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 0000000000..ea0efcf900 --- /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/409] 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 ea0efcf900..2ba04ccd0c 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/409] 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 2ba04ccd0c..e0e03dfef9 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/409] 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 e0e03dfef9..5add33f651 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/409] 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 1ea45887aa..8633e7d79b 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/409] 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 3d2ee1ea10..cc5cd9ef5f 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/409] 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 7f7ce899dc..2fa95ba2af 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/409] 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 5add33f651..3d23862b3e 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/409] 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 f33be04ea2..6987f1e567 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/409] :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 6987f1e567..6273b04cec 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/409] 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 6273b04cec..8669c1d6e9 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/409] :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 9cb953ee48..3b1775ec3e 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 8669c1d6e9..88086e3ccb 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/409] 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 b09df3b557..362cd7f337 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/409] 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 362cd7f337..642b0d2717 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/409] 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 2e3f73e6db..89f7255c6e 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/409] 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 89f7255c6e..629d573f93 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/409] 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 18e682fb87..64737bfa18 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/409] 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 629d573f93..63a42c5ef1 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/409] 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 64737bfa18..7d6f3ccd51 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/409] :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 7d6f3ccd51..11cffdec76 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/409] 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 3d23862b3e..10fe455c40 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/409] 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 10fe455c40..d36bdf69d1 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/409] 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 d36bdf69d1..6c0b44cf61 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/409] 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 6c0b44cf61..70380fec0b 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/409] 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 70380fec0b..e4264ee003 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/409] 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 e4264ee003..11b29eeb51 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/409] 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 11b29eeb51..116b249c0b 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/409] 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 116b249c0b..cfc78b20ed 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/409] 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 642b0d2717..b1fc26d8b5 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/409] 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 b1fc26d8b5..1f1931f82e 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/409] 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 cfc78b20ed..fa66f6486f 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/409] 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 fa66f6486f..70a25cd89d 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/409] 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 1f1931f82e..de0be90163 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/409] 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 70a25cd89d..828250ea73 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/409] 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 8633e7d79b..e3aa99513c 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/409] =?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 de0be90163..e5678d278f 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/409] 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 e5678d278f..5b9948d59d 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/409] 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 5b9948d59d..00a7676242 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/409] 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 00a7676242..ba6a328e71 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/409] 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 ba6a328e71..c5facd9bf9 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/409] 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 c5facd9bf9..f14dd56cec 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/409] 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 f14dd56cec..776f3ebd78 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/409] 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 d44d44c33c..a44860af83 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/409] 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 e3aa99513c..31a0a6b72f 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/409] :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 31a0a6b72f..110decc1c7 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/409] 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 88086e3ccb..8d6ab9ad95 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/409] 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 110decc1c7..e23bc01c98 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/409] 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 2f7738eab7..b5989ab3fe 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/409] 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 a44860af83..6d731db651 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/409] 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 993c5aa02b..90a9e9c58c 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/409] 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 90a9e9c58c..d6cd353dcb 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/409] 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 c4e2385e72..ae11008366 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/409] 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 dc2fcd1077..fafe8028a8 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 672ae15dee..f233f0ca40 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/409] 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 d6cd353dcb..f784d3da28 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/409] 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 f784d3da28..c713ad845b 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/409] 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 274f85c013..4733eb2476 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/409] 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 821983cbf5..d5718814fa 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/409] 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 7ffeb2b38f..6b7a19c6e0 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 db41139e7b..4ade80eaf6 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/409] 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 ae11008366..2933f218be 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/409] 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 2933f218be..d1bc6e1161 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/409] 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 ec16a26ae0..dbc0f8f440 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/409] 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 d5aa351483..4aedd51159 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/409] 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 dbc0f8f440..538825d7b5 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/409] 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 6b7a19c6e0..6c9b8b0707 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 538825d7b5..91a5e1d300 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/409] :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 35e55f883f..81c80e0a43 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/409] 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 776f3ebd78..7b71f591dd 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/409] 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 14aef60080..b4213f8217 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/409] 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 7b71f591dd..e16769805f 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/409] :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 e16769805f..5653d947fe 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/409] :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 5653d947fe..382c4e41bd 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/409] 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 4aedd51159..fdfa143ef4 100644 --- a/lib/views/commit-view.js +++ b/lib/views/commit-view.js @@ -164,7 +164,7 @@ export default class CommitView extends React.Component {
Date: Tue, 13 Nov 2018 16:05:43 -0500 Subject: [PATCH 313/409] :fire: unused import --- test/models/patch/multi-file-patch.test.js | 1 - 1 file changed, 1 deletion(-) diff --git a/test/models/patch/multi-file-patch.test.js b/test/models/patch/multi-file-patch.test.js index 663c452558..3ef8b38660 100644 --- a/test/models/patch/multi-file-patch.test.js +++ b/test/models/patch/multi-file-patch.test.js @@ -1,5 +1,4 @@ import dedent from 'dedent-js'; -import {Point} from 'atom'; import {multiFilePatchBuilder, filePatchBuilder} from '../../builder/patch'; From c068bbf94e34667ea78a556057a06810aca46875 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 13 Nov 2018 16:20:06 -0500 Subject: [PATCH 314/409] Accept a pending argument in openFile action method --- lib/controllers/multi-file-patch-controller.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/controllers/multi-file-patch-controller.js b/lib/controllers/multi-file-patch-controller.js index 1f4794c431..247f802c2a 100644 --- a/lib/controllers/multi-file-patch-controller.js +++ b/lib/controllers/multi-file-patch-controller.js @@ -109,9 +109,9 @@ export default class MultiFilePatchController extends React.Component { return this.props.surfaceFileAtPath(filePatch.getPath(), this.props.stagingStatus); } - async openFile(filePatch, positions) { + async openFile(filePatch, positions, pending) { const absolutePath = path.join(this.props.repository.getWorkingDirectoryPath(), filePatch.getPath()); - const editor = await this.props.workspace.open(absolutePath, {pending: true}); + const editor = await this.props.workspace.open(absolutePath, {pending}); if (positions.length > 0) { editor.setCursorBufferPosition(positions[0], {autoscroll: false}); for (const position of positions.slice(1)) { From b653c0fb706e8a67ee36b465ab4fc4a0432b96d5 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 13 Nov 2018 16:20:30 -0500 Subject: [PATCH 315/409] Unit tests for opening multiple files --- lib/views/multi-file-patch-view.js | 5 +- test/views/multi-file-patch-view.test.js | 58 +++++++++++++++++------- 2 files changed, 44 insertions(+), 19 deletions(-) diff --git a/lib/views/multi-file-patch-view.js b/lib/views/multi-file-patch-view.js index d189c15da0..f33573c5d1 100644 --- a/lib/views/multi-file-patch-view.js +++ b/lib/views/multi-file-patch-view.js @@ -905,9 +905,10 @@ export default class MultiFilePatchView extends React.Component { return null; }); - return Promise.all(Array.from(cursorsByFilePatch).map(value => { + const pending = cursorsByFilePatch.size === 1; + return Promise.all(Array.from(cursorsByFilePatch, value => { const [filePatch, cursors] = value; - return this.props.openFile(filePatch, cursors); + return this.props.openFile(filePatch, cursors, pending); })); } diff --git a/test/views/multi-file-patch-view.test.js b/test/views/multi-file-patch-view.test.js index e1851f46ca..ca8b85280a 100644 --- a/test/views/multi-file-patch-view.test.js +++ b/test/views/multi-file-patch-view.test.js @@ -1293,24 +1293,32 @@ describe('MultiFilePatchView', function() { }); }); - describe('opening the file when there is only one file patch', function() { + describe('jump to file', function() { let mfp, fp; beforeEach(function() { - 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'); - }); - filePatch.addHunk(h => { - h.oldRow(10); - h.unchanged('0003').added('0004', '0005').deleted('0006').unchanged('0007').added('0008').deleted('0009').unchanged('0010'); - }); - }).build(); + 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'); + }); + filePatch.addHunk(h => { + h.oldRow(10); + h.unchanged('0003').added('0004', '0005').deleted('0006').unchanged('0007').added('0008').deleted('0009').unchanged('0010'); + }); + }) + .addFilePatch(filePatch => { + filePatch.setOldFile(f => f.path('other.txt')); + filePatch.addHunk(h => { + h.oldRow(10); + h.unchanged('0011').added('0012').unchanged('0013'); + }); + }) + .build(); mfp = multiFilePatch; - assert.lengthOf(mfp.getFilePatches(), 1); fp = mfp.getFilePatches()[0]; }); @@ -1322,7 +1330,7 @@ describe('MultiFilePatchView', function() { editor.setCursorBufferPosition([7, 2]); atomEnv.commands.dispatch(wrapper.getDOMNode(), 'github:jump-to-file'); - assert.isTrue(openFile.calledWith(fp, [[13, 2]])); + assert.isTrue(openFile.calledWith(fp, [[13, 2]], true)); }); it('opens the file at a current added row', function() { @@ -1334,7 +1342,7 @@ describe('MultiFilePatchView', function() { atomEnv.commands.dispatch(wrapper.getDOMNode(), 'github:jump-to-file'); - assert.isTrue(openFile.calledWith(fp, [[14, 3]])); + assert.isTrue(openFile.calledWith(fp, [[14, 3]], true)); }); it('opens the file at the beginning of the previous added or unchanged row', function() { @@ -1346,7 +1354,7 @@ describe('MultiFilePatchView', function() { atomEnv.commands.dispatch(wrapper.getDOMNode(), 'github:jump-to-file'); - assert.isTrue(openFile.calledWith(fp, [[15, 0]])); + assert.isTrue(openFile.calledWith(fp, [[15, 0]], true)); }); it('preserves multiple cursors', function() { @@ -1369,7 +1377,23 @@ describe('MultiFilePatchView', function() { [11, 2], [2, 3], [15, 0], - ])); + ], true)); + }); + + it('opens non-pending editors when opening multiple', function() { + const openFile = sinon.spy(); + const wrapper = mount(buildApp({multiFilePatch: mfp, openFile})); + + const editor = wrapper.find('AtomTextEditor').instance().getModel(); + editor.setSelectedBufferRanges([ + [[4, 0], [4, 0]], + [[12, 0], [12, 0]], + ]); + + atomEnv.commands.dispatch(wrapper.getDOMNode(), 'github:jump-to-file'); + + assert.isTrue(openFile.calledWith(mfp.getFilePatches()[0], [[11, 0]], false)); + assert.isTrue(openFile.calledWith(mfp.getFilePatches()[1], [[10, 0]], false)); }); }); }); From 65a8c222c4c34fbbe2572e1bbbe26ed5e893ee97 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 13 Nov 2018 16:28:18 -0500 Subject: [PATCH 316/409] Update file spanning state when manipulating selected rows manually --- lib/controllers/multi-file-patch-controller.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/controllers/multi-file-patch-controller.js b/lib/controllers/multi-file-patch-controller.js index 247f802c2a..a74f8ebe48 100644 --- a/lib/controllers/multi-file-patch-controller.js +++ b/lib/controllers/multi-file-patch-controller.js @@ -132,7 +132,8 @@ export default class MultiFilePatchController extends React.Component { async toggleRows(rowSet, nextSelectionMode) { let chosenRows = rowSet; if (chosenRows) { - await this.selectedRowsChanged(chosenRows, nextSelectionMode); + const nextMultipleFileSelections = this.props.multiFilePatch.spansMultipleFiles(chosenRows); + await this.selectedRowsChanged(chosenRows, nextSelectionMode, nextMultipleFileSelections); } else { chosenRows = this.state.selectedRows; } @@ -194,7 +195,8 @@ export default class MultiFilePatchController extends React.Component { let chosenRows = rowSet; if (chosenRows) { - await this.selectedRowsChanged(chosenRows, nextSelectionMode); + const nextMultipleFileSelections = this.props.multiFilePatch.spansMultipleFiles(chosenRows); + await this.selectedRowsChanged(chosenRows, nextSelectionMode, nextMultipleFileSelections); } else { chosenRows = this.state.selectedRows; } From a367b787a259e4ee1ed64fe9bfb572c223f4c2e1 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Tue, 13 Nov 2018 16:22:32 -0800 Subject: [PATCH 317/409] Jump to correct file if there is no diff selection for file --- lib/views/multi-file-patch-view.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/views/multi-file-patch-view.js b/lib/views/multi-file-patch-view.js index f33573c5d1..9de3b4ede9 100644 --- a/lib/views/multi-file-patch-view.js +++ b/lib/views/multi-file-patch-view.js @@ -320,7 +320,7 @@ export default class MultiFilePatchView extends React.Component { undoLastDiscard={() => this.undoLastDiscardFromButton(filePatch)} diveIntoMirrorPatch={() => this.props.diveIntoMirrorPatch(filePatch)} - openFile={this.didOpenFile} + openFile={() => this.didOpenFile(filePatch)} toggleFile={() => this.props.toggleFile(filePatch)} /> @@ -848,7 +848,7 @@ export default class MultiFilePatchView extends React.Component { }); } - didOpenFile() { + didOpenFile(selectedFilePatch) { const cursorsByFilePatch = new Map(); this.refEditor.map(editor => { @@ -905,6 +905,11 @@ export default class MultiFilePatchView extends React.Component { return null; }); + const filePatchesWithCursors = new Set(cursorsByFilePatch.values()); + if (!filePatchesWithCursors.has(selectedFilePatch)) { + return this.props.openFile(selectedFilePatch, [], {pending: true}); + } + const pending = cursorsByFilePatch.size === 1; return Promise.all(Array.from(cursorsByFilePatch, value => { const [filePatch, cursors] = value; From 7a18876891068a242f014ad606f5943c458c84ea Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Tue, 13 Nov 2018 16:25:29 -0800 Subject: [PATCH 318/409] Jump to first changed line Or more specifically, first line in hunk, which is often a context line --- 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 9de3b4ede9..a0773a0d7e 100644 --- a/lib/views/multi-file-patch-view.js +++ b/lib/views/multi-file-patch-view.js @@ -907,7 +907,9 @@ export default class MultiFilePatchView extends React.Component { const filePatchesWithCursors = new Set(cursorsByFilePatch.values()); if (!filePatchesWithCursors.has(selectedFilePatch)) { - return this.props.openFile(selectedFilePatch, [], {pending: true}); + const [firstHunk] = selectedFilePatch.getHunks(); + const cursorRow = firstHunk ? firstHunk.getNewStartRow() - 1 : 0; + return this.props.openFile(selectedFilePatch, [[cursorRow, 0]], {pending: true}); } const pending = cursorsByFilePatch.size === 1; From 7bd749cd505f8ce8dd696b0cdec402b54722bff1 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Tue, 13 Nov 2018 16:26:27 -0800 Subject: [PATCH 319/409] Drop the rest in an `else` clause --- lib/views/multi-file-patch-view.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/views/multi-file-patch-view.js b/lib/views/multi-file-patch-view.js index a0773a0d7e..6a1bc9475d 100644 --- a/lib/views/multi-file-patch-view.js +++ b/lib/views/multi-file-patch-view.js @@ -910,14 +910,14 @@ export default class MultiFilePatchView extends React.Component { const [firstHunk] = selectedFilePatch.getHunks(); const cursorRow = firstHunk ? firstHunk.getNewStartRow() - 1 : 0; return this.props.openFile(selectedFilePatch, [[cursorRow, 0]], {pending: true}); + } else { + const pending = cursorsByFilePatch.size === 1; + return Promise.all(Array.from(cursorsByFilePatch, value => { + const [filePatch, cursors] = value; + return this.props.openFile(filePatch, cursors, pending); + })); } - const pending = cursorsByFilePatch.size === 1; - return Promise.all(Array.from(cursorsByFilePatch, value => { - const [filePatch, cursors] = value; - return this.props.openFile(filePatch, cursors, pending); - })); - } getSelectedRows() { From 1685f11732f4636f754500ac31b421da3f3ec53b Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Tue, 13 Nov 2018 16:34:02 -0800 Subject: [PATCH 320/409] Check for existence of `selectedFilePatch` before accessing methods on it --- 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 6a1bc9475d..564225959b 100644 --- a/lib/views/multi-file-patch-view.js +++ b/lib/views/multi-file-patch-view.js @@ -906,7 +906,7 @@ export default class MultiFilePatchView extends React.Component { }); const filePatchesWithCursors = new Set(cursorsByFilePatch.values()); - if (!filePatchesWithCursors.has(selectedFilePatch)) { + if (selectedFilePatch && !filePatchesWithCursors.has(selectedFilePatch)) { const [firstHunk] = selectedFilePatch.getHunks(); const cursorRow = firstHunk ? firstHunk.getNewStartRow() - 1 : 0; return this.props.openFile(selectedFilePatch, [[cursorRow, 0]], {pending: true}); From bed19dc744f2844e789c6d8c8ebb05570440c825 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Tue, 13 Nov 2018 16:37:13 -0800 Subject: [PATCH 321/409] Don't mistake filePatch param with command event --- lib/views/multi-file-patch-view.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/views/multi-file-patch-view.js b/lib/views/multi-file-patch-view.js index 564225959b..26bc45fd1e 100644 --- a/lib/views/multi-file-patch-view.js +++ b/lib/views/multi-file-patch-view.js @@ -320,7 +320,7 @@ export default class MultiFilePatchView extends React.Component { undoLastDiscard={() => this.undoLastDiscardFromButton(filePatch)} diveIntoMirrorPatch={() => this.props.diveIntoMirrorPatch(filePatch)} - openFile={() => this.didOpenFile(filePatch)} + openFile={() => this.didOpenFile({selectedFilePatch: filePatch})} toggleFile={() => this.props.toggleFile(filePatch)} /> @@ -848,7 +848,7 @@ export default class MultiFilePatchView extends React.Component { }); } - didOpenFile(selectedFilePatch) { + didOpenFile({selectedFilePatch} = {}) { const cursorsByFilePatch = new Map(); this.refEditor.map(editor => { From 2a70de1d1064978e7ae9b742c7178c3e5aeaecfc Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Tue, 13 Nov 2018 16:46:59 -0800 Subject: [PATCH 322/409] Oops we want to be getting the keys of `cursorsByFilePatch` --- 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 26bc45fd1e..5b615d8910 100644 --- a/lib/views/multi-file-patch-view.js +++ b/lib/views/multi-file-patch-view.js @@ -905,7 +905,7 @@ export default class MultiFilePatchView extends React.Component { return null; }); - const filePatchesWithCursors = new Set(cursorsByFilePatch.values()); + const filePatchesWithCursors = new Set(cursorsByFilePatch.keys()); if (selectedFilePatch && !filePatchesWithCursors.has(selectedFilePatch)) { const [firstHunk] = selectedFilePatch.getHunks(); const cursorRow = firstHunk ? firstHunk.getNewStartRow() - 1 : 0; From 62be5a4bc6fb380f124dce41827e26175d39256f Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Tue, 13 Nov 2018 17:06:30 -0800 Subject: [PATCH 323/409] Pass the right argument for pending panes --- 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 5b615d8910..ae473fcfd6 100644 --- a/lib/views/multi-file-patch-view.js +++ b/lib/views/multi-file-patch-view.js @@ -909,7 +909,7 @@ export default class MultiFilePatchView extends React.Component { if (selectedFilePatch && !filePatchesWithCursors.has(selectedFilePatch)) { const [firstHunk] = selectedFilePatch.getHunks(); const cursorRow = firstHunk ? firstHunk.getNewStartRow() - 1 : 0; - return this.props.openFile(selectedFilePatch, [[cursorRow, 0]], {pending: true}); + return this.props.openFile(selectedFilePatch, [[cursorRow, 0]], true); } else { const pending = cursorsByFilePatch.size === 1; return Promise.all(Array.from(cursorsByFilePatch, value => { From 18a08d9cc4ce1c85514f6e80046e8fcfcd3a7efc Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Tue, 13 Nov 2018 17:09:54 -0800 Subject: [PATCH 324/409] Add tests for jumping to file from header button --- test/views/multi-file-patch-view.test.js | 37 ++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/test/views/multi-file-patch-view.test.js b/test/views/multi-file-patch-view.test.js index ca8b85280a..23d4b3f490 100644 --- a/test/views/multi-file-patch-view.test.js +++ b/test/views/multi-file-patch-view.test.js @@ -1395,6 +1395,43 @@ describe('MultiFilePatchView', function() { assert.isTrue(openFile.calledWith(mfp.getFilePatches()[0], [[11, 0]], false)); assert.isTrue(openFile.calledWith(mfp.getFilePatches()[1], [[10, 0]], false)); }); + + describe('didOpenFile(selectedFilePatch)', function() { + describe('when there is a selection in the selectedFilePatch', function() { + it('opens the file and places the cursor corresponding to the selection', function() { + const openFile = sinon.spy(); + const wrapper = mount(buildApp({multiFilePatch: mfp, openFile})); + + const editor = wrapper.find('AtomTextEditor').instance().getModel(); + editor.setSelectedBufferRanges([ + [[4, 0], [4, 0]], // cursor in first file patch + ]); + + const firstFilePatch = mfp.getFilePatches()[0]; + wrapper.instance().didOpenFile({selectedFilePatch: firstFilePatch}); + + assert.isTrue(openFile.calledWith(firstFilePatch, [[11, 0]], true)); + }); + }); + + describe('when there are no selections in the selectedFilePatch', function() { + it('opens the file and places the cursor at the beginning of the first hunk', function() { + const openFile = sinon.spy(); + const wrapper = mount(buildApp({multiFilePatch: mfp, openFile})); + + const editor = wrapper.find('AtomTextEditor').instance().getModel(); + editor.setSelectedBufferRanges([ + [[4, 0], [4, 0]], // cursor in first file patch + ]); + + const secondFilePatch = mfp.getFilePatches()[1]; + const firstHunkBufferRow = secondFilePatch.getHunks()[0].getNewStartRow() - 1; + wrapper.instance().didOpenFile({selectedFilePatch: secondFilePatch}); + + assert.isTrue(openFile.calledWith(secondFilePatch, [[firstHunkBufferRow, 0]], true)); + }); + }); + }); }); }); }); From 4fe18c5928c202b639084ef19dd300b1ad393d1e Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Tue, 13 Nov 2018 17:46:36 -0800 Subject: [PATCH 325/409] :art: test to get us back to 100% test coverage of new lines --- lib/views/file-patch-header-view.js | 2 +- test/views/multi-file-patch-view.test.js | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/views/file-patch-header-view.js b/lib/views/file-patch-header-view.js index b715423a3e..b7d83f1fab 100644 --- a/lib/views/file-patch-header-view.js +++ b/lib/views/file-patch-header-view.js @@ -107,7 +107,7 @@ export default class FilePatchHeaderView extends React.Component { return ( - Date: Tue, 13 Nov 2018 21:44:56 -0500 Subject: [PATCH 326/409] :shirt: --- lib/views/file-patch-header-view.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/views/file-patch-header-view.js b/lib/views/file-patch-header-view.js index b7d83f1fab..7b37cf810f 100644 --- a/lib/views/file-patch-header-view.js +++ b/lib/views/file-patch-header-view.js @@ -107,7 +107,10 @@ export default class FilePatchHeaderView extends React.Component { return ( - Date: Wed, 14 Nov 2018 13:08:37 -0500 Subject: [PATCH 327/409] Delete extraneous test that slipped in with the merge conflict --- .../controllers/file-patch-controller.test.js | 460 ------------------ 1 file changed, 460 deletions(-) delete mode 100644 test/controllers/file-patch-controller.test.js diff --git a/test/controllers/file-patch-controller.test.js b/test/controllers/file-patch-controller.test.js deleted file mode 100644 index 2ea7d2d90e..0000000000 --- a/test/controllers/file-patch-controller.test.js +++ /dev/null @@ -1,460 +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 FilePatch from '../../lib/models/patch/file-patch'; -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'); - - // No-op - assert.isNull(await wrapper.find('FilePatchView').prop('toggleFile')()); - - // Simulate an identical patch arriving too soon - wrapper.setProps({filePatch: filePatch.clone()}); - - // Still a no-op - assert.isNull(await wrapper.find('FilePatchView').prop('toggleFile')()); - - // Simulate updated patch arrival - const promise = wrapper.instance().patchChangePromise; - wrapper.setProps({filePatch: FilePatch.createNull()}); - await promise; - - // Performs an operation again - assert.strictEqual(await wrapper.find('FilePatchView').prop('toggleFile')(), 'staged'); - }); - }); - - 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() { - if (process.env.ATOM_GITHUB_SKIP_SYMLINKS) { - this.skip(); - return; - } - - 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() { - if (process.env.ATOM_GITHUB_SKIP_SYMLINKS) { - this.skip(); - return; - } - - 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'); - }); -}); From 5e123b5c5d8657784d38f7986bd0bce1b8a5f8eb Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 14 Nov 2018 13:08:59 -0500 Subject: [PATCH 328/409] Move test changes into MultiFilePatchController --- .../multi-file-patch-controller.test.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/test/controllers/multi-file-patch-controller.test.js b/test/controllers/multi-file-patch-controller.test.js index 4c601e76fc..14a1209a1e 100644 --- a/test/controllers/multi-file-patch-controller.test.js +++ b/test/controllers/multi-file-patch-controller.test.js @@ -4,6 +4,7 @@ import React from 'react'; import {shallow} from 'enzyme'; import MultiFilePatchController from '../../lib/controllers/multi-file-patch-controller'; +import MultiFilePatch from '../../lib/models/patch/multi-file-patch'; import * as reporterProxy from '../../lib/reporter-proxy'; import {multiFilePatchBuilder} from '../builder/patch'; import {cloneRepository, buildRepository} from '../helpers'; @@ -154,14 +155,22 @@ describe('MultiFilePatchController', function() { const wrapper = shallow(buildApp({stagingStatus: 'unstaged'})); assert.strictEqual(await wrapper.find('MultiFilePatchView').prop('toggleFile')(filePatch), 'staged'); - wrapper.setProps({stagingStatus: 'staged'}); + // No-op assert.isNull(await wrapper.find('MultiFilePatchView').prop('toggleFile')(filePatch)); - const promise = wrapper.instance().patchChangePromise; + // Simulate an identical patch arriving too soon wrapper.setProps({multiFilePatch: multiFilePatch.clone()}); + + // Still a no-op + assert.isNull(await wrapper.find('MultiFilePatchView').prop('toggleFile')(filePatch)); + + // Simulate updated patch arrival + const promise = wrapper.instance().patchChangePromise; + wrapper.setProps({multiFilePatch: new MultiFilePatch({})}); await promise; - assert.strictEqual(await wrapper.find('MultiFilePatchView').prop('toggleFile')(filePatch), 'unstaged'); + // Performs an operation again + assert.strictEqual(await wrapper.find('MultiFilePatchView').prop('toggleFile')(filePatch), 'staged'); }); }); From 0adec32e79d245fdd9f7cff46852306dc6af1742 Mon Sep 17 00:00:00 2001 From: Tilde Ann Thurium Date: Wed, 14 Nov 2018 13:24:00 -0800 Subject: [PATCH 329/409] Fix error where getFilePath is not a function on a CommitPreviewItem Just early return if it's not a ChangedFileItem, to avoid doing the check in the first place. Co-Authored-By: Katrina Uychaco --- lib/views/staging-view.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/views/staging-view.js b/lib/views/staging-view.js index fb4587ea0a..8629ff8953 100644 --- a/lib/views/staging-view.js +++ b/lib/views/staging-view.js @@ -762,11 +762,13 @@ export default class StagingView extends React.Component { const pendingItem = pane.getPendingItem(); if (!pendingItem || !pendingItem.getRealItem) { return false; } const realItem = pendingItem.getRealItem(); - const isDiffViewItem = realItem instanceof ChangedFileItem; + if (!(realItem instanceof ChangedFileItem)) { + return false; + } // We only want to update pending diff views for currently active repo const isInActiveRepo = realItem.getWorkingDirectory() === this.props.workingDirectoryPath; const isStale = !this.changedFileExists(realItem.getFilePath(), realItem.getStagingStatus()); - return isDiffViewItem && isInActiveRepo && isStale; + return isInActiveRepo && isStale; }); } From 3a62b791f3d9bbf54da2b4044558a0386ad5554a Mon Sep 17 00:00:00 2001 From: Tilde Ann Thurium Date: Wed, 14 Nov 2018 13:33:55 -0800 Subject: [PATCH 330/409] props only work if you pass them to child components Co-Authored-By: Katrina Uychaco --- lib/containers/changed-file-container.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/containers/changed-file-container.js b/lib/containers/changed-file-container.js index eb6a8a7f8a..d4661a8ac1 100644 --- a/lib/containers/changed-file-container.js +++ b/lib/containers/changed-file-container.js @@ -52,10 +52,13 @@ export default class ChangedFileContainer extends React.Component { return ; } + const {multiFilePatch, hasUndoHistory, isPartiallyStaged} = data; + return ( ); From bc58a8ab66867a585696366ca6d7d34bd82d9052 Mon Sep 17 00:00:00 2001 From: Tilde Ann Thurium Date: Wed, 14 Nov 2018 13:45:17 -0800 Subject: [PATCH 331/409] :art: Make UI more consistent Co-Authored-By: Katrina Uychaco --- lib/views/file-patch-header-view.js | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/lib/views/file-patch-header-view.js b/lib/views/file-patch-header-view.js index 7b37cf810f..a01e01c5f1 100644 --- a/lib/views/file-patch-header-view.js +++ b/lib/views/file-patch-header-view.js @@ -3,7 +3,6 @@ import PropTypes from 'prop-types'; import cx from 'classnames'; import RefHolder from '../models/ref-holder'; -import Tooltip from '../atom/tooltip'; export default class FilePatchHeaderView extends React.Component { static propTypes = { @@ -76,11 +75,11 @@ export default class FilePatchHeaderView extends React.Component { const attrs = this.props.stagingStatus === 'unstaged' ? { iconClass: 'icon-tasklist', - tooltipText: 'View staged changes', + buttonText: 'View Staged', } : { iconClass: 'icon-list-unordered', - tooltipText: 'View unstaged changes', + buttonText: 'View Unstaged', }; return ( @@ -88,19 +87,15 @@ export default class FilePatchHeaderView extends React.Component { ); } renderOpenFileButton() { - let buttonText = 'Jump to file'; + let buttonText = 'Jump To File'; if (this.props.hasMultipleFileSelections) { buttonText += 's'; } @@ -113,11 +108,6 @@ export default class FilePatchHeaderView extends React.Component { onClick={this.props.openFile}> {buttonText} - ); } From f49d4db56c62e88c20374b748733778dcce5433d Mon Sep 17 00:00:00 2001 From: Tilde Ann Thurium Date: Wed, 14 Nov 2018 16:10:43 -0800 Subject: [PATCH 332/409] we don't need to test for no stinkin tooltips --- test/views/file-patch-header-view.test.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/test/views/file-patch-header-view.test.js b/test/views/file-patch-header-view.test.js index 93d647748b..d626ecfee0 100644 --- a/test/views/file-patch-header-view.test.js +++ b/test/views/file-patch-header-view.test.js @@ -76,8 +76,6 @@ describe('FilePatchHeaderView', function() { wrapper.find(`button.${buttonClass}`).simulate('click'); assert.isTrue(diveIntoMirrorPatch.called, `${buttonClass} click did nothing`); - - assert.isTrue(wrapper.find('Tooltip').someWhere(n => n.prop('title') === tooltip)); }; } @@ -128,12 +126,12 @@ describe('FilePatchHeaderView', function() { it('is singular when selections exist within a single file patch', function() { const wrapper = shallow(buildApp({hasMultipleFileSelections: false})); - assert.strictEqual(wrapper.find('button.icon-code').text(), 'Jump to file'); + assert.strictEqual(wrapper.find('button.icon-code').text(), 'Jump To File'); }); it('is plural when selections exist within multiple file patches', function() { const wrapper = shallow(buildApp({hasMultipleFileSelections: true})); - assert.strictEqual(wrapper.find('button.icon-code').text(), 'Jump to files'); + assert.strictEqual(wrapper.find('button.icon-code').text(), 'Jump To Files'); }); }); From 1fc35c4c3546ccf3cbaabec6a1d8b5d2b1d6e96d Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Wed, 14 Nov 2018 18:12:36 -0800 Subject: [PATCH 333/409] Explicitly only render Undo Discard button if ChangedFileItem --- lib/items/changed-file-item.js | 1 + lib/items/commit-preview-item.js | 1 + lib/views/file-patch-header-view.js | 10 ++++++++-- lib/views/multi-file-patch-view.js | 1 + 4 files changed, 11 insertions(+), 2 deletions(-) diff --git a/lib/items/changed-file-item.js b/lib/items/changed-file-item.js index 57ddc3302b..bff241401c 100644 --- a/lib/items/changed-file-item.js +++ b/lib/items/changed-file-item.js @@ -78,6 +78,7 @@ export default class ChangedFileItem extends React.Component { return ( Date: Wed, 14 Nov 2018 18:14:38 -0800 Subject: [PATCH 334/409] Explicitly only render Undo Discard button if ChangedFileItem --- lib/views/file-patch-header-view.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/lib/views/file-patch-header-view.js b/lib/views/file-patch-header-view.js index 20bb1e2e27..5d7bbd6eda 100644 --- a/lib/views/file-patch-header-view.js +++ b/lib/views/file-patch-header-view.js @@ -62,15 +62,16 @@ export default class FilePatchHeaderView extends React.Component { } renderUndoDiscardButton() { - if (!this.props.hasUndoHistory || this.props.stagingStatus !== 'unstaged') { + const unstagedChangedFileItem = this.props.itemType === ChangedFileItem && this.props.stagingStatus === 'unstaged'; + if (unstagedChangedFileItem && this.props.hasUndoHistory) { + return ( + + ); + } else { return null; } - - return ( - - ); } renderMirrorPatchButton() { From 1f685962622e29bdb37da2253a29161e3a2c0bed Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Wed, 14 Nov 2018 18:15:12 -0800 Subject: [PATCH 335/409] Add `itemType` prop types for :shirt: --- lib/views/file-patch-header-view.js | 2 ++ lib/views/multi-file-patch-view.js | 3 +++ 2 files changed, 5 insertions(+) diff --git a/lib/views/file-patch-header-view.js b/lib/views/file-patch-header-view.js index 5d7bbd6eda..348d5efbd2 100644 --- a/lib/views/file-patch-header-view.js +++ b/lib/views/file-patch-header-view.js @@ -21,6 +21,8 @@ export default class FilePatchHeaderView extends React.Component { diveIntoMirrorPatch: PropTypes.func.isRequired, openFile: PropTypes.func.isRequired, toggleFile: PropTypes.func.isRequired, + + itemType: PropTypes.oneOf([ChangedFileItem, CommitPreviewItem]).isRequired, }; constructor(props) { diff --git a/lib/views/multi-file-patch-view.js b/lib/views/multi-file-patch-view.js index 14e2385b5b..4f0fdb015c 100644 --- a/lib/views/multi-file-patch-view.js +++ b/lib/views/multi-file-patch-view.js @@ -16,6 +16,8 @@ import FilePatchHeaderView from './file-patch-header-view'; import FilePatchMetaView from './file-patch-meta-view'; import HunkHeaderView from './hunk-header-view'; import RefHolder from '../models/ref-holder'; +import ChangedFileItem from '../items/changed-file-item'; +import CommitPreviewItem from '../items/commit-preview-item'; const executableText = { 100644: 'non executable', @@ -57,6 +59,7 @@ export default class MultiFilePatchView extends React.Component { discardRows: PropTypes.func.isRequired, refInitialFocus: RefHolderPropType, + itemType: PropTypes.oneOf([ChangedFileItem, CommitPreviewItem]).isRequired, } static defaultProps = { From 4b53ba75dfcd3d7091baf76b9efa11a20c680bd6 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Wed, 14 Nov 2018 18:41:28 -0800 Subject: [PATCH 336/409] Make basename bold in FilePatchHeaderView --- lib/views/file-patch-header-view.js | 25 +++++++++++++++++++++++-- styles/file-patch-view.less | 4 ++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/lib/views/file-patch-header-view.js b/lib/views/file-patch-header-view.js index 348d5efbd2..b157e70230 100644 --- a/lib/views/file-patch-header-view.js +++ b/lib/views/file-patch-header-view.js @@ -1,3 +1,5 @@ +import path from 'path'; + import React, {Fragment} from 'react'; import PropTypes from 'prop-types'; import cx from 'classnames'; @@ -46,9 +48,28 @@ export default class FilePatchHeaderView extends React.Component { renderTitle() { if (this.props.itemType === ChangedFileItem) { const status = this.props.stagingStatus; - return `${status[0].toUpperCase()}${status.slice(1)} Changes for ${this.props.relPath}`; + return ( + {status[0].toUpperCase()}{status.slice(1)} Changes for {this.renderPath()} + ); } else { - return this.props.relPath; + return this.renderPath(); + } + } + + renderPath() { + const dirname = path.dirname(this.props.relPath); + const basename = path.basename(this.props.relPath); + + if (dirname === '.') { + return {basename}; + } else { + // TODO: double check that the forward-slash works cross-platform... + // iirc git always uses `/` for paths, even on Windows + return ( + + {dirname}/{basename} + + ); } } diff --git a/styles/file-patch-view.less b/styles/file-patch-view.less index 63a42c5ef1..f20069c650 100644 --- a/styles/file-patch-view.less +++ b/styles/file-patch-view.less @@ -261,3 +261,7 @@ } } } + +.gitub-FilePatchHeaderView-basename { + font-weight: bold; +} From ea07e4d8aa976777a6e508d40462fee3207dab05 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Wed, 14 Nov 2018 18:50:30 -0800 Subject: [PATCH 337/409] Fix tests to include itemType --- test/views/file-patch-header-view.test.js | 30 +++++++++++++++++------ 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/test/views/file-patch-header-view.test.js b/test/views/file-patch-header-view.test.js index d626ecfee0..8e8d514e66 100644 --- a/test/views/file-patch-header-view.test.js +++ b/test/views/file-patch-header-view.test.js @@ -2,6 +2,7 @@ import React from 'react'; import {shallow} from 'enzyme'; import FilePatchHeaderView from '../../lib/views/file-patch-header-view'; +import ChangedFileItem from '../../lib/items/changed-file-item'; describe('FilePatchHeaderView', function() { let atomEnv; @@ -17,6 +18,7 @@ describe('FilePatchHeaderView', function() { function buildApp(overrideProps = {}) { return ( Date: Wed, 14 Nov 2018 19:54:15 -0800 Subject: [PATCH 338/409] Update docs/react-component-atlas.md Co-Authored-By: annthurium --- docs/react-component-atlas.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/react-component-atlas.md b/docs/react-component-atlas.md index 81c80e0a43..7341d3cec0 100644 --- a/docs/react-component-atlas.md +++ b/docs/react-component-atlas.md @@ -75,7 +75,7 @@ This is a high-level overview of the structure of the React component tree that > > [``](/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. > > > > > From 989c90ddf4f39725299f23a5ee2c323149ac838b Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Thu, 15 Nov 2018 11:16:00 -0500 Subject: [PATCH 339/409] Update docs/react-component-atlas.md Co-Authored-By: smashwilson --- docs/react-component-atlas.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/react-component-atlas.md b/docs/react-component-atlas.md index 7341d3cec0..d40cd142de 100644 --- a/docs/react-component-atlas.md +++ b/docs/react-component-atlas.md @@ -37,7 +37,7 @@ This is a high-level overview of the structure of the React component tree that > > > [``](/lig/items/commit-preview-item.js) > > > [``](/lib/containers/commit-preview-container.js) > > > -> > > Allows users to view all unstaged commits in one pane. +> > > The workspace-center pane that appears when looking at _all_ the staged changes that will be going into the next commit. > > > > > > [``](/lib/views/remote-selector-view.js) > > > From a16e7f2f049caaa98d7ae914059138b9416372c1 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 15 Nov 2018 11:38:34 -0500 Subject: [PATCH 340/409] :world: CommitPreviewItem is beneath RootController, not GitHubTabItem Separate MultiFilePatchController and MultiFilePatchView and replicate them --- docs/react-component-atlas.md | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/docs/react-component-atlas.md b/docs/react-component-atlas.md index d40cd142de..a4ae15f3e2 100644 --- a/docs/react-component-atlas.md +++ b/docs/react-component-atlas.md @@ -34,11 +34,6 @@ 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) -> > > -> > > The workspace-center pane that appears when looking at _all_ the staged changes that will be going into the next commit. -> > > > > > [``](/lib/views/remote-selector-view.js) > > > > > > Shown if the current repository has more than one remote that's identified as a github.com remote. @@ -71,13 +66,24 @@ 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/containers/changed-file-container.js) -> > [``](/lib/controllers/multi-file-patch-controller.js) -> > [``](/lib/views/multi-file-patch-view.js) +> > [``](/lib/items/changed-file-item.js) +> > [``](/lib/containers/changed-file-container.js) > > > > The workspace-center pane that appears when looking at the staged or unstaged changes associated with a file. > > +> > > [``](/lib/controllers/multi-file-patch-controller.js) +> > > [``](/lib/views/multi-file-patch-view.js) +> > > +> > > Render a sequence of git-generated file patches within a TextEditor, using decorations to include contextually +> > > relevant controls. +> +> > [``](/lig/items/commit-preview-item.js) +> > [``](/lib/containers/commit-preview-container.js) +> > +> > The workspace-center pane that appears when looking at _all_ the staged changes that will be going into the next commit. > > +> > > [``](/lib/controllers/multi-file-patch-controller.js) +> > > [``](/lib/views/multi-file-patch-view.js) > > > [``](/lib/items/issueish-detail-item.js) > > [``](/lib/containers/issueish-detail-container.js) From 64c9608d0eb54cc245a4ab4162a94162b05c47a6 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 15 Nov 2018 11:40:13 -0500 Subject: [PATCH 341/409] Use an object spread for that data why not --- lib/containers/changed-file-container.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/containers/changed-file-container.js b/lib/containers/changed-file-container.js index d4661a8ac1..1c3ed45a70 100644 --- a/lib/containers/changed-file-container.js +++ b/lib/containers/changed-file-container.js @@ -52,13 +52,9 @@ export default class ChangedFileContainer extends React.Component { return ; } - const {multiFilePatch, hasUndoHistory, isPartiallyStaged} = data; - return ( ); From 9aa1a6051ed01dc5cb24d6610f9c6e7855b94ae9 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 15 Nov 2018 11:57:07 -0500 Subject: [PATCH 342/409] Pass a Decoration's className to the created DOM element --- lib/atom/decoration.js | 3 ++- test/atom/decoration.test.js | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/atom/decoration.js b/lib/atom/decoration.js index 841b8bd22e..9e3049f1a2 100644 --- a/lib/atom/decoration.js +++ b/lib/atom/decoration.js @@ -2,6 +2,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; import {Disposable} from 'event-kit'; +import cx from 'classnames'; import {createItem, autobind, extractProps} from '../helpers'; import {RefHolderPropType} from '../prop-types'; @@ -49,7 +50,7 @@ class BareDecoration extends React.Component { this.item = null; if (['gutter', 'overlay', 'block'].includes(this.props.type)) { this.domNode = document.createElement('div'); - this.domNode.className = 'react-atom-decoration'; + this.domNode.className = cx('react-atom-decoration', this.props.className); } } diff --git a/test/atom/decoration.test.js b/test/atom/decoration.test.js index dcde9c4412..2e9fd4fdcd 100644 --- a/test/atom/decoration.test.js +++ b/test/atom/decoration.test.js @@ -44,7 +44,7 @@ describe('Decoration', function() { it('creates a block decoration', function() { const app = ( - +
This is a subtree
@@ -55,7 +55,9 @@ describe('Decoration', function() { const args = editor.decorateMarker.firstCall.args; assert.equal(args[0], marker); assert.equal(args[1].type, 'block'); - const child = args[1].item.getElement().firstElementChild; + const element = args[1].item.getElement(); + assert.strictEqual(element.className, 'react-atom-decoration parent'); + const child = element.firstElementChild; assert.equal(child.className, 'decoration-subtree'); assert.equal(child.textContent, 'This is a subtree'); }); From 3d63ab0e548c2f3813a2ca17ccab5910142ee531 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 15 Nov 2018 11:57:27 -0500 Subject: [PATCH 343/409] Set and style a more specific CSS class for control blocks --- lib/views/multi-file-patch-view.js | 4 ++-- styles/file-patch-view.less | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/views/multi-file-patch-view.js b/lib/views/multi-file-patch-view.js index 4f0fdb015c..6990f98ba6 100644 --- a/lib/views/multi-file-patch-view.js +++ b/lib/views/multi-file-patch-view.js @@ -306,7 +306,7 @@ export default class MultiFilePatchView extends React.Component { return ( - + {this.renderExecutableModeChangeMeta(filePatch)} {this.renderSymlinkChangeMeta(filePatch)} @@ -484,7 +484,7 @@ export default class MultiFilePatchView extends React.Component { return ( - + Date: Thu, 15 Nov 2018 12:24:27 -0500 Subject: [PATCH 344/409] File.modes constants for well-known file modes --- lib/models/patch/file.js | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/lib/models/patch/file.js b/lib/models/patch/file.js index c9017ec56f..0c893ca4f1 100644 --- a/lib/models/patch/file.js +++ b/lib/models/patch/file.js @@ -1,4 +1,18 @@ export default class File { + static modes = { + // Non-executable, non-symlink + NORMAL: '100644', + + // +x bit set + EXECUTABLE: '100755', + + // Soft link to another filesystem location + SYMLINK: '120000', + + // Submodule mount point + GITLINK: '160000', + } + constructor({path, mode, symlink}) { this.path = path; this.mode = mode; @@ -18,15 +32,15 @@ export default class File { } isSymlink() { - return this.getMode() === '120000'; + return this.getMode() === this.constructor.modes.SYMLINK; } isRegularFile() { - return this.getMode() === '100644' || this.getMode() === '100755'; + return this.getMode() === this.constructor.modes.NORMAL || this.getMode() === this.constructor.modes.EXECUTABLE; } isExecutable() { - return this.getMode() === '100755'; + return this.getMode() === this.constructor.modes.EXECUTABLE; } isPresent() { From 8fc267fd4e1b05668ecda84c38574937ad42adbd Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 15 Nov 2018 12:24:41 -0500 Subject: [PATCH 345/409] Use File mode constants where possible --- lib/git-shell-out-strategy.js | 15 ++++++++------- lib/models/patch/builder.js | 6 +++--- lib/views/multi-file-patch-view.js | 5 +++-- test/builder/patch.js | 4 ++-- 4 files changed, 16 insertions(+), 14 deletions(-) diff --git a/lib/git-shell-out-strategy.js b/lib/git-shell-out-strategy.js index 3f152c5f58..19b422f502 100644 --- a/lib/git-shell-out-strategy.js +++ b/lib/git-shell-out-strategy.js @@ -20,6 +20,7 @@ import { normalizeGitHelperPath, toNativePathSep, toGitPathSep, LINE_ENDING_REGEX, CO_AUTHOR_REGEX, } from './helpers'; import GitTimingsView from './views/git-timings-view'; +import File from './models/patch/file'; import WorkerManager from './worker-manager'; const MAX_STATUS_OUTPUT_LENGTH = 1024 * 1024 * 10; @@ -640,12 +641,12 @@ export default class GitShellOutStrategy { let mode; let realpath; if (executable) { - mode = '100755'; + mode = File.modes.EXECUTABLE; } else if (symlink) { - mode = '120000'; + mode = File.modes.SYMLINK; realpath = await fs.realpath(absPath); } else { - mode = '100644'; + mode = File.modes.NORMAL; } rawDiffs.push(buildAddedFilePatch(filePath, binary ? null : contents, mode, realpath)); @@ -1061,11 +1062,11 @@ export default class GitShellOutStrategy { const executable = await isFileExecutable(path.join(this.workingDir, filePath)); const symlink = await isFileSymlink(path.join(this.workingDir, filePath)); if (executable) { - return '100755'; + return File.modes.EXECUTABLE; } else if (symlink) { - return '120000'; + return File.modes.SYMLINK; } else { - return '100644'; + return File.modes.NORMAL; } } } @@ -1080,7 +1081,7 @@ function buildAddedFilePatch(filePath, contents, mode, realpath) { if (contents) { const noNewLine = contents[contents.length - 1] !== '\n'; let lines; - if (mode === '120000') { + if (mode === File.modes.SYMLINK) { lines = [`+${toGitPathSep(realpath)}`, '\\ No newline at end of file']; } else { lines = contents.trim().split(LINE_ENDING_REGEX).map(line => `+${line}`); diff --git a/lib/models/patch/builder.js b/lib/models/patch/builder.js index 061745f443..aea6acae74 100644 --- a/lib/models/patch/builder.js +++ b/lib/models/patch/builder.js @@ -68,8 +68,8 @@ function emptyDiffFilePatch() { } function singleDiffFilePatch(diff, layeredBuffer) { - const wasSymlink = diff.oldMode === '120000'; - const isSymlink = diff.newMode === '120000'; + const wasSymlink = diff.oldMode === File.modes.SYMLINK; + const isSymlink = diff.newMode === File.modes.SYMLINK; const [hunks, patchMarker] = buildHunks(diff, layeredBuffer); @@ -97,7 +97,7 @@ function singleDiffFilePatch(diff, layeredBuffer) { function dualDiffFilePatch(diff1, diff2, layeredBuffer) { let modeChangeDiff, contentChangeDiff; - if (diff1.oldMode === '120000' || diff1.newMode === '120000') { + if (diff1.oldMode === File.modes.SYMLINK || diff1.newMode === File.modes.SYMLINK) { modeChangeDiff = diff1; contentChangeDiff = diff2; } else { diff --git a/lib/views/multi-file-patch-view.js b/lib/views/multi-file-patch-view.js index 6990f98ba6..47f41dcaeb 100644 --- a/lib/views/multi-file-patch-view.js +++ b/lib/views/multi-file-patch-view.js @@ -18,10 +18,11 @@ import HunkHeaderView from './hunk-header-view'; import RefHolder from '../models/ref-holder'; import ChangedFileItem from '../items/changed-file-item'; import CommitPreviewItem from '../items/commit-preview-item'; +import File from '../models/patch/file'; const executableText = { - 100644: 'non executable', - 100755: 'executable', + [File.modes.NORMAL]: 'non executable', + [File.modes.EXECUTABLE]: 'executable', }; const NBSP_CHARACTER = '\u00a0'; diff --git a/test/builder/patch.js b/test/builder/patch.js index ee863bc262..18ff5067b5 100644 --- a/test/builder/patch.js +++ b/test/builder/patch.js @@ -82,7 +82,7 @@ class FilePatchBuilder { constructor(layeredBuffer = null) { this.layeredBuffer = layeredBuffer; - this.oldFile = new File({path: 'file', mode: '100644'}); + this.oldFile = new File({path: 'file', mode: File.modes.NORMAL}); this.newFile = null; this.patchBuilder = new PatchBuilder(this.layeredBuffer); @@ -143,7 +143,7 @@ class FilePatchBuilder { class FileBuilder { constructor() { this._path = 'file.txt'; - this._mode = '100644'; + this._mode = File.modes.NORMAL; this._symlink = null; } From 0ec9039246f5a820f94b4ff01ea501fc8b0bdf28 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 15 Nov 2018 12:37:24 -0500 Subject: [PATCH 346/409] Go into more detail on why that opener is necessary --- test/controllers/commit-controller.test.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/test/controllers/commit-controller.test.js b/test/controllers/commit-controller.test.js index 9a35a22eb6..19b307dc1a 100644 --- a/test/controllers/commit-controller.test.js +++ b/test/controllers/commit-controller.test.js @@ -30,7 +30,14 @@ describe('CommitController', function() { const noop = () => {}; const store = new UserStore({config}); - // Ensure the Workspace doesn't mangle atom-github://... URIs + // Ensure the Workspace doesn't mangle atom-github://... URIs. + // If you don't have an opener registered for a non-standard URI protocol, the Workspace coerces it into a file URI + // and tries to open it with a TextEditor. In the process, the URI gets mangled: + // + // atom.workspace.open('atom-github://unknown/whatever').then(item => console.log(item.getURI())) + // > 'atom-github:/unknown/whatever' + // + // Adding an opener that creates fake items prevents it from doing this and keeps the URIs unchanged. const pattern = new URIPattern(CommitPreviewItem.uriPattern); workspace.addOpener(uri => { if (pattern.matches(uri).ok()) { From 707f13646b99533707dd6f7f5b62f4e9ac831e38 Mon Sep 17 00:00:00 2001 From: Tilde Ann Thurium Date: Thu, 15 Nov 2018 09:57:39 -0800 Subject: [PATCH 347/409] tweak symlink changes styles - The meta title text was weirdly big. Now it's not. - The meta title text should render underneath the file name, to make it clear which file it belongs to. --- lib/views/multi-file-patch-view.js | 2 +- styles/file-patch-view.less | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/views/multi-file-patch-view.js b/lib/views/multi-file-patch-view.js index 47f41dcaeb..1155432903 100644 --- a/lib/views/multi-file-patch-view.js +++ b/lib/views/multi-file-patch-view.js @@ -310,7 +310,6 @@ export default class MultiFilePatchView extends React.Component { {this.renderExecutableModeChangeMeta(filePatch)} - {this.renderSymlinkChangeMeta(filePatch)} this.didOpenFile({selectedFilePatch: filePatch})} toggleFile={() => this.props.toggleFile(filePatch)} /> + {this.renderSymlinkChangeMeta(filePatch)} diff --git a/styles/file-patch-view.less b/styles/file-patch-view.less index 037b2b5bc7..3b4fec51bb 100644 --- a/styles/file-patch-view.less +++ b/styles/file-patch-view.less @@ -112,14 +112,14 @@ &-metaHeader { display: flex; align-items: center; - padding: @component-padding; + padding: @component-padding / 2; background-color: @background-color-highlight; } &-metaTitle { flex: 1; margin: 0; - font-size: 1.25em; + font-size: 1.0em; line-height: 1.5; overflow: hidden; text-overflow: ellipsis; From 43c16bf54f5ffbe14a3e6595e718b60bf1acc75e Mon Sep 17 00:00:00 2001 From: Tilde Ann Thurium Date: Thu, 15 Nov 2018 10:12:19 -0800 Subject: [PATCH 348/409] executable mode change styling tweaks Executable mode changes should also go under the FilePatchView title so it's clear which file the mode change belongs to. --- 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 1155432903..f94658c2bf 100644 --- a/lib/views/multi-file-patch-view.js +++ b/lib/views/multi-file-patch-view.js @@ -309,7 +309,6 @@ export default class MultiFilePatchView extends React.Component { - {this.renderExecutableModeChangeMeta(filePatch)} this.didOpenFile({selectedFilePatch: filePatch})} toggleFile={() => this.props.toggleFile(filePatch)} /> + {this.renderExecutableModeChangeMeta(filePatch)} {this.renderSymlinkChangeMeta(filePatch)} From 04ed3c888c9dcbf7274b10ee561c4607285a2b0e Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Thu, 15 Nov 2018 13:03:23 -0800 Subject: [PATCH 349/409] :fire: unused cache key for stagedChangesSinceParentCommit --- lib/models/repository-states/present.js | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js index 22a803c9e9..7ef7879840 100644 --- a/lib/models/repository-states/present.js +++ b/lib/models/repository-states/present.js @@ -120,8 +120,6 @@ export default class Present extends State { if (filePathEndsWith(fullPath, '.git', 'index')) { keys.add(Keys.stagedChanges); - // todo: stagedChangesSinceParentCommit appears to be dead code and can be removed - keys.add(Keys.stagedChangesSinceParentCommit); keys.add(Keys.filePatch.all); keys.add(Keys.index.all); keys.add(Keys.statusBundle); @@ -348,7 +346,6 @@ export default class Present extends State { () => [ Keys.statusBundle, Keys.stagedChanges, - Keys.stagedChangesSinceParentCommit, Keys.filePatch.all, Keys.index.all, ], @@ -371,7 +368,6 @@ export default class Present extends State { return this.invalidate( () => [ Keys.statusBundle, - Keys.stagedChangesSinceParentCommit, Keys.stagedChanges, ...Keys.filePatch.eachWithFileOpts([filePath], [{staged: false}, {staged: true}]), Keys.index.oneWith(filePath), @@ -386,7 +382,6 @@ export default class Present extends State { return this.invalidate( () => [ Keys.stagedChanges, - Keys.stagedChangesSinceParentCommit, Keys.lastCommit, Keys.recentCommits, Keys.authors, @@ -408,7 +403,6 @@ export default class Present extends State { () => [ Keys.statusBundle, Keys.stagedChanges, - Keys.stagedChangesSinceParentCommit, ...paths.map(fileName => Keys.index.oneWith(fileName)), ...Keys.filePatch.eachWithFileOpts(paths, [{staged: true}]), ], @@ -421,7 +415,6 @@ export default class Present extends State { undoLastCommit() { return this.invalidate( () => [ - Keys.stagedChangesSinceParentCommit, Keys.lastCommit, Keys.recentCommits, Keys.authors, @@ -1036,8 +1029,6 @@ const Keys = { stagedChanges: new CacheKey('staged-changes'), - stagedChangesSinceParentCommit: new CacheKey('staged-changes-since-parent-commit'), - filePatch: { _optKey: ({staged}) => { return staged ? 's' : 'u'; @@ -1116,12 +1107,10 @@ const Keys = { ...Keys.filePatch.eachWithFileOpts(fileNames, [{staged: true}]), ...fileNames.map(Keys.index.oneWith), Keys.stagedChanges, - Keys.stagedChangesSinceParentCommit, ], headOperationKeys: () => [ ...Keys.filePatch.eachWithOpts({staged: true}), - Keys.stagedChangesSinceParentCommit, Keys.lastCommit, Keys.recentCommits, Keys.authors, From b48fcebf3c066256cd9d94aaf473e6bed172e99f Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 15 Nov 2018 19:33:54 -0500 Subject: [PATCH 350/409] Use `path.sep` to join dirname and basename --- lib/views/file-patch-header-view.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/views/file-patch-header-view.js b/lib/views/file-patch-header-view.js index b157e70230..8f7db7d8e2 100644 --- a/lib/views/file-patch-header-view.js +++ b/lib/views/file-patch-header-view.js @@ -63,11 +63,9 @@ export default class FilePatchHeaderView extends React.Component { if (dirname === '.') { return {basename}; } else { - // TODO: double check that the forward-slash works cross-platform... - // iirc git always uses `/` for paths, even on Windows return ( - {dirname}/{basename} + {dirname}{path.sep}{basename} ); } From da9fe4ce6e2ff69e152d9e60722ea42f6fdf53f5 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 15 Nov 2018 22:11:35 -0500 Subject: [PATCH 351/409] Avoid double "\\ No newline at end of file" --- lib/git-shell-out-strategy.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/git-shell-out-strategy.js b/lib/git-shell-out-strategy.js index 19b422f502..25d0122a32 100644 --- a/lib/git-shell-out-strategy.js +++ b/lib/git-shell-out-strategy.js @@ -1079,11 +1079,13 @@ export default class GitShellOutStrategy { function buildAddedFilePatch(filePath, contents, mode, realpath) { const hunks = []; if (contents) { - const noNewLine = contents[contents.length - 1] !== '\n'; + let noNewLine; let lines; if (mode === File.modes.SYMLINK) { + noNewLine = false; lines = [`+${toGitPathSep(realpath)}`, '\\ No newline at end of file']; } else { + noNewLine = contents[contents.length - 1] !== '\n'; lines = contents.trim().split(LINE_ENDING_REGEX).map(line => `+${line}`); } if (noNewLine) { lines.push('\\ No newline at end of file'); } From 6147c86a7f917c38b7798b09758cfd4f2916e0e2 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 15 Nov 2018 22:11:45 -0500 Subject: [PATCH 352/409] Skip filesystem events with no path --- lib/models/repository-states/present.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js index 7ef7879840..cc3ef01db9 100644 --- a/lib/models/repository-states/present.js +++ b/lib/models/repository-states/present.js @@ -184,6 +184,10 @@ export default class Present extends State { for (let i = 0; i < events.length; i++) { const event = events[i]; + if (!event.path) { + continue; + } + if (filePathEndsWith(event.path, '.git', 'MERGE_HEAD')) { if (event.action === 'created') { if (this.isCommitMessageClean()) { From f6f56d684ab5580c4a7b775fb6b45f48a4b3388c Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 15 Nov 2018 22:12:38 -0500 Subject: [PATCH 353/409] Remove unnecessary --- lib/views/multi-file-patch-view.js | 39 ++++++++++++++---------------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/lib/views/multi-file-patch-view.js b/lib/views/multi-file-patch-view.js index f94658c2bf..65dd925e12 100644 --- a/lib/views/multi-file-patch-view.js +++ b/lib/views/multi-file-patch-view.js @@ -308,27 +308,24 @@ export default class MultiFilePatchView extends React.Component { - - - 0} - hasUndoHistory={this.props.hasUndoHistory} - hasMultipleFileSelections={this.props.hasMultipleFileSelections} - - tooltips={this.props.tooltips} - - undoLastDiscard={() => this.undoLastDiscardFromButton(filePatch)} - diveIntoMirrorPatch={() => this.props.diveIntoMirrorPatch(filePatch)} - openFile={() => this.didOpenFile({selectedFilePatch: filePatch})} - toggleFile={() => this.props.toggleFile(filePatch)} - /> - {this.renderExecutableModeChangeMeta(filePatch)} - {this.renderSymlinkChangeMeta(filePatch)} - + 0} + hasUndoHistory={this.props.hasUndoHistory} + hasMultipleFileSelections={this.props.hasMultipleFileSelections} + + tooltips={this.props.tooltips} + + undoLastDiscard={() => this.undoLastDiscardFromButton(filePatch)} + diveIntoMirrorPatch={() => this.props.diveIntoMirrorPatch(filePatch)} + openFile={() => this.didOpenFile({selectedFilePatch: filePatch})} + toggleFile={() => this.props.toggleFile(filePatch)} + /> + {this.renderSymlinkChangeMeta(filePatch)} + {this.renderExecutableModeChangeMeta(filePatch)} From b7049eef752c62fbe648e449fb809bd4dd811989 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 15 Nov 2018 22:12:53 -0500 Subject: [PATCH 354/409] Wait for new unstaged patch to arrive --- test/integration/file-patch.test.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/integration/file-patch.test.js b/test/integration/file-patch.test.js index 9cd3dfdcde..7cd7d714a8 100644 --- a/test/integration/file-patch.test.js +++ b/test/integration/file-patch.test.js @@ -511,6 +511,12 @@ describe('integration: file patches', function() { getPatchEditor('unstaged', 'sample.js').selectAll(); getPatchItem('unstaged', 'sample.js').find('.github-HunkHeaderView-stageButton').simulate('click'); + await patchContent( + 'unstaged', 'sample.js', + [repoPath('target.txt'), 'selected'], + [' No newline at end of file'], + ); + assert.isTrue(getPatchItem('unstaged', 'sample.js').find('.github-FilePatchView-metaTitle').exists()); await clickFileInGitTab('staged', 'sample.js'); From 83ccf8f19993dea56fcd93f8565e0de729a7f31f Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 15 Nov 2018 22:39:30 -0500 Subject: [PATCH 355/409] Adapt the expected path separator to the current platform --- test/views/file-patch-header-view.test.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/test/views/file-patch-header-view.test.js b/test/views/file-patch-header-view.test.js index 8e8d514e66..6dae9a60a1 100644 --- a/test/views/file-patch-header-view.test.js +++ b/test/views/file-patch-header-view.test.js @@ -1,10 +1,12 @@ import React from 'react'; import {shallow} from 'enzyme'; +import path from 'path'; import FilePatchHeaderView from '../../lib/views/file-patch-header-view'; import ChangedFileItem from '../../lib/items/changed-file-item'; describe('FilePatchHeaderView', function() { + const relPath = path.join('dir', 'a.txt'); let atomEnv; beforeEach(function() { @@ -19,7 +21,7 @@ describe('FilePatchHeaderView', function() { return ( Date: Thu, 15 Nov 2018 22:42:54 -0500 Subject: [PATCH 356/409] Default itemType to avoid the PropTypes warning --- test/views/file-patch-header-view.test.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/views/file-patch-header-view.test.js b/test/views/file-patch-header-view.test.js index 6dae9a60a1..2d4e0acb65 100644 --- a/test/views/file-patch-header-view.test.js +++ b/test/views/file-patch-header-view.test.js @@ -4,6 +4,7 @@ import path from 'path'; import FilePatchHeaderView from '../../lib/views/file-patch-header-view'; import ChangedFileItem from '../../lib/items/changed-file-item'; +import CommitPreviewItem from '../../lib/items/commit-preview-item'; describe('FilePatchHeaderView', function() { const relPath = path.join('dir', 'a.txt'); @@ -20,7 +21,7 @@ describe('FilePatchHeaderView', function() { function buildApp(overrideProps = {}) { return ( Date: Thu, 15 Nov 2018 16:35:03 -0800 Subject: [PATCH 357/409] Workspace item watcher only cares about active items So we can drop the option, and rename the class --- lib/controllers/commit-controller.js | 1 - lib/watch-workspace-item.js | 7 +++---- test/watch-workspace-item.test.js | 4 ++-- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/lib/controllers/commit-controller.js b/lib/controllers/commit-controller.js index f74e936503..94175cbe66 100644 --- a/lib/controllers/commit-controller.js +++ b/lib/controllers/commit-controller.js @@ -60,7 +60,6 @@ export default class CommitController extends React.Component { CommitPreviewItem.buildURI(this.props.repository.getWorkingDirectoryPath()), this, 'commitPreviewActive', - {active: true}, ); this.subscriptions.add(this.previewWatcher); } diff --git a/lib/watch-workspace-item.js b/lib/watch-workspace-item.js index fafe8028a8..bff7714121 100644 --- a/lib/watch-workspace-item.js +++ b/lib/watch-workspace-item.js @@ -2,13 +2,12 @@ import {CompositeDisposable} from 'atom'; import URIPattern from './atom/uri-pattern'; -class ActiveItemWatcher { - constructor(workspace, pattern, component, stateKey, opts) { +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.opts = opts; this.activeItem = this.isActiveItem(); this.subs = new CompositeDisposable(); @@ -67,7 +66,7 @@ class ActiveItemWatcher { } export function watchWorkspaceItem(workspace, pattern, component, stateKey) { - return new ActiveItemWatcher(workspace, pattern, component, stateKey) + return new ItemWatcher(workspace, pattern, component, stateKey) .setInitialState() .subscribeToWorkspace(); } diff --git a/test/watch-workspace-item.test.js b/test/watch-workspace-item.test.js index f233f0ca40..1df9ca25c9 100644 --- a/test/watch-workspace-item.test.js +++ b/test/watch-workspace-item.test.js @@ -67,14 +67,14 @@ describe('watchWorkspaceItem', function() { assert.isTrue(component.state.theKey); }); - it('is true when the pane is open and active in any pane', async function() { + it.only('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}); + sub = watchWorkspaceItem(workspace, 'atom-github://some-item', component, 'someKey'); assert.isTrue(component.state.someKey); }); From 098edb405c3c5e49bc047d5f370331d25f4fca5e Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Thu, 15 Nov 2018 16:58:55 -0800 Subject: [PATCH 358/409] :fire: props that aren't used in `CommitPreviewItem` --- lib/controllers/root-controller.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/controllers/root-controller.js b/lib/controllers/root-controller.js index 6fd7dbb3ac..e1f73588de 100644 --- a/lib/controllers/root-controller.js +++ b/lib/controllers/root-controller.js @@ -355,9 +355,6 @@ export default class RootController extends React.Component { keymaps={this.props.keymaps} tooltips={this.props.tooltips} config={this.props.config} - discardLines={this.discardLines} - undoLastDiscard={this.undoLastDiscard} - surfaceFileAtPath={this.surfaceFromFileAtPath} /> )} From 83622791175023b81df331138fb8988df3e0a9b9 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Thu, 15 Nov 2018 16:59:24 -0800 Subject: [PATCH 359/409] :fire: mysterious handleClick prop in MultiFilePatchController --- lib/controllers/multi-file-patch-controller.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/controllers/multi-file-patch-controller.js b/lib/controllers/multi-file-patch-controller.js index 6df6e91821..d5d0c25425 100644 --- a/lib/controllers/multi-file-patch-controller.js +++ b/lib/controllers/multi-file-patch-controller.js @@ -25,7 +25,6 @@ export default class MultiFilePatchController extends React.Component { discardLines: PropTypes.func, undoLastDiscard: PropTypes.func, surfaceFileAtPath: PropTypes.func, - handleClick: PropTypes.func, } constructor(props) { From 7eb7db03a8263440df2521e920d34bb19e82700c Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Thu, 15 Nov 2018 18:28:52 -0800 Subject: [PATCH 360/409] Invalidate `stagedChanges` key when undoing last commit or head moves --- lib/models/repository-states/present.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js index cc3ef01db9..a9f9691a2a 100644 --- a/lib/models/repository-states/present.js +++ b/lib/models/repository-states/present.js @@ -419,6 +419,7 @@ export default class Present extends State { undoLastCommit() { return this.invalidate( () => [ + Keys.stagedChanges, Keys.lastCommit, Keys.recentCommits, Keys.authors, @@ -1115,6 +1116,7 @@ const Keys = { headOperationKeys: () => [ ...Keys.filePatch.eachWithOpts({staged: true}), + Keys.stagedChanges, Keys.lastCommit, Keys.recentCommits, Keys.authors, From cd7003add3c5cfc11d55162822dbdd6158a3dad2 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Thu, 15 Nov 2018 19:39:11 -0800 Subject: [PATCH 361/409] :fire: useEditorAutoHeight since we are now using a single editor --- lib/views/multi-file-patch-view.js | 6 ------ test/views/multi-file-patch-view.test.js | 9 --------- 2 files changed, 15 deletions(-) diff --git a/lib/views/multi-file-patch-view.js b/lib/views/multi-file-patch-view.js index 65dd925e12..2cc6f06acc 100644 --- a/lib/views/multi-file-patch-view.js +++ b/lib/views/multi-file-patch-view.js @@ -39,7 +39,6 @@ export default class MultiFilePatchView extends React.Component { hasMultipleFileSelections: PropTypes.bool.isRequired, repository: PropTypes.object.isRequired, hasUndoHistory: PropTypes.bool, - useEditorAutoHeight: PropTypes.bool, workspace: PropTypes.object.isRequired, commands: PropTypes.object.isRequired, @@ -63,10 +62,6 @@ export default class MultiFilePatchView extends React.Component { itemType: PropTypes.oneOf([ChangedFileItem, CommitPreviewItem]).isRequired, } - static defaultProps = { - useEditorAutoHeight: false, - } - constructor(props) { super(props); autobind( @@ -236,7 +231,6 @@ export default class MultiFilePatchView extends React.Component { buffer={this.props.multiFilePatch.getBuffer()} lineNumberGutterVisible={false} autoWidth={false} - autoHeight={this.props.useEditorAutoHeight} readOnly={true} softWrapped={true} diff --git a/test/views/multi-file-patch-view.test.js b/test/views/multi-file-patch-view.test.js index 021a1b055b..11cf5037c2 100644 --- a/test/views/multi-file-patch-view.test.js +++ b/test/views/multi-file-patch-view.test.js @@ -137,15 +137,6 @@ describe('MultiFilePatchView', function() { assert.strictEqual(editor.instance().getModel().getText(), filePatches.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 218d6cbb8aa9599f47db2432197f24f05afd25bb Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Thu, 15 Nov 2018 20:02:22 -0800 Subject: [PATCH 362/409] Actually we want autoHeight set to false on AtomTextEditor --- lib/views/multi-file-patch-view.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/views/multi-file-patch-view.js b/lib/views/multi-file-patch-view.js index 2cc6f06acc..16399a227d 100644 --- a/lib/views/multi-file-patch-view.js +++ b/lib/views/multi-file-patch-view.js @@ -231,6 +231,7 @@ export default class MultiFilePatchView extends React.Component { buffer={this.props.multiFilePatch.getBuffer()} lineNumberGutterVisible={false} autoWidth={false} + autoHeight={false} readOnly={true} softWrapped={true} From 1c1b2d2595e154fa521fe797069eb413276a9978 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Thu, 15 Nov 2018 21:13:08 -0800 Subject: [PATCH 363/409] :fire: .only --- 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 1df9ca25c9..0d63a4c95d 100644 --- a/test/watch-workspace-item.test.js +++ b/test/watch-workspace-item.test.js @@ -67,7 +67,7 @@ describe('watchWorkspaceItem', function() { assert.isTrue(component.state.theKey); }); - it.only('is true when the pane is open and active in any pane', async function() { + 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'); From 3b5d0307d921a1fcb2e364f992145f01fad3c38a Mon Sep 17 00:00:00 2001 From: Vanessa Yuen Date: Fri, 16 Nov 2018 15:48:57 +0100 Subject: [PATCH 364/409] =?UTF-8?q?=F0=9F=93=9D=20comment=20for=20`interse?= =?UTF-8?q?ctRows`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/models/patch/region.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/models/patch/region.js b/lib/models/patch/region.js index f08901772f..3afaef844d 100644 --- a/lib/models/patch/region.js +++ b/lib/models/patch/region.js @@ -25,6 +25,16 @@ class Region { return this.getRange().intersectsRow(row); } + /* + * intersectRows breaks a Region into runs of rows that are included in + * rowSet and rows that are not. For example: + * @this Region row 10-20 + * @param rowSet row 11, 12, 13, 17, 19 + * @param includeGaps true (whether the result will include gaps or not) + * @return an array of regions like this: + * (10, gap = true) (11, 12, 13, gap = false) (14, 15, 16, gap = true) + * (17, gap = false) (18, gap = true) (19, gap = false) (20, gap = true) + */ intersectRows(rowSet, includeGaps) { const intersections = []; let withinIntersection = false; From d599b3c15da09e20d5fefcba7a777dcdf90f6d21 Mon Sep 17 00:00:00 2001 From: Tilde Ann Thurium Date: Fri, 16 Nov 2018 08:57:48 -0800 Subject: [PATCH 365/409] :fire: unnecessary button class name `btn-secondary` does nothing --- 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 fdfa143ef4..de83bee13f 100644 --- a/lib/views/commit-view.js +++ b/lib/views/commit-view.js @@ -164,7 +164,7 @@ export default class CommitView extends React.Component {
diff --git a/styles/file-patch-view.less b/styles/file-patch-view.less index 7f3f9f21ac..3de411746a 100644 --- a/styles/file-patch-view.less +++ b/styles/file-patch-view.less @@ -98,10 +98,11 @@ } // Meta section + // Used for mode changes &-meta { - padding: @component-padding; - border-bottom: 1px solid @base-border-color; + font-family: @font-family; + padding-top: @component-padding; } &-metaContainer { @@ -112,15 +113,14 @@ &-metaHeader { display: flex; align-items: center; - padding: @component-padding / 2; - background-color: @background-color-highlight; + font-size: .9em; + border-bottom: 1px solid @base-border-color; } &-metaTitle { flex: 1; margin: 0; - font-size: 1.0em; - line-height: 1.5; + padding-left: @component-padding; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @@ -128,14 +128,25 @@ &-metaControls { margin-left: @component-padding; + } - .btn { - &.icon-move-up::before, - &.icon-move-down::before { - font-size: 1em; - margin-right: .5em; - vertical-align: baseline; - } + &-metaButton { + line-height: 1.9; // Magic number to match the hunk height + padding-left: @component-padding; + padding-right: @component-padding; + font-family: @font-family; + border: none; + border-left: 1px solid @base-border-color; + background-color: transparent; + cursor: default; + &:hover { background-color: mix(@syntax-text-color, @syntax-background-color, 4%); } + &:active { background-color: mix(@syntax-text-color, @syntax-background-color, 2%); } + + &.icon-move-up::before, + &.icon-move-down::before { + font-size: 1em; + margin-right: .25em; + vertical-align: baseline; } }