From 1c0eaab73ed97cf1aeaa4992af417249d17477ce Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Wed, 18 Oct 2017 18:47:17 -0700 Subject: [PATCH 0001/5882] Add config setting for hard-wrapping commit message body --- lib/controllers/commit-view-controller.js | 24 +++++++- lib/controllers/git-tab-controller.js | 1 + lib/controllers/root-controller.js | 1 + lib/models/repository-states/present.js | 24 +------- lib/views/git-tab-view.js | 1 + package.json | 5 ++ .../commit-view-controller.test.js | 56 ++++++++++++++++++- test/controllers/git-tab-controller.test.js | 7 ++- test/models/repository.test.js | 40 ------------- 9 files changed, 91 insertions(+), 68 deletions(-) diff --git a/lib/controllers/commit-view-controller.js b/lib/controllers/commit-view-controller.js index 5cc3892627..e5a846a588 100644 --- a/lib/controllers/commit-view-controller.js +++ b/lib/controllers/commit-view-controller.js @@ -131,7 +131,11 @@ export default class CommitViewController { .catch(() => null); } } else { - await this.props.commit(message); + let formattedMessage = message.replace(/^#.*$/mg, '').trim(); // strip out comments + if (this.props.config && this.props.config.get('github.automaticCommitMessageWrapping')) { + formattedMessage = wrapCommitMessage(formattedMessage); + } + await this.props.commit(formattedMessage); deleteFileOrFolder(this.getCommitMessagePath()) .catch(() => null); } @@ -267,3 +271,21 @@ export default class CommitViewController { return etch.destroy(this); } } + +function wrapCommitMessage(message) { + // hard wrap message (except for first line) at 72 characters + let results = []; + message.split('\n').forEach((line, index) => { + if (line.length <= 72 || index === 0) { + results.push(line); + } else { + const matches = line.match(/.{1,72}(\s|$)|\S+?(\s|$)/g) + .map(match => { + return match.endsWith('\n') ? match.substr(0, match.length - 1) : match; + }); + results = results.concat(matches); + } + }); + + return results.join('\n'); +} diff --git a/lib/controllers/git-tab-controller.js b/lib/controllers/git-tab-controller.js index 4246da4969..5644e03073 100644 --- a/lib/controllers/git-tab-controller.js +++ b/lib/controllers/git-tab-controller.js @@ -58,6 +58,7 @@ export default class GitTabController { notificationManager={this.props.notificationManager} project={this.props.project} confirm={this.props.confirm} + config={this.props.config} initializeRepo={this.props.initializeRepo} didSelectFilePath={this.props.didSelectFilePath} stageFilePatch={this.stageFilePatch} diff --git a/lib/controllers/root-controller.js b/lib/controllers/root-controller.js index abfd92728c..2c5ac85646 100644 --- a/lib/controllers/root-controller.js +++ b/lib/controllers/root-controller.js @@ -202,6 +202,7 @@ export default class RootController extends React.Component { grammars={this.props.grammars} project={this.props.project} confirm={this.props.confirm} + config={this.props.config} repository={this.props.repository} initializeRepo={this.initializeRepo} resolutionProgress={this.props.resolutionProgress} diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js index 145d06d7c3..2fbf5986ff 100644 --- a/lib/models/repository-states/present.js +++ b/lib/models/repository-states/present.js @@ -195,8 +195,7 @@ export default class Present extends State { ...Keys.filePatch.eachWithOpts({staged: true}), Keys.headDescription, ]) - commit(rawMessage, options) { - const message = typeof rawMessage === 'string' ? formatCommitMessage(rawMessage) : rawMessage; + commit(message, options) { return this.git().commit(message, options); } @@ -625,27 +624,6 @@ function partition(array, predicate) { return [matches, nonmatches]; } -function formatCommitMessage(message) { - // strip out comments - const messageWithoutComments = message.replace(/^#.*$/mg, '').trim(); - - // hard wrap message (except for first line) at 72 characters - let results = []; - messageWithoutComments.split('\n').forEach((line, index) => { - if (line.length <= 72 || index === 0) { - results.push(line); - } else { - const matches = line.match(/.{1,72}(\s|$)|\S+?(\s|$)/g) - .map(match => { - return match.endsWith('\n') ? match.substr(0, match.length - 1) : match; - }); - results = results.concat(matches); - } - }); - - return results.join('\n'); -} - function buildFilePatchesFromRawDiffs(rawDiffs) { let diffLineNumber = 0; return rawDiffs.map(patch => { diff --git a/lib/views/git-tab-view.js b/lib/views/git-tab-view.js index 3369220df0..9558dc9d24 100644 --- a/lib/views/git-tab-view.js +++ b/lib/views/git-tab-view.js @@ -117,6 +117,7 @@ export default class GitTabView { /> 0} mergeConflictsExist={this.props.mergeConflicts.length > 0} prepareToCommit={this.props.prepareToCommit} diff --git a/package.json b/package.json index fd0e657629..888b3f176c 100644 --- a/package.json +++ b/package.json @@ -160,6 +160,11 @@ "type": "boolean", "default": false, "description": "Capture CPU profiles" + }, + "automaticCommitMessageWrapping": { + "type": "boolean", + "default": true, + "description": "Insert new lines at 72 characters in commit message body in commit box. Does not apply to expanded commit editors, where message formatting is preserved." } }, "deserializers": { diff --git a/test/controllers/commit-view-controller.test.js b/test/controllers/commit-view-controller.test.js index 77a23d5cee..195cd12129 100644 --- a/test/controllers/commit-view-controller.test.js +++ b/test/controllers/commit-view-controller.test.js @@ -10,7 +10,7 @@ import CommitViewController, {COMMIT_GRAMMAR_SCOPE} from '../../lib/controllers/ import {cloneRepository, buildRepository} from '../helpers'; describe('CommitViewController', function() { - let atomEnvironment, workspace, commandRegistry, notificationManager, grammars, lastCommit; + let atomEnvironment, workspace, commandRegistry, notificationManager, grammars, lastCommit, config; beforeEach(function() { atomEnvironment = global.buildAtomEnvironment(); @@ -18,6 +18,7 @@ describe('CommitViewController', function() { commandRegistry = atomEnvironment.commands; notificationManager = atomEnvironment.notifications; grammars = atomEnvironment.grammars; + config = atomEnvironment.config; lastCommit = new Commit('a1e23fd45', 'last commit message'); }); @@ -109,6 +110,7 @@ describe('CommitViewController', function() { commandRegistry, notificationManager, grammars, + config, lastCommit, repository, commit, @@ -149,6 +151,58 @@ describe('CommitViewController', function() { assert.equal(controller.amendingCommitMessage, 'amending'); }); + describe('message formatting', function() { + let commitSpy; + beforeEach(function() { + commitSpy = sinon.stub().returns(Promise.resolve()); + controller.update({commit: commitSpy}); + }); + + it('strips out comments', async function() { + await controller.commit([ + 'Make a commit', + '', + '# Comments:', + '# blah blah blah', + '# other stuff', + ].join('\n')); + + assert.deepEqual(commitSpy.args[0][0], 'Make a commit'); + }); + + it('wraps the commit message body at 72 characters if github.automaticCommitMessageWrapping is true', async function() { + config.set('github.automaticCommitMessageWrapping', false); + + await controller.commit([ + 'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor', + '', + 'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.', + ].join('\n')); + + assert.deepEqual(commitSpy.args[0][0].split('\n'), [ + 'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor', + '', + 'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.', + ]); + + commitSpy.reset(); + config.set('github.automaticCommitMessageWrapping', true); + + await controller.commit([ + 'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor', + '', + 'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.', + ].join('\n')); + + assert.deepEqual(commitSpy.args[0][0].split('\n'), [ + 'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor', + '', + 'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ', + 'ut aliquip ex ea commodo consequat.', + ]); + }); + }); + describe('toggling between commit box and commit editor', function() { it('transfers the commit message contents of the last editor', async function() { controller.refs.commitView.editor.setText('message in box'); diff --git a/test/controllers/git-tab-controller.test.js b/test/controllers/git-tab-controller.test.js index 5d567176c8..1c0dbc9024 100644 --- a/test/controllers/git-tab-controller.test.js +++ b/test/controllers/git-tab-controller.test.js @@ -12,7 +12,7 @@ import Repository, {AbortMergeError, CommitError} from '../../lib/models/reposit import ResolutionProgress from '../../lib/models/conflicts/resolution-progress'; describe('GitTabController', function() { - let atomEnvironment, workspace, workspaceElement, commandRegistry, notificationManager; + let atomEnvironment, workspace, workspaceElement, commandRegistry, notificationManager, config; let resolutionProgress, refreshResolutionProgress, destroyFilePatchPaneItems; beforeEach(function() { @@ -20,6 +20,7 @@ describe('GitTabController', function() { workspace = atomEnvironment.workspace; commandRegistry = atomEnvironment.commands; notificationManager = atomEnvironment.notifications; + config = atomEnvironment.config; workspaceElement = atomEnvironment.views.getView(workspace); @@ -441,7 +442,7 @@ describe('GitTabController', function() { const ensureGitTab = () => Promise.resolve(false); controller = new GitTabController({ - workspace, commandRegistry, repository, didChangeAmending, prepareToCommit, ensureGitTab, + workspace, commandRegistry, config, repository, didChangeAmending, prepareToCommit, ensureGitTab, resolutionProgress, refreshResolutionProgress, destroyFilePatchPaneItems, }); await controller.getLastModelDataRefreshPromise(); @@ -481,7 +482,7 @@ describe('GitTabController', function() { fs.unlinkSync(path.join(workdirPath, 'b.txt')); const ensureGitTab = () => Promise.resolve(false); const controller = new GitTabController({ - workspace, commandRegistry, repository, ensureGitTab, didChangeAmending: sinon.stub(), + workspace, commandRegistry, config, repository, ensureGitTab, didChangeAmending: sinon.stub(), resolutionProgress, refreshResolutionProgress, destroyFilePatchPaneItems, }); await controller.getLastModelDataRefreshPromise(); diff --git a/test/models/repository.test.js b/test/models/repository.test.js index 466b426332..73ad1db56f 100644 --- a/test/models/repository.test.js +++ b/test/models/repository.test.js @@ -450,46 +450,6 @@ describe('Repository', function() { assert.equal((await repository.getLastCommit()).getSha(), mergeBase.getSha()); }); - it('wraps the commit message body at 72 characters', async function() { - const workingDirPath = await cloneRepository('three-files'); - const repo = new Repository(workingDirPath); - await repo.getLoadPromise(); - - fs.writeFileSync(path.join(workingDirPath, 'subdir-1', 'a.txt'), 'qux\nfoo\nbar\n', 'utf8'); - await repo.stageFiles([path.join('subdir-1', 'a.txt')]); - await repo.commit([ - 'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor', - '', - 'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.', - ].join('\n')); - - const message = (await repo.getLastCommit()).getMessage(); - assert.deepEqual(message.split('\n'), [ - 'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor', - '', - 'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi', - 'ut aliquip ex ea commodo consequat.', - ]); - }); - - it('strips out comments', async function() { - const workingDirPath = await cloneRepository('three-files'); - const repo = new Repository(workingDirPath); - await repo.getLoadPromise(); - - fs.writeFileSync(path.join(workingDirPath, 'subdir-1', 'a.txt'), 'qux\nfoo\nbar\n', 'utf8'); - await repo.stageFiles([path.join('subdir-1', 'a.txt')]); - await repo.commit([ - 'Make a commit', - '', - '# Comments:', - '# blah blah blah', - '# other stuff', - ].join('\n')); - - assert.deepEqual((await repo.getLastCommit()).getMessage(), 'Make a commit'); - }); - it('clears the stored resolution progress'); it('executes hook scripts with a sane environment', async function() { From 558b40aa8f6f359e121c0da9e7a008e893b191d8 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Wed, 18 Oct 2017 19:31:56 -0700 Subject: [PATCH 0002/5882] Wordsmith config option description --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 888b3f176c..915ca158a1 100644 --- a/package.json +++ b/package.json @@ -164,7 +164,7 @@ "automaticCommitMessageWrapping": { "type": "boolean", "default": true, - "description": "Insert new lines at 72 characters in commit message body in commit box. Does not apply to expanded commit editors, where message formatting is preserved." + "description": "Hard wrap commit message body in commit box to 72 characters. Does not apply to expanded commit editors, where message formatting is preserved." } }, "deserializers": { From 3e97a9e3ed80d21add11e22a3c223246ed31b489 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Wed, 18 Oct 2017 23:41:45 -0700 Subject: [PATCH 0003/5882] WIP --- lib/controllers/commit-view-controller.js | 1 + lib/controllers/git-tab-controller.js | 1 + lib/controllers/root-controller.js | 1 + lib/views/commit-view.js | 9 +++++++++ lib/views/git-tab-view.js | 1 + styles/commit-view.less | 12 ++++++++++++ 6 files changed, 25 insertions(+) diff --git a/lib/controllers/commit-view-controller.js b/lib/controllers/commit-view-controller.js index e5a846a588..7fdd025433 100644 --- a/lib/controllers/commit-view-controller.js +++ b/lib/controllers/commit-view-controller.js @@ -95,6 +95,7 @@ export default class CommitViewController { return ( To disable, go to github package settings.', + class: 'github-CommitView-info-tooltip', + delay: {hide: 50000000}, + }), ); + + console.log(global.e = this.element); } destroy() { @@ -79,6 +87,7 @@ export default class CommitView { autoHeight={false} scrollPastEnd={false} /> +
+ onClick={this.createBranch} disabled={disableControls}> New Branch
); } + componentWillReceiveProps(nextProps) { + const currentBranch = nextProps.currentBranch.getName(); + const branchNames = nextProps.branches.map(branch => branch.getName()); + const hasNewBranch = branchNames.indexOf(this.state.checkedOutBranch); + if (currentBranch === this.state.checkedOutBranch && hasNewBranch) { + this.restoreControls(); + if (this.state.createNew) { this.setState({createNew: false}); } + } + } + @autobind async didSelectItem(event) { const branchName = event.target.value; - await this.props.checkout(branchName); + await this.checkout(branchName); } @autobind async createBranch() { if (this.state.createNew) { const branchName = this.editorElement.getModel().getText().trim(); - await this.props.checkout(branchName, {createNew: true}); - this.setState({createNew: false}); + await this.checkout(branchName, {createNew: true}); } else { - this.setState({createNew: true}, () => this.editorElement.focus()); + this.setState({createNew: true}, () => { + console.log(global.element = this.editorElement); + this.editorElement.focus(); + }); + } + } + + @autobind + async checkout(branchName, options) { + this.disableControls(branchName); + try { + await this.props.checkout(branchName, options); + } catch (error) { + this.restoreControls(); + } + } + + @autobind + async disableControls(branchName) { + if (this.editorElement) { + this.editorElement.getModel().component.setInputEnabled(false); + this.editorElement.classList.remove('is-focused'); + } + this.setState({checkedOutBranch: branchName}); + } + + @autobind + async restoreControls() { + if (this.editorElement) { + this.editorElement.getModel().component.setInputEnabled(true); + this.editorElement.focus(); + this.editorElement.classList.add('is-focused'); } + this.setState({checkedOutBranch: null}); } @autobind diff --git a/styles/branch-menu-view.less b/styles/branch-menu-view.less index cdb7e0b375..2a70e36d72 100644 --- a/styles/branch-menu-view.less +++ b/styles/branch-menu-view.less @@ -9,6 +9,15 @@ &-item { margin: @component-padding / 2; + + // TODO: make this work + // Sync animation + .icon-sync::before { + @keyframes github-BranchViewSync-animation { + 100% { transform: rotate(360deg); } + } + animation: github-BranchViewSync-animation 2s linear 30; // limit to 1min in case something gets stuck + } } .icon-git-branch { @@ -45,6 +54,10 @@ font-size: inherit; text-align: left; } + atom-text-editor[disabled="true"] { + color: @text-color-subtle; + pointer-events: none; + } } &-message { From c1d032f3c000d68233ffee04537c0d0418186d0e Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Mon, 30 Oct 2017 20:52:56 -0700 Subject: [PATCH 0028/5882] Fix StatusBarTileController tests. Build repository with pipelines --- .../status-bar-tile-controller.test.js | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/test/controllers/status-bar-tile-controller.test.js b/test/controllers/status-bar-tile-controller.test.js index ea0e9abfc0..fb1a51d184 100644 --- a/test/controllers/status-bar-tile-controller.test.js +++ b/test/controllers/status-bar-tile-controller.test.js @@ -13,7 +13,7 @@ import BranchView from '../../lib/views/branch-view'; import PushPullView from '../../lib/views/push-pull-view'; import ChangedFilesCountView from '../../lib/views/changed-files-count-view'; -describe('StatusBarTileController', function() { +describe.only('StatusBarTileController', function() { let atomEnvironment; let workspace, workspaceElement, commandRegistry, notificationManager, tooltips, confirm; let component; @@ -117,7 +117,6 @@ describe('StatusBarTileController', function() { await assert.async.equal(tip.querySelector('select').value, 'branch'); selectOption(tip, 'master'); - // TODO: test optimistic rendering await until(async () => { const branch2 = await repository.getCurrentBranch(); @@ -165,9 +164,9 @@ describe('StatusBarTileController', function() { }); describe('checking out newly created branches', function() { - xit('can check out newly created branches', async function() { + it('can check out newly created branches', async function() { const workdirPath = await cloneRepository('three-files'); - const repository = await buildRepository(workdirPath); + const repository = await buildRepositoryWithPipeline(workdirPath, {confirm, notificationManager, workspace}); const wrapper = mount(React.cloneElement(component, {repository})); await wrapper.instance().refreshModelData(); @@ -189,26 +188,22 @@ describe('StatusBarTileController', function() { tip.querySelector('atom-text-editor').getModel().setText('new-branch'); tip.querySelector('button').click(); - // TODO: optimistic rendering - await until(async () => { - // await wrapper.instance().refreshModelData(); - // return tip.querySelectorAll('select').length === 1; const branch1 = await repository.getCurrentBranch(); return branch1.getName() === 'new-branch' && !branch1.isDetached(); }); - // await wrapper.instance().refreshModelData(); + repository.refresh(); // clear cache manually, since we're not listening for file system events here await assert.async.equal(tip.querySelector('select').value, 'new-branch'); assert.lengthOf(tip.querySelectorAll('.github-BranchMenuView-editor'), 0); assert.lengthOf(tip.querySelectorAll('select'), 1); }); - xit('forgets newly created branches on repository change', async function() { + it('can check out newly created branches', async function() { const [repo0, repo1] = await Promise.all( [0, 1].map(async () => { const workdirPath = await cloneRepository('three-files'); - return buildRepository(workdirPath); + return buildRepositoryWithPipeline(workdirPath, {confirm, notificationManager, workspace}); }), ); @@ -216,23 +211,25 @@ describe('StatusBarTileController', function() { await wrapper.instance().refreshModelData(); const tip = getTooltipNode(wrapper, BranchView); - // Create a new branch - tip.querySelector('button').click(); - tip.querySelector('atom-text-editor').getModel().setText('created-branch'); tip.querySelector('button').click(); - await assert.async.lengthOf(tip.querySelectorAll('select'), 1); + // TODO: why does this test fail if 'newer-branch' is something like 'create-branch'? + tip.querySelector('atom-text-editor').getModel().setText('newer-branch'); + tip.querySelector('button').click(); - console.log(global.tip = tip); - // debugger; - await assert.async.equal(getTooltipNode(wrapper, BranchView).querySelector('select').value, 'created-branch'); + await until(async () => { + const branch1 = await repo0.getCurrentBranch(); + return branch1.getName() === 'newer-branch' && !branch1.isDetached(); + }); + repo0.refresh(); // clear cache manually, since we're not listening for file system events here + await assert.async.equal(tip.querySelector('select').value, 'newer-branch'); wrapper.setProps({repository: repo1}); await wrapper.instance().refreshModelData(); assert.equal(tip.querySelector('select').value, 'master'); const options = Array.from(tip.querySelectorAll('option'), node => node.value); - assert.notInclude(options, 'created-branch'); + assert.notInclude(options, 'newer-branch'); }); it('displays an error message if branch already exists', async function() { @@ -265,7 +262,9 @@ describe('StatusBarTileController', function() { const branch1 = await repository.getCurrentBranch(); assert.equal(branch1.getName(), 'branch'); assert.isFalse(branch1.isDetached()); - assert.equal(tip.querySelector('select').value, 'branch'); + + assert.lengthOf(tip.querySelectorAll('.github-BranchMenuView-editor'), 1); + assert.equal(tip.querySelector('atom-text-editor').getModel().getText(), 'master'); }); }); @@ -505,12 +504,13 @@ describe('StatusBarTileController', function() { const wrapper = mount(React.cloneElement(component, {repository})); await wrapper.instance().refreshModelData(); - sinon.spy(repository, 'push'); + sinon.spy(repository.git, 'push'); commandRegistry.dispatch(workspaceElement, 'github:force-push'); assert.equal(confirm.callCount, 1); - assert.isTrue(repository.push.calledWith('master', sinon.match({force: true, setUpstream: false}))); + await assert.async.isTrue(repository.git.push.calledWith('origin', 'master', sinon.match({force: true, setUpstream: false}))); + await assert.async.isFalse(repository.getOperationStates().pushInProgress); }); it('displays a warning notification when pull results in merge conflicts', async function() { From b2ccc7fa245ce3a0768ddf4ef19aeffc6b704929 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Mon, 30 Oct 2017 20:53:35 -0700 Subject: [PATCH 0029/5882] :fire: comment --- lib/models/repository-states/present.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js index 6c6257cb15..058a9fc91d 100644 --- a/lib/models/repository-states/present.js +++ b/lib/models/repository-states/present.js @@ -64,7 +64,7 @@ export default class Present extends State { pullInProgress: false, fetchInProgress: false, commitInProgress: false, - checkoutInProgress: false, // value is branchName when in progress. better way? + checkoutInProgress: false, }; this.amending = false; From 25f0327bb826dde313a95ad0ba9d409a9c0ab805 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Mon, 30 Oct 2017 21:00:31 -0700 Subject: [PATCH 0030/5882] Rethrow errors and don't log them --- lib/get-repo-pipeline-manager.js | 53 +++++++++++++++++--------------- 1 file changed, 28 insertions(+), 25 deletions(-) diff --git a/lib/get-repo-pipeline-manager.js b/lib/get-repo-pipeline-manager.js index d2a43ccf84..76de979f7f 100644 --- a/lib/get-repo-pipeline-manager.js +++ b/lib/get-repo-pipeline-manager.js @@ -25,8 +25,11 @@ export default function({confirm, notificationManager, workspace}) { }); pushPipeline.addMiddleware('set-push-in-progress', async (next, repository, branchName, options) => { repository.setOperationProgressState('push', true); - await next(); - repository.setOperationProgressState('push', false); + try { + await next(); + } finally { + repository.setOperationProgressState('push', false); + } }); pushPipeline.addMiddleware('failed-to-push-error', async (next, repository, branchName, options) => { try { @@ -41,21 +44,23 @@ export default function({confirm, notificationManager, workspace}) { dismissable: true, }); } else { - console.error(error); notificationManager.addError('Unable to push', { description: `
${error.stdErr}
`, dismissable: true, }); } - return error; + throw error; } }); const pullPipeline = pipelineManager.getPipeline(pipelineManager.actionKeys.PULL); pullPipeline.addMiddleware('set-pull-in-progress', async (next, repository, branchName) => { repository.setOperationProgressState('pull', true); - await next(); - repository.setOperationProgressState('pull', false); + try { + await next(); + } finally { + repository.setOperationProgressState('pull', false); + } }); pullPipeline.addMiddleware('failed-to-pull-error', async (next, repository, branchName) => { try { @@ -79,21 +84,23 @@ export default function({confirm, notificationManager, workspace}) { dismissable: true, }); } else { - console.error(error); notificationManager.addError('Unable to pull', { description: `
${error.stdErr}
`, dismissable: true, }); } - return error; + throw error; } }); const fetchPipeline = pipelineManager.getPipeline(pipelineManager.actionKeys.FETCH); fetchPipeline.addMiddleware('set-fetch-in-progress', async (next, repository) => { repository.setOperationProgressState('fetch', true); - await next(); - repository.setOperationProgressState('fetch', false); + try { + await next(); + } finally { + repository.setOperationProgressState('fetch', false); + } }); fetchPipeline.addMiddleware('failed-to-fetch-error', async (next, repository) => { try { @@ -101,12 +108,11 @@ export default function({confirm, notificationManager, workspace}) { return result; } catch (error) { if (!(error instanceof GitError)) { throw error; } - console.error(error); notificationManager.addError('Unable to fetch', { description: `
${error.stdErr}
`, dismissable: true, }); - return error; + throw error; } }); @@ -136,8 +142,6 @@ export default function({confirm, notificationManager, workspace}) { description = `\`${branchName}\` already exists. Choose another branch name.`; } else if (error.stdErr.match(/error: you need to resolve your current index first/)) { description = 'You must first resolve merge conflicts.'; - } else { - console.error(error); } notificationManager.addError(message, {description, dismissable: true}); throw error; @@ -172,8 +176,11 @@ export default function({confirm, notificationManager, workspace}) { }); commitPipeline.addMiddleware('set-commit-in-progress', async (next, repository) => { repository.setOperationProgressState('commit', true); - await next(); - repository.setOperationProgressState('commit', false); + try { + await next(); + } finally { + repository.setOperationProgressState('commit', false); + } }); commitPipeline.addMiddleware('failed-to-commit-error', async (next, repository) => { try { @@ -185,15 +192,11 @@ export default function({confirm, notificationManager, workspace}) { return result; } catch (error) { if (!(error instanceof GitError)) { throw error; } - const message = 'Unable to commit'; - let description = `
${error.stdErr || error.stack}
`; - if (error.code === 'ECONFLICT') { - description = 'All merge conflicts must be resolved first.'; - } else { - console.error(error); - } - notificationManager.addError(message, {description, dismissable: true}); - return error; + notificationManager.addError('Unable to commit', { + description: `
${error.stdErr}
`, + dismissable: true, + }); + throw error; } }); From 75d036cb44a1a197cbe79e496dcb03d15c8c61b4 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Mon, 30 Oct 2017 21:00:49 -0700 Subject: [PATCH 0031/5882] :fire: unused Error classes --- lib/models/repository.js | 19 ------------------- test/controllers/git-tab-controller.test.js | 19 ------------------- 2 files changed, 38 deletions(-) diff --git a/lib/models/repository.js b/lib/models/repository.js index 7908458645..a99686a339 100644 --- a/lib/models/repository.js +++ b/lib/models/repository.js @@ -13,25 +13,6 @@ const MERGE_MARKER_REGEX = /^(>|<){7} \S+$/m; // Internal option keys used to designate the desired initial state of a Repository. const initialStateSym = Symbol('initialState'); -export class AbortMergeError extends Error { - constructor(code, filePath) { - super(); - this.message = `${code}: ${filePath}.`; - this.code = code; - this.path = filePath; - this.stack = new Error().stack; - } -} - -export class CommitError extends Error { - constructor(code) { - super(); - this.message = `Commit error: ${code}.`; - this.code = code; - this.stack = new Error().stack; - } -} - export default class Repository { constructor(workingDirectoryPath, gitStrategy = null, options = {}) { this.workingDirectoryPath = workingDirectoryPath; diff --git a/test/controllers/git-tab-controller.test.js b/test/controllers/git-tab-controller.test.js index ba779a48c4..9121f5be0f 100644 --- a/test/controllers/git-tab-controller.test.js +++ b/test/controllers/git-tab-controller.test.js @@ -148,25 +148,6 @@ describe('GitTabController', function() { }); describe('abortMerge()', function() { - // it('shows an error notification when abortMerge() throws an EDIRTYSTAGED exception', async function() { - // const workdirPath = await cloneRepository('three-files'); - // const repository = await buildRepository(workdirPath); - // sinon.stub(repository, 'abortMerge').callsFake(async () => { - // await Promise.resolve(); - // throw new AbortMergeError('EDIRTYSTAGED', 'a.txt'); - // }); - // - // const confirm = sinon.stub(); - // const controller = new GitTabController({ - // workspace, commandRegistry, notificationManager, confirm, repository, - // resolutionProgress, refreshResolutionProgress, - // }); - // assert.equal(notificationManager.getNotifications().length, 0); - // confirm.returns(0); - // await controller.abortMerge(); - // assert.equal(notificationManager.getNotifications().length, 1); - // }); - it('resets merge related state', async function() { const workdirPath = await cloneRepository('merge-conflict'); const repository = await buildRepository(workdirPath); From 1a2232225c5c90e9406f68cd93fb4d1520cde88b Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Mon, 30 Oct 2017 21:08:12 -0700 Subject: [PATCH 0032/5882] :fire: .only --- test/controllers/status-bar-tile-controller.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/controllers/status-bar-tile-controller.test.js b/test/controllers/status-bar-tile-controller.test.js index fb1a51d184..331a133d01 100644 --- a/test/controllers/status-bar-tile-controller.test.js +++ b/test/controllers/status-bar-tile-controller.test.js @@ -13,7 +13,7 @@ import BranchView from '../../lib/views/branch-view'; import PushPullView from '../../lib/views/push-pull-view'; import ChangedFilesCountView from '../../lib/views/changed-files-count-view'; -describe.only('StatusBarTileController', function() { +describe('StatusBarTileController', function() { let atomEnvironment; let workspace, workspaceElement, commandRegistry, notificationManager, tooltips, confirm; let component; From a4646832c4e320569eaee849f0dcd87f75c8940f Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Mon, 30 Oct 2017 22:58:30 -0700 Subject: [PATCH 0033/5882] :fire: ModelStateRegistry tests --- .../controllers/file-patch-controller.test.js | 13 +------ test/controllers/root-controller.test.js | 37 ------------------- 2 files changed, 1 insertion(+), 49 deletions(-) diff --git a/test/controllers/file-patch-controller.test.js b/test/controllers/file-patch-controller.test.js index f7e63f1c14..9ac62cd73d 100644 --- a/test/controllers/file-patch-controller.test.js +++ b/test/controllers/file-patch-controller.test.js @@ -15,7 +15,7 @@ import Switchboard from '../../lib/switchboard'; describe('FilePatchController', function() { let atomEnv, commandRegistry, tooltips, deserializers; - let component, switchboard, repository, filePath, getFilePatchForPath, repositoryStateRegistry, workdirPath; + let component, switchboard, repository, filePath, getFilePatchForPath, workdirPath; let discardLines, didSurfaceFile, didDiveIntoFilePath, quietlySelectItem, undoLastDiscard, openFiles, getRepositoryForWorkdir; let getSelectedStagingViewItems; @@ -34,16 +34,6 @@ describe('FilePatchController', function() { undoLastDiscard = sinon.spy(); openFiles = sinon.spy(); getSelectedStagingViewItems = sinon.spy(); - repositoryStateRegistry = { - getStateForModel() { - return {amending: false}; - }, - onDidUpdate() { - return { - dispose() {}, - }; - }, - }; workdirPath = await cloneRepository('multi-line-file'); repository = await buildRepository(workdirPath); @@ -73,7 +63,6 @@ describe('FilePatchController', function() { undoLastDiscard={undoLastDiscard} openFiles={openFiles} getRepositoryForWorkdir={getRepositoryForWorkdir} - repositoryStateRegistry={repositoryStateRegistry} workingDirectoryPath={repository.getWorkingDirectoryPath()} getSelectedStagingViewItems={getSelectedStagingViewItems} uri={'some/uri'} diff --git a/test/controllers/root-controller.test.js b/test/controllers/root-controller.test.js index b4897dead3..e0f725b611 100644 --- a/test/controllers/root-controller.test.js +++ b/test/controllers/root-controller.test.js @@ -150,43 +150,6 @@ describe('RootController', function() { } }); - xdescribe('when amend mode is toggled in the staging panel while viewing a staged change', function() { - it('updates the amending state and saves it to the repositoryStateRegistry', async function() { - const workdirPath = await cloneRepository('multiple-commits'); - const repository = await buildRepository(workdirPath); - - app = React.cloneElement(app, {repository}); - const wrapper = shallow(app); - - const repositoryStateRegistry = wrapper.instance().repositoryStateRegistry; - - sinon.stub(repositoryStateRegistry, 'setStateForModel'); - sinon.stub(repositoryStateRegistry, 'save'); - - assert.isFalse(wrapper.state('amending')); - - await wrapper.instance().didChangeAmending(true); - assert.isTrue(wrapper.state('amending')); - assert.equal(repositoryStateRegistry.setStateForModel.callCount, 1); - assert.deepEqual(repositoryStateRegistry.setStateForModel.args[0], [ - repository, - {amending: true}, - ]); - assert.equal(repositoryStateRegistry.save.callCount, 1); - - repositoryStateRegistry.setStateForModel.reset(); - repositoryStateRegistry.save.reset(); - await wrapper.instance().didChangeAmending(false); - assert.isFalse(wrapper.state('amending')); - assert.equal(repositoryStateRegistry.setStateForModel.callCount, 1); - assert.deepEqual(repositoryStateRegistry.setStateForModel.args[0], [ - repository, - {amending: false}, - ]); - assert.equal(repositoryStateRegistry.save.callCount, 1); - }); - }); - ['git', 'github'].forEach(function(tabName) { describe(`${tabName} tab tracker`, function() { let wrapper, tabTracker, mockDockItem; From 47f4becf9cbb064b454421e02cf8c74482aae27d Mon Sep 17 00:00:00 2001 From: Steven Date: Tue, 31 Oct 2017 14:49:24 -0700 Subject: [PATCH 0034/5882] Adding visual indication a commit is in progress --- lib/views/commit-view.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/views/commit-view.js b/lib/views/commit-view.js index bd6069cb05..746eb7ca71 100644 --- a/lib/views/commit-view.js +++ b/lib/views/commit-view.js @@ -153,6 +153,8 @@ export default class CommitView { commitButtonText() { if (this.props.isAmending) { return `Amend commit (${shortenSha(this.props.lastCommit.getSha())})`; + } else if (this.props.isCommitting) { + return 'Working...'; } else { if (this.props.branchName) { return `Commit to ${this.props.branchName}`; From 4888c39878f06be9284ae1049883230962cc6000 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Tue, 31 Oct 2017 17:08:40 -0700 Subject: [PATCH 0035/5882] Fix flaky test --- test/controllers/git-tab-controller.test.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/controllers/git-tab-controller.test.js b/test/controllers/git-tab-controller.test.js index 9121f5be0f..54b9c0b23d 100644 --- a/test/controllers/git-tab-controller.test.js +++ b/test/controllers/git-tab-controller.test.js @@ -168,10 +168,10 @@ describe('GitTabController', function() { confirm.returns(0); await controller.abortMerge(); - await controller.getLastModelDataRefreshPromise(); - modelData = controller.repositoryObserver.getActiveModelData(); - - assert.equal(modelData.mergeConflicts.length, 0); + await until(() => { + modelData = controller.repositoryObserver.getActiveModelData(); + return modelData.mergeConflicts.length === 0; + }); assert.isFalse(modelData.isMerging); assert.isNull(modelData.mergeMessage); }); From 86ca2fc5315171ecc1461088459e8e93dce797ca Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Tue, 31 Oct 2017 17:22:11 -0700 Subject: [PATCH 0036/5882] Extract helper methods from get-repo-pipeline-manager.js --- lib/controllers/root-controller.js | 25 ++++---------------- lib/get-repo-pipeline-manager.js | 36 ++++------------------------ lib/helpers.js | 38 ++++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 52 deletions(-) diff --git a/lib/controllers/root-controller.js b/lib/controllers/root-controller.js index 5d716a0c20..2b3889fc89 100644 --- a/lib/controllers/root-controller.js +++ b/lib/controllers/root-controller.js @@ -24,7 +24,8 @@ import RepositoryConflictController from './repository-conflict-controller'; import GithubLoginModel from '../models/github-login-model'; import Conflict from '../models/conflicts/conflict'; import Switchboard from '../switchboard'; -import {copyFile, deleteFileOrFolder, realPath, toNativePathSep} from '../helpers'; +import {copyFile, deleteFileOrFolder, realPath, toNativePathSep, + destroyFilePatchPaneItems, destroyEmptyFilePatchPaneItems} from '../helpers'; import {GitError} from '../git-shell-out-strategy'; function getPropsFromUri(uri) { @@ -190,7 +191,6 @@ export default class RootController extends React.Component { discardWorkDirChangesForPaths={this.discardWorkDirChangesForPaths} undoLastDiscard={this.undoLastDiscard} refreshResolutionProgress={this.refreshResolutionProgress} - destroyFilePatchPaneItems={this.destroyFilePatchPaneItems} /> @@ -438,28 +438,13 @@ export default class RootController extends React.Component { } @autobind - destroyFilePatchPaneItems({onlyStaged} = {}) { - const itemsToDestroy = this.getFilePatchPaneItems({onlyStaged}); - itemsToDestroy.forEach(item => item.destroy()); + destroyFilePatchPaneItems() { + destroyFilePatchPaneItems({onlyStaged: false}, this.props.workspace); } @autobind destroyEmptyFilePatchPaneItems() { - const itemsToDestroy = this.getFilePatchPaneItems({empty: true}); - itemsToDestroy.forEach(item => item.destroy()); - } - - getFilePatchPaneItems({onlyStaged, empty} = {}) { - return this.props.workspace.getPaneItems().filter(item => { - const isFilePatchItem = item && item.getRealItem && item.getRealItem() instanceof FilePatchController; - if (onlyStaged) { - return isFilePatchItem && item.stagingStatus === 'staged'; - } else if (empty) { - return isFilePatchItem ? item.isEmpty() : false; - } else { - return isFilePatchItem; - } - }); + destroyEmptyFilePatchPaneItems(this.props.workspace); } @autobind diff --git a/lib/get-repo-pipeline-manager.js b/lib/get-repo-pipeline-manager.js index 76de979f7f..8a50da74f1 100644 --- a/lib/get-repo-pipeline-manager.js +++ b/lib/get-repo-pipeline-manager.js @@ -3,7 +3,8 @@ import path from 'path'; import ActionPipelineManager from './action-pipeline'; import {GitError} from './git-shell-out-strategy'; import {deleteFileOrFolder} from './helpers'; -import FilePatchController from './controllers/file-patch-controller'; + +import {getCommitMessagePath, getCommitMessageEditors, destroyFilePatchPaneItems} from './helpers'; export default function({confirm, notificationManager, workspace}) { const pipelineManager = new ActionPipelineManager({ @@ -159,7 +160,7 @@ export default function({confirm, notificationManager, workspace}) { return choice === 0; } - const commitMessageEditors = getCommitMessageEditors(repository); + const commitMessageEditors = getCommitMessageEditors(repository, workspace); if (commitMessageEditors.length > 0) { if (!commitMessageEditors.some(e => e.isModified()) || confirmCommit()) { await next(); @@ -188,7 +189,7 @@ export default function({confirm, notificationManager, workspace}) { repository.setAmending(false); repository.setAmendingCommitMessage(''); repository.setRegularCommitMessage(''); - destroyFilePatchPaneItems({onlyStaged: true}); + destroyFilePatchPaneItems({onlyStaged: true}, workspace); return result; } catch (error) { if (!(error instanceof GitError)) { throw error; } @@ -200,34 +201,5 @@ export default function({confirm, notificationManager, workspace}) { } }); - function getCommitMessagePath(repository) { - return path.join(repository.getGitDirectoryPath(), 'ATOM_COMMIT_EDITMSG'); - } - - function getCommitMessageEditors(repository) { - if (!repository.isPresent()) { - return []; - } - return workspace.getTextEditors().filter(editor => editor.getPath() === getCommitMessagePath(repository)); - } - - function destroyFilePatchPaneItems({onlyStaged} = {}) { - const itemsToDestroy = getFilePatchPaneItems({onlyStaged}); - itemsToDestroy.forEach(item => item.destroy()); - } - - function getFilePatchPaneItems({onlyStaged, empty} = {}) { - return workspace.getPaneItems().filter(item => { - const isFilePatchItem = item && item.getRealItem && item.getRealItem() instanceof FilePatchController; - if (onlyStaged) { - return isFilePatchItem && item.stagingStatus === 'staged'; - } else if (empty) { - return isFilePatchItem ? item.isEmpty() : false; - } else { - return isFilePatchItem; - } - }); - } - return pipelineManager; } diff --git a/lib/helpers.js b/lib/helpers.js index 8a294cb867..31b39a652f 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -312,3 +312,41 @@ export function toSentence(array) { } }, ''); } + + +// Repository and workspace helpers + +export function getCommitMessagePath(repository) { + return path.join(repository.getGitDirectoryPath(), 'ATOM_COMMIT_EDITMSG'); +} + +export function getCommitMessageEditors(repository, workspace) { + if (!repository.isPresent()) { + return []; + } + return workspace.getTextEditors().filter(editor => editor.getPath() === getCommitMessagePath(repository)); +} + +import FilePatchController from './controllers/file-patch-controller'; +export function getFilePatchPaneItems({onlyStaged, empty} = {}, workspace) { + return workspace.getPaneItems().filter(item => { + const isFilePatchItem = item && item.getRealItem && item.getRealItem() instanceof FilePatchController; + if (onlyStaged) { + return isFilePatchItem && item.stagingStatus === 'staged'; + } else if (empty) { + return isFilePatchItem ? item.isEmpty() : false; + } else { + return isFilePatchItem; + } + }); +} + +export function destroyFilePatchPaneItems({onlyStaged} = {}, workspace) { + const itemsToDestroy = getFilePatchPaneItems({onlyStaged}, workspace); + itemsToDestroy.forEach(item => item.destroy()); +} + +export function destroyEmptyFilePatchPaneItems(workspace) { + const itemsToDestroy = getFilePatchPaneItems({empty: true}, workspace); + itemsToDestroy.forEach(item => item.destroy()); +} From baf950d5ee6e0297cd79992b7c5bae0feff694e1 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Tue, 31 Oct 2017 17:28:19 -0700 Subject: [PATCH 0037/5882] Fix test description that accidentally got lost --- test/controllers/status-bar-tile-controller.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/controllers/status-bar-tile-controller.test.js b/test/controllers/status-bar-tile-controller.test.js index 331a133d01..3fb503a0d2 100644 --- a/test/controllers/status-bar-tile-controller.test.js +++ b/test/controllers/status-bar-tile-controller.test.js @@ -199,7 +199,7 @@ describe('StatusBarTileController', function() { assert.lengthOf(tip.querySelectorAll('select'), 1); }); - it('can check out newly created branches', async function() { + it('forgets newly created branches on repository change', async function() { const [repo0, repo1] = await Promise.all( [0, 1].map(async () => { const workdirPath = await cloneRepository('three-files'); From cea7d392d2053430606ad808a739302d9da98c0f Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Tue, 31 Oct 2017 17:38:12 -0700 Subject: [PATCH 0038/5882] :shirt: --- lib/controllers/commit-view-controller.js | 3 ++- lib/controllers/root-controller.js | 1 + lib/controllers/status-bar-tile-controller.js | 4 ---- lib/get-repo-pipeline-manager.js | 6 +----- lib/github-package.js | 5 +++-- lib/models/repository-states/present.js | 9 +++++++-- lib/views/branch-menu-view.js | 10 ++++++---- 7 files changed, 20 insertions(+), 18 deletions(-) diff --git a/lib/controllers/commit-view-controller.js b/lib/controllers/commit-view-controller.js index b22c7012f7..4f78b40de7 100644 --- a/lib/controllers/commit-view-controller.js +++ b/lib/controllers/commit-view-controller.js @@ -65,7 +65,8 @@ export default class CommitViewController { this.props = {...this.props, ...props}; // If we just checked the "amend" box and we don't yet have a saved amending message, // initialize it to be the message from the last commit. - if (!wasAmending && this.props.isAmending && !this.getAmendingCommitMessage() && this.props.lastCommit.isPresent()) { + const switchToAmending = !wasAmending && this.props.isAmending; + if (switchToAmending && !this.getAmendingCommitMessage() && this.props.lastCommit.isPresent()) { this.setAmendingCommitMessage(props.lastCommit.getMessage()); } else if (!wasMerging && this.props.isMerging && !this.getRegularCommitMessage()) { this.setRegularCommitMessage(this.props.mergeMessage || ''); diff --git a/lib/controllers/root-controller.js b/lib/controllers/root-controller.js index 2b3889fc89..f3609e754c 100644 --- a/lib/controllers/root-controller.js +++ b/lib/controllers/root-controller.js @@ -68,6 +68,7 @@ export default class RootController extends React.Component { destroyGithubTabItem: PropTypes.func.isRequired, filePatchItems: PropTypes.array, removeFilePatchItem: PropTypes.func.isRequired, + pipelineManager: PropTypes.object.isRequired, } static defaultProps = { diff --git a/lib/controllers/status-bar-tile-controller.js b/lib/controllers/status-bar-tile-controller.js index f59e077dce..31ddcfc6b1 100644 --- a/lib/controllers/status-bar-tile-controller.js +++ b/lib/controllers/status-bar-tile-controller.js @@ -61,10 +61,6 @@ export default class StatusBarTileController extends React.Component { toggleGitTab: () => {}, } - constructor(props, context) { - super(props, context); - } - getChangedFilesCount() { const {stagedFiles, unstagedFiles, mergeConflictFiles} = this.props.statusesForChangedFiles; const changedFiles = new Set(); diff --git a/lib/get-repo-pipeline-manager.js b/lib/get-repo-pipeline-manager.js index 8a50da74f1..9f7c686265 100644 --- a/lib/get-repo-pipeline-manager.js +++ b/lib/get-repo-pipeline-manager.js @@ -1,10 +1,6 @@ -import path from 'path'; - import ActionPipelineManager from './action-pipeline'; import {GitError} from './git-shell-out-strategy'; -import {deleteFileOrFolder} from './helpers'; - -import {getCommitMessagePath, getCommitMessageEditors, destroyFilePatchPaneItems} from './helpers'; +import {deleteFileOrFolder, getCommitMessagePath, getCommitMessageEditors, destroyFilePatchPaneItems} from './helpers'; export default function({confirm, notificationManager, workspace}) { const pipelineManager = new ActionPipelineManager({ diff --git a/lib/github-package.js b/lib/github-package.js index de4327c373..8e0dd56e7d 100644 --- a/lib/github-package.js +++ b/lib/github-package.js @@ -419,7 +419,7 @@ export default class GithubPackage { @autobind async cloneRepositoryForProjectPath(remoteUrl, projectPath) { const context = this.contextPool.getContext(projectPath); - // TODO: do we need to pass pipelineManager? or is this just discarded immediately and recreated later in workdir context + // TODO: do we need to pass pipelineManager? or is this discarded immediately & recreated later in workdir context const repository = context.isPresent() ? context.getRepository() : new Repository(projectPath); await repository.clone(remoteUrl); @@ -434,7 +434,8 @@ export default class GithubPackage { @autobind getRepositoryForWorkdir(projectPath) { - return this.guessedContext ? Repository.loadingGuess({pipelineManager: this.pipelineManager}) : this.contextPool.getContext(projectPath).getRepository(); + const loadingGuessRepo = Repository.loadingGuess({pipelineManager: this.pipelineManager}); + return this.guessedContext ? loadingGuessRepo : this.contextPool.getContext(projectPath).getRepository(); } getActiveWorkdir() { diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js index 058a9fc91d..1d72153c1c 100644 --- a/lib/models/repository-states/present.js +++ b/lib/models/repository-states/present.js @@ -259,7 +259,8 @@ export default class Present extends State { Keys.headDescription, ]) commit(rawMessage, options) { - return this.executePipelineAction('COMMIT', async (rawMessage, options) => { + // eslint-disable-next-line no-shadow + return this.executePipelineAction('COMMIT', (rawMessage, options) => { const message = typeof rawMessage === 'string' ? formatCommitMessage(rawMessage) : rawMessage; return this.git().commit(message, options); }, rawMessage, options); @@ -315,7 +316,8 @@ export default class Present extends State { Keys.headDescription, ]) checkout(revision, options = {}) { - return this.executePipelineAction('CHECKOUT', async (revision, options) => { + // eslint-disable-next-line no-shadow + return this.executePipelineAction('CHECKOUT', (revision, options) => { return this.git().checkout(revision, options); }, revision, options); } @@ -337,6 +339,7 @@ export default class Present extends State { Keys.headDescription, ]) async fetch(branchName) { + // eslint-disable-next-line no-shadow return this.executePipelineAction('FETCH', async branchName => { const remote = await this.getRemoteForBranch(branchName); if (!remote.isPresent()) { @@ -352,6 +355,7 @@ export default class Present extends State { Keys.headDescription, ]) pull(branchName) { + // eslint-disable-next-line no-shadow return this.executePipelineAction('PULL', async branchName => { const remote = await this.getRemoteForBranch(branchName); if (!remote.isPresent()) { @@ -375,6 +379,7 @@ export default class Present extends State { }) push(branchName, options) { + // eslint-disable-next-line no-shadow return this.executePipelineAction('PUSH', async (branchName, options) => { const remote = await this.getRemoteForBranch(branchName); return this.git().push(remote.getNameOr('origin'), branchName, options); diff --git a/lib/views/branch-menu-view.js b/lib/views/branch-menu-view.js index b8c56409ea..0f8b41876f 100644 --- a/lib/views/branch-menu-view.js +++ b/lib/views/branch-menu-view.js @@ -72,7 +72,10 @@ export default class BranchMenuView extends React.Component { ); - const iconClasses = cx('github-BranchMenuView-item', 'icon', {'icon-git-branch': !disableControls, 'icon-sync': disableControls}); + const iconClasses = cx('github-BranchMenuView-item', 'icon', { + 'icon-git-branch': !disableControls, + 'icon-sync': disableControls, + }); return (
@@ -114,7 +117,6 @@ export default class BranchMenuView extends React.Component { await this.checkout(branchName, {createNew: true}); } else { this.setState({createNew: true}, () => { - console.log(global.element = this.editorElement); this.editorElement.focus(); }); } @@ -131,7 +133,7 @@ export default class BranchMenuView extends React.Component { } @autobind - async disableControls(branchName) { + disableControls(branchName) { if (this.editorElement) { this.editorElement.getModel().component.setInputEnabled(false); this.editorElement.classList.remove('is-focused'); @@ -140,7 +142,7 @@ export default class BranchMenuView extends React.Component { } @autobind - async restoreControls() { + restoreControls() { if (this.editorElement) { this.editorElement.getModel().component.setInputEnabled(true); this.editorElement.focus(); From db22ce596d49f6d327bce1778d63e546767255e0 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Thu, 2 Nov 2017 13:50:58 -0700 Subject: [PATCH 0039/5882] Extract OperationStates class --- lib/controllers/commit-view-controller.js | 2 +- lib/controllers/status-bar-tile-controller.js | 11 +-- lib/get-repo-pipeline-manager.js | 20 ++--- lib/models/repository-states/present.js | 90 +++++++++++++++---- lib/models/repository-states/state.js | 5 -- lib/models/repository.js | 1 - .../status-bar-tile-controller.test.js | 2 +- 7 files changed, 88 insertions(+), 43 deletions(-) diff --git a/lib/controllers/commit-view-controller.js b/lib/controllers/commit-view-controller.js index 4f78b40de7..f92a99bf08 100644 --- a/lib/controllers/commit-view-controller.js +++ b/lib/controllers/commit-view-controller.js @@ -76,7 +76,7 @@ export default class CommitViewController { render() { const message = this.getCommitMessage(); - const isCommitting = this.props.repository.getOperationStates().commitInProgress; + const isCommitting = this.props.repository.getOperationStates().isCommitInProgress(); return ( remote.getName() === 'origin').length > 0; }, - operationStates: repository.getOperationStates(), }); }, }) @@ -50,14 +49,12 @@ export default class StatusBarTileController extends React.Component { originExists: PropTypes.bool, toggleGitTab: PropTypes.func, ensureGitTabVisible: PropTypes.func, - operationStates: PropTypes.object, } static defaultProps = { currentBranch: nullBranch, branches: [], currentRemote: nullRemote, - operationStates: {}, toggleGitTab: () => {}, } @@ -112,10 +109,10 @@ export default class StatusBarTileController extends React.Component { return null; } - // const operations = this.props.operations; - // const pushInProgress = operations.getPush().inProgress(); - - const {pushInProgress, pullInProgress, fetchInProgress} = this.props.operationStates; + const operationStates = this.props.repository.getOperationStates(); + const pushInProgress = operationStates.isPushInProgress(); + const pullInProgress = operationStates.isPullInProgress(); + const fetchInProgress = operationStates.isFetchInProgress(); return ( diff --git a/lib/get-repo-pipeline-manager.js b/lib/get-repo-pipeline-manager.js index 9f7c686265..27bea39160 100644 --- a/lib/get-repo-pipeline-manager.js +++ b/lib/get-repo-pipeline-manager.js @@ -21,11 +21,11 @@ export default function({confirm, notificationManager, workspace}) { } }); pushPipeline.addMiddleware('set-push-in-progress', async (next, repository, branchName, options) => { - repository.setOperationProgressState('push', true); + repository.getOperationStates().setPushInProgress(true); try { await next(); } finally { - repository.setOperationProgressState('push', false); + repository.getOperationStates().setPushInProgress(false); } }); pushPipeline.addMiddleware('failed-to-push-error', async (next, repository, branchName, options) => { @@ -52,11 +52,11 @@ export default function({confirm, notificationManager, workspace}) { const pullPipeline = pipelineManager.getPipeline(pipelineManager.actionKeys.PULL); pullPipeline.addMiddleware('set-pull-in-progress', async (next, repository, branchName) => { - repository.setOperationProgressState('pull', true); + repository.getOperationStates().setPullInProgress(true); try { await next(); } finally { - repository.setOperationProgressState('pull', false); + repository.getOperationStates().setPullInProgress(false); } }); pullPipeline.addMiddleware('failed-to-pull-error', async (next, repository, branchName) => { @@ -92,11 +92,11 @@ export default function({confirm, notificationManager, workspace}) { const fetchPipeline = pipelineManager.getPipeline(pipelineManager.actionKeys.FETCH); fetchPipeline.addMiddleware('set-fetch-in-progress', async (next, repository) => { - repository.setOperationProgressState('fetch', true); + repository.getOperationStates().setFetchInProgress(true); try { await next(); } finally { - repository.setOperationProgressState('fetch', false); + repository.getOperationStates().setFetchInProgress(false); } }); fetchPipeline.addMiddleware('failed-to-fetch-error', async (next, repository) => { @@ -115,11 +115,11 @@ export default function({confirm, notificationManager, workspace}) { const checkoutPipeline = pipelineManager.getPipeline(pipelineManager.actionKeys.CHECKOUT); checkoutPipeline.addMiddleware('set-checkout-in-progress', async (next, repository, branchName) => { - repository.setOperationProgressState('checkout', true); + repository.getOperationStates().setCheckoutInProgress(true); try { await next(); } finally { - repository.setOperationProgressState('checkout', false); + repository.getOperationStates().setCheckoutInProgress(false); } }); checkoutPipeline.addMiddleware('failed-to-checkout-error', async (next, repository, branchName, options) => { @@ -172,11 +172,11 @@ export default function({confirm, notificationManager, workspace}) { .catch(() => null); }); commitPipeline.addMiddleware('set-commit-in-progress', async (next, repository) => { - repository.setOperationProgressState('commit', true); + repository.getOperationStates().setCommitInProgress(true); try { await next(); } finally { - repository.setOperationProgressState('commit', false); + repository.getOperationStates().setCommitInProgress(false); } }); commitPipeline.addMiddleware('failed-to-commit-error', async (next, repository) => { diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js index 1d72153c1c..fb98cd253a 100644 --- a/lib/models/repository-states/present.js +++ b/lib/models/repository-states/present.js @@ -56,16 +56,7 @@ export default class Present extends State { {maxHistoryLength: 60}, ); - // this.repositoryOperations = new RepositoryOperations() - - // RepositoryOperations/InProgress with nice API - this.operationStates = { - pushInProgress: false, - pullInProgress: false, - fetchInProgress: false, - commitInProgress: false, - checkoutInProgress: false, - }; + this.operationStates = new OperationStates({didUpdate: this.didUpdate.bind(this)}); this.amending = false; this.amendingCommitMessage = ''; @@ -112,14 +103,6 @@ export default class Present extends State { return this.regularCommitMessage; } - setOperationProgressState(type, value) { - const oldValue = this.operationStates[type + 'InProgress']; - this.operationStates[type + 'InProgress'] = value; - if (oldValue !== value) { - this.didUpdate(); - } - } - getOperationStates() { return this.operationStates; } @@ -964,3 +947,74 @@ const Keys = { Keys.statusBundle, ], }; + +class OperationStates { + constructor(options) { + this.didUpdate = options.didUpdate || () => {} + this.pushInProgress = false; + this.pullInProgress = false; + this.fetchInProgress = false; + this.commitInProgress = false; + this.checkoutInProgress = false; + } + + isPushInProgress() { + return this.pushInProgress; + } + + isPullInProgress() { + return this.pullInProgress; + } + + isFetchInProgress() { + return this.fetchInProgress; + } + + isCommitInProgress() { + return this.commitInProgress; + } + + isCheckoutInProgress() { + return this.checkoutInProgress; + } + + setPushInProgress(value) { + const oldValue = this.pushInProgress + this.pushInProgress = value; + if (oldValue !== value) { + this.didUpdate() + } + } + + setPullInProgress(value) { + const oldValue = this.pullInProgress + this.pullInProgress = value; + if (oldValue !== value) { + this.didUpdate() + } + } + + setFetchInProgress(value) { + const oldValue = this.fetchInProgress + this.fetchInProgress = value; + if (oldValue !== value) { + this.didUpdate() + } + } + + setCommitInProgress(value) { + const oldValue = this.commitInProgress + this.commitInProgress = value; + if (oldValue !== value) { + this.didUpdate() + } + } + + setCheckoutInProgress(value) { + const oldValue = this.checkoutInProgress + this.checkoutInProgress = value; + if (oldValue !== value) { + this.didUpdate() + } + } +} diff --git a/lib/models/repository-states/state.js b/lib/models/repository-states/state.js index d4c14ac208..86a8cdbc3a 100644 --- a/lib/models/repository-states/state.js +++ b/lib/models/repository-states/state.js @@ -423,11 +423,6 @@ export default class State { // Atom repo state - @shouldDelegate - setOperationProgressState() { - return unsupportedOperationPromise(this, 'setOperationProgressState'); - } - @shouldDelegate getOperationStates() { return {}; diff --git a/lib/models/repository.js b/lib/models/repository.js index a99686a339..0889be6adf 100644 --- a/lib/models/repository.js +++ b/lib/models/repository.js @@ -310,7 +310,6 @@ const delegates = [ 'getDiscardHistory', 'getLastHistorySnapshots', - 'setOperationProgressState', 'getOperationStates', 'setAmending', 'isAmending', diff --git a/test/controllers/status-bar-tile-controller.test.js b/test/controllers/status-bar-tile-controller.test.js index 3fb503a0d2..53c6d109e7 100644 --- a/test/controllers/status-bar-tile-controller.test.js +++ b/test/controllers/status-bar-tile-controller.test.js @@ -510,7 +510,7 @@ describe('StatusBarTileController', function() { assert.equal(confirm.callCount, 1); await assert.async.isTrue(repository.git.push.calledWith('origin', 'master', sinon.match({force: true, setUpstream: false}))); - await assert.async.isFalse(repository.getOperationStates().pushInProgress); + await assert.async.isFalse(repository.getOperationStates().isPushInProgress()); }); it('displays a warning notification when pull results in merge conflicts', async function() { From aa003803b241e0356d33efafbf9acbefdbec8c5b Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Thu, 2 Nov 2017 14:05:17 -0700 Subject: [PATCH 0040/5882] Correctly retreive progress state if repository is not present --- lib/controllers/commit-view-controller.js | 3 ++- lib/models/repository-states/present.js | 22 +++++++++++----------- lib/models/repository-states/state.js | 2 +- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/lib/controllers/commit-view-controller.js b/lib/controllers/commit-view-controller.js index f92a99bf08..0d09dd74c2 100644 --- a/lib/controllers/commit-view-controller.js +++ b/lib/controllers/commit-view-controller.js @@ -76,7 +76,8 @@ export default class CommitViewController { render() { const message = this.getCommitMessage(); - const isCommitting = this.props.repository.getOperationStates().isCommitInProgress(); + const operationStates = this.props.repository.getOperationStates(); + const isCommitting = operationStates ? operationStates.isCommitInProgress() : false; return ( {} + this.didUpdate = options.didUpdate || (() => {}); this.pushInProgress = false; this.pullInProgress = false; this.fetchInProgress = false; @@ -979,42 +979,42 @@ class OperationStates { } setPushInProgress(value) { - const oldValue = this.pushInProgress + const oldValue = this.pushInProgress; this.pushInProgress = value; if (oldValue !== value) { - this.didUpdate() + this.didUpdate(); } } setPullInProgress(value) { - const oldValue = this.pullInProgress + const oldValue = this.pullInProgress; this.pullInProgress = value; if (oldValue !== value) { - this.didUpdate() + this.didUpdate(); } } setFetchInProgress(value) { - const oldValue = this.fetchInProgress + const oldValue = this.fetchInProgress; this.fetchInProgress = value; if (oldValue !== value) { - this.didUpdate() + this.didUpdate(); } } setCommitInProgress(value) { - const oldValue = this.commitInProgress + const oldValue = this.commitInProgress; this.commitInProgress = value; if (oldValue !== value) { - this.didUpdate() + this.didUpdate(); } } setCheckoutInProgress(value) { - const oldValue = this.checkoutInProgress + const oldValue = this.checkoutInProgress; this.checkoutInProgress = value; if (oldValue !== value) { - this.didUpdate() + this.didUpdate(); } } } diff --git a/lib/models/repository-states/state.js b/lib/models/repository-states/state.js index 86a8cdbc3a..1a8c20bf3e 100644 --- a/lib/models/repository-states/state.js +++ b/lib/models/repository-states/state.js @@ -425,7 +425,7 @@ export default class State { @shouldDelegate getOperationStates() { - return {}; + return null; } @shouldDelegate From a94e04afaa93a415d4ce9b5fd46189b1b9d3b0ef Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Thu, 2 Nov 2017 14:10:19 -0700 Subject: [PATCH 0041/5882] We want the first branch too --- lib/views/branch-menu-view.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/views/branch-menu-view.js b/lib/views/branch-menu-view.js index 0f8b41876f..ec30866467 100644 --- a/lib/views/branch-menu-view.js +++ b/lib/views/branch-menu-view.js @@ -97,7 +97,7 @@ export default class BranchMenuView extends React.Component { componentWillReceiveProps(nextProps) { const currentBranch = nextProps.currentBranch.getName(); const branchNames = nextProps.branches.map(branch => branch.getName()); - const hasNewBranch = branchNames.indexOf(this.state.checkedOutBranch); + const hasNewBranch = branchNames.includes(this.state.checkedOutBranch); if (currentBranch === this.state.checkedOutBranch && hasNewBranch) { this.restoreControls(); if (this.state.createNew) { this.setState({createNew: false}); } From 27aca32d013ef4a4a45660e5b5aab662f7aa86f9 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Thu, 2 Nov 2017 14:12:01 -0700 Subject: [PATCH 0042/5882] Address proptype errors --- lib/controllers/root-controller.js | 2 +- test/controllers/root-controller.test.js | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/controllers/root-controller.js b/lib/controllers/root-controller.js index f3609e754c..e65a7440e8 100644 --- a/lib/controllers/root-controller.js +++ b/lib/controllers/root-controller.js @@ -68,7 +68,7 @@ export default class RootController extends React.Component { destroyGithubTabItem: PropTypes.func.isRequired, filePatchItems: PropTypes.array, removeFilePatchItem: PropTypes.func.isRequired, - pipelineManager: PropTypes.object.isRequired, + pipelineManager: PropTypes.object, } static defaultProps = { diff --git a/test/controllers/root-controller.test.js b/test/controllers/root-controller.test.js index e0f725b611..2cbbd37bf4 100644 --- a/test/controllers/root-controller.test.js +++ b/test/controllers/root-controller.test.js @@ -12,8 +12,9 @@ import ResolutionProgress from '../../lib/models/conflicts/resolution-progress'; import RootController from '../../lib/controllers/root-controller'; -describe('RootController', function() { - let atomEnv, workspace, commandRegistry, notificationManager, tooltips, config, confirm, deserializers, grammars, app; +describe.only('RootController', function() { + let atomEnv, app; + let workspace, commandRegistry, notificationManager, tooltips, config, confirm, deserializers, grammars, project; let getRepositoryForWorkdir, destroyGitTabItem, destroyGithubTabItem, removeFilePatchItem; beforeEach(function() { @@ -25,6 +26,7 @@ describe('RootController', function() { notificationManager = atomEnv.notifications; tooltips = atomEnv.tooltips; config = atomEnv.config; + project = atomEnv.project; getRepositoryForWorkdir = sinon.stub(); destroyGitTabItem = sinon.spy(); @@ -45,6 +47,7 @@ describe('RootController', function() { tooltips={tooltips} config={config} confirm={confirm} + project={project} repository={absentRepository} resolutionProgress={emptyResolutionProgress} startOpen={false} From b6c6ffbaadb23c3ca8b36acf94f79e49fcf43db6 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Thu, 2 Nov 2017 14:14:45 -0700 Subject: [PATCH 0043/5882] :fire: .only --- test/controllers/root-controller.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/controllers/root-controller.test.js b/test/controllers/root-controller.test.js index 2cbbd37bf4..8ed002e6ec 100644 --- a/test/controllers/root-controller.test.js +++ b/test/controllers/root-controller.test.js @@ -12,7 +12,7 @@ import ResolutionProgress from '../../lib/models/conflicts/resolution-progress'; import RootController from '../../lib/controllers/root-controller'; -describe.only('RootController', function() { +describe('RootController', function() { let atomEnv, app; let workspace, commandRegistry, notificationManager, tooltips, config, confirm, deserializers, grammars, project; let getRepositoryForWorkdir, destroyGitTabItem, destroyGithubTabItem, removeFilePatchItem; From 8f1422856b8ef5400a0aeb2ab9e24490195e4e67 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Thu, 2 Nov 2017 15:19:37 -0700 Subject: [PATCH 0044/5882] Catch errors to get rid of red in console during tests --- lib/controllers/status-bar-tile-controller.js | 2 +- .../status-bar-tile-controller.test.js | 16 ++++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/lib/controllers/status-bar-tile-controller.js b/lib/controllers/status-bar-tile-controller.js index 09843560f0..5f86e934f7 100644 --- a/lib/controllers/status-bar-tile-controller.js +++ b/lib/controllers/status-bar-tile-controller.js @@ -185,7 +185,7 @@ export default class StatusBarTileController extends React.Component { } @autobind - push({force, setUpstream}) { + push({force, setUpstream} = {}) { return this.props.repository.push(this.props.currentBranch.getName(), {force, setUpstream}); } diff --git a/test/controllers/status-bar-tile-controller.test.js b/test/controllers/status-bar-tile-controller.test.js index 53c6d109e7..d4bff7a5cd 100644 --- a/test/controllers/status-bar-tile-controller.test.js +++ b/test/controllers/status-bar-tile-controller.test.js @@ -410,7 +410,11 @@ describe('StatusBarTileController', function() { assert.equal(pushButton.textContent.trim(), 'Push (1)'); assert.equal(pullButton.textContent.trim(), 'Pull (2)'); - pushButton.click(); + try { + await wrapper.instance().getWrappedComponentInstance().push(); + } catch (e) { + assert(e, 'is error'); + } await wrapper.instance().refreshModelData(); await assert.async.isTrue(notificationManager.addError.called); @@ -522,13 +526,13 @@ describe('StatusBarTileController', function() { const wrapper = mount(React.cloneElement(component, {repository})); await wrapper.instance().refreshModelData(); - const tip = getTooltipNode(wrapper, PushPullView); - - const pullButton = tip.querySelector('button.github-PushPullMenuView-pull'); - sinon.stub(notificationManager, 'addWarning'); - pullButton.click(); + try { + await wrapper.instance().getWrappedComponentInstance().pull(); + } catch (e) { + assert(e, 'is error'); + } await wrapper.instance().refreshModelData(); await assert.async.isTrue(notificationManager.addWarning.called); From 0b8914e3945a91e70d73a4778ef7d4746f145c9e Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Thu, 2 Nov 2017 15:26:43 -0700 Subject: [PATCH 0045/5882] :shirt: --- lib/models/repository-states/present.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js index ee68808269..be773b93a7 100644 --- a/lib/models/repository-states/present.js +++ b/lib/models/repository-states/present.js @@ -321,12 +321,12 @@ export default class Present extends State { Keys.statusBundle, Keys.headDescription, ]) - async fetch(branchName) { + fetch(branchName) { // eslint-disable-next-line no-shadow return this.executePipelineAction('FETCH', async branchName => { const remote = await this.getRemoteForBranch(branchName); if (!remote.isPresent()) { - return; + return null; } return this.git().fetch(remote.getName(), branchName); }, branchName); @@ -342,7 +342,7 @@ export default class Present extends State { return this.executePipelineAction('PULL', async branchName => { const remote = await this.getRemoteForBranch(branchName); if (!remote.isPresent()) { - return; + return null; } return this.git().pull(remote.getName(), branchName); }, branchName); From 9faf8e9095284d511c9d70ae2afa5963d044f3ee Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Thu, 2 Nov 2017 15:41:39 -0700 Subject: [PATCH 0046/5882] Use await rather than promise chaining --- lib/get-repo-pipeline-manager.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/get-repo-pipeline-manager.js b/lib/get-repo-pipeline-manager.js index 27bea39160..8798d11476 100644 --- a/lib/get-repo-pipeline-manager.js +++ b/lib/get-repo-pipeline-manager.js @@ -168,8 +168,11 @@ export default function({confirm, notificationManager, workspace}) { }); commitPipeline.addMiddleware('clean-up-disk-commit-msg', async (next, repository) => { await next(); - deleteFileOrFolder(getCommitMessagePath(repository)) - .catch(() => null); + try { + await deleteFileOrFolder(getCommitMessagePath(repository)); + } catch (error) { + // do nothing + } }); commitPipeline.addMiddleware('set-commit-in-progress', async (next, repository) => { repository.getOperationStates().setCommitInProgress(true); From 8ac0b241d11a2cbe9c85a31e4217ee0b35b2f8dc Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Thu, 2 Nov 2017 15:42:10 -0700 Subject: [PATCH 0047/5882] Catch errors for git operations in controllers --- lib/controllers/git-tab-controller.js | 6 +++- lib/controllers/status-bar-tile-controller.js | 32 ++++++++++++++----- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/lib/controllers/git-tab-controller.js b/lib/controllers/git-tab-controller.js index ac24d597e4..37b231f03e 100644 --- a/lib/controllers/git-tab-controller.js +++ b/lib/controllers/git-tab-controller.js @@ -268,7 +268,11 @@ export default class GitTabController { @autobind async commit(message) { - await this.getActiveRepository().commit(message, {amend: this.isAmending()}); + try { + await this.getActiveRepository().commit(message, {amend: this.isAmending()}); + } catch (e) { + // do nothing + } } @autobind diff --git a/lib/controllers/status-bar-tile-controller.js b/lib/controllers/status-bar-tile-controller.js index 5f86e934f7..f2cf17a76d 100644 --- a/lib/controllers/status-bar-tile-controller.js +++ b/lib/controllers/status-bar-tile-controller.js @@ -180,22 +180,38 @@ export default class StatusBarTileController extends React.Component { } @autobind - checkout(branchName, options) { - return this.props.repository.checkout(branchName, options); + async checkout(branchName, options) { + try { + await this.props.repository.checkout(branchName, options); + } catch (e) { + // do nothing + } } @autobind - push({force, setUpstream} = {}) { - return this.props.repository.push(this.props.currentBranch.getName(), {force, setUpstream}); + async push({force, setUpstream} = {}) { + try { + await this.props.repository.push(this.props.currentBranch.getName(), {force, setUpstream}); + } catch (e) { + // do nothing + } } @autobind - pull() { - return this.props.repository.pull(this.props.currentBranch.getName()); + async pull() { + try { + await this.props.repository.pull(this.props.currentBranch.getName()); + } catch (e) { + // do nothing + } } @autobind - fetch() { - return this.props.repository.fetch(this.props.currentBranch.getName()); + async fetch() { + try { + await this.props.repository.fetch(this.props.currentBranch.getName()); + } catch (e) { + // do nothing + } } } From c8eaa861ea2bb327d8447bfbe8516ee961541a24 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Thu, 2 Nov 2017 16:03:37 -0700 Subject: [PATCH 0048/5882] Better to catch errors in views --- lib/controllers/git-tab-controller.js | 8 ++--- lib/controllers/status-bar-tile-controller.js | 32 +++++-------------- lib/views/commit-view.js | 6 +++- lib/views/push-pull-menu-view.js | 24 ++++++++++---- 4 files changed, 33 insertions(+), 37 deletions(-) diff --git a/lib/controllers/git-tab-controller.js b/lib/controllers/git-tab-controller.js index 37b231f03e..ccbdf9ce3d 100644 --- a/lib/controllers/git-tab-controller.js +++ b/lib/controllers/git-tab-controller.js @@ -267,12 +267,8 @@ export default class GitTabController { } @autobind - async commit(message) { - try { - await this.getActiveRepository().commit(message, {amend: this.isAmending()}); - } catch (e) { - // do nothing - } + commit(message) { + return this.getActiveRepository().commit(message, {amend: this.isAmending()}); } @autobind diff --git a/lib/controllers/status-bar-tile-controller.js b/lib/controllers/status-bar-tile-controller.js index f2cf17a76d..5f86e934f7 100644 --- a/lib/controllers/status-bar-tile-controller.js +++ b/lib/controllers/status-bar-tile-controller.js @@ -180,38 +180,22 @@ export default class StatusBarTileController extends React.Component { } @autobind - async checkout(branchName, options) { - try { - await this.props.repository.checkout(branchName, options); - } catch (e) { - // do nothing - } + checkout(branchName, options) { + return this.props.repository.checkout(branchName, options); } @autobind - async push({force, setUpstream} = {}) { - try { - await this.props.repository.push(this.props.currentBranch.getName(), {force, setUpstream}); - } catch (e) { - // do nothing - } + push({force, setUpstream} = {}) { + return this.props.repository.push(this.props.currentBranch.getName(), {force, setUpstream}); } @autobind - async pull() { - try { - await this.props.repository.pull(this.props.currentBranch.getName()); - } catch (e) { - // do nothing - } + pull() { + return this.props.repository.pull(this.props.currentBranch.getName()); } @autobind - async fetch() { - try { - await this.props.repository.fetch(this.props.currentBranch.getName()); - } catch (e) { - // do nothing - } + fetch() { + return this.props.repository.fetch(this.props.currentBranch.getName()); } } diff --git a/lib/views/commit-view.js b/lib/views/commit-view.js index bd6069cb05..488cbf575b 100644 --- a/lib/views/commit-view.js +++ b/lib/views/commit-view.js @@ -124,7 +124,11 @@ export default class CommitView { @autobind async commit() { if (await this.props.prepareToCommit() && this.isCommitButtonEnabled()) { - await this.props.commit(this.editor.getText()); + try { + await this.props.commit(this.editor.getText()); + } catch (e) { + // do nothing + } } else { this.setFocus(CommitView.focus.EDITOR); } diff --git a/lib/views/push-pull-menu-view.js b/lib/views/push-pull-menu-view.js index 1b4c598a4c..434f05432e 100644 --- a/lib/views/push-pull-menu-view.js +++ b/lib/views/push-pull-menu-view.js @@ -102,17 +102,29 @@ export default class PushPullMenuView extends React.Component { } @autobind - fetch() { - return this.props.fetch(); + async fetch() { + try { + return this.props.fetch(); + } catch (e) { + // do nothing + } } @autobind - pull() { - return this.props.pull(); + async pull() { + try { + return this.props.pull(); + } catch (e) { + // do nothing + } } @autobind - push(e) { - return this.props.push({force: e.metaKey || e.ctrlKey, setUpstream: !this.props.currentRemote.isPresent()}); + async push(evt) { + try { + await this.props.push({force: evt.metaKey || evt.ctrlKey, setUpstream: !this.props.currentRemote.isPresent()}); + } catch (e) { + // do nothing + } } } From 8159237996b41dbed446aec4be058165d1eb3d34 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Thu, 2 Nov 2017 16:15:06 -0700 Subject: [PATCH 0049/5882] Hide/show branch select list to prevent flicker When a @@ -72,11 +85,6 @@ export default class BranchMenuView extends React.Component { ); - const iconClasses = cx('github-BranchMenuView-item', 'icon', { - 'icon-git-branch': !disableControls, - 'icon-sync': disableControls, - }); - return (
@@ -86,7 +94,8 @@ export default class BranchMenuView extends React.Component {
- { this.state.createNew ? newBranchEditor : selectBranchView } + {newBranchEditor} + {selectBranchView}
diff --git a/styles/branch-menu-view.less b/styles/branch-menu-view.less index 2a70e36d72..066d498e07 100644 --- a/styles/branch-menu-view.less +++ b/styles/branch-menu-view.less @@ -2,6 +2,10 @@ .github-BranchMenuView { + .hidden { + display: none + } + &-selector { display: flex; align-items: center; From 6331fa80b1f76a9a533dfdcc8ab66ff80695884f Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Thu, 2 Nov 2017 16:26:42 -0700 Subject: [PATCH 0050/5882] Make spinner actually spin --- styles/branch-menu-view.less | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/styles/branch-menu-view.less b/styles/branch-menu-view.less index 066d498e07..421d38d6cf 100644 --- a/styles/branch-menu-view.less +++ b/styles/branch-menu-view.less @@ -13,15 +13,13 @@ &-item { margin: @component-padding / 2; + } - // TODO: make this work - // Sync animation - .icon-sync::before { - @keyframes github-BranchViewSync-animation { - 100% { transform: rotate(360deg); } - } - animation: github-BranchViewSync-animation 2s linear 30; // limit to 1min in case something gets stuck + .icon-sync::before { + @keyframes github-BranchViewSync-animation { + 100% { transform: rotate(360deg); } } + animation: github-BranchViewSync-animation 2s linear 30; // limit to 1min in case something gets stuck } .icon-git-branch { From d6efcbb6c427f2c17a6acef600ef67894a043451 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Tue, 7 Nov 2017 21:05:51 -0800 Subject: [PATCH 0051/5882] Click icon to toggle hard wrap --- lib/controllers/commit-view-controller.js | 1 + lib/views/commit-view.js | 29 ++++++++++++++++++----- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/lib/controllers/commit-view-controller.js b/lib/controllers/commit-view-controller.js index 7fdd025433..336bacedc6 100644 --- a/lib/controllers/commit-view-controller.js +++ b/lib/controllers/commit-view-controller.js @@ -96,6 +96,7 @@ export default class CommitViewController { To disable, go to github package settings.', + title: 'Toggle hard wrap on commit', class: 'github-CommitView-info-tooltip', - delay: {hide: 50000000}, }), + props.config.onDidChange('github.automaticCommitMessageWrapping', () => etch.update(this)), ); - - console.log(global.e = this.element); } destroy() { @@ -87,7 +86,7 @@ export default class CommitView { autoHeight={false} scrollPastEnd={false} /> -
+ {this.renderHardWrapIcons()} From 1fb600c91431c72afe84fb935cab76b24053964c Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Tue, 21 Nov 2017 13:38:43 -0800 Subject: [PATCH 0088/5882] Determine amending option in Repository#commit --- lib/controllers/git-tab-controller.js | 2 +- lib/models/repository-states/present.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/controllers/git-tab-controller.js b/lib/controllers/git-tab-controller.js index ccbdf9ce3d..b4a8eca000 100644 --- a/lib/controllers/git-tab-controller.js +++ b/lib/controllers/git-tab-controller.js @@ -268,7 +268,7 @@ export default class GitTabController { @autobind commit(message) { - return this.getActiveRepository().commit(message, {amend: this.isAmending()}); + return this.getActiveRepository().commit(message); } @autobind diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js index 77d940b46a..bd33383f4f 100644 --- a/lib/models/repository-states/present.js +++ b/lib/models/repository-states/present.js @@ -246,7 +246,8 @@ export default class Present extends State { // eslint-disable-next-line no-shadow return this.executePipelineAction('COMMIT', (rawMessage, options) => { const message = typeof rawMessage === 'string' ? formatCommitMessage(rawMessage) : rawMessage; - return this.git().commit(message, options); + const opts = {...options, amend: this.isAmending()}; + return this.git().commit(message, opts); }, rawMessage, options); } From bf658265402a378c894976e8a8663848d6d3e354 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Tue, 21 Nov 2017 13:54:33 -0800 Subject: [PATCH 0089/5882] Get text editor component directly --- lib/views/branch-menu-view.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/views/branch-menu-view.js b/lib/views/branch-menu-view.js index 2b6a0d0394..11e9fa65ba 100644 --- a/lib/views/branch-menu-view.js +++ b/lib/views/branch-menu-view.js @@ -155,7 +155,7 @@ export default class BranchMenuView extends React.Component { @autobind disableControls(branchName) { if (this.editorElement) { - this.editorElement.getModel().component.setInputEnabled(false); + this.editorElement.getComponent().setInputEnabled(false); this.editorElement.classList.remove('is-focused'); } } @@ -163,7 +163,7 @@ export default class BranchMenuView extends React.Component { @autobind restoreControls() { if (this.editorElement) { - this.editorElement.getModel().component.setInputEnabled(true); + this.editorElement.getComponent().setInputEnabled(true); this.editorElement.focus(); this.editorElement.classList.add('is-focused'); } From a2cab74d14394b76fe022d1400eda75e55cb60e0 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Tue, 21 Nov 2017 14:24:33 -0800 Subject: [PATCH 0090/5882] Fix test for amending change --- test/models/repository.test.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/models/repository.test.js b/test/models/repository.test.js index 466b426332..28c46c3a51 100644 --- a/test/models/repository.test.js +++ b/test/models/repository.test.js @@ -418,7 +418,8 @@ describe('Repository', function() { const lastCommit = await repo.git.getHeadCommit(); const lastCommitParent = await repo.git.getCommit('HEAD~'); - await repo.commit('amend last commit', {amend: true, allowEmpty: true}); + repo.setAmending(true); + await repo.commit('amend last commit', {allowEmpty: true}); const amendedCommit = await repo.git.getHeadCommit(); const amendedCommitParent = await repo.git.getCommit('HEAD~'); assert.notDeepEqual(lastCommit, amendedCommit); From 6475d54b8a19f65c167841a515dfbaa8d9124ad0 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Thu, 9 Nov 2017 13:47:52 -0800 Subject: [PATCH 0091/5882] Clear out notifications before testing that new ones are added (cherry picked from commit 0f4c792044682a9b12b066da3e9f21e13629568a) --- test/controllers/git-tab-controller.test.js | 8 ++++---- test/views/staging-view.test.js | 5 +++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/test/controllers/git-tab-controller.test.js b/test/controllers/git-tab-controller.test.js index 5d567176c8..0fd69417a7 100644 --- a/test/controllers/git-tab-controller.test.js +++ b/test/controllers/git-tab-controller.test.js @@ -150,7 +150,7 @@ describe('GitTabController', function() { }); describe('abortMerge()', function() { - it('shows an error notification when abortMerge() throws an EDIRTYSTAGED exception', async function() { + it.only('shows an error notification when abortMerge() throws an EDIRTYSTAGED exception', async function() { const workdirPath = await cloneRepository('three-files'); const repository = await buildRepository(workdirPath); sinon.stub(repository, 'abortMerge').callsFake(async () => { @@ -163,7 +163,7 @@ describe('GitTabController', function() { workspace, commandRegistry, notificationManager, confirm, repository, resolutionProgress, refreshResolutionProgress, }); - assert.equal(notificationManager.getNotifications().length, 0); + notificationManager.clear(); // clear out any notifications confirm.returns(0); await controller.abortMerge(); assert.equal(notificationManager.getNotifications().length, 1); @@ -227,7 +227,7 @@ describe('GitTabController', function() { }); describe('commit(message)', function() { - it('shows an error notification when committing throws an ECONFLICT exception', async function() { + it.only('shows an error notification when committing throws an ECONFLICT exception', async function() { const workdirPath = await cloneRepository('three-files'); const repository = await buildRepository(workdirPath); sinon.stub(repository, 'commit').callsFake(async () => { @@ -239,7 +239,7 @@ describe('GitTabController', function() { workspace, commandRegistry, notificationManager, repository, resolutionProgress, refreshResolutionProgress, }); - assert.equal(notificationManager.getNotifications().length, 0); + notificationManager.clear(); // clear out any notifications await controller.commit(); assert.equal(notificationManager.getNotifications().length, 1); }); diff --git a/test/views/staging-view.test.js b/test/views/staging-view.test.js index e11968d46f..988a3f6def 100644 --- a/test/views/staging-view.test.js +++ b/test/views/staging-view.test.js @@ -310,7 +310,7 @@ describe('StagingView', function() { }); describe('when the file doesn\'t exist', function() { - it('shows an info notification and does not open the file', async function() { + it.only('shows an info notification and does not open the file', async function() { sinon.spy(notificationManager, 'addInfo'); const view = new StagingView({ @@ -320,9 +320,10 @@ describe('StagingView', function() { sinon.stub(view, 'fileExists').returns(false); - assert.equal(notificationManager.getNotifications().length, 0); + notificationManager.clear(); // clear out notifications await view.showMergeConflictFileForPath('conflict.txt'); + assert.equal(notificationManager.getNotifications().length, 1); assert.equal(workspace.open.callCount, 0); assert.equal(notificationManager.addInfo.callCount, 1); assert.deepEqual(notificationManager.addInfo.args[0], ['File has been deleted.']); From 9416e71095bdee2202725dca03f0d4a4d264b90e Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Thu, 9 Nov 2017 13:56:50 -0800 Subject: [PATCH 0092/5882] :fire: .only's used for selectively running tests on CI (cherry picked from commit 0464ca9a63ea85a846c4e4943a086930e9256301) --- test/controllers/git-tab-controller.test.js | 4 ++-- test/views/staging-view.test.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test/controllers/git-tab-controller.test.js b/test/controllers/git-tab-controller.test.js index 0fd69417a7..e78c3ce4a8 100644 --- a/test/controllers/git-tab-controller.test.js +++ b/test/controllers/git-tab-controller.test.js @@ -150,7 +150,7 @@ describe('GitTabController', function() { }); describe('abortMerge()', function() { - it.only('shows an error notification when abortMerge() throws an EDIRTYSTAGED exception', async function() { + it('shows an error notification when abortMerge() throws an EDIRTYSTAGED exception', async function() { const workdirPath = await cloneRepository('three-files'); const repository = await buildRepository(workdirPath); sinon.stub(repository, 'abortMerge').callsFake(async () => { @@ -227,7 +227,7 @@ describe('GitTabController', function() { }); describe('commit(message)', function() { - it.only('shows an error notification when committing throws an ECONFLICT exception', async function() { + it('shows an error notification when committing throws an ECONFLICT exception', async function() { const workdirPath = await cloneRepository('three-files'); const repository = await buildRepository(workdirPath); sinon.stub(repository, 'commit').callsFake(async () => { diff --git a/test/views/staging-view.test.js b/test/views/staging-view.test.js index 988a3f6def..7a794a88bd 100644 --- a/test/views/staging-view.test.js +++ b/test/views/staging-view.test.js @@ -310,7 +310,7 @@ describe('StagingView', function() { }); describe('when the file doesn\'t exist', function() { - it.only('shows an info notification and does not open the file', async function() { + it('shows an info notification and does not open the file', async function() { sinon.spy(notificationManager, 'addInfo'); const view = new StagingView({ From 50d6cc13f985c6fe34e47031c382fef026bf68f4 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Thu, 9 Nov 2017 13:47:52 -0800 Subject: [PATCH 0093/5882] Clear out notifications before testing that new ones are added (cherry picked from commit 0f4c792044682a9b12b066da3e9f21e13629568a) --- test/controllers/git-tab-controller.test.js | 8 ++++---- test/views/staging-view.test.js | 5 +++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/test/controllers/git-tab-controller.test.js b/test/controllers/git-tab-controller.test.js index 5d567176c8..0fd69417a7 100644 --- a/test/controllers/git-tab-controller.test.js +++ b/test/controllers/git-tab-controller.test.js @@ -150,7 +150,7 @@ describe('GitTabController', function() { }); describe('abortMerge()', function() { - it('shows an error notification when abortMerge() throws an EDIRTYSTAGED exception', async function() { + it.only('shows an error notification when abortMerge() throws an EDIRTYSTAGED exception', async function() { const workdirPath = await cloneRepository('three-files'); const repository = await buildRepository(workdirPath); sinon.stub(repository, 'abortMerge').callsFake(async () => { @@ -163,7 +163,7 @@ describe('GitTabController', function() { workspace, commandRegistry, notificationManager, confirm, repository, resolutionProgress, refreshResolutionProgress, }); - assert.equal(notificationManager.getNotifications().length, 0); + notificationManager.clear(); // clear out any notifications confirm.returns(0); await controller.abortMerge(); assert.equal(notificationManager.getNotifications().length, 1); @@ -227,7 +227,7 @@ describe('GitTabController', function() { }); describe('commit(message)', function() { - it('shows an error notification when committing throws an ECONFLICT exception', async function() { + it.only('shows an error notification when committing throws an ECONFLICT exception', async function() { const workdirPath = await cloneRepository('three-files'); const repository = await buildRepository(workdirPath); sinon.stub(repository, 'commit').callsFake(async () => { @@ -239,7 +239,7 @@ describe('GitTabController', function() { workspace, commandRegistry, notificationManager, repository, resolutionProgress, refreshResolutionProgress, }); - assert.equal(notificationManager.getNotifications().length, 0); + notificationManager.clear(); // clear out any notifications await controller.commit(); assert.equal(notificationManager.getNotifications().length, 1); }); diff --git a/test/views/staging-view.test.js b/test/views/staging-view.test.js index e11968d46f..988a3f6def 100644 --- a/test/views/staging-view.test.js +++ b/test/views/staging-view.test.js @@ -310,7 +310,7 @@ describe('StagingView', function() { }); describe('when the file doesn\'t exist', function() { - it('shows an info notification and does not open the file', async function() { + it.only('shows an info notification and does not open the file', async function() { sinon.spy(notificationManager, 'addInfo'); const view = new StagingView({ @@ -320,9 +320,10 @@ describe('StagingView', function() { sinon.stub(view, 'fileExists').returns(false); - assert.equal(notificationManager.getNotifications().length, 0); + notificationManager.clear(); // clear out notifications await view.showMergeConflictFileForPath('conflict.txt'); + assert.equal(notificationManager.getNotifications().length, 1); assert.equal(workspace.open.callCount, 0); assert.equal(notificationManager.addInfo.callCount, 1); assert.deepEqual(notificationManager.addInfo.args[0], ['File has been deleted.']); From 24af5fef874ae853689a5fb678c011c32cbd4b3f Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Thu, 9 Nov 2017 13:56:50 -0800 Subject: [PATCH 0094/5882] :fire: .only's used for selectively running tests on CI (cherry picked from commit 0464ca9a63ea85a846c4e4943a086930e9256301) --- test/controllers/git-tab-controller.test.js | 4 ++-- test/views/staging-view.test.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test/controllers/git-tab-controller.test.js b/test/controllers/git-tab-controller.test.js index 0fd69417a7..e78c3ce4a8 100644 --- a/test/controllers/git-tab-controller.test.js +++ b/test/controllers/git-tab-controller.test.js @@ -150,7 +150,7 @@ describe('GitTabController', function() { }); describe('abortMerge()', function() { - it.only('shows an error notification when abortMerge() throws an EDIRTYSTAGED exception', async function() { + it('shows an error notification when abortMerge() throws an EDIRTYSTAGED exception', async function() { const workdirPath = await cloneRepository('three-files'); const repository = await buildRepository(workdirPath); sinon.stub(repository, 'abortMerge').callsFake(async () => { @@ -227,7 +227,7 @@ describe('GitTabController', function() { }); describe('commit(message)', function() { - it.only('shows an error notification when committing throws an ECONFLICT exception', async function() { + it('shows an error notification when committing throws an ECONFLICT exception', async function() { const workdirPath = await cloneRepository('three-files'); const repository = await buildRepository(workdirPath); sinon.stub(repository, 'commit').callsFake(async () => { diff --git a/test/views/staging-view.test.js b/test/views/staging-view.test.js index 988a3f6def..7a794a88bd 100644 --- a/test/views/staging-view.test.js +++ b/test/views/staging-view.test.js @@ -310,7 +310,7 @@ describe('StagingView', function() { }); describe('when the file doesn\'t exist', function() { - it.only('shows an info notification and does not open the file', async function() { + it('shows an info notification and does not open the file', async function() { sinon.spy(notificationManager, 'addInfo'); const view = new StagingView({ From ccf1be95f1eccb0a56887b76355664b6722dab46 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Thu, 9 Nov 2017 13:47:52 -0800 Subject: [PATCH 0095/5882] Clear out notifications before testing that new ones are added (cherry picked from commit 0f4c792044682a9b12b066da3e9f21e13629568a) --- test/controllers/git-tab-controller.test.js | 8 ++++---- test/views/staging-view.test.js | 5 +++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/test/controllers/git-tab-controller.test.js b/test/controllers/git-tab-controller.test.js index 5d567176c8..0fd69417a7 100644 --- a/test/controllers/git-tab-controller.test.js +++ b/test/controllers/git-tab-controller.test.js @@ -150,7 +150,7 @@ describe('GitTabController', function() { }); describe('abortMerge()', function() { - it('shows an error notification when abortMerge() throws an EDIRTYSTAGED exception', async function() { + it.only('shows an error notification when abortMerge() throws an EDIRTYSTAGED exception', async function() { const workdirPath = await cloneRepository('three-files'); const repository = await buildRepository(workdirPath); sinon.stub(repository, 'abortMerge').callsFake(async () => { @@ -163,7 +163,7 @@ describe('GitTabController', function() { workspace, commandRegistry, notificationManager, confirm, repository, resolutionProgress, refreshResolutionProgress, }); - assert.equal(notificationManager.getNotifications().length, 0); + notificationManager.clear(); // clear out any notifications confirm.returns(0); await controller.abortMerge(); assert.equal(notificationManager.getNotifications().length, 1); @@ -227,7 +227,7 @@ describe('GitTabController', function() { }); describe('commit(message)', function() { - it('shows an error notification when committing throws an ECONFLICT exception', async function() { + it.only('shows an error notification when committing throws an ECONFLICT exception', async function() { const workdirPath = await cloneRepository('three-files'); const repository = await buildRepository(workdirPath); sinon.stub(repository, 'commit').callsFake(async () => { @@ -239,7 +239,7 @@ describe('GitTabController', function() { workspace, commandRegistry, notificationManager, repository, resolutionProgress, refreshResolutionProgress, }); - assert.equal(notificationManager.getNotifications().length, 0); + notificationManager.clear(); // clear out any notifications await controller.commit(); assert.equal(notificationManager.getNotifications().length, 1); }); diff --git a/test/views/staging-view.test.js b/test/views/staging-view.test.js index e11968d46f..988a3f6def 100644 --- a/test/views/staging-view.test.js +++ b/test/views/staging-view.test.js @@ -310,7 +310,7 @@ describe('StagingView', function() { }); describe('when the file doesn\'t exist', function() { - it('shows an info notification and does not open the file', async function() { + it.only('shows an info notification and does not open the file', async function() { sinon.spy(notificationManager, 'addInfo'); const view = new StagingView({ @@ -320,9 +320,10 @@ describe('StagingView', function() { sinon.stub(view, 'fileExists').returns(false); - assert.equal(notificationManager.getNotifications().length, 0); + notificationManager.clear(); // clear out notifications await view.showMergeConflictFileForPath('conflict.txt'); + assert.equal(notificationManager.getNotifications().length, 1); assert.equal(workspace.open.callCount, 0); assert.equal(notificationManager.addInfo.callCount, 1); assert.deepEqual(notificationManager.addInfo.args[0], ['File has been deleted.']); From b875a703e4ab2e2ba3720692c9cce2136917576b Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Thu, 9 Nov 2017 13:56:50 -0800 Subject: [PATCH 0096/5882] :fire: .only's used for selectively running tests on CI (cherry picked from commit 0464ca9a63ea85a846c4e4943a086930e9256301) --- test/controllers/git-tab-controller.test.js | 4 ++-- test/views/staging-view.test.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test/controllers/git-tab-controller.test.js b/test/controllers/git-tab-controller.test.js index 0fd69417a7..e78c3ce4a8 100644 --- a/test/controllers/git-tab-controller.test.js +++ b/test/controllers/git-tab-controller.test.js @@ -150,7 +150,7 @@ describe('GitTabController', function() { }); describe('abortMerge()', function() { - it.only('shows an error notification when abortMerge() throws an EDIRTYSTAGED exception', async function() { + it('shows an error notification when abortMerge() throws an EDIRTYSTAGED exception', async function() { const workdirPath = await cloneRepository('three-files'); const repository = await buildRepository(workdirPath); sinon.stub(repository, 'abortMerge').callsFake(async () => { @@ -227,7 +227,7 @@ describe('GitTabController', function() { }); describe('commit(message)', function() { - it.only('shows an error notification when committing throws an ECONFLICT exception', async function() { + it('shows an error notification when committing throws an ECONFLICT exception', async function() { const workdirPath = await cloneRepository('three-files'); const repository = await buildRepository(workdirPath); sinon.stub(repository, 'commit').callsFake(async () => { diff --git a/test/views/staging-view.test.js b/test/views/staging-view.test.js index 988a3f6def..7a794a88bd 100644 --- a/test/views/staging-view.test.js +++ b/test/views/staging-view.test.js @@ -310,7 +310,7 @@ describe('StagingView', function() { }); describe('when the file doesn\'t exist', function() { - it.only('shows an info notification and does not open the file', async function() { + it('shows an info notification and does not open the file', async function() { sinon.spy(notificationManager, 'addInfo'); const view = new StagingView({ From 299d18ce0d020b0bae227ddec28858614d74b0a7 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Mon, 27 Nov 2017 12:17:28 -0800 Subject: [PATCH 0097/5882] Use `--cleanup=strip` to remove comments/whitespace from commit message --- lib/controllers/commit-view-controller.js | 2 +- lib/git-shell-out-strategy.js | 4 +- .../commit-view-controller.test.js | 12 ---- test/git-strategies.test.js | 66 +++++++++++++++---- 4 files changed, 58 insertions(+), 26 deletions(-) diff --git a/lib/controllers/commit-view-controller.js b/lib/controllers/commit-view-controller.js index 4c671d4305..9c1eda6c5d 100644 --- a/lib/controllers/commit-view-controller.js +++ b/lib/controllers/commit-view-controller.js @@ -133,7 +133,7 @@ export default class CommitViewController { .catch(() => null); } } else { - let formattedMessage = message.replace(/^#.*$/mg, '').trim(); // strip out comments + let formattedMessage = message; if (this.props.config.get('github.automaticCommitMessageWrapping')) { formattedMessage = wrapCommitMessage(formattedMessage); } diff --git a/lib/git-shell-out-strategy.js b/lib/git-shell-out-strategy.js index 4cbb7d07a8..e94aa78f26 100644 --- a/lib/git-shell-out-strategy.js +++ b/lib/git-shell-out-strategy.js @@ -371,9 +371,9 @@ export default class GitShellOutStrategy { } commit(message, {allowEmpty, amend} = {}) { - let args = ['commit']; + let args = ['commit', '--cleanup=strip']; if (typeof message === 'object') { - args = args.concat(['--cleanup=strip', '-F', message.filePath]); + args = args.concat(['-F', message.filePath]); } else if (typeof message === 'string') { args = args.concat(['-m', message]); } else { diff --git a/test/controllers/commit-view-controller.test.js b/test/controllers/commit-view-controller.test.js index d2f2889da3..a3c6ae5573 100644 --- a/test/controllers/commit-view-controller.test.js +++ b/test/controllers/commit-view-controller.test.js @@ -160,18 +160,6 @@ describe('CommitViewController', function() { controller.update({commit: commitSpy}); }); - it('strips out comments', async function() { - await controller.commit([ - 'Make a commit', - '', - '# Comments:', - '# blah blah blah', - '# other stuff', - ].join('\n')); - - assert.deepEqual(commitSpy.args[0][0], 'Make a commit'); - }); - it('wraps the commit message body at 72 characters if github.automaticCommitMessageWrapping is true', async function() { config.set('github.automaticCommitMessageWrapping', false); diff --git a/test/git-strategies.test.js b/test/git-strategies.test.js index 0921977c4b..c4da85ce65 100644 --- a/test/git-strategies.test.js +++ b/test/git-strategies.test.js @@ -511,17 +511,61 @@ import {fsStat, normalizeGitHelperPath, writeFile, getTempDir} from '../lib/help }); }); - describe('commit(message, options) where amend option is true', function() { - it('amends the last commit', async function() { - const workingDirPath = await cloneRepository('multiple-commits'); - const git = createTestStrategy(workingDirPath); - const lastCommit = await git.getHeadCommit(); - const lastCommitParent = await git.getCommit('HEAD~'); - await git.commit('amend last commit', {amend: true, allowEmpty: true}); - const amendedCommit = await git.getHeadCommit(); - const amendedCommitParent = await git.getCommit('HEAD~'); - assert.notDeepEqual(lastCommit, amendedCommit); - assert.deepEqual(lastCommitParent, amendedCommitParent); + describe('commit(message, options)', function() { + describe('formatting commit message', function() { + let message; + beforeEach(function() { + message = [ + ' Make a commit ', + '', + '# Comments:', + '# blah blah blah', + '', + '', + '', + 'other stuff ', + '', + '', + '', + ].join('\n'); + }); + + it('strips out comments and whitespace from message passed', async function() { + const workingDirPath = await cloneRepository('multiple-commits'); + const git = createTestStrategy(workingDirPath); + + await git.commit(message, {allowEmpty: true}); + + const lastCommit = await git.getHeadCommit(); + assert.deepEqual(lastCommit.message, 'Make a commit\n\nother stuff'); + }); + + it('strips out comments and whitespace from message at specified file path', async function() { + const workingDirPath = await cloneRepository('multiple-commits'); + const git = createTestStrategy(workingDirPath); + + const commitMessagePath = path.join(workingDirPath, 'commit-message.txt'); + fs.writeFileSync(commitMessagePath, message); + + await git.commit({filePath: commitMessagePath}, {allowEmpty: true}); + + const lastCommit = await git.getHeadCommit(); + assert.deepEqual(lastCommit.message, 'Make a commit\n\nother stuff'); + }); + }); + + describe('when amend option is true', function() { + it('amends the last commit', async function() { + const workingDirPath = await cloneRepository('multiple-commits'); + const git = createTestStrategy(workingDirPath); + const lastCommit = await git.getHeadCommit(); + const lastCommitParent = await git.getCommit('HEAD~'); + await git.commit('amend last commit', {amend: true, allowEmpty: true}); + const amendedCommit = await git.getHeadCommit(); + const amendedCommitParent = await git.getCommit('HEAD~'); + assert.notDeepEqual(lastCommit, amendedCommit); + assert.deepEqual(lastCommitParent, amendedCommitParent); + }); }); }); From 765f0902bfa1e8e40199b3e5985a40f2266cb346 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Mon, 27 Nov 2017 12:37:45 -0800 Subject: [PATCH 0098/5882] WIP use custom hard-wrap icon --- img/hardwrap-disabled.svg | 6 ++++++ img/hardwrap-enabled.svg | 3 +++ lib/views/commit-view.js | 4 ++-- styles/_global.less | 19 +++++++++++++++++++ 4 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 img/hardwrap-disabled.svg create mode 100644 img/hardwrap-enabled.svg diff --git a/img/hardwrap-disabled.svg b/img/hardwrap-disabled.svg new file mode 100644 index 0000000000..4544b7663a --- /dev/null +++ b/img/hardwrap-disabled.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/img/hardwrap-enabled.svg b/img/hardwrap-enabled.svg new file mode 100644 index 0000000000..45e6bdc38a --- /dev/null +++ b/img/hardwrap-enabled.svg @@ -0,0 +1,3 @@ + + + diff --git a/lib/views/commit-view.js b/lib/views/commit-view.js index 6994689d75..f1ab06cf39 100644 --- a/lib/views/commit-view.js +++ b/lib/views/commit-view.js @@ -123,8 +123,8 @@ export default class CommitView { const notApplicable = this.props.deactivateCommitBox || singleLineMessage; return (
-
-
+ +
); } diff --git a/styles/_global.less b/styles/_global.less index a97fa0eba7..fb55099459 100644 --- a/styles/_global.less +++ b/styles/_global.less @@ -57,3 +57,22 @@ .status-error { color: @gh-background-color-red; } + + +.icon-hardwrap-enabled { + background: url('atom://github/img/hardwrap-enabled.svg'); + background-repeat: no-repeat; + + display: inline-block; + width: 16px; + height: 16px; +} + +.icon-hardwrap-disabled { + background: url('atom://github/img/hardwrap-disabled.svg'); + background-repeat: no-repeat; + + display: inline-block; + width: 16px; + height: 16px; +} From ee71d5a2fb54ab988fb87547e9f879c3d7ea820f Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Mon, 27 Nov 2017 12:43:16 -0800 Subject: [PATCH 0099/5882] :shirt: Fix new linting errors These appear to have shown up when a dep updated to a newer version --- lib/controllers/issueish-pane-item-controller.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/controllers/issueish-pane-item-controller.js b/lib/controllers/issueish-pane-item-controller.js index c2b451f5f1..bba1ce75f0 100644 --- a/lib/controllers/issueish-pane-item-controller.js +++ b/lib/controllers/issueish-pane-item-controller.js @@ -82,8 +82,8 @@ export default class IssueishPaneItemController extends React.Component {

- The API endpoint returned a unauthorized error. Please try to re-authenticate with the endpoint. -

+ The API endpoint returned a unauthorized error. Please try to re-authenticate with the endpoint. +

); @@ -118,8 +118,8 @@ export default class IssueishPaneItemController extends React.Component {

Error

- An unknown error occurred -

+ An unknown error occurred +

From 855c8864a99163a7932012149ecb527860016b60 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Thu, 9 Nov 2017 13:47:52 -0800 Subject: [PATCH 0100/5882] Clear out notifications before testing that new ones are added (cherry picked from commit 0f4c792044682a9b12b066da3e9f21e13629568a) --- test/controllers/git-tab-controller.test.js | 8 ++++---- test/views/staging-view.test.js | 5 +++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/test/controllers/git-tab-controller.test.js b/test/controllers/git-tab-controller.test.js index 5d567176c8..0fd69417a7 100644 --- a/test/controllers/git-tab-controller.test.js +++ b/test/controllers/git-tab-controller.test.js @@ -150,7 +150,7 @@ describe('GitTabController', function() { }); describe('abortMerge()', function() { - it('shows an error notification when abortMerge() throws an EDIRTYSTAGED exception', async function() { + it.only('shows an error notification when abortMerge() throws an EDIRTYSTAGED exception', async function() { const workdirPath = await cloneRepository('three-files'); const repository = await buildRepository(workdirPath); sinon.stub(repository, 'abortMerge').callsFake(async () => { @@ -163,7 +163,7 @@ describe('GitTabController', function() { workspace, commandRegistry, notificationManager, confirm, repository, resolutionProgress, refreshResolutionProgress, }); - assert.equal(notificationManager.getNotifications().length, 0); + notificationManager.clear(); // clear out any notifications confirm.returns(0); await controller.abortMerge(); assert.equal(notificationManager.getNotifications().length, 1); @@ -227,7 +227,7 @@ describe('GitTabController', function() { }); describe('commit(message)', function() { - it('shows an error notification when committing throws an ECONFLICT exception', async function() { + it.only('shows an error notification when committing throws an ECONFLICT exception', async function() { const workdirPath = await cloneRepository('three-files'); const repository = await buildRepository(workdirPath); sinon.stub(repository, 'commit').callsFake(async () => { @@ -239,7 +239,7 @@ describe('GitTabController', function() { workspace, commandRegistry, notificationManager, repository, resolutionProgress, refreshResolutionProgress, }); - assert.equal(notificationManager.getNotifications().length, 0); + notificationManager.clear(); // clear out any notifications await controller.commit(); assert.equal(notificationManager.getNotifications().length, 1); }); diff --git a/test/views/staging-view.test.js b/test/views/staging-view.test.js index e11968d46f..988a3f6def 100644 --- a/test/views/staging-view.test.js +++ b/test/views/staging-view.test.js @@ -310,7 +310,7 @@ describe('StagingView', function() { }); describe('when the file doesn\'t exist', function() { - it('shows an info notification and does not open the file', async function() { + it.only('shows an info notification and does not open the file', async function() { sinon.spy(notificationManager, 'addInfo'); const view = new StagingView({ @@ -320,9 +320,10 @@ describe('StagingView', function() { sinon.stub(view, 'fileExists').returns(false); - assert.equal(notificationManager.getNotifications().length, 0); + notificationManager.clear(); // clear out notifications await view.showMergeConflictFileForPath('conflict.txt'); + assert.equal(notificationManager.getNotifications().length, 1); assert.equal(workspace.open.callCount, 0); assert.equal(notificationManager.addInfo.callCount, 1); assert.deepEqual(notificationManager.addInfo.args[0], ['File has been deleted.']); From 815e703c8a7fbf8ec78e6cf1049adf57dc088d91 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Thu, 9 Nov 2017 13:56:50 -0800 Subject: [PATCH 0101/5882] :fire: .only's used for selectively running tests on CI (cherry picked from commit 0464ca9a63ea85a846c4e4943a086930e9256301) --- test/controllers/git-tab-controller.test.js | 4 ++-- test/views/staging-view.test.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test/controllers/git-tab-controller.test.js b/test/controllers/git-tab-controller.test.js index 0fd69417a7..e78c3ce4a8 100644 --- a/test/controllers/git-tab-controller.test.js +++ b/test/controllers/git-tab-controller.test.js @@ -150,7 +150,7 @@ describe('GitTabController', function() { }); describe('abortMerge()', function() { - it.only('shows an error notification when abortMerge() throws an EDIRTYSTAGED exception', async function() { + it('shows an error notification when abortMerge() throws an EDIRTYSTAGED exception', async function() { const workdirPath = await cloneRepository('three-files'); const repository = await buildRepository(workdirPath); sinon.stub(repository, 'abortMerge').callsFake(async () => { @@ -227,7 +227,7 @@ describe('GitTabController', function() { }); describe('commit(message)', function() { - it.only('shows an error notification when committing throws an ECONFLICT exception', async function() { + it('shows an error notification when committing throws an ECONFLICT exception', async function() { const workdirPath = await cloneRepository('three-files'); const repository = await buildRepository(workdirPath); sinon.stub(repository, 'commit').callsFake(async () => { diff --git a/test/views/staging-view.test.js b/test/views/staging-view.test.js index 988a3f6def..7a794a88bd 100644 --- a/test/views/staging-view.test.js +++ b/test/views/staging-view.test.js @@ -310,7 +310,7 @@ describe('StagingView', function() { }); describe('when the file doesn\'t exist', function() { - it.only('shows an info notification and does not open the file', async function() { + it('shows an info notification and does not open the file', async function() { sinon.spy(notificationManager, 'addInfo'); const view = new StagingView({ From 74323775a661572d226cad1a777185d95c28eb5b Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Mon, 27 Nov 2017 13:47:56 -0800 Subject: [PATCH 0102/5882] Prepare 0.8.3 release --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d833073612..ec900456d4 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "github", "main": "./lib/index", - "version": "0.8.2", + "version": "0.8.3", "description": "GitHub integration", "repository": "https://github.com/atom/github", "license": "MIT", From 85bbb820045228eed2cd0d1de8052bdd8a3b451c Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Tue, 28 Nov 2017 20:03:09 -0800 Subject: [PATCH 0103/5882] Use `readonly` attribute to disable editor when checkout is in progress --- lib/views/branch-menu-view.js | 28 +--------------------------- styles/branch-menu-view.less | 2 +- 2 files changed, 2 insertions(+), 28 deletions(-) diff --git a/lib/views/branch-menu-view.js b/lib/views/branch-menu-view.js index 11e9fa65ba..c62f9abc90 100644 --- a/lib/views/branch-menu-view.js +++ b/lib/views/branch-menu-view.js @@ -61,12 +61,7 @@ export default class BranchMenuView extends React.Component { { this.editorElement = e; }} mini={true} - softWrapped={true} - placeholderText="enter new branch name" - lineNumberGutterVisible={false} - showInvisibles={false} - scrollPastEnd={false} - disabled={disableControls} + readonly={disableControls ? true : undefined} />
); @@ -109,7 +104,6 @@ export default class BranchMenuView extends React.Component { const branchNames = nextProps.branches.map(branch => branch.getName()); const hasNewBranch = branchNames.includes(this.state.checkedOutBranch); if (currentBranch === this.state.checkedOutBranch && hasNewBranch) { - this.restoreControls(); this.setState({checkedOutBranch: null}); if (this.state.createNew) { this.setState({createNew: false}); } } @@ -135,13 +129,10 @@ export default class BranchMenuView extends React.Component { @autobind async checkout(branchName, options) { - this.disableControls(branchName); this.setState({checkedOutBranch: branchName}); try { await this.props.checkout(branchName, options); } catch (error) { - // restore controls immediately, otherwise wait until branch appears in branch list - this.restoreControls(); this.setState({checkedOutBranch: null}); if (error instanceof GitError) { // eslint-disable-next-line no-console @@ -152,23 +143,6 @@ export default class BranchMenuView extends React.Component { } } - @autobind - disableControls(branchName) { - if (this.editorElement) { - this.editorElement.getComponent().setInputEnabled(false); - this.editorElement.classList.remove('is-focused'); - } - } - - @autobind - restoreControls() { - if (this.editorElement) { - this.editorElement.getComponent().setInputEnabled(true); - this.editorElement.focus(); - this.editorElement.classList.add('is-focused'); - } - } - @autobind cancelCreateNewBranch() { this.setState({createNew: false}); diff --git a/styles/branch-menu-view.less b/styles/branch-menu-view.less index 421d38d6cf..bc28be5cd0 100644 --- a/styles/branch-menu-view.less +++ b/styles/branch-menu-view.less @@ -56,7 +56,7 @@ font-size: inherit; text-align: left; } - atom-text-editor[disabled="true"] { + atom-text-editor[readonly] { color: @text-color-subtle; pointer-events: none; } From 1850911963b1f554d6ec8ac19f2894c8e5c520cf Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Tue, 28 Nov 2017 20:21:14 -0800 Subject: [PATCH 0104/5882] Access editor element's innerText instead of grabbing the model --- lib/views/branch-menu-view.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/views/branch-menu-view.js b/lib/views/branch-menu-view.js index c62f9abc90..424b549514 100644 --- a/lib/views/branch-menu-view.js +++ b/lib/views/branch-menu-view.js @@ -118,7 +118,7 @@ export default class BranchMenuView extends React.Component { @autobind async createBranch() { if (this.state.createNew) { - const branchName = this.editorElement.getModel().getText().trim(); + const branchName = this.editorElement.innerText.trim(); await this.checkout(branchName, {createNew: true}); } else { this.setState({createNew: true}, () => { From eda7871ac817a9fa5f8f9fb57184171bd600f54f Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Tue, 28 Nov 2017 22:50:39 -0800 Subject: [PATCH 0105/5882] Fix borken tests --- test/controllers/status-bar-tile-controller.test.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/controllers/status-bar-tile-controller.test.js b/test/controllers/status-bar-tile-controller.test.js index 2648209de4..c32f7628c0 100644 --- a/test/controllers/status-bar-tile-controller.test.js +++ b/test/controllers/status-bar-tile-controller.test.js @@ -194,9 +194,9 @@ describe('StatusBarTileController', function() { assert.isTrue(selectList.className.includes('hidden')); assert.isFalse(tip.querySelector('.github-BranchMenuView-editor').className.includes('hidden')); - tip.querySelector('atom-text-editor').getModel().setText('new-branch'); + tip.querySelector('atom-text-editor').innerText = 'new-branch'; tip.querySelector('button').click(); - assert.isTrue(editor.hasAttribute('disabled')); + assert.isTrue(editor.hasAttribute('readonly')); await until(async () => { const branch1 = await repository.getCurrentBranch(); @@ -229,7 +229,7 @@ describe('StatusBarTileController', function() { assert.equal(tip.querySelector('select').value, 'branch'); createNewButton.click(); - tip.querySelector('atom-text-editor').getModel().setText('master'); + tip.querySelector('atom-text-editor').innerText = 'master'; createNewButton.click(); assert.isTrue(createNewButton.hasAttribute('disabled')); @@ -243,7 +243,7 @@ describe('StatusBarTileController', function() { assert.isFalse(branch1.isDetached()); assert.lengthOf(tip.querySelectorAll('.github-BranchMenuView-editor'), 1); - assert.equal(tip.querySelector('atom-text-editor').getModel().getText(), 'master'); + assert.equal(tip.querySelector('atom-text-editor').innerText, 'master'); assert.isFalse(createNewButton.hasAttribute('disabled')); }); }); From d6439edd996251f3ea8caadd809c4764f5fc7af4 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Wed, 29 Nov 2017 14:47:39 -0800 Subject: [PATCH 0106/5882] Inline hard-wrap icon SVGs and style using less variable --- lib/views/commit-view.js | 17 ++++++++++++++--- styles/_global.less | 19 ------------------- styles/commit-view.less | 3 +++ 3 files changed, 17 insertions(+), 22 deletions(-) diff --git a/lib/views/commit-view.js b/lib/views/commit-view.js index f1ab06cf39..27683e1059 100644 --- a/lib/views/commit-view.js +++ b/lib/views/commit-view.js @@ -122,9 +122,20 @@ export default class CommitView { const hardWrap = this.props.config.get('github.automaticCommitMessageWrapping'); const notApplicable = this.props.deactivateCommitBox || singleLineMessage; return ( -
- - +
+
+ + + + + + +
+
+ + + +
); } diff --git a/styles/_global.less b/styles/_global.less index fb55099459..a97fa0eba7 100644 --- a/styles/_global.less +++ b/styles/_global.less @@ -57,22 +57,3 @@ .status-error { color: @gh-background-color-red; } - - -.icon-hardwrap-enabled { - background: url('atom://github/img/hardwrap-enabled.svg'); - background-repeat: no-repeat; - - display: inline-block; - width: 16px; - height: 16px; -} - -.icon-hardwrap-disabled { - background: url('atom://github/img/hardwrap-disabled.svg'); - background-repeat: no-repeat; - - display: inline-block; - width: 16px; - height: 16px; -} diff --git a/styles/commit-view.less b/styles/commit-view.less index f6c48d4683..824277ef1f 100644 --- a/styles/commit-view.less +++ b/styles/commit-view.less @@ -121,6 +121,9 @@ } } + .hard-wrap-icons { + fill: @text-color + } } From ce1a18370a80ab0c17afb42d30789153171eab83 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Tue, 5 Dec 2017 14:14:57 -0800 Subject: [PATCH 0107/5882] :art: hard-wrap icon. Thanks @donokuda! --- lib/views/commit-view.js | 8 ++++---- styles/commit-view.less | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/views/commit-view.js b/lib/views/commit-view.js index 27683e1059..f7e5e43dce 100644 --- a/lib/views/commit-view.js +++ b/lib/views/commit-view.js @@ -125,15 +125,15 @@ export default class CommitView {
- - - + + +
- +
diff --git a/styles/commit-view.less b/styles/commit-view.less index 824277ef1f..63bc972750 100644 --- a/styles/commit-view.less +++ b/styles/commit-view.less @@ -17,7 +17,7 @@ position: absolute; right: 20px; bottom: 0; - padding: @component-padding / 2; + padding: (@component-padding / 5) (@component-padding / 2); line-height: 1; border: none; background-color: @syntax-background-color; From 6ed4b6c4092adeafac735b71f51220d01bfb0612 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Tue, 5 Dec 2017 18:10:51 -0800 Subject: [PATCH 0108/5882] :shirt: --- lib/views/commit-view.js | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/lib/views/commit-view.js b/lib/views/commit-view.js index f7e5e43dce..75c25bfe92 100644 --- a/lib/views/commit-view.js +++ b/lib/views/commit-view.js @@ -121,19 +121,30 @@ export default class CommitView { const singleLineMessage = this.editor && this.editor.getText().split(LINE_ENDING_REGEX).length === 1; const hardWrap = this.props.config.get('github.automaticCommitMessageWrapping'); const notApplicable = this.props.deactivateCommitBox || singleLineMessage; + + const svgPaths = { + hardWrapEnabled: { + path1: 'M7.058 10.2h-.975v2.4L2 9l4.083-3.6v2.4h.97l1.202 1.203L7.058 10.2zm2.525-4.865V4.2h2.334v1.14l-1.164 1.165-1.17-1.17z', // eslint-disable-line max-len + path2: 'M7.842 6.94l2.063 2.063-2.122 2.12.908.91 2.123-2.123 1.98 1.98.85-.848L11.58 8.98l2.12-2.123-.824-.825-2.122 2.12-2.062-2.06z', // eslint-disable-line max-len + }, + hardWrapDisabled: { + path1: 'M11.917 8.4c0 .99-.788 1.8-1.75 1.8H6.083v2.4L2 9l4.083-3.6v2.4h3.5V4.2h2.334v4.2z', + }, + }; + return (
- - + +
- +
From dc9fb5340cf989fea11768cf9f23f0465b79af77 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Tue, 5 Dec 2017 18:14:27 -0800 Subject: [PATCH 0109/5882] Use refs instead of accessing etch element --- lib/views/commit-view.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/lib/views/commit-view.js b/lib/views/commit-view.js index 75c25bfe92..aa444e682a 100644 --- a/lib/views/commit-view.js +++ b/lib/views/commit-view.js @@ -84,7 +84,7 @@ export default class CommitView { scrollPastEnd={false} /> {this.renderHardWrapIcons()} -
@@ -133,7 +133,7 @@ export default class CommitView { }; return ( -
+
@@ -160,13 +160,11 @@ export default class CommitView { } registerTooltips() { - const expandButton = this.element.getElementsByClassName('github-CommitView-expandButton')[0]; - const hardWrapButton = this.element.getElementsByClassName('github-CommitView-hardwrap')[0]; - this.expandTooltip = this.props.tooltips.add(expandButton, { + this.expandTooltip = this.props.tooltips.add(this.refs.expandButton, { title: 'Expand commit message editor', class: 'github-CommitView-expandButton-tooltip', }); - this.hardWrapTooltip = this.props.tooltips.add(hardWrapButton, { + this.hardWrapTooltip = this.props.tooltips.add(this.refs.hardWrapButton, { title: 'Toggle hard wrap on commit', class: 'github-CommitView-hardwrap-tooltip', }); From 4fabf526914c3dcec03d6258a0f775c76eeffabc Mon Sep 17 00:00:00 2001 From: Steven Date: Wed, 6 Dec 2017 14:48:19 -0800 Subject: [PATCH 0110/5882] Adding delay to "Working..." message --- lib/views/commit-view.js | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/lib/views/commit-view.js b/lib/views/commit-view.js index ae10a18bc9..40ac3163cc 100644 --- a/lib/views/commit-view.js +++ b/lib/views/commit-view.js @@ -21,6 +21,35 @@ export default class CommitView { constructor(props) { this.props = props; + // We don't want the user to see the UI flicker in the case + // the commit takes a very small time to complete. Instead we + // will only show the working message if we are working for longer + // than 1 second as per https://www.nngroup.com/articles/response-times-3-important-limits/ + // + // The closure is created to restrict variable access + this.shouldShowWorking = (() => { + let showWorking = false; + let timeoutHandle = null; + + return () => { + if (this.props.isCommitting) { + if (!showWorking && timeoutHandle === null) { + timeoutHandle = setTimeout(() => { + showWorking = true; + etch.update(this); + timeoutHandle = null; + }, 1000); + } + } else { + clearTimeout(timeoutHandle); + timeoutHandle = null; + showWorking = false; + } + + return showWorking; + }; + })(); + etch.initialize(this); this.editor = this.refs.editor; @@ -154,10 +183,11 @@ export default class CommitView { (this.props.deactivateCommitBox || (this.editor && this.editor.getText().length !== 0)); } + @autobind commitButtonText() { if (this.props.isAmending) { return `Amend commit (${shortenSha(this.props.lastCommit.getSha())})`; - } else if (this.props.isCommitting) { + } else if (this.shouldShowWorking()) { return 'Working...'; } else { if (this.props.branchName) { From a0025851907750bbd4665bde052aaff068236183 Mon Sep 17 00:00:00 2001 From: Steven Date: Wed, 6 Dec 2017 15:02:01 -0800 Subject: [PATCH 0111/5882] We don't need @autobind here --- lib/views/commit-view.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/views/commit-view.js b/lib/views/commit-view.js index 40ac3163cc..4917a92098 100644 --- a/lib/views/commit-view.js +++ b/lib/views/commit-view.js @@ -183,7 +183,6 @@ export default class CommitView { (this.props.deactivateCommitBox || (this.editor && this.editor.getText().length !== 0)); } - @autobind commitButtonText() { if (this.props.isAmending) { return `Amend commit (${shortenSha(this.props.lastCommit.getSha())})`; From a40a6b2a6e5c94594eb6373f346f67cae50c7347 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 8 Dec 2017 17:35:59 -0800 Subject: [PATCH 0112/5882] Prepare 0.9.0 release --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1871efddee..350ce6667b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "github", "main": "./lib/index", - "version": "0.8.3", + "version": "0.9.0", "description": "GitHub integration", "repository": "https://github.com/atom/github", "license": "MIT", From 1afa8b880b772e7368782869ebb44f9822bebca7 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Thu, 14 Dec 2017 11:42:47 +0100 Subject: [PATCH 0113/5882] Manually add/remove 'is-focused' class on editor element --- lib/views/branch-menu-view.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/views/branch-menu-view.js b/lib/views/branch-menu-view.js index 424b549514..ea34021753 100644 --- a/lib/views/branch-menu-view.js +++ b/lib/views/branch-menu-view.js @@ -104,6 +104,7 @@ export default class BranchMenuView extends React.Component { const branchNames = nextProps.branches.map(branch => branch.getName()); const hasNewBranch = branchNames.includes(this.state.checkedOutBranch); if (currentBranch === this.state.checkedOutBranch && hasNewBranch) { + this.editorElement.classList.add('is-focused'); this.setState({checkedOutBranch: null}); if (this.state.createNew) { this.setState({createNew: false}); } } @@ -129,10 +130,12 @@ export default class BranchMenuView extends React.Component { @autobind async checkout(branchName, options) { + this.editorElement.classList.remove('is-focused'); this.setState({checkedOutBranch: branchName}); try { await this.props.checkout(branchName, options); } catch (error) { + this.editorElement.classList.add('is-focused'); this.setState({checkedOutBranch: null}); if (error instanceof GitError) { // eslint-disable-next-line no-console From 0a2e165d1b1f64e73a9e2ca5ca985d373728c41d Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Thu, 14 Dec 2017 11:43:41 +0100 Subject: [PATCH 0114/5882] Prepare 0.9.1 release --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 350ce6667b..3cf420dc10 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "github", "main": "./lib/index", - "version": "0.9.0", + "version": "0.9.1", "description": "GitHub integration", "repository": "https://github.com/atom/github", "license": "MIT", From 47c665471f736483dccdb63cf8984581490069e8 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 4 Jan 2018 15:08:08 -0800 Subject: [PATCH 0115/5882] :arrow_up: what-the-status@1.0.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3cf420dc10..db0fa7cde8 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,7 @@ "tinycolor2": "^1.4.1", "tree-kill": "^1.1.0", "what-the-diff": "^0.3.0", - "what-the-status": "^1.0.2", + "what-the-status": "^1.0.3", "yubikiri": "1.0.0" }, "devDependencies": { From fe55b1410a4c0be94f6f4199a57915f6e066a42c Mon Sep 17 00:00:00 2001 From: David Wilson Date: Thu, 4 Jan 2018 15:05:39 -0800 Subject: [PATCH 0116/5882] Fix #1067: Bypass external diff tools --- lib/git-shell-out-strategy.js | 2 +- test/git-strategies.test.js | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/git-shell-out-strategy.js b/lib/git-shell-out-strategy.js index 5ebd9f4e79..a95532fe23 100644 --- a/lib/git-shell-out-strategy.js +++ b/lib/git-shell-out-strategy.js @@ -461,7 +461,7 @@ export default class GitShellOutStrategy { } async getDiffForFilePath(filePath, {staged, baseCommit} = {}) { - let args = ['diff', '--no-prefix', '--no-renames', '--diff-filter=u']; + let args = ['diff', '--no-prefix', '--no-ext-diff', '--no-renames', '--diff-filter=u']; if (staged) { args.push('--staged'); } if (baseCommit) { args.push(baseCommit); } args = args.concat(['--', toGitPathSep(filePath)]); diff --git a/test/git-strategies.test.js b/test/git-strategies.test.js index c4da85ce65..2ddff0ed13 100644 --- a/test/git-strategies.test.js +++ b/test/git-strategies.test.js @@ -203,6 +203,18 @@ import {fsStat, normalizeGitHelperPath, writeFile, getTempDir} from '../lib/help assert.isUndefined(diffOutput); }); + it('bypasses external diff tools', async function() { + const workingDirPath = await cloneRepository('three-files'); + const git = createTestStrategy(workingDirPath); + + fs.writeFileSync(path.join(workingDirPath, 'a.txt'), 'qux\nfoo\nbar\n', 'utf8'); + process.env.GIT_EXTERNAL_DIFF = 'bogus_app_name'; + const diffOutput = await git.getDiffForFilePath('a.txt'); + delete process.env.GIT_EXTERNAL_DIFF; + + assert.isDefined(diffOutput); + }); + describe('when the file is unstaged', function() { it('returns a diff comparing the working directory copy of the file and the version on the index', async function() { const workingDirPath = await cloneRepository('three-files'); From e8f88374c5eb6eac077ddbfc58fc74e9a450a637 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 4 Jan 2018 15:51:34 -0800 Subject: [PATCH 0117/5882] Change GSOS to handle diffs with multiple patches --- lib/git-shell-out-strategy.js | 8 +++-- lib/models/repository-states/present.js | 19 ++++++---- test/git-strategies.test.js | 46 ++++++++++++------------- 3 files changed, 40 insertions(+), 33 deletions(-) diff --git a/lib/git-shell-out-strategy.js b/lib/git-shell-out-strategy.js index 5ebd9f4e79..d0c8331495 100644 --- a/lib/git-shell-out-strategy.js +++ b/lib/git-shell-out-strategy.js @@ -460,7 +460,7 @@ export default class GitShellOutStrategy { return output.trim().split(LINE_ENDING_REGEX).map(toNativePathSep); } - async getDiffForFilePath(filePath, {staged, baseCommit} = {}) { + async getDiffsForFilePath(filePath, {staged, baseCommit} = {}) { let args = ['diff', '--no-prefix', '--no-renames', '--diff-filter=u']; if (staged) { args.push('--staged'); } if (baseCommit) { args.push(baseCommit); } @@ -491,8 +491,10 @@ export default class GitShellOutStrategy { const binary = isBinary(contents); rawDiffs.push(buildAddedFilePatch(filePath, binary ? null : contents, executable)); } - if (rawDiffs.length > 1) { throw new Error(`Expected 0 or 1 diffs for ${filePath} but got ${rawDiffs.length}`); } - return rawDiffs[0]; + if (rawDiffs.length > 2) { + throw new Error(`Expected between 0 and 2 diffs for ${filePath} but got ${rawDiffs.length}`); + } + return rawDiffs; } /** diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js index 724db6d470..8ece741852 100644 --- a/lib/models/repository-states/present.js +++ b/lib/models/repository-states/present.js @@ -565,9 +565,9 @@ export default class Present extends State { options.baseCommit = 'HEAD~'; } - const rawDiff = await this.git().getDiffForFilePath(filePath, options); - if (rawDiff) { - const [filePatch] = buildFilePatchesFromRawDiffs([rawDiff]); + const rawDiffs = await this.git().getDiffsForFilePath(filePath, options); + if (rawDiffs.length > 0) { + const filePatch = buildFilePatchFromRawDiffs(rawDiffs); return filePatch; } else { return null; @@ -688,9 +688,10 @@ function partition(array, predicate) { return [matches, nonmatches]; } -function buildFilePatchesFromRawDiffs(rawDiffs) { - let diffLineNumber = 0; - return rawDiffs.map(patch => { +function buildFilePatchFromRawDiffs(rawDiffs) { + if (rawDiffs.length === 1) { + const patch = rawDiffs[0]; + let diffLineNumber = 0; const hunks = patch.hunks.map(hunk => { let oldLineNumber = hunk.oldStartLine; let newLineNumber = hunk.newStartLine; @@ -725,7 +726,11 @@ function buildFilePatchesFromRawDiffs(rawDiffs) { ); }); return new FilePatch(patch.oldPath, patch.newPath, patch.status, hunks); - }); + } else if (rawDiffs.length === 2) { + + } else { + throw new Error(`Unexpected number of diffs: ${rawDiffs.length}`); + } } class Cache { diff --git a/test/git-strategies.test.js b/test/git-strategies.test.js index c4da85ce65..888d0a3e28 100644 --- a/test/git-strategies.test.js +++ b/test/git-strategies.test.js @@ -187,20 +187,20 @@ import {fsStat, normalizeGitHelperPath, writeFile, getTempDir} from '../lib/help }); }); - describe('getDiffForFilePath', function() { + describe('getDiffsForFilePath', function() { it('returns an empty array if there are no modified, added, or deleted files', async function() { const workingDirPath = await cloneRepository('three-files'); const git = createTestStrategy(workingDirPath); - const diffOutput = await git.getDiffForFilePath('a.txt'); - assert.isUndefined(diffOutput); + const diffOutput = await git.getDiffsForFilePath('a.txt'); + assert.deepEqual(diffOutput, []); }); it('ignores merge conflict files', async function() { const workingDirPath = await cloneRepository('merge-conflict'); const git = createTestStrategy(workingDirPath); - const diffOutput = await git.getDiffForFilePath('added-to-both.txt'); - assert.isUndefined(diffOutput); + const diffOutput = await git.getDiffsForFilePath('added-to-both.txt'); + assert.deepEqual(diffOutput, []); }); describe('when the file is unstaged', function() { @@ -210,7 +210,7 @@ import {fsStat, normalizeGitHelperPath, writeFile, getTempDir} from '../lib/help fs.writeFileSync(path.join(workingDirPath, 'a.txt'), 'qux\nfoo\nbar\n', 'utf8'); fs.renameSync(path.join(workingDirPath, 'c.txt'), path.join(workingDirPath, 'd.txt')); - assertDeepPropertyVals(await git.getDiffForFilePath('a.txt'), { + assertDeepPropertyVals(await git.getDiffsForFilePath('a.txt'), [{ oldPath: 'a.txt', newPath: 'a.txt', oldMode: '100644', @@ -230,9 +230,9 @@ import {fsStat, normalizeGitHelperPath, writeFile, getTempDir} from '../lib/help }, ], status: 'modified', - }); + }]); - assertDeepPropertyVals(await git.getDiffForFilePath('c.txt'), { + assertDeepPropertyVals(await git.getDiffsForFilePath('c.txt'), [{ oldPath: 'c.txt', newPath: null, oldMode: '100644', @@ -248,9 +248,9 @@ import {fsStat, normalizeGitHelperPath, writeFile, getTempDir} from '../lib/help }, ], status: 'deleted', - }); + }]); - assertDeepPropertyVals(await git.getDiffForFilePath('d.txt'), { + assertDeepPropertyVals(await git.getDiffsForFilePath('d.txt'), [{ oldPath: null, newPath: 'd.txt', oldMode: null, @@ -266,7 +266,7 @@ import {fsStat, normalizeGitHelperPath, writeFile, getTempDir} from '../lib/help }, ], status: 'added', - }); + }]); }); }); @@ -278,7 +278,7 @@ import {fsStat, normalizeGitHelperPath, writeFile, getTempDir} from '../lib/help fs.renameSync(path.join(workingDirPath, 'c.txt'), path.join(workingDirPath, 'd.txt')); await git.exec(['add', '.']); - assertDeepPropertyVals(await git.getDiffForFilePath('a.txt', {staged: true}), { + assertDeepPropertyVals(await git.getDiffsForFilePath('a.txt', {staged: true}), [{ oldPath: 'a.txt', newPath: 'a.txt', oldMode: '100644', @@ -298,9 +298,9 @@ import {fsStat, normalizeGitHelperPath, writeFile, getTempDir} from '../lib/help }, ], status: 'modified', - }); + }]); - assertDeepPropertyVals(await git.getDiffForFilePath('c.txt', {staged: true}), { + assertDeepPropertyVals(await git.getDiffsForFilePath('c.txt', {staged: true}), [{ oldPath: 'c.txt', newPath: null, oldMode: '100644', @@ -316,9 +316,9 @@ import {fsStat, normalizeGitHelperPath, writeFile, getTempDir} from '../lib/help }, ], status: 'deleted', - }); + }]); - assertDeepPropertyVals(await git.getDiffForFilePath('d.txt', {staged: true}), { + assertDeepPropertyVals(await git.getDiffsForFilePath('d.txt', {staged: true}), [{ oldPath: null, newPath: 'd.txt', oldMode: null, @@ -334,7 +334,7 @@ import {fsStat, normalizeGitHelperPath, writeFile, getTempDir} from '../lib/help }, ], status: 'added', - }); + }]); }); }); @@ -343,7 +343,7 @@ import {fsStat, normalizeGitHelperPath, writeFile, getTempDir} from '../lib/help const workingDirPath = await cloneRepository('multiple-commits'); const git = createTestStrategy(workingDirPath); - assertDeepPropertyVals(await git.getDiffForFilePath('file.txt', {staged: true, baseCommit: 'HEAD~'}), { + assertDeepPropertyVals(await git.getDiffsForFilePath('file.txt', {staged: true, baseCommit: 'HEAD~'}), [{ oldPath: 'file.txt', newPath: 'file.txt', oldMode: '100644', @@ -359,7 +359,7 @@ import {fsStat, normalizeGitHelperPath, writeFile, getTempDir} from '../lib/help }, ], status: 'modified', - }); + }]); }); }); @@ -368,7 +368,7 @@ import {fsStat, normalizeGitHelperPath, writeFile, getTempDir} from '../lib/help const workingDirPath = await cloneRepository('three-files'); const git = createTestStrategy(workingDirPath); fs.writeFileSync(path.join(workingDirPath, 'new-file.txt'), 'qux\nfoo\nbar\n', 'utf8'); - assertDeepPropertyVals(await git.getDiffForFilePath('new-file.txt'), { + assertDeepPropertyVals(await git.getDiffsForFilePath('new-file.txt'), [{ oldPath: null, newPath: 'new-file.txt', oldMode: null, @@ -388,7 +388,7 @@ import {fsStat, normalizeGitHelperPath, writeFile, getTempDir} from '../lib/help }, ], status: 'added', - }); + }]); }); @@ -401,14 +401,14 @@ import {fsStat, normalizeGitHelperPath, writeFile, getTempDir} from '../lib/help data.writeUInt8(i + 200, i); } fs.writeFileSync(path.join(workingDirPath, 'new-file.bin'), data); - assertDeepPropertyVals(await git.getDiffForFilePath('new-file.bin'), { + assertDeepPropertyVals(await git.getDiffsForFilePath('new-file.bin'), [{ oldPath: null, newPath: 'new-file.bin', oldMode: null, newMode: '100644', hunks: [], status: 'added', - }); + }]); }); }); }); From b9b18627c4ece8efb4daa71fe267fa20b7929ce3 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 4 Jan 2018 17:56:32 -0800 Subject: [PATCH 0118/5882] Refactor FilePatch construction Extract loose properties (like path, mode, hunks, etc) into first class File and Patch classes --- lib/controllers/file-patch-controller.js | 5 +- lib/models/file-patch.js | 124 ++++++++++++++---- lib/models/repository-states/present.js | 9 +- .../controllers/file-patch-controller.test.js | 28 ++-- test/models/file-patch.test.js | 40 +++--- test/models/repository.test.js | 52 ++++---- 6 files changed, 173 insertions(+), 85 deletions(-) diff --git a/lib/controllers/file-patch-controller.js b/lib/controllers/file-patch-controller.js index 4c4d61b532..a50568e2fa 100644 --- a/lib/controllers/file-patch-controller.js +++ b/lib/controllers/file-patch-controller.js @@ -134,10 +134,11 @@ export default class FilePatchController extends React.Component { this.resolveFilePatchLoadedPromise(); if (!this.destroyed) { this.setState({filePatch, isPartiallyStaged}); } } else { - // TODO: Prevent flicker after staging/unstaging const oldFilePatch = this.state.filePatch; if (oldFilePatch) { - filePatch = new FilePatch(oldFilePatch.getOldPath(), oldFilePatch.getNewPath(), oldFilePatch.getStatus(), []); + filePatch = oldFilePatch.clone({ + patch: oldFilePatch.getPatch().clone({hunks: []}), + }); if (!this.destroyed) { this.setState({filePatch, isPartiallyStaged}); } } } diff --git a/lib/models/file-patch.js b/lib/models/file-patch.js index 1cf9f67ae5..e93032f9ba 100644 --- a/lib/models/file-patch.js +++ b/lib/models/file-patch.js @@ -1,23 +1,95 @@ import Hunk from './hunk'; import {toGitPathSep} from '../helpers'; -export default class FilePatch { - constructor(oldPath, newPath, status, hunks) { - this.oldPath = oldPath; - this.newPath = newPath; +class File { + constructor({path, mode, symlink}) { + this.path = path; + this.mode = mode; + this.symlink = symlink; + } + + getPath() { + return this.path; + } + + getMode() { + return this.mode; + } + + getSymlink() { + return this.symlink; + } + + clone(opts = {}) { + return new File({ + path: opts.path || this.path, + mode: opts.mode || this.mode, + symlink: opts.symlink || this.symlink, + }); + } +} + +class Patch { + constructor({status, hunks}) { this.status = status; this.hunks = hunks; - this.changedLineCount = this.hunks.reduce((acc, hunk) => { + } + + getStatus() { + return this.status; + } + + getHunks() { + return this.hunks; + } + + clone(opts = {}) { + return new Patch({ + status: opts.status || this.status, + hunks: opts.hunks || this.hunks, + }); + } +} + +export default class FilePatch { + static File = File; + static Patch = Patch; + + constructor(oldFile, newFile, patch) { + this.oldFile = oldFile; + this.newFile = newFile; + this.patch = patch; + + this.changedLineCount = this.getHunks().reduce((acc, hunk) => { return acc + hunk.getLines().filter(line => line.isChanged()).length; }, 0); } + clone(opts = {}) { + const oldFile = opts.oldFile || this.getOldFile(); + const newFile = opts.newFile || this.getNewFile(); + const patch = opts.patch || this.patch; + return new FilePatch(oldFile, newFile, patch); + } + + getOldFile() { + return this.oldFile; + } + + getNewFile() { + return this.newFile; + } + + getPatch() { + return this.patch; + } + getOldPath() { - return this.oldPath; + return this.getOldFile().getPath(); } getNewPath() { - return this.newPath; + return this.getNewFile().getPath(); } getPath() { @@ -25,11 +97,11 @@ export default class FilePatch { } getStatus() { - return this.status; + return this.getPatch().getStatus(); } getHunks() { - return this.hunks; + return this.getPatch().getHunks(); } getStagePatchForHunk(selectedHunk) { @@ -72,12 +144,10 @@ export default class FilePatch { delta += newRowCount - hunk.getNewRowCount(); } - return new FilePatch( - this.getOldPath(), - this.getNewPath() ? this.getNewPath() : this.getOldPath(), - this.getStatus(), - hunks, - ); + return this.clone({ + newFile: this.getNewPath() ? this.getNewFile() : this.getOldFile(), + patch: this.getPatch().clone({hunks}), + }); } getUnstagePatch() { @@ -96,12 +166,14 @@ export default class FilePatch { throw new Error(`Unknown Status: ${this.getStatus()}`); } const invertedHunks = this.getHunks().map(h => h.invert()); - return new FilePatch( - this.getNewPath(), - this.getOldPath(), - invertedStatus, - invertedHunks, - ); + return this.clone({ + oldFile: this.getNewFile(), + newFile: this.getOldFile(), + patch: this.getPatch().clone({ + status: invertedStatus, + hunks: invertedHunks, + }), + }); } getUnstagePatchForHunk(hunk) { @@ -144,12 +216,10 @@ export default class FilePatch { delta += oldRowCount - hunk.getOldRowCount(); } - return new FilePatch( - this.getOldPath() ? this.getOldPath() : this.getNewPath(), - this.getNewPath(), - this.getStatus(), - hunks, - ).getUnstagePatch(); + return this.clone({ + oldFile: this.getOldPath() ? this.getOldFile() : this.getNewFile(), + patch: this.getPatch().clone({hunks}), + }).getUnstagePatch(); } toString() { diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js index 8ece741852..fcf3361228 100644 --- a/lib/models/repository-states/present.js +++ b/lib/models/repository-states/present.js @@ -690,9 +690,9 @@ function partition(array, predicate) { function buildFilePatchFromRawDiffs(rawDiffs) { if (rawDiffs.length === 1) { - const patch = rawDiffs[0]; + const diff = rawDiffs[0]; let diffLineNumber = 0; - const hunks = patch.hunks.map(hunk => { + const hunks = diff.hunks.map(hunk => { let oldLineNumber = hunk.oldStartLine; let newLineNumber = hunk.newStartLine; const hunkLines = hunk.lines.map(line => { @@ -725,7 +725,10 @@ function buildFilePatchFromRawDiffs(rawDiffs) { hunkLines, ); }); - return new FilePatch(patch.oldPath, patch.newPath, patch.status, hunks); + const oldFile = new FilePatch.File({path: diff.oldPath, mode: diff.oldMode, symlink: null}); + const newFile = new FilePatch.File({path: diff.newPath, mode: diff.newMode, symlink: null}); + const patch = new FilePatch.Patch({status: diff.status, hunks}); + return new FilePatch(oldFile, newFile, patch); } else if (rawDiffs.length === 2) { } else { diff --git a/test/controllers/file-patch-controller.test.js b/test/controllers/file-patch-controller.test.js index 9ac62cd73d..a77f67ab57 100644 --- a/test/controllers/file-patch-controller.test.js +++ b/test/controllers/file-patch-controller.test.js @@ -13,6 +13,14 @@ import HunkLine from '../../lib/models/hunk-line'; import ResolutionProgress from '../../lib/models/conflicts/resolution-progress'; import Switchboard from '../../lib/switchboard'; +function createFilePatch(oldFilePath, newFilePath, status, hunks) { + const oldFile = new FilePatch.File({path: oldFilePath}); + const newFile = new FilePatch.File({path: newFilePath}); + const patch = new FilePatch.Patch({status, hunks}); + + return new FilePatch(oldFile, newFile, patch); +} + describe('FilePatchController', function() { let atomEnv, commandRegistry, tooltips, deserializers; let component, switchboard, repository, filePath, getFilePatchForPath, workdirPath; @@ -105,7 +113,7 @@ describe('FilePatchController', function() { new HunkLine('line-5', 'added', 5, 5), new HunkLine('line-6', 'added', 6, 6), ]); - const filePatch = new FilePatch(filePath, filePath, 'modified', [hunk1]); + const filePatch = createFilePatch(filePath, filePath, 'modified', [hunk1]); getFilePatchForPath.returns(filePatch); @@ -125,7 +133,7 @@ describe('FilePatchController', function() { new HunkLine('line-5', 'added', 5, 5), new HunkLine('line-6', 'added', 6, 6), ]); - const filePatch = new FilePatch(filePath, filePath, 'modified', [hunk]); + const filePatch = createFilePatch(filePath, filePath, 'modified', [hunk]); getFilePatchForPath.returns(filePatch); const wrapper = mount(React.cloneElement(component, { @@ -147,8 +155,8 @@ describe('FilePatchController', function() { new HunkLine('line-5', 'added', 5, 5), new HunkLine('line-6', 'added', 6, 6), ]); - const filePatch1 = new FilePatch(filePath, filePath, 'modified', [hunk]); - const filePatch2 = new FilePatch('b.txt', 'b.txt', 'modified', [hunk]); + const filePatch1 = createFilePatch(filePath, filePath, 'modified', [hunk]); + const filePatch2 = createFilePatch('b.txt', 'b.txt', 'modified', [hunk]); getFilePatchForPath.returns(filePatch1); @@ -206,7 +214,7 @@ describe('FilePatchController', function() { }); it('renders FilePatchView only if FilePatch has hunks', async function() { - const emptyFilePatch = new FilePatch(filePath, filePath, 'modified', []); + const emptyFilePatch = createFilePatch(filePath, filePath, 'modified', []); getFilePatchForPath.returns(emptyFilePatch); const wrapper = mount(React.cloneElement(component, {filePath})); @@ -215,7 +223,7 @@ describe('FilePatchController', function() { assert.isTrue(wrapper.find('FilePatchView').text().includes('File has no contents')); const hunk1 = new Hunk(0, 0, 1, 1, '', [new HunkLine('line-1', 'added', 1, 1)]); - const filePatch = new FilePatch(filePath, filePath, 'modified', [hunk1]); + const filePatch = createFilePatch(filePath, filePath, 'modified', [hunk1]); getFilePatchForPath.returns(filePatch); wrapper.instance().onRepoRefresh(repository); @@ -226,7 +234,7 @@ describe('FilePatchController', function() { it('updates the FilePatch after a repo update', async function() { const hunk1 = new Hunk(5, 5, 2, 1, '', [new HunkLine('line-1', 'added', -1, 5)]); const hunk2 = new Hunk(8, 8, 1, 1, '', [new HunkLine('line-5', 'deleted', 8, -1)]); - const filePatch0 = new FilePatch(filePath, filePath, 'modified', [hunk1, hunk2]); + const filePatch0 = createFilePatch(filePath, filePath, 'modified', [hunk1, hunk2]); getFilePatchForPath.returns(filePatch0); const wrapper = shallow(React.cloneElement(component, {filePath})); @@ -239,7 +247,7 @@ describe('FilePatchController', function() { assert.isTrue(view0.find({hunk: hunk2}).exists()); const hunk3 = new Hunk(8, 8, 1, 1, '', [new HunkLine('line-10', 'modified', 10, 10)]); - const filePatch1 = new FilePatch(filePath, filePath, 'modified', [hunk1, hunk3]); + const filePatch1 = createFilePatch(filePath, filePath, 'modified', [hunk1, hunk3]); getFilePatchForPath.returns(filePatch1); wrapper.instance().onRepoRefresh(repository); @@ -253,7 +261,7 @@ describe('FilePatchController', function() { }); it('invokes a didSurfaceFile callback with the current file path', async function() { - const filePatch = new FilePatch(filePath, filePath, 'modified', [new Hunk(1, 1, 1, 3, '', [])]); + const filePatch = createFilePatch(filePath, filePath, 'modified', [new Hunk(1, 1, 1, 3, '', [])]); getFilePatchForPath.returns(filePatch); const wrapper = mount(React.cloneElement(component, {filePath})); @@ -278,7 +286,7 @@ describe('FilePatchController', function() { }; const hunk = new Hunk(5, 5, 2, 1, '', [new HunkLine('line-1', 'added', -1, 5)]); - const filePatch = new FilePatch(filePath, filePath, 'modified', [hunk]); + const filePatch = createFilePatch(filePath, filePath, 'modified', [hunk]); getFilePatchForPath.returns(filePatch); const wrapper = mount(React.cloneElement(component, { diff --git a/test/models/file-patch.test.js b/test/models/file-patch.test.js index 833214532b..a6a8f6da48 100644 --- a/test/models/file-patch.test.js +++ b/test/models/file-patch.test.js @@ -7,10 +7,18 @@ import FilePatch from '../../lib/models/file-patch'; import Hunk from '../../lib/models/hunk'; import HunkLine from '../../lib/models/hunk-line'; +function createFilePatch(oldFilePath, newFilePath, status, hunks) { + const oldFile = new FilePatch.File({path: oldFilePath}); + const newFile = new FilePatch.File({path: newFilePath}); + const patch = new FilePatch.Patch({status, hunks}); + + return new FilePatch(oldFile, newFile, patch); +} + describe('FilePatch', function() { describe('getStagePatchForLines()', function() { it('returns a new FilePatch that applies only the specified lines', function() { - const filePatch = new FilePatch('a.txt', 'a.txt', 'modified', [ + const filePatch = createFilePatch('a.txt', 'a.txt', 'modified', [ new Hunk(1, 1, 1, 3, '', [ new HunkLine('line-1', 'added', -1, 1), new HunkLine('line-2', 'added', -1, 2), @@ -34,7 +42,7 @@ describe('FilePatch', function() { ]), ]); const linesFromHunk2 = filePatch.getHunks()[1].getLines().slice(1, 4); - assert.deepEqual(filePatch.getStagePatchForLines(new Set(linesFromHunk2)), new FilePatch( + assert.deepEqual(filePatch.getStagePatchForLines(new Set(linesFromHunk2)), createFilePatch( 'a.txt', 'a.txt', 'modified', [ new Hunk(5, 5, 5, 4, '', [ new HunkLine('line-4', 'unchanged', 5, 5), @@ -51,7 +59,7 @@ describe('FilePatch', function() { const linesFromHunk1 = filePatch.getHunks()[0].getLines().slice(0, 1); const linesFromHunk3 = filePatch.getHunks()[2].getLines().slice(1, 2); const selectedLines = linesFromHunk2.concat(linesFromHunk1, linesFromHunk3); - assert.deepEqual(filePatch.getStagePatchForLines(new Set(selectedLines)), new FilePatch( + assert.deepEqual(filePatch.getStagePatchForLines(new Set(selectedLines)), createFilePatch( 'a.txt', 'a.txt', 'modified', [ new Hunk(1, 1, 1, 2, '', [ new HunkLine('line-1', 'added', -1, 1), @@ -77,7 +85,7 @@ describe('FilePatch', function() { describe('staging lines from deleted files', function() { it('handles staging part of the file', function() { - const filePatch = new FilePatch('a.txt', null, 'deleted', [ + const filePatch = createFilePatch('a.txt', null, 'deleted', [ new Hunk(1, 0, 3, 0, '', [ new HunkLine('line-1', 'deleted', 1, -1), new HunkLine('line-2', 'deleted', 2, -1), @@ -85,7 +93,7 @@ describe('FilePatch', function() { ]), ]); const linesFromHunk = filePatch.getHunks()[0].getLines().slice(0, 2); - assert.deepEqual(filePatch.getStagePatchForLines(new Set(linesFromHunk)), new FilePatch( + assert.deepEqual(filePatch.getStagePatchForLines(new Set(linesFromHunk)), createFilePatch( 'a.txt', 'a.txt', 'deleted', [ new Hunk(1, 1, 3, 1, '', [ new HunkLine('line-1', 'deleted', 1, -1), @@ -97,7 +105,7 @@ describe('FilePatch', function() { }); it('handles staging all lines, leaving nothing unstaged', function() { - const filePatch = new FilePatch('a.txt', null, 'deleted', [ + const filePatch = createFilePatch('a.txt', null, 'deleted', [ new Hunk(1, 0, 3, 0, '', [ new HunkLine('line-1', 'deleted', 1, -1), new HunkLine('line-2', 'deleted', 2, -1), @@ -105,7 +113,7 @@ describe('FilePatch', function() { ]), ]); const linesFromHunk = filePatch.getHunks()[0].getLines(); - assert.deepEqual(filePatch.getStagePatchForLines(new Set(linesFromHunk)), new FilePatch( + assert.deepEqual(filePatch.getStagePatchForLines(new Set(linesFromHunk)), createFilePatch( 'a.txt', null, 'deleted', [ new Hunk(1, 0, 3, 0, '', [ new HunkLine('line-1', 'deleted', 1, -1), @@ -120,7 +128,7 @@ describe('FilePatch', function() { describe('getUnstagePatchForLines()', function() { it('returns a new FilePatch that applies only the specified lines', function() { - const filePatch = new FilePatch('a.txt', 'a.txt', 'modified', [ + const filePatch = createFilePatch('a.txt', 'a.txt', 'modified', [ new Hunk(1, 1, 1, 3, '', [ new HunkLine('line-1', 'added', -1, 1), new HunkLine('line-2', 'added', -1, 2), @@ -145,7 +153,7 @@ describe('FilePatch', function() { ]); const lines = new Set(filePatch.getHunks()[1].getLines().slice(1, 5)); filePatch.getHunks()[2].getLines().forEach(line => lines.add(line)); - assert.deepEqual(filePatch.getUnstagePatchForLines(lines), new FilePatch( + assert.deepEqual(filePatch.getUnstagePatchForLines(lines), createFilePatch( 'a.txt', 'a.txt', 'modified', [ new Hunk(7, 7, 4, 4, '', [ new HunkLine('line-4', 'unchanged', 7, 7), @@ -167,7 +175,7 @@ describe('FilePatch', function() { describe('unstaging lines from an added file', function() { it('handles unstaging part of the file', function() { - const filePatch = new FilePatch(null, 'a.txt', 'added', [ + const filePatch = createFilePatch(null, 'a.txt', 'added', [ new Hunk(0, 1, 0, 3, '', [ new HunkLine('line-1', 'added', -1, 1), new HunkLine('line-2', 'added', -1, 2), @@ -175,7 +183,7 @@ describe('FilePatch', function() { ]), ]); const linesFromHunk = filePatch.getHunks()[0].getLines().slice(0, 2); - assert.deepEqual(filePatch.getUnstagePatchForLines(new Set(linesFromHunk)), new FilePatch( + assert.deepEqual(filePatch.getUnstagePatchForLines(new Set(linesFromHunk)), createFilePatch( 'a.txt', 'a.txt', 'deleted', [ new Hunk(1, 1, 3, 1, '', [ new HunkLine('line-1', 'deleted', 1, -1), @@ -187,7 +195,7 @@ describe('FilePatch', function() { }); it('handles unstaging all lines, leaving nothign staged', function() { - const filePatch = new FilePatch(null, 'a.txt', 'added', [ + const filePatch = createFilePatch(null, 'a.txt', 'added', [ new Hunk(0, 1, 0, 3, '', [ new HunkLine('line-1', 'added', -1, 1), new HunkLine('line-2', 'added', -1, 2), @@ -196,7 +204,7 @@ describe('FilePatch', function() { ]); const linesFromHunk = filePatch.getHunks()[0].getLines(); - assert.deepEqual(filePatch.getUnstagePatchForLines(new Set(linesFromHunk)), new FilePatch( + assert.deepEqual(filePatch.getUnstagePatchForLines(new Set(linesFromHunk)), createFilePatch( 'a.txt', null, 'deleted', [ new Hunk(1, 0, 3, 0, '', [ new HunkLine('line-1', 'deleted', 1, -1), @@ -210,7 +218,7 @@ describe('FilePatch', function() { }); it('handles newly added files', function() { - const filePatch = new FilePatch(null, 'a.txt', 'added', [ + const filePatch = createFilePatch(null, 'a.txt', 'added', [ new Hunk(0, 1, 0, 3, '', [ new HunkLine('line-1', 'added', -1, 1), new HunkLine('line-2', 'added', -1, 2), @@ -218,7 +226,7 @@ describe('FilePatch', function() { ]), ]); const linesFromHunk = filePatch.getHunks()[0].getLines().slice(0, 2); - assert.deepEqual(filePatch.getUnstagePatchForLines(new Set(linesFromHunk)), new FilePatch( + assert.deepEqual(filePatch.getUnstagePatchForLines(new Set(linesFromHunk)), createFilePatch( 'a.txt', 'a.txt', 'deleted', [ new Hunk(1, 1, 3, 1, '', [ new HunkLine('line-1', 'deleted', 1, -1), @@ -283,7 +291,7 @@ describe('FilePatch', function() { const oldPath = path.join('foo', 'bar', 'old.js'); const newPath = path.join('baz', 'qux', 'new.js'); - const patch = new FilePatch(oldPath, newPath, 'modified', []); + const patch = createFilePatch(oldPath, newPath, 'modified', []); assert.equal(patch.getHeaderString(), dedent` --- a/foo/bar/old.js +++ b/baz/qux/new.js diff --git a/test/models/repository.test.js b/test/models/repository.test.js index 1c8083917a..28c44cfc5a 100644 --- a/test/models/repository.test.js +++ b/test/models/repository.test.js @@ -226,36 +226,34 @@ describe('Repository', function() { status: 'modified', }, ]); - assertDeepPropertyVals(await repo.getFilePatchForPath('file.txt', {staged: true, amending: true}), { - oldPath: 'file.txt', - newPath: 'file.txt', - status: 'modified', - hunks: [ - { - lines: [ - {status: 'deleted', text: 'two', oldLineNumber: 1, newLineNumber: -1}, - {status: 'added', text: 'three', oldLineNumber: -1, newLineNumber: 1}, - ], - }, - ], - }); + const filePatch1 = await repo.getFilePatchForPath('file.txt', {staged: true, amending: true}); + assert.equal(filePatch1.getOldPath(), 'file.txt'); + assert.equal(filePatch1.getNewPath(), 'file.txt'); + assert.equal(filePatch1.getStatus(), 'modified'); + assertDeepPropertyVals(filePatch1.getHunks(), [ + { + lines: [ + {status: 'deleted', text: 'two', oldLineNumber: 1, newLineNumber: -1}, + {status: 'added', text: 'three', oldLineNumber: -1, newLineNumber: 1}, + ], + }, + ]); await repo.stageFiles(['file.txt']); repo.refresh(); - assertDeepPropertyVals(await repo.getFilePatchForPath('file.txt', {staged: true, amending: true}), { - oldPath: 'file.txt', - newPath: 'file.txt', - status: 'modified', - hunks: [ - { - lines: [ - {status: 'deleted', text: 'two', oldLineNumber: 1, newLineNumber: -1}, - {status: 'added', text: 'three', oldLineNumber: -1, newLineNumber: 1}, - {status: 'added', text: 'four', oldLineNumber: -1, newLineNumber: 2}, - ], - }, - ], - }); + const filePatch2 = await repo.getFilePatchForPath('file.txt', {staged: true, amending: true}); + assert.equal(filePatch2.getOldPath(), 'file.txt'); + assert.equal(filePatch2.getNewPath(), 'file.txt'); + assert.equal(filePatch2.getStatus(), 'modified'); + assertDeepPropertyVals(filePatch2.getHunks(), [ + { + lines: [ + {status: 'deleted', text: 'two', oldLineNumber: 1, newLineNumber: -1}, + {status: 'added', text: 'three', oldLineNumber: -1, newLineNumber: 1}, + {status: 'added', text: 'four', oldLineNumber: -1, newLineNumber: 2}, + ], + }, + ]); await repo.stageFilesFromParentCommit(['file.txt']); repo.refresh(); From 32b9bb5519cf36588787f5a67f5708187efbba27 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Fri, 5 Jan 2018 13:49:36 -0800 Subject: [PATCH 0119/5882] Show filemode change notification in diff view --- lib/controllers/file-patch-controller.js | 9 +++++++- lib/views/file-patch-view.js | 26 ++++++++++++++++++++++++ styles/file-patch-view.less | 25 +++++++++++++++++++++++ 3 files changed, 59 insertions(+), 1 deletion(-) diff --git a/lib/controllers/file-patch-controller.js b/lib/controllers/file-patch-controller.js index a50568e2fa..0e85da4d4c 100644 --- a/lib/controllers/file-patch-controller.js +++ b/lib/controllers/file-patch-controller.js @@ -137,6 +137,8 @@ export default class FilePatchController extends React.Component { const oldFilePatch = this.state.filePatch; if (oldFilePatch) { filePatch = oldFilePatch.clone({ + oldFile: oldFilePatch.oldFile.clone({mode: null}), + newFile: oldFilePatch.newFile.clone({mode: null}), patch: oldFilePatch.getPatch().clone({hunks: []}), }); if (!this.destroyed) { this.setState({filePatch, isPartiallyStaged}); } @@ -165,7 +167,11 @@ export default class FilePatchController extends React.Component { } render() { - const hunks = this.state.filePatch ? this.state.filePatch.getHunks() : []; + const fp = this.state.filePatch; + const hunks = fp ? fp.getHunks() : []; + const modeChange = fp && fp.didModeChange() ? + {oldMode: fp.getOldMode(), newMode: fp.getNewMode()} : + null; const repository = this.repositoryObserver.getActiveModel(); if (repository.isUndetermined() || repository.isLoading()) { return ( @@ -194,6 +200,7 @@ export default class FilePatchController extends React.Component { lineCount={this.lineCount} handleShowDiffClick={this.handleShowDiffClick} hunks={hunks} + modeChange={modeChange} filePath={this.props.filePath} workingDirectoryPath={this.getWorkingDirectory()} stagingStatus={this.state.stagingStatus} diff --git a/lib/views/file-patch-view.js b/lib/views/file-patch-view.js index d0b19c44a0..609d440e25 100644 --- a/lib/views/file-patch-view.js +++ b/lib/views/file-patch-view.js @@ -18,6 +18,7 @@ export default class FilePatchView extends React.Component { tooltips: PropTypes.object.isRequired, filePath: PropTypes.string.isRequired, hunks: PropTypes.arrayOf(PropTypes.object).isRequired, + modeChange: PropTypes.shape({oldMode: PropTypes.string.isRequired, newMode: PropTypes.string.isRequired}), stagingStatus: PropTypes.oneOf(['unstaged', 'staged']).isRequired, isPartiallyStaged: PropTypes.bool.isRequired, hasUndoHistory: PropTypes.bool.isRequired, @@ -160,6 +161,7 @@ export default class FilePatchView extends React.Component {
+ {this.renderModeChange(unstaged)} {this.props.displayLargeDiffMessage ? this.renderLargeDiffMessage() : this.renderHunks()}
@@ -250,6 +252,25 @@ export default class FilePatchView extends React.Component { ); } + @autobind + renderModeChange(unstaged) { + const {modeChange} = this.props; + if (!modeChange) { return null; } + + return ( +
+ + File changed mode from {modeChange.oldMode} to {modeChange.newMode} + + +
+ ); + } + componentWillUnmount() { this.disposables.dispose(); } @@ -521,6 +542,11 @@ export default class FilePatchView extends React.Component { this.didConfirm(); } + @autobind + async stageOrUnstageModeChange() { + console.log("Let's stage a mode change!"); + } + @autobind discardSelection() { const selectedLines = this.state.selection.getSelectedLines(); diff --git a/styles/file-patch-view.less b/styles/file-patch-view.less index ec5af70c9e..29693d022f 100644 --- a/styles/file-patch-view.less +++ b/styles/file-patch-view.less @@ -26,6 +26,31 @@ } } + &-mode-change { + padding: @component-padding/2; + padding-left: @component-padding; + display: flex; + flex-direction: row; + border-bottom: 1px solid @base-border-color; + + span { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + margin-right: 10px; + } + + .btn { + font-size: .9em; + &.icon-move-up::before, + &.icon-move-down::before { + font-size: 1em; + margin-right: .5em; + } + } + } + &-title { flex: 1; overflow: hidden; From c4d7f68f4e8de834ac585e06defcb23d0163f6fd Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Fri, 5 Jan 2018 13:49:49 -0800 Subject: [PATCH 0120/5882] Copy non-undefined values in FilePatch.*#clone methods --- lib/models/file-patch.js | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/lib/models/file-patch.js b/lib/models/file-patch.js index e93032f9ba..230b7198f8 100644 --- a/lib/models/file-patch.js +++ b/lib/models/file-patch.js @@ -22,9 +22,9 @@ class File { clone(opts = {}) { return new File({ - path: opts.path || this.path, - mode: opts.mode || this.mode, - symlink: opts.symlink || this.symlink, + path: opts.path !== undefined ? opts.path : this.path, + mode: opts.mode !== undefined ? opts.mode : this.mode, + symlink: opts.symlink !== undefined ? opts.symlink : this.symlink, }); } } @@ -45,8 +45,8 @@ class Patch { clone(opts = {}) { return new Patch({ - status: opts.status || this.status, - hunks: opts.hunks || this.hunks, + status: opts.status !== undefined ? opts.status : this.status, + hunks: opts.hunks !== undefined ? opts.hunks : this.hunks, }); } } @@ -66,9 +66,9 @@ export default class FilePatch { } clone(opts = {}) { - const oldFile = opts.oldFile || this.getOldFile(); - const newFile = opts.newFile || this.getNewFile(); - const patch = opts.patch || this.patch; + const oldFile = opts.oldFile !== undefined ? opts.oldFile : this.getOldFile(); + const newFile = opts.newFile !== undefined ? opts.newFile : this.getNewFile(); + const patch = opts.patch !== undefined ? opts.patch : this.patch; return new FilePatch(oldFile, newFile, patch); } @@ -92,6 +92,21 @@ export default class FilePatch { return this.getNewFile().getPath(); } + getOldMode() { + return this.getOldFile().getMode(); + } + + getNewMode() { + return this.getNewFile().getMode(); + } + + didModeChange() { + // Not a mode change if it's a creation or deletion + const oldMode = this.getOldMode(); + const newMode = this.getNewMode(); + return oldMode && newMode && oldMode !== newMode; + } + getPath() { return this.getOldPath() || this.getNewPath(); } From b206d2e29ea2e5fa191fe5bb52533f16776b7698 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 5 Jan 2018 13:52:00 -0800 Subject: [PATCH 0121/5882] :arrow_up: what-the-diff@0.4.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index db0fa7cde8..26d5381a1a 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "temp": "^0.8.3", "tinycolor2": "^1.4.1", "tree-kill": "^1.1.0", - "what-the-diff": "^0.3.0", + "what-the-diff": "^0.4.0", "what-the-status": "^1.0.3", "yubikiri": "1.0.0" }, From 6d961512ca66be3c667ab6446c0a0db537ee8adc Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 5 Jan 2018 16:20:06 -0800 Subject: [PATCH 0122/5882] Add ability to stage file mode changes --- lib/controllers/file-patch-controller.js | 46 ++++++++++++++++++++++++ lib/git-shell-out-strategy.js | 22 +++++++++++- lib/github-package.js | 4 +++ lib/models/repository-states/present.js | 5 +++ lib/models/repository.js | 1 + lib/views/file-patch-view.js | 2 +- test/models/repository.test.js | 24 +++++++++++++ 7 files changed, 102 insertions(+), 2 deletions(-) diff --git a/lib/controllers/file-patch-controller.js b/lib/controllers/file-patch-controller.js index 0e85da4d4c..9ddd695a3a 100644 --- a/lib/controllers/file-patch-controller.js +++ b/lib/controllers/file-patch-controller.js @@ -207,6 +207,7 @@ export default class FilePatchController extends React.Component { isPartiallyStaged={this.state.isPartiallyStaged} attemptLineStageOperation={this.attemptLineStageOperation} attemptHunkStageOperation={this.attemptHunkStageOperation} + attemptModeStageOperation={this.attemptModeStageOperation} didSurfaceFile={this.didSurfaceFile} didDiveIntoCorrespondingFilePatch={this.diveIntoCorrespondingFilePatch} switchboard={this.props.switchboard} @@ -277,6 +278,37 @@ export default class FilePatchController extends React.Component { } } + async stageModeChange(mode) { + this.props.switchboard.didBeginStageOperation({stage: true, mode: true}); + + await this.repositoryObserver.getActiveModel().stageFileModeChange( + this.props.filePath, mode, + ); + this.props.switchboard.didFinishStageOperation({stage: true, mode: true}); + } + + async unstageModeChange(mode) { + this.props.switchboard.didBeginStageOperation({unstage: true, mode: true}); + + await this.repositoryObserver.getActiveModel().stageFileModeChange( + this.props.filePath, mode, + ); + this.props.switchboard.didFinishStageOperation({unstage: true, mode: true}); + } + + stageOrUnstageModeChange() { + const stagingStatus = this.state.stagingStatus; + const oldMode = this.state.filePatch.getOldMode(); + const newMode = this.state.filePatch.getNewMode(); + if (stagingStatus === 'unstaged') { + return this.stageModeChange(newMode); + } else if (stagingStatus === 'staged') { + return this.unstageModeChange(oldMode); + } else { + throw new Error(`Unknown stagingStatus: ${stagingStatus}`); + } + } + @autobind attemptHunkStageOperation(hunk) { if (this.stagingOperationInProgress) { @@ -291,6 +323,20 @@ export default class FilePatchController extends React.Component { this.stageOrUnstageHunk(hunk); } + @autobind + attemptModeStageOperation() { + if (this.stagingOperationInProgress) { + return; + } + + this.stagingOperationInProgress = true; + this.props.switchboard.getChangePatchPromise().then(() => { + this.stagingOperationInProgress = false; + }); + + this.stageOrUnstageModeChange(); + } + async stageLines(lines) { this.props.switchboard.didBeginStageOperation({stage: true, line: true}); diff --git a/lib/git-shell-out-strategy.js b/lib/git-shell-out-strategy.js index 77e7503217..7cfb02f828 100644 --- a/lib/git-shell-out-strategy.js +++ b/lib/git-shell-out-strategy.js @@ -188,7 +188,7 @@ export default class GitShellOutStrategy { env.GIT_TRACE_CURL = 'true'; } - const opts = {env}; + let opts = {env}; if (stdin) { opts.stdin = stdin; @@ -199,6 +199,11 @@ export default class GitShellOutStrategy { console.time(`git:${formattedArgs}`); } return new Promise(async (resolve, reject) => { + if (options.beforeRun) { + const newArgsOpts = await options.beforeRun({args, opts}); + args = newArgsOpts.args; // eslint-disable-line no-param-reassign + opts = newArgsOpts.opts; + } const {promise, cancel} = this.executeGitCommand(args, opts, timingMarker); let expectCancel = false; if (gitPromptServer) { @@ -373,6 +378,21 @@ export default class GitShellOutStrategy { return this.exec(args, {writeOperation: true}); } + stageFileModeChange(filename, newMode) { + const indexReadPromise = this.exec(['ls-files', '-s', '--', filename]); + return this.exec(['update-index', '--cacheinfo', `${newMode},,${filename}`], { + writeOperation: true, + beforeRun: async function determineArgs({args, opts}) { + const index = await indexReadPromise; + const oid = index.substr(7, 40); + return { + opts, + args: ['update-index', '--cacheinfo', `${newMode},${oid},${filename}`], + }; + }, + }); + } + applyPatch(patch, {index} = {}) { const args = ['apply', '-']; if (index) { args.splice(1, 0, '--cached'); } diff --git a/lib/github-package.js b/lib/github-package.js index ae080ab5e3..dc96e0f7f7 100644 --- a/lib/github-package.js +++ b/lib/github-package.js @@ -96,10 +96,14 @@ export default class GithubPackage { yardstick.begin('stageLine'); } else if (payload.stage && payload.hunk) { yardstick.begin('stageHunk'); + } else if (payload.stage && payload.mode) { + yardstick.begin('stageMode'); } else if (payload.unstage && payload.line) { yardstick.begin('unstageLine'); } else if (payload.unstage && payload.hunk) { yardstick.begin('unstageHunk'); + } else if (payload.unstage && payload.mode) { + yardstick.begin('unstageMode'); } }), this.switchboard.onDidUpdateRepository(() => { diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js index fcf3361228..246b99a0b6 100644 --- a/lib/models/repository-states/present.js +++ b/lib/models/repository-states/present.js @@ -223,6 +223,11 @@ export default class Present extends State { return this.git().unstageFiles(paths, 'HEAD~'); } + @invalidate(path => Keys.cacheOperationKeys([path])) + stageFileModeChange(path, fileMode) { + return this.git().stageFileModeChange(path, fileMode); + } + @invalidate(filePatch => Keys.cacheOperationKeys([filePatch.getOldPath(), filePatch.getNewPath()])) applyPatchToIndex(filePatch) { const patchStr = filePatch.getHeaderString() + filePatch.toString(); diff --git a/lib/models/repository.js b/lib/models/repository.js index 0889be6adf..9a5e6399f4 100644 --- a/lib/models/repository.js +++ b/lib/models/repository.js @@ -250,6 +250,7 @@ const delegates = [ 'stageFiles', 'unstageFiles', 'stageFilesFromParentCommit', + 'stageFileModeChange', 'applyPatchToIndex', 'applyPatchToWorkdir', diff --git a/lib/views/file-patch-view.js b/lib/views/file-patch-view.js index 609d440e25..e05d0b9fd2 100644 --- a/lib/views/file-patch-view.js +++ b/lib/views/file-patch-view.js @@ -544,7 +544,7 @@ export default class FilePatchView extends React.Component { @autobind async stageOrUnstageModeChange() { - console.log("Let's stage a mode change!"); + this.props.attemptModeStageOperation(); } @autobind diff --git a/test/models/repository.test.js b/test/models/repository.test.js index 28c44cfc5a..4e7501bd97 100644 --- a/test/models/repository.test.js +++ b/test/models/repository.test.js @@ -259,6 +259,30 @@ describe('Repository', function() { repo.refresh(); assert.deepEqual(await repo.getStagedChangesSinceParentCommit(), []); }); + + it('can stage and unstage file modes without staging file contents', async function() { + const workingDirPath = await cloneRepository('three-files'); + const repo = new Repository(workingDirPath); + await repo.getLoadPromise(); + const filePath = 'a.txt'; + + async function indexModeAndOid(filename) { + const output = await repo.git.exec(['ls-files', '-s', '--', filename]); + const parts = output.split(' '); + return {mode: parts[0], oid: parts[1]}; + } + + const {mode, oid} = await indexModeAndOid(path.join(workingDirPath, filePath)); + assert.equal(mode, '100644'); + fs.chmodSync(path.join(workingDirPath, filePath), 0o755); + fs.writeFileSync(path.join(workingDirPath, filePath), 'qux\nfoo\nbar\n', 'utf8'); + + await repo.stageFileModeChange(filePath, '100755'); + assert.deepEqual(await indexModeAndOid(filePath), {mode: '100755', oid}); + + await repo.stageFileModeChange(filePath, '100644'); + assert.deepEqual(await indexModeAndOid(filePath), {mode: '100644', oid}); + }); }); describe('getFilePatchForPath', function() { From a56a962f9d61fbc2cdd66c89e251c91af8eb5f11 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 5 Jan 2018 16:31:15 -0800 Subject: [PATCH 0123/5882] Stage entire file instead of all lines/hunks when clicking "Stage File" from header Previously this was leaving file mode changes behind --- lib/controllers/file-patch-controller.js | 41 ++++++++++++++++++++++++ lib/github-package.js | 4 +++ lib/views/file-patch-view.js | 3 +- 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/lib/controllers/file-patch-controller.js b/lib/controllers/file-patch-controller.js index 9ddd695a3a..6adf9f2e70 100644 --- a/lib/controllers/file-patch-controller.js +++ b/lib/controllers/file-patch-controller.js @@ -207,6 +207,7 @@ export default class FilePatchController extends React.Component { isPartiallyStaged={this.state.isPartiallyStaged} attemptLineStageOperation={this.attemptLineStageOperation} attemptHunkStageOperation={this.attemptHunkStageOperation} + attemptFileStageOperation={this.attemptFileStageOperation} attemptModeStageOperation={this.attemptModeStageOperation} didSurfaceFile={this.didSurfaceFile} didDiveIntoCorrespondingFilePatch={this.diveIntoCorrespondingFilePatch} @@ -278,6 +279,32 @@ export default class FilePatchController extends React.Component { } } + async stageFile() { + this.props.switchboard.didBeginStageOperation({stage: true, file: true}); + + await this.repositoryObserver.getActiveModel().stageFiles([this.props.filePath]); + this.props.switchboard.didFinishStageOperation({stage: true, file: true}); + } + + async unstageFile() { + this.props.switchboard.didBeginStageOperation({unstage: true, file: true}); + + await this.repositoryObserver.getActiveModel().unstageFiles([this.props.filePath]); + + this.props.switchboard.didFinishStageOperation({unstage: true, file: true}); + } + + stageOrUnstageFile() { + const stagingStatus = this.state.stagingStatus; + if (stagingStatus === 'unstaged') { + return this.stageFile(); + } else if (stagingStatus === 'staged') { + return this.unstageFile(); + } else { + throw new Error(`Unknown stagingStatus: ${stagingStatus}`); + } + } + async stageModeChange(mode) { this.props.switchboard.didBeginStageOperation({stage: true, mode: true}); @@ -323,6 +350,20 @@ export default class FilePatchController extends React.Component { this.stageOrUnstageHunk(hunk); } + @autobind + attemptFileStageOperation() { + if (this.stagingOperationInProgress) { + return; + } + + this.stagingOperationInProgress = true; + this.props.switchboard.getChangePatchPromise().then(() => { + this.stagingOperationInProgress = false; + }); + + this.stageOrUnstageFile(); + } + @autobind attemptModeStageOperation() { if (this.stagingOperationInProgress) { diff --git a/lib/github-package.js b/lib/github-package.js index dc96e0f7f7..19322e6d66 100644 --- a/lib/github-package.js +++ b/lib/github-package.js @@ -96,12 +96,16 @@ export default class GithubPackage { yardstick.begin('stageLine'); } else if (payload.stage && payload.hunk) { yardstick.begin('stageHunk'); + } else if (payload.stage && payload.file) { + yardstick.begin('stageFile'); } else if (payload.stage && payload.mode) { yardstick.begin('stageMode'); } else if (payload.unstage && payload.line) { yardstick.begin('unstageLine'); } else if (payload.unstage && payload.hunk) { yardstick.begin('unstageHunk'); + } else if (payload.unstage && payload.file) { + yardstick.begin('unstageFile'); } else if (payload.unstage && payload.mode) { yardstick.begin('unstageMode'); } diff --git a/lib/views/file-patch-view.js b/lib/views/file-patch-view.js index e05d0b9fd2..63d3271b6f 100644 --- a/lib/views/file-patch-view.js +++ b/lib/views/file-patch-view.js @@ -538,8 +538,7 @@ export default class FilePatchView extends React.Component { @autobind async stageOrUnstageAll() { - await this.selectAll(); - this.didConfirm(); + this.props.attemptFileStageOperation(); } @autobind From 5768673b0ad2f689859ab3505a143dcc31080424 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 5 Jan 2018 16:50:08 -0800 Subject: [PATCH 0124/5882] Handle falsey children in Commands component --- lib/views/commands.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/views/commands.js b/lib/views/commands.js index 0547b615e4..9469592983 100644 --- a/lib/views/commands.js +++ b/lib/views/commands.js @@ -21,7 +21,7 @@ export default class Commands extends React.Component { return (
{React.Children.map(this.props.children, child => { - return React.cloneElement(child, {registry, target}); + return child ? React.cloneElement(child, {registry, target}) : null; })}
); From 3803d78dc6fb4dc58b1d584f1612ac9dd1a1061c Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 5 Jan 2018 16:50:26 -0800 Subject: [PATCH 0125/5882] Add command for staging/unstaging file mode change --- lib/views/file-patch-view.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/views/file-patch-view.js b/lib/views/file-patch-view.js index 63d3271b6f..d26df8bc75 100644 --- a/lib/views/file-patch-view.js +++ b/lib/views/file-patch-view.js @@ -197,6 +197,11 @@ export default class FilePatchView extends React.Component { command="core:undo" callback={() => this.props.hasUndoHistory && this.props.undoLastDiscard()} /> + {this.props.modeChange && + } From 47da5a173ca44a12521b182fc11076f9d32e877f Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 5 Jan 2018 17:17:38 -0800 Subject: [PATCH 0126/5882] :shirt: --- lib/controllers/file-patch-controller.js | 1 - lib/models/repository-states/present.js | 6 +++--- lib/views/file-patch-view.js | 6 ++++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/controllers/file-patch-controller.js b/lib/controllers/file-patch-controller.js index 6adf9f2e70..d513ea0a46 100644 --- a/lib/controllers/file-patch-controller.js +++ b/lib/controllers/file-patch-controller.js @@ -10,7 +10,6 @@ import {autobind} from 'core-decorators'; import Switchboard from '../switchboard'; import FilePatchView from '../views/file-patch-view'; import ModelObserver from '../models/model-observer'; -import FilePatch from '../models/file-patch'; export default class FilePatchController extends React.Component { static propTypes = { diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js index 246b99a0b6..7dd49f39c4 100644 --- a/lib/models/repository-states/present.js +++ b/lib/models/repository-states/present.js @@ -223,9 +223,9 @@ export default class Present extends State { return this.git().unstageFiles(paths, 'HEAD~'); } - @invalidate(path => Keys.cacheOperationKeys([path])) - stageFileModeChange(path, fileMode) { - return this.git().stageFileModeChange(path, fileMode); + @invalidate(filePath => Keys.cacheOperationKeys([filePath])) + stageFileModeChange(filePath, fileMode) { + return this.git().stageFileModeChange(filePath, fileMode); } @invalidate(filePatch => Keys.cacheOperationKeys([filePatch.getOldPath(), filePatch.getNewPath()])) diff --git a/lib/views/file-patch-view.js b/lib/views/file-patch-view.js index d26df8bc75..7ee06b5a1b 100644 --- a/lib/views/file-patch-view.js +++ b/lib/views/file-patch-view.js @@ -24,6 +24,8 @@ export default class FilePatchView extends React.Component { hasUndoHistory: PropTypes.bool.isRequired, attemptLineStageOperation: PropTypes.func.isRequired, attemptHunkStageOperation: PropTypes.func.isRequired, + attemptFileStageOperation: PropTypes.func.isRequired, + attemptModeStageOperation: PropTypes.func.isRequired, discardLines: PropTypes.func.isRequired, undoLastDiscard: PropTypes.func.isRequired, openCurrentFile: PropTypes.func.isRequired, @@ -542,12 +544,12 @@ export default class FilePatchView extends React.Component { } @autobind - async stageOrUnstageAll() { + stageOrUnstageAll() { this.props.attemptFileStageOperation(); } @autobind - async stageOrUnstageModeChange() { + stageOrUnstageModeChange() { this.props.attemptModeStageOperation(); } From 52b8aff7e64eb81d82df65150041fb151887ab9f Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 5 Jan 2018 18:26:32 -0800 Subject: [PATCH 0127/5882] Add typechange status for changed file and display as modified --- lib/helpers.js | 1 + lib/models/repository-states/present.js | 1 + 2 files changed, 2 insertions(+) diff --git a/lib/helpers.js b/lib/helpers.js index 13a1469b3b..fecdf56579 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -243,6 +243,7 @@ export const classNameForStatus = { added: 'added', deleted: 'removed', modified: 'modified', + typechange: 'modified', equivalent: 'ignored', }; diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js index 7dd49f39c4..29935918fe 100644 --- a/lib/models/repository-states/present.js +++ b/lib/models/repository-states/present.js @@ -487,6 +487,7 @@ export default class Present extends State { M: 'modified', D: 'deleted', U: 'modified', + T: 'typechange', }; const stagedFiles = {}; From d90603565879cb3021c7cbf6bb5ddef0884f93bb Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 5 Jan 2018 18:28:38 -0800 Subject: [PATCH 0128/5882] Parse & create FilePatch objs from dual diffs associated w/ typechanges --- lib/models/repository-states/present.js | 96 ++++++++++++++++--------- 1 file changed, 61 insertions(+), 35 deletions(-) diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js index 29935918fe..c49d56b1d7 100644 --- a/lib/models/repository-states/present.js +++ b/lib/models/repository-states/present.js @@ -694,49 +694,75 @@ function partition(array, predicate) { return [matches, nonmatches]; } +function buildHunksFromDiff(diff) { + let diffLineNumber = 0; + return diff.hunks.map(hunk => { + let oldLineNumber = hunk.oldStartLine; + let newLineNumber = hunk.newStartLine; + const hunkLines = hunk.lines.map(line => { + const status = HunkLine.statusMap[line[0]]; + const text = line.slice(1); + let hunkLine; + if (status === 'unchanged') { + hunkLine = new HunkLine(text, status, oldLineNumber, newLineNumber, diffLineNumber++); + oldLineNumber++; + newLineNumber++; + } else if (status === 'added') { + hunkLine = new HunkLine(text, status, -1, newLineNumber, diffLineNumber++); + newLineNumber++; + } else if (status === 'deleted') { + hunkLine = new HunkLine(text, status, oldLineNumber, -1, diffLineNumber++); + oldLineNumber++; + } else if (status === 'nonewline') { + hunkLine = new HunkLine(text.substr(1), status, -1, -1, diffLineNumber++); + } else { + throw new Error(`unknow status type: ${status}`); + } + return hunkLine; + }); + return new Hunk( + hunk.oldStartLine, + hunk.newStartLine, + hunk.oldLineCount, + hunk.newLineCount, + hunk.heading, + hunkLines, + ); + }); +} + function buildFilePatchFromRawDiffs(rawDiffs) { if (rawDiffs.length === 1) { const diff = rawDiffs[0]; - let diffLineNumber = 0; - const hunks = diff.hunks.map(hunk => { - let oldLineNumber = hunk.oldStartLine; - let newLineNumber = hunk.newStartLine; - const hunkLines = hunk.lines.map(line => { - const status = HunkLine.statusMap[line[0]]; - const text = line.slice(1); - let hunkLine; - if (status === 'unchanged') { - hunkLine = new HunkLine(text, status, oldLineNumber, newLineNumber, diffLineNumber++); - oldLineNumber++; - newLineNumber++; - } else if (status === 'added') { - hunkLine = new HunkLine(text, status, -1, newLineNumber, diffLineNumber++); - newLineNumber++; - } else if (status === 'deleted') { - hunkLine = new HunkLine(text, status, oldLineNumber, -1, diffLineNumber++); - oldLineNumber++; - } else if (status === 'nonewline') { - hunkLine = new HunkLine(text.substr(1), status, -1, -1, diffLineNumber++); - } else { - throw new Error(`unknow status type: ${status}`); - } - return hunkLine; - }); - return new Hunk( - hunk.oldStartLine, - hunk.newStartLine, - hunk.oldLineCount, - hunk.newLineCount, - hunk.heading, - hunkLines, - ); - }); + const hunks = buildHunksFromDiff(diff); const oldFile = new FilePatch.File({path: diff.oldPath, mode: diff.oldMode, symlink: null}); const newFile = new FilePatch.File({path: diff.newPath, mode: diff.newMode, symlink: null}); const patch = new FilePatch.Patch({status: diff.status, hunks}); return new FilePatch(oldFile, newFile, patch); } else if (rawDiffs.length === 2) { - + let modeChangeDiff, contentChangeDiff; + if (rawDiffs[0].oldMode === '120000' || rawDiffs[0].newMode === '120000') { + modeChangeDiff = rawDiffs[0]; + contentChangeDiff = rawDiffs[1]; + } else { + modeChangeDiff = rawDiffs[1]; + contentChangeDiff = rawDiffs[0]; + } + const hunks = buildHunksFromDiff(contentChangeDiff); + const filePath = contentChangeDiff.oldPath || contentChangeDiff.newPath; + const symlink = modeChangeDiff.hunks[0].lines[0].slice(1); + let oldFile, newFile; + if (modeChangeDiff.status === 'added') { + oldFile = new FilePatch.File({path: filePath, mode: contentChangeDiff.oldMode, symlink: null}); + newFile = new FilePatch.File({path: filePath, mode: modeChangeDiff.newMode, symlink}); + } else if (modeChangeDiff.status === 'deleted') { + oldFile = new FilePatch.File({path: filePath, mode: modeChangeDiff.oldMode, symlink}); + newFile = new FilePatch.File({path: filePath, mode: contentChangeDiff.newMode, symlink: null}); + } else { + throw new Error(`Invalid mode change diff status: ${modeChangeDiff.status}`); + } + const patch = new FilePatch.Patch({status: 'typechange', hunks}); + return new FilePatch(oldFile, newFile, patch); } else { throw new Error(`Unexpected number of diffs: ${rawDiffs.length}`); } From 70a6b12572e8c79f3b77aadb8b97411234423630 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 5 Jan 2018 18:30:37 -0800 Subject: [PATCH 0129/5882] Display descriptive symlink message in FilePatchView for typechange case --- lib/controllers/file-patch-controller.js | 2 +- lib/models/file-patch.js | 4 ++ lib/views/file-patch-view.js | 54 ++++++++++++++++++------ 3 files changed, 47 insertions(+), 13 deletions(-) diff --git a/lib/controllers/file-patch-controller.js b/lib/controllers/file-patch-controller.js index d513ea0a46..8d9eaea552 100644 --- a/lib/controllers/file-patch-controller.js +++ b/lib/controllers/file-patch-controller.js @@ -169,7 +169,7 @@ export default class FilePatchController extends React.Component { const fp = this.state.filePatch; const hunks = fp ? fp.getHunks() : []; const modeChange = fp && fp.didModeChange() ? - {oldMode: fp.getOldMode(), newMode: fp.getNewMode()} : + {oldMode: fp.getOldMode(), newMode: fp.getNewMode(), symlink: fp.getSymlink()} : null; const repository = this.repositoryObserver.getActiveModel(); if (repository.isUndetermined() || repository.isLoading()) { diff --git a/lib/models/file-patch.js b/lib/models/file-patch.js index 230b7198f8..32fce36dbb 100644 --- a/lib/models/file-patch.js +++ b/lib/models/file-patch.js @@ -107,6 +107,10 @@ export default class FilePatch { return oldMode && newMode && oldMode !== newMode; } + getSymlink() { + return this.getOldFile().getSymlink() || this.getNewFile().getSymlink(); + } + getPath() { return this.getOldPath() || this.getNewPath(); } diff --git a/lib/views/file-patch-view.js b/lib/views/file-patch-view.js index 7ee06b5a1b..6a1ec807b2 100644 --- a/lib/views/file-patch-view.js +++ b/lib/views/file-patch-view.js @@ -263,19 +263,49 @@ export default class FilePatchView extends React.Component { renderModeChange(unstaged) { const {modeChange} = this.props; if (!modeChange) { return null; } + const symlinkDeleted = modeChange.oldMode === '120000'; + const symlinkAdded = modeChange.newMode === '120000'; - return ( -
- - File changed mode from {modeChange.oldMode} to {modeChange.newMode} - - -
- ); + if (symlinkDeleted) { + return ( +
+ + Symlink to {modeChange.symlink} deleted and replaced with the following contents: + + +
+ ); + } else if (symlinkAdded) { + return ( +
+ + File contents deleted and replaced with symlink to {modeChange.symlink} + + +
+ ); + } else { + return ( +
+ + File changed mode from {modeChange.oldMode} to {modeChange.newMode} + + +
+ ); + } } componentWillUnmount() { From b1121b1712cd0686eafe8a5673932639d7a248c4 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 5 Jan 2018 20:26:33 -0800 Subject: [PATCH 0130/5882] Properly parse & build FilePatch object for symlink change w/ single diff --- lib/models/repository-states/present.js | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js index c49d56b1d7..346e3d3acd 100644 --- a/lib/models/repository-states/present.js +++ b/lib/models/repository-states/present.js @@ -733,10 +733,28 @@ function buildHunksFromDiff(diff) { function buildFilePatchFromRawDiffs(rawDiffs) { if (rawDiffs.length === 1) { + const wasSymlink = rawDiffs[0].oldMode === '120000'; + const isSymlink = rawDiffs[0].newMode === '120000'; const diff = rawDiffs[0]; const hunks = buildHunksFromDiff(diff); - const oldFile = new FilePatch.File({path: diff.oldPath, mode: diff.oldMode, symlink: null}); - const newFile = new FilePatch.File({path: diff.newPath, mode: diff.newMode, symlink: null}); + let oldFile, newFile; + if (wasSymlink && !isSymlink) { + const symlink = diff.hunks[0].lines[0].slice(1); + oldFile = new FilePatch.File({path: diff.oldPath, mode: diff.oldMode, symlink}); + newFile = new FilePatch.File({path: diff.newPath, mode: diff.newMode, symlink: null}); + } else if (!wasSymlink && isSymlink) { + const symlink = diff.hunks[0].lines[0].slice(1); + oldFile = new FilePatch.File({path: diff.oldPath, mode: diff.oldMode, symlink: null}); + newFile = new FilePatch.File({path: diff.newPath, mode: diff.newMode, symlink}); + } else if (wasSymlink && isSymlink) { + const oldSymlink = diff.hunks[0].lines[0].slice(1); + const newSymlink = diff.hunks[0].lines[2].slice(1); + oldFile = new FilePatch.File({path: diff.oldPath, mode: diff.oldMode, symlink: oldSymlink}); + newFile = new FilePatch.File({path: diff.newPath, mode: diff.newMode, symlink: newSymlink}); + } else { + oldFile = new FilePatch.File({path: diff.oldPath, mode: diff.oldMode, symlink: null}); + newFile = new FilePatch.File({path: diff.newPath, mode: diff.newMode, symlink: null}); + } const patch = new FilePatch.Patch({status: diff.status, hunks}); return new FilePatch(oldFile, newFile, patch); } else if (rawDiffs.length === 2) { From 8f0a4a4213da7b4fa03070cb4efd0006c4d25b32 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 5 Jan 2018 20:27:35 -0800 Subject: [PATCH 0131/5882] :art: refactor buildFilePatchFromRawDiffs --- lib/models/repository-states/present.js | 102 +++++++++++++----------- 1 file changed, 55 insertions(+), 47 deletions(-) diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js index 346e3d3acd..28d440ab2a 100644 --- a/lib/models/repository-states/present.js +++ b/lib/models/repository-states/present.js @@ -731,56 +731,64 @@ function buildHunksFromDiff(diff) { }); } +function buildFilePatchFromSingleDiff(rawDiff) { + const wasSymlink = rawDiff.oldMode === '120000'; + const isSymlink = rawDiff.newMode === '120000'; + const diff = rawDiff; + const hunks = buildHunksFromDiff(diff); + let oldFile, newFile; + if (wasSymlink && !isSymlink) { + const symlink = diff.hunks[0].lines[0].slice(1); + oldFile = new FilePatch.File({path: diff.oldPath, mode: diff.oldMode, symlink}); + newFile = new FilePatch.File({path: diff.newPath, mode: diff.newMode, symlink: null}); + } else if (!wasSymlink && isSymlink) { + const symlink = diff.hunks[0].lines[0].slice(1); + oldFile = new FilePatch.File({path: diff.oldPath, mode: diff.oldMode, symlink: null}); + newFile = new FilePatch.File({path: diff.newPath, mode: diff.newMode, symlink}); + } else if (wasSymlink && isSymlink) { + const oldSymlink = diff.hunks[0].lines[0].slice(1); + const newSymlink = diff.hunks[0].lines[2].slice(1); + oldFile = new FilePatch.File({path: diff.oldPath, mode: diff.oldMode, symlink: oldSymlink}); + newFile = new FilePatch.File({path: diff.newPath, mode: diff.newMode, symlink: newSymlink}); + } else { + oldFile = new FilePatch.File({path: diff.oldPath, mode: diff.oldMode, symlink: null}); + newFile = new FilePatch.File({path: diff.newPath, mode: diff.newMode, symlink: null}); + } + const patch = new FilePatch.Patch({status: diff.status, hunks}); + return new FilePatch(oldFile, newFile, patch); +} + +function buildFilePatchFromDualDiffs(diff1, diff2) { + let modeChangeDiff, contentChangeDiff; + if (diff1.oldMode === '120000' || diff1.newMode === '120000') { + modeChangeDiff = diff1; + contentChangeDiff = diff2; + } else { + modeChangeDiff = diff2; + contentChangeDiff = diff1; + } + const hunks = buildHunksFromDiff(contentChangeDiff); + const filePath = contentChangeDiff.oldPath || contentChangeDiff.newPath; + const symlink = modeChangeDiff.hunks[0].lines[0].slice(1); + let oldFile, newFile; + if (modeChangeDiff.status === 'added') { + oldFile = new FilePatch.File({path: filePath, mode: contentChangeDiff.oldMode, symlink: null}); + newFile = new FilePatch.File({path: filePath, mode: modeChangeDiff.newMode, symlink}); + } else if (modeChangeDiff.status === 'deleted') { + oldFile = new FilePatch.File({path: filePath, mode: modeChangeDiff.oldMode, symlink}); + newFile = new FilePatch.File({path: filePath, mode: contentChangeDiff.newMode, symlink: null}); + } else { + throw new Error(`Invalid mode change diff status: ${modeChangeDiff.status}`); + } + const patch = new FilePatch.Patch({status: 'typechange', hunks}); + return new FilePatch(oldFile, newFile, patch); +} + function buildFilePatchFromRawDiffs(rawDiffs) { if (rawDiffs.length === 1) { - const wasSymlink = rawDiffs[0].oldMode === '120000'; - const isSymlink = rawDiffs[0].newMode === '120000'; - const diff = rawDiffs[0]; - const hunks = buildHunksFromDiff(diff); - let oldFile, newFile; - if (wasSymlink && !isSymlink) { - const symlink = diff.hunks[0].lines[0].slice(1); - oldFile = new FilePatch.File({path: diff.oldPath, mode: diff.oldMode, symlink}); - newFile = new FilePatch.File({path: diff.newPath, mode: diff.newMode, symlink: null}); - } else if (!wasSymlink && isSymlink) { - const symlink = diff.hunks[0].lines[0].slice(1); - oldFile = new FilePatch.File({path: diff.oldPath, mode: diff.oldMode, symlink: null}); - newFile = new FilePatch.File({path: diff.newPath, mode: diff.newMode, symlink}); - } else if (wasSymlink && isSymlink) { - const oldSymlink = diff.hunks[0].lines[0].slice(1); - const newSymlink = diff.hunks[0].lines[2].slice(1); - oldFile = new FilePatch.File({path: diff.oldPath, mode: diff.oldMode, symlink: oldSymlink}); - newFile = new FilePatch.File({path: diff.newPath, mode: diff.newMode, symlink: newSymlink}); - } else { - oldFile = new FilePatch.File({path: diff.oldPath, mode: diff.oldMode, symlink: null}); - newFile = new FilePatch.File({path: diff.newPath, mode: diff.newMode, symlink: null}); - } - const patch = new FilePatch.Patch({status: diff.status, hunks}); - return new FilePatch(oldFile, newFile, patch); + return buildFilePatchFromSingleDiff(rawDiffs[0]); } else if (rawDiffs.length === 2) { - let modeChangeDiff, contentChangeDiff; - if (rawDiffs[0].oldMode === '120000' || rawDiffs[0].newMode === '120000') { - modeChangeDiff = rawDiffs[0]; - contentChangeDiff = rawDiffs[1]; - } else { - modeChangeDiff = rawDiffs[1]; - contentChangeDiff = rawDiffs[0]; - } - const hunks = buildHunksFromDiff(contentChangeDiff); - const filePath = contentChangeDiff.oldPath || contentChangeDiff.newPath; - const symlink = modeChangeDiff.hunks[0].lines[0].slice(1); - let oldFile, newFile; - if (modeChangeDiff.status === 'added') { - oldFile = new FilePatch.File({path: filePath, mode: contentChangeDiff.oldMode, symlink: null}); - newFile = new FilePatch.File({path: filePath, mode: modeChangeDiff.newMode, symlink}); - } else if (modeChangeDiff.status === 'deleted') { - oldFile = new FilePatch.File({path: filePath, mode: modeChangeDiff.oldMode, symlink}); - newFile = new FilePatch.File({path: filePath, mode: contentChangeDiff.newMode, symlink: null}); - } else { - throw new Error(`Invalid mode change diff status: ${modeChangeDiff.status}`); - } - const patch = new FilePatch.Patch({status: 'typechange', hunks}); - return new FilePatch(oldFile, newFile, patch); + return buildFilePatchFromDualDiffs(rawDiffs[0], rawDiffs[1]); } else { throw new Error(`Unexpected number of diffs: ${rawDiffs.length}`); } From 574e29fde7b92edc68e77d684ac65033e12abcec Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 5 Jan 2018 20:30:30 -0800 Subject: [PATCH 0132/5882] Correctly build added file patch for new symlink file --- lib/git-shell-out-strategy.js | 28 +++++++++++++++++++++++----- lib/helpers.js | 21 +++++++++++++++++++++ 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/lib/git-shell-out-strategy.js b/lib/git-shell-out-strategy.js index 7cfb02f828..bf68f2e775 100644 --- a/lib/git-shell-out-strategy.js +++ b/lib/git-shell-out-strategy.js @@ -13,7 +13,7 @@ import GitTempDir from './git-temp-dir'; import AsyncQueue from './async-queue'; import { getDugitePath, getAtomHelperPath, - readFile, fileExists, fsStat, writeFile, isFileExecutable, isBinary, + readFile, fileExists, fsStat, writeFile, isFileExecutable, isFileSymlink, isBinary, getRealPath, normalizeGitHelperPath, toNativePathSep, toGitPathSep, } from './helpers'; import GitTimingsView from './views/git-timings-view'; @@ -507,9 +507,21 @@ export default class GitShellOutStrategy { // add untracked file const absPath = path.join(this.workingDir, filePath); const executable = await isFileExecutable(absPath); + const symlink = await isFileSymlink(absPath); const contents = await readFile(absPath); const binary = isBinary(contents); - rawDiffs.push(buildAddedFilePatch(filePath, binary ? null : contents, executable)); + let mode; + let realpath; + if (executable) { + mode = '100755'; + } else if (symlink) { + mode = '120000'; + realpath = await getRealPath(absPath); + } else { + mode = '100644'; + } + + rawDiffs.push(buildAddedFilePatch(filePath, binary ? null : contents, mode, realpath)); } if (rawDiffs.length > 2) { throw new Error(`Expected between 0 and 2 diffs for ${filePath} but got ${rawDiffs.length}`); @@ -765,11 +777,17 @@ export default class GitShellOutStrategy { } } -function buildAddedFilePatch(filePath, contents, executable) { +function buildAddedFilePatch(filePath, contents, mode, realpath) { const hunks = []; if (contents) { const noNewLine = contents[contents.length - 1] !== '\n'; - const lines = contents.trim().split(LINE_ENDING_REGEX).map(line => `+${line}`); + let lines; + if (mode === '120000') { + // TODO: normalize path separators? + lines = [`+${realpath}`, '\\ No newline at end of file']; + } else { + lines = contents.trim().split(LINE_ENDING_REGEX).map(line => `+${line}`); + } if (noNewLine) { lines.push('\\ No newline at end of file'); } hunks.push({ lines, @@ -784,7 +802,7 @@ function buildAddedFilePatch(filePath, contents, executable) { oldPath: null, newPath: toNativePathSep(filePath), oldMode: null, - newMode: executable ? '100755' : '100644', + newMode: mode, status: 'added', hunks, }; diff --git a/lib/helpers.js b/lib/helpers.js index fecdf56579..5bf3e91b59 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -224,11 +224,32 @@ export function fsStat(absoluteFilePath) { }); } +export function fsLStat(absoluteFilePath) { + return new Promise((resolve, reject) => { + fs.lstat(absoluteFilePath, (err, stats) => { + if (err) { reject(err); } else { resolve(stats); } + }); + }); +} + export async function isFileExecutable(absoluteFilePath) { const stat = await fsStat(absoluteFilePath); return stat.mode & fs.constants.S_IXUSR; // eslint-disable-line no-bitwise } +export async function isFileSymlink(absoluteFilePath) { + const stat = await fsLStat(absoluteFilePath); + return stat.isSymbolicLink(); +} + +export function getRealPath(absoluteFilePath) { + return new Promise((resolve, reject) => { + fs.realpath(absoluteFilePath, (err, resolvedPath) => { + if (err) { reject(err); } else { resolve(resolvedPath); } + }); + }); +} + export function mkdirs(directory) { return new Promise((resolve, reject) => { fs.mkdirs(directory, err => (err ? reject(err) : resolve())); From 3791609b2af20f16d6f9aa96099641d9fffd9949 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 5 Jan 2018 20:32:12 -0800 Subject: [PATCH 0133/5882] Add symlink mode to GSOS#getFileMode --- lib/git-shell-out-strategy.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/git-shell-out-strategy.js b/lib/git-shell-out-strategy.js index bf68f2e775..dda57b90b8 100644 --- a/lib/git-shell-out-strategy.js +++ b/lib/git-shell-out-strategy.js @@ -768,7 +768,14 @@ export default class GitShellOutStrategy { return output.slice(0, 6); } else { const executable = await isFileExecutable(path.join(this.workingDir, filePath)); - return executable ? '100755' : '100644'; + const symlink = await isFileSymlink(path.join(this.workingDir, filePath)); + if (executable) { + return '100755'; + } else if (symlink) { + return '120000'; + } else { + return '100644'; + } } } From 6551df3aa698332e0a2594a1bb0f2cdb9bd004bb Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 5 Jan 2018 20:33:21 -0800 Subject: [PATCH 0134/5882] Add FilePatch#didChangeExecutableMode and FilePatch#didChangeSymlinkMode --- lib/models/file-patch.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/lib/models/file-patch.js b/lib/models/file-patch.js index 32fce36dbb..dadc8cc59c 100644 --- a/lib/models/file-patch.js +++ b/lib/models/file-patch.js @@ -100,14 +100,21 @@ export default class FilePatch { return this.getNewFile().getMode(); } - didModeChange() { - // Not a mode change if it's a creation or deletion + didChangeExecutableMode() { const oldMode = this.getOldMode(); const newMode = this.getNewMode(); - return oldMode && newMode && oldMode !== newMode; + return oldMode === '100755' && newMode !== '100755' || + oldMode !== '100755' && newMode === '100755'; } - getSymlink() { + didChangeSymlinkMode() { + const oldMode = this.getOldMode(); + const newMode = this.getNewMode(); + return oldMode === '120000' && newMode !== '120000' || + oldMode !== '120000' && newMode === '120000'; + } + + hasSymlink() { return this.getOldFile().getSymlink() || this.getNewFile().getSymlink(); } From 704ab398dfe7d453122a450e64236d00c595dccb Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 5 Jan 2018 20:36:16 -0800 Subject: [PATCH 0135/5882] Render correct message for executable mode change & symlink change --- lib/controllers/file-patch-controller.js | 10 ++++-- lib/views/file-patch-view.js | 44 +++++++++++++++++------- 2 files changed, 38 insertions(+), 16 deletions(-) diff --git a/lib/controllers/file-patch-controller.js b/lib/controllers/file-patch-controller.js index 8d9eaea552..29bc7942a9 100644 --- a/lib/controllers/file-patch-controller.js +++ b/lib/controllers/file-patch-controller.js @@ -168,8 +168,11 @@ export default class FilePatchController extends React.Component { render() { const fp = this.state.filePatch; const hunks = fp ? fp.getHunks() : []; - const modeChange = fp && fp.didModeChange() ? - {oldMode: fp.getOldMode(), newMode: fp.getNewMode(), symlink: fp.getSymlink()} : + const executableModeChange = fp && fp.didChangeExecutableMode() ? + {oldMode: fp.getOldMode(), newMode: fp.getNewMode()} : + null; + const symlinkChange = fp && fp.hasSymlink() ? + {oldSymlink: fp.getOldFile().getSymlink(), newSymlink: fp.getNewFile().getSymlink(), status: fp.getStatus()} : null; const repository = this.repositoryObserver.getActiveModel(); if (repository.isUndetermined() || repository.isLoading()) { @@ -199,7 +202,8 @@ export default class FilePatchController extends React.Component { lineCount={this.lineCount} handleShowDiffClick={this.handleShowDiffClick} hunks={hunks} - modeChange={modeChange} + executableModeChange={executableModeChange} + symlinkChange={symlinkChange} filePath={this.props.filePath} workingDirectoryPath={this.getWorkingDirectory()} stagingStatus={this.state.stagingStatus} diff --git a/lib/views/file-patch-view.js b/lib/views/file-patch-view.js index 6a1ec807b2..d96e2d1ecd 100644 --- a/lib/views/file-patch-view.js +++ b/lib/views/file-patch-view.js @@ -163,7 +163,8 @@ export default class FilePatchView extends React.Component {
- {this.renderModeChange(unstaged)} + {this.renderExecutableModeChange(unstaged)} + {this.renderSymlinkChange(unstaged)} {this.props.displayLargeDiffMessage ? this.renderLargeDiffMessage() : this.renderHunks()}
@@ -260,17 +261,34 @@ export default class FilePatchView extends React.Component { } @autobind - renderModeChange(unstaged) { - const {modeChange} = this.props; - if (!modeChange) { return null; } - const symlinkDeleted = modeChange.oldMode === '120000'; - const symlinkAdded = modeChange.newMode === '120000'; + renderExecutableModeChange(unstaged) { + const {executableModeChange} = this.props; + if (!executableModeChange) { return null; } + return ( +
+ + File changed mode from {executableModeChange.oldMode} to {executableModeChange.newMode} + + +
+ ); + } - if (symlinkDeleted) { + @autobind + renderSymlinkChange(unstaged) { + const {symlinkChange} = this.props; + if (!symlinkChange) { return null; } + const {oldSymlink, newSymlink} = symlinkChange; + + if (oldSymlink && !newSymlink) { return (
- Symlink to {modeChange.symlink} deleted and replaced with the following contents: + Symlink to {oldSymlink} deleted.
); - } else if (symlinkAdded) { + } else if (!oldSymlink && newSymlink) { return (
- File contents deleted and replaced with symlink to {modeChange.symlink} + Symlink to {newSymlink} created.
); - } else { + } else if (oldSymlink && newSymlink) { return (
- File changed mode from {modeChange.oldMode} to {modeChange.newMode} + Symlink changed from {oldSymlink} to {newSymlink}.
); From 9bd174282448e88576e12ecbb9957767d48067e9 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 5 Jan 2018 20:36:57 -0800 Subject: [PATCH 0136/5882] Only render hunks for symlink change if file contents changed as well --- lib/views/file-patch-view.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/views/file-patch-view.js b/lib/views/file-patch-view.js index d96e2d1ecd..de75763a32 100644 --- a/lib/views/file-patch-view.js +++ b/lib/views/file-patch-view.js @@ -96,6 +96,10 @@ export default class FilePatchView extends React.Component { } renderHunks() { + // Render hunks for symlink change only if 'typechange' (which indicates symlink change AND file content change) + const {symlinkChange} = this.props; + if (symlinkChange && symlinkChange.status !== 'typechange') { return null; } + const selectedHunks = this.state.selection.getSelectedHunks(); const selectedLines = this.state.selection.getSelectedLines(); const headHunk = this.state.selection.getHeadHunk(); From 4c82bf1c403002efce1e75aa939478924824dad0 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 5 Jan 2018 20:46:55 -0800 Subject: [PATCH 0137/5882] :shirt: --- lib/views/file-patch-view.js | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/lib/views/file-patch-view.js b/lib/views/file-patch-view.js index de75763a32..df6a22f572 100644 --- a/lib/views/file-patch-view.js +++ b/lib/views/file-patch-view.js @@ -18,7 +18,15 @@ export default class FilePatchView extends React.Component { tooltips: PropTypes.object.isRequired, filePath: PropTypes.string.isRequired, hunks: PropTypes.arrayOf(PropTypes.object).isRequired, - modeChange: PropTypes.shape({oldMode: PropTypes.string.isRequired, newMode: PropTypes.string.isRequired}), + executableModeChange: PropTypes.shape({ + oldMode: PropTypes.string.isRequired, + newMode: PropTypes.string.isRequired, + }), + symlinkChange: PropTypes.shape({ + oldSymlink: PropTypes.string, + newSymlink: PropTypes.string, + status: PropTypes.string.isRequired, + }), stagingStatus: PropTypes.oneOf(['unstaged', 'staged']).isRequired, isPartiallyStaged: PropTypes.bool.isRequired, hasUndoHistory: PropTypes.bool.isRequired, @@ -204,7 +212,7 @@ export default class FilePatchView extends React.Component { command="core:undo" callback={() => this.props.hasUndoHistory && this.props.undoLastDiscard()} /> - {this.props.modeChange && + {(this.props.executableModeChange || this.props.symlinkChange) && - File changed mode from {executableModeChange.oldMode} to {executableModeChange.newMode} + File changed mode from {executableModeChange.oldMode} + to {executableModeChange.newMode}
); + } else { + return new Error('Symlink change detected, but missing symlink paths'); } } From 89969a11dab55d034ce0816eeed74e8aab5eed88 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 5 Jan 2018 20:53:07 -0800 Subject: [PATCH 0138/5882] Fix borked test --- test/git-strategies.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/git-strategies.test.js b/test/git-strategies.test.js index 02f6a6596d..b6dddead71 100644 --- a/test/git-strategies.test.js +++ b/test/git-strategies.test.js @@ -209,7 +209,7 @@ import {fsStat, normalizeGitHelperPath, writeFile, getTempDir} from '../lib/help fs.writeFileSync(path.join(workingDirPath, 'a.txt'), 'qux\nfoo\nbar\n', 'utf8'); process.env.GIT_EXTERNAL_DIFF = 'bogus_app_name'; - const diffOutput = await git.getDiffForFilePath('a.txt'); + const diffOutput = await git.getDiffsForFilePath('a.txt'); delete process.env.GIT_EXTERNAL_DIFF; assert.isDefined(diffOutput); From cc71d2ec10efb4c1d37639e57d180b296eb0e0d7 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 5 Jan 2018 21:49:23 -0800 Subject: [PATCH 0139/5882] Don't register github:stage-or-unstage-file-mode-change for symlink change --- 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 df6a22f572..2aabae5439 100644 --- a/lib/views/file-patch-view.js +++ b/lib/views/file-patch-view.js @@ -212,7 +212,7 @@ export default class FilePatchView extends React.Component { command="core:undo" callback={() => this.props.hasUndoHistory && this.props.undoLastDiscard()} /> - {(this.props.executableModeChange || this.props.symlinkChange) && + {this.props.executableModeChange && Date: Fri, 5 Jan 2018 22:57:43 -0800 Subject: [PATCH 0140/5882] WIP --- lib/controllers/file-patch-controller.js | 40 ++++++++++++++++++++++++ lib/git-shell-out-strategy.js | 1 + lib/github-package.js | 4 +++ lib/models/file-patch.js | 2 +- lib/views/file-patch-view.js | 11 +++++-- 5 files changed, 55 insertions(+), 3 deletions(-) diff --git a/lib/controllers/file-patch-controller.js b/lib/controllers/file-patch-controller.js index 29bc7942a9..3461dfa9aa 100644 --- a/lib/controllers/file-patch-controller.js +++ b/lib/controllers/file-patch-controller.js @@ -212,6 +212,7 @@ export default class FilePatchController extends React.Component { attemptHunkStageOperation={this.attemptHunkStageOperation} attemptFileStageOperation={this.attemptFileStageOperation} attemptModeStageOperation={this.attemptModeStageOperation} + attemptSymlinkStageOperation={this.attemptSymlinkStageOperation} didSurfaceFile={this.didSurfaceFile} didDiveIntoCorrespondingFilePatch={this.diveIntoCorrespondingFilePatch} switchboard={this.props.switchboard} @@ -339,6 +340,31 @@ export default class FilePatchController extends React.Component { } } + async stageSymlinkChange() { + this.props.switchboard.didBeginStageOperation({stage: true, symlink: true}); + debugger; + await this.repositoryObserver.getActiveModel().stageFiles([this.props.filePath]); + this.props.switchboard.didFinishStageOperation({stage: true, symlink: true}); + } + + async unstageSymlinkChange() { + this.props.switchboard.didBeginStageOperation({unstage: true, symlink: true}); + + await this.repositoryObserver.getActiveModel().unstageFiles([this.props.filePath]); + this.props.switchboard.didFinishStageOperation({unstage: true, symlink: true}); + } + + stageOrUnstageSymlinkChange() { + const stagingStatus = this.state.stagingStatus; + if (stagingStatus === 'unstaged') { + return this.stageSymlinkChange(); + } else if (stagingStatus === 'staged') { + return this.unstageSymlinkChange(); + } else { + throw new Error(`Unknown stagingStatus: ${stagingStatus}`); + } + } + @autobind attemptHunkStageOperation(hunk) { if (this.stagingOperationInProgress) { @@ -381,6 +407,20 @@ export default class FilePatchController extends React.Component { this.stageOrUnstageModeChange(); } + @autobind + attemptSymlinkStageOperation() { + if (this.stagingOperationInProgress) { + return; + } + + this.stagingOperationInProgress = true; + this.props.switchboard.getChangePatchPromise().then(() => { + this.stagingOperationInProgress = false; + }); + + this.stageOrUnstageSymlinkChange(); + } + async stageLines(lines) { this.props.switchboard.didBeginStageOperation({stage: true, line: true}); diff --git a/lib/git-shell-out-strategy.js b/lib/git-shell-out-strategy.js index dda57b90b8..28154a8cf2 100644 --- a/lib/git-shell-out-strategy.js +++ b/lib/git-shell-out-strategy.js @@ -369,6 +369,7 @@ export default class GitShellOutStrategy { stageFiles(paths) { if (paths.length === 0) { return Promise.resolve(null); } const args = ['add'].concat(paths.map(toGitPathSep)); + console.warn(args); return this.exec(args, {writeOperation: true}); } diff --git a/lib/github-package.js b/lib/github-package.js index 19322e6d66..73ef2a558a 100644 --- a/lib/github-package.js +++ b/lib/github-package.js @@ -100,6 +100,8 @@ export default class GithubPackage { yardstick.begin('stageFile'); } else if (payload.stage && payload.mode) { yardstick.begin('stageMode'); + } else if (payload.stage && payload.symlink) { + yardstick.begin('stageSymlink'); } else if (payload.unstage && payload.line) { yardstick.begin('unstageLine'); } else if (payload.unstage && payload.hunk) { @@ -108,6 +110,8 @@ export default class GithubPackage { yardstick.begin('unstageFile'); } else if (payload.unstage && payload.mode) { yardstick.begin('unstageMode'); + } else if (payload.unstage && payload.symlink) { + yardstick.begin('unstageSymlink'); } }), this.switchboard.onDidUpdateRepository(() => { diff --git a/lib/models/file-patch.js b/lib/models/file-patch.js index dadc8cc59c..c6817a0300 100644 --- a/lib/models/file-patch.js +++ b/lib/models/file-patch.js @@ -189,7 +189,7 @@ export default class FilePatch { invertedStatus = 'added'; break; default: - throw new Error(`Unknown Status: ${this.getStatus()}`); + // throw new Error(`Unknown Status: ${this.getStatus()}`); } const invertedHunks = this.getHunks().map(h => h.invert()); return this.clone({ diff --git a/lib/views/file-patch-view.js b/lib/views/file-patch-view.js index 2aabae5439..c8b03b3fd3 100644 --- a/lib/views/file-patch-view.js +++ b/lib/views/file-patch-view.js @@ -34,6 +34,7 @@ export default class FilePatchView extends React.Component { attemptHunkStageOperation: PropTypes.func.isRequired, attemptFileStageOperation: PropTypes.func.isRequired, attemptModeStageOperation: PropTypes.func.isRequired, + attemptSymlinkStageOperation: PropTypes.func.isRequired, discardLines: PropTypes.func.isRequired, undoLastDiscard: PropTypes.func.isRequired, openCurrentFile: PropTypes.func.isRequired, @@ -305,7 +306,7 @@ export default class FilePatchView extends React.Component {
@@ -318,7 +319,7 @@ export default class FilePatchView extends React.Component {
@@ -616,6 +617,12 @@ export default class FilePatchView extends React.Component { this.props.attemptModeStageOperation(); } + @autobind + stageOrUnstageSymlinkChange() { + debugger; + this.props.attemptSymlinkStageOperation(); + } + @autobind discardSelection() { const selectedLines = this.state.selection.getSelectedLines(); From ee1879de382a93ddd80dbf4c20cae83c6c7043de Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Tue, 9 Jan 2018 12:42:19 -0800 Subject: [PATCH 0141/5882] Correctly handle staging/unstaging symlink change when changed lines present --- lib/controllers/file-patch-controller.js | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/lib/controllers/file-patch-controller.js b/lib/controllers/file-patch-controller.js index 3461dfa9aa..e5a6b18abe 100644 --- a/lib/controllers/file-patch-controller.js +++ b/lib/controllers/file-patch-controller.js @@ -342,15 +342,28 @@ export default class FilePatchController extends React.Component { async stageSymlinkChange() { this.props.switchboard.didBeginStageOperation({stage: true, symlink: true}); - debugger; - await this.repositoryObserver.getActiveModel().stageFiles([this.props.filePath]); + + const filePatch = this.state.filePatch; + const addedLines = filePatch && filePatch.getHunks() && filePatch.getHunks()[0].getLines()[0].getStatus() === 'added'; + if (filePatch.getStatus() === 'typechange' && addedLines) { + await this.repositoryObserver.getActiveModel().git.exec(['rm', '--cached', this.props.filePath]); + } else { + await this.repositoryObserver.getActiveModel().stageFiles([this.props.filePath]); + } this.props.switchboard.didFinishStageOperation({stage: true, symlink: true}); } async unstageSymlinkChange() { this.props.switchboard.didBeginStageOperation({unstage: true, symlink: true}); - await this.repositoryObserver.getActiveModel().unstageFiles([this.props.filePath]); + const filePatch = this.state.filePatch; + const deletedLines = filePatch && filePatch.getHunks() && + filePatch.getHunks()[0].getLines()[0].getStatus() === 'deleted'; + if (filePatch.getStatus() === 'typechange' && deletedLines) { + await this.repositoryObserver.getActiveModel().git.exec(['rm', '--cached', this.props.filePath]); + } else { + await this.repositoryObserver.getActiveModel().unstageFiles([this.props.filePath]); + } this.props.switchboard.didFinishStageOperation({unstage: true, symlink: true}); } From 53f7893f650dfe21eff603bf75656c1f35fb2ec9 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Tue, 9 Jan 2018 12:42:59 -0800 Subject: [PATCH 0142/5882] :fire: debugger --- lib/views/file-patch-view.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/views/file-patch-view.js b/lib/views/file-patch-view.js index c8b03b3fd3..ad271e9d4f 100644 --- a/lib/views/file-patch-view.js +++ b/lib/views/file-patch-view.js @@ -619,7 +619,6 @@ export default class FilePatchView extends React.Component { @autobind stageOrUnstageSymlinkChange() { - debugger; this.props.attemptSymlinkStageOperation(); } From 0c28b3d9bf40ed40fea2469ad80629f5af0c3511 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Tue, 9 Jan 2018 12:48:52 -0800 Subject: [PATCH 0143/5882] Oops. Refer to correct method when staging symlink change --- 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 ad271e9d4f..92486ae643 100644 --- a/lib/views/file-patch-view.js +++ b/lib/views/file-patch-view.js @@ -332,7 +332,7 @@ export default class FilePatchView extends React.Component {
From 6ca5d848bc6daa3c4bdad87565d9d4068fe5ab97 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Tue, 9 Jan 2018 12:49:42 -0800 Subject: [PATCH 0144/5882] :art: Refactor render methods in FilePatchView --- lib/views/file-patch-view.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/views/file-patch-view.js b/lib/views/file-patch-view.js index 92486ae643..8afa242178 100644 --- a/lib/views/file-patch-view.js +++ b/lib/views/file-patch-view.js @@ -176,8 +176,8 @@ export default class FilePatchView extends React.Component {
- {this.renderExecutableModeChange(unstaged)} - {this.renderSymlinkChange(unstaged)} + {this.props.executableModeChange && this.renderExecutableModeChange(unstaged)} + {this.props.symlinkChange && this.renderSymlinkChange(unstaged)} {this.props.displayLargeDiffMessage ? this.renderLargeDiffMessage() : this.renderHunks()}
@@ -276,7 +276,6 @@ export default class FilePatchView extends React.Component { @autobind renderExecutableModeChange(unstaged) { const {executableModeChange} = this.props; - if (!executableModeChange) { return null; } return (
@@ -295,7 +294,6 @@ export default class FilePatchView extends React.Component { @autobind renderSymlinkChange(unstaged) { const {symlinkChange} = this.props; - if (!symlinkChange) { return null; } const {oldSymlink, newSymlink} = symlinkChange; if (oldSymlink && !newSymlink) { From 0b159b85da2c77f0c06b54db583ab4f4a29bdfd6 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Tue, 9 Jan 2018 14:47:03 -0800 Subject: [PATCH 0145/5882] Correctly handle staging lines that require symlink change to be staged --- lib/controllers/file-patch-controller.js | 31 +++++++++++++----------- lib/models/file-patch.js | 11 ++++++++- lib/models/repository-states/present.js | 6 +++-- lib/views/file-patch-view.js | 19 ++++++++++++--- 4 files changed, 46 insertions(+), 21 deletions(-) diff --git a/lib/controllers/file-patch-controller.js b/lib/controllers/file-patch-controller.js index e5a6b18abe..022346a1bd 100644 --- a/lib/controllers/file-patch-controller.js +++ b/lib/controllers/file-patch-controller.js @@ -172,8 +172,12 @@ export default class FilePatchController extends React.Component { {oldMode: fp.getOldMode(), newMode: fp.getNewMode()} : null; const symlinkChange = fp && fp.hasSymlink() ? - {oldSymlink: fp.getOldFile().getSymlink(), newSymlink: fp.getNewFile().getSymlink(), status: fp.getStatus()} : - null; + { + oldSymlink: fp.getOldFile().getSymlink(), + newSymlink: fp.getNewFile().getSymlink(), + typechange: fp.hasTypechange(), + filePatchStatus: fp.getStatus(), + } : null; const repository = this.repositoryObserver.getActiveModel(); if (repository.isUndetermined() || repository.isLoading()) { return ( @@ -344,8 +348,7 @@ export default class FilePatchController extends React.Component { this.props.switchboard.didBeginStageOperation({stage: true, symlink: true}); const filePatch = this.state.filePatch; - const addedLines = filePatch && filePatch.getHunks() && filePatch.getHunks()[0].getLines()[0].getStatus() === 'added'; - if (filePatch.getStatus() === 'typechange' && addedLines) { + if (filePatch.hasTypechange() && filePatch.getStatus() === 'added') { await this.repositoryObserver.getActiveModel().git.exec(['rm', '--cached', this.props.filePath]); } else { await this.repositoryObserver.getActiveModel().stageFiles([this.props.filePath]); @@ -357,9 +360,7 @@ export default class FilePatchController extends React.Component { this.props.switchboard.didBeginStageOperation({unstage: true, symlink: true}); const filePatch = this.state.filePatch; - const deletedLines = filePatch && filePatch.getHunks() && - filePatch.getHunks()[0].getLines()[0].getStatus() === 'deleted'; - if (filePatch.getStatus() === 'typechange' && deletedLines) { + if (filePatch.hasTypechange() && filePatch.getStatus() === 'deleted') { await this.repositoryObserver.getActiveModel().git.exec(['rm', '--cached', this.props.filePath]); } else { await this.repositoryObserver.getActiveModel().unstageFiles([this.props.filePath]); @@ -421,17 +422,19 @@ export default class FilePatchController extends React.Component { } @autobind - attemptSymlinkStageOperation() { + attemptSymlinkStageOperation({noSwitchboard} = {}) { if (this.stagingOperationInProgress) { - return; + return null; } - this.stagingOperationInProgress = true; - this.props.switchboard.getChangePatchPromise().then(() => { - this.stagingOperationInProgress = false; - }); + if (!noSwitchboard) { + this.stagingOperationInProgress = true; + this.props.switchboard.getChangePatchPromise().then(() => { + this.stagingOperationInProgress = false; + }); + } - this.stageOrUnstageSymlinkChange(); + return this.stageOrUnstageSymlinkChange(); } async stageLines(lines) { diff --git a/lib/models/file-patch.js b/lib/models/file-patch.js index c6817a0300..1491a704fa 100644 --- a/lib/models/file-patch.js +++ b/lib/models/file-patch.js @@ -30,9 +30,10 @@ class File { } class Patch { - constructor({status, hunks}) { + constructor({status, hunks, typechange}) { this.status = status; this.hunks = hunks; + this.typechange = !!typechange; } getStatus() { @@ -43,6 +44,10 @@ class Patch { return this.hunks; } + hasTypechange() { + return this.typechange; + } + clone(opts = {}) { return new Patch({ status: opts.status !== undefined ? opts.status : this.status, @@ -118,6 +123,10 @@ export default class FilePatch { return this.getOldFile().getSymlink() || this.getNewFile().getSymlink(); } + hasTypechange() { + return this.getPatch().hasTypechange(); + } + getPath() { return this.getOldPath() || this.getNewPath(); } diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js index 28d440ab2a..89a8ad8a46 100644 --- a/lib/models/repository-states/present.js +++ b/lib/models/repository-states/present.js @@ -770,17 +770,19 @@ function buildFilePatchFromDualDiffs(diff1, diff2) { const hunks = buildHunksFromDiff(contentChangeDiff); const filePath = contentChangeDiff.oldPath || contentChangeDiff.newPath; const symlink = modeChangeDiff.hunks[0].lines[0].slice(1); - let oldFile, newFile; + let oldFile, newFile, status; if (modeChangeDiff.status === 'added') { oldFile = new FilePatch.File({path: filePath, mode: contentChangeDiff.oldMode, symlink: null}); newFile = new FilePatch.File({path: filePath, mode: modeChangeDiff.newMode, symlink}); + status = 'deleted'; // contents were deleted and replaced with symlink } else if (modeChangeDiff.status === 'deleted') { oldFile = new FilePatch.File({path: filePath, mode: modeChangeDiff.oldMode, symlink}); newFile = new FilePatch.File({path: filePath, mode: contentChangeDiff.newMode, symlink: null}); + status = 'added'; // contents were added after symlink was deleted } else { throw new Error(`Invalid mode change diff status: ${modeChangeDiff.status}`); } - const patch = new FilePatch.Patch({status: 'typechange', hunks}); + const patch = new FilePatch.Patch({status, hunks, typechange: true}); return new FilePatch(oldFile, newFile, patch); } diff --git a/lib/views/file-patch-view.js b/lib/views/file-patch-view.js index 8afa242178..5a5fc83ce0 100644 --- a/lib/views/file-patch-view.js +++ b/lib/views/file-patch-view.js @@ -107,7 +107,7 @@ export default class FilePatchView extends React.Component { renderHunks() { // Render hunks for symlink change only if 'typechange' (which indicates symlink change AND file content change) const {symlinkChange} = this.props; - if (symlinkChange && symlinkChange.status !== 'typechange') { return null; } + if (symlinkChange && !symlinkChange.typechange) { return null; } const selectedHunks = this.state.selection.getSelectedHunks(); const selectedLines = this.state.selection.getSelectedLines(); @@ -555,7 +555,18 @@ export default class FilePatchView extends React.Component { return this.state.selection.getNextUpdatePromise(); } - didClickStageButtonForHunk(hunk) { + async didClickStageButtonForHunk(hunk) { + // For the case when there is a symlink change AND added/deleted file, we must first stage/unstage the symlink + // before staging/unstaging the lines + const {symlinkChange, stagingStatus} = this.props; + const unstagedSymlinkChangeWithAddedLines = symlinkChange && symlinkChange.filePatchStatus === 'added' && + stagingStatus === 'unstaged'; + const stagedSymlinkChangeWithDeletedLines = symlinkChange && symlinkChange.filePatchStatus === 'deleted' && + stagingStatus === 'staged'; + if (unstagedSymlinkChangeWithAddedLines || stagedSymlinkChangeWithDeletedLines) { + await this.stageOrUnstageSymlinkChange({noSwitchboard: true}); + } + if (this.state.selection.getSelectedHunks().has(hunk)) { this.props.attemptLineStageOperation(this.state.selection.getSelectedLines()); } else { @@ -616,8 +627,8 @@ export default class FilePatchView extends React.Component { } @autobind - stageOrUnstageSymlinkChange() { - this.props.attemptSymlinkStageOperation(); + stageOrUnstageSymlinkChange(options) { + return this.props.attemptSymlinkStageOperation(options); } @autobind From 34bebfc35458ace1023be6aca2eb1c173c5b9727 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Tue, 9 Jan 2018 16:49:18 -0800 Subject: [PATCH 0146/5882] Handle file deletion/creation for typechanges when staging last line --- lib/models/file-patch.js | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/lib/models/file-patch.js b/lib/models/file-patch.js index 1491a704fa..697d91015f 100644 --- a/lib/models/file-patch.js +++ b/lib/models/file-patch.js @@ -145,7 +145,15 @@ export default class FilePatch { getStagePatchForLines(selectedLines) { if (this.changedLineCount === [...selectedLines].filter(line => line.isChanged()).length) { - return this; + 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: new File({path: null, mode: null, symlink: null}), + }); + } else { + return this; + } } let delta = 0; @@ -217,7 +225,16 @@ export default class FilePatch { getUnstagePatchForLines(selectedLines) { if (this.changedLineCount === [...selectedLines].filter(line => line.isChanged()).length) { - return this.getUnstagePatch(); + if (this.hasTypechange() && this.getStatus() === 'added') { + // handle special case when a file was created after a symlink was deleted. + // In order to unstage the file creation, we must ensure that the unstage patch has no new file, + // so when the patch is applied to the index, there file will be removed from the index + return this.getUnstagePatch().clone({ + newFile: new File({path: null, mode: null, symlink: null}), + }); + } else { + return this.getUnstagePatch(); + } } let delta = 0; From e56126ff8fa8bd2f4af36c613b58b48c33d2a3f3 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Tue, 9 Jan 2018 16:52:45 -0800 Subject: [PATCH 0147/5882] Add command to stage/unstage symlink change --- lib/views/file-patch-view.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/views/file-patch-view.js b/lib/views/file-patch-view.js index 5a5fc83ce0..54568ca881 100644 --- a/lib/views/file-patch-view.js +++ b/lib/views/file-patch-view.js @@ -218,6 +218,11 @@ export default class FilePatchView extends React.Component { command="github:stage-or-unstage-file-mode-change" callback={this.stageOrUnstageModeChange} />} + {this.props.symlinkChange && + } From 2b31a8e191aba2616244c9069420408dcded0b46 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Tue, 9 Jan 2018 17:04:48 -0800 Subject: [PATCH 0148/5882] Clear out symlinks for file patch used to render empty diff after repo update --- 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 022346a1bd..10488fb80d 100644 --- a/lib/controllers/file-patch-controller.js +++ b/lib/controllers/file-patch-controller.js @@ -136,9 +136,9 @@ export default class FilePatchController extends React.Component { const oldFilePatch = this.state.filePatch; if (oldFilePatch) { filePatch = oldFilePatch.clone({ - oldFile: oldFilePatch.oldFile.clone({mode: null}), - newFile: oldFilePatch.newFile.clone({mode: null}), - patch: oldFilePatch.getPatch().clone({hunks: []}), + oldFile: oldFilePatch.oldFile.clone({mode: null, symlink: null}), + newFile: oldFilePatch.newFile.clone({mode: null, symlink: null}), + patch: oldFilePatch.getPatch().clone({hunks: [], typechange: null}), }); if (!this.destroyed) { this.setState({filePatch, isPartiallyStaged}); } } From 3111ae1abccc4d385597c05517631dac1d6608cf Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Tue, 9 Jan 2018 17:05:52 -0800 Subject: [PATCH 0149/5882] :fire: console statement --- lib/git-shell-out-strategy.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/git-shell-out-strategy.js b/lib/git-shell-out-strategy.js index 28154a8cf2..dda57b90b8 100644 --- a/lib/git-shell-out-strategy.js +++ b/lib/git-shell-out-strategy.js @@ -369,7 +369,6 @@ export default class GitShellOutStrategy { stageFiles(paths) { if (paths.length === 0) { return Promise.resolve(null); } const args = ['add'].concat(paths.map(toGitPathSep)); - console.warn(args); return this.exec(args, {writeOperation: true}); } From b884da4035d2891326c9e5d52aa910a8ef9c64fb Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Tue, 9 Jan 2018 17:12:56 -0800 Subject: [PATCH 0150/5882] Add props to get rid of errors in console for tests --- test/views/file-patch-view.test.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/test/views/file-patch-view.test.js b/test/views/file-patch-view.test.js index d13c23aa99..4292216d0e 100644 --- a/test/views/file-patch-view.test.js +++ b/test/views/file-patch-view.test.js @@ -9,7 +9,8 @@ import {assertEqualSets} from '../helpers'; describe('FilePatchView', function() { let atomEnv, commandRegistry, tooltips, component; - let attemptLineStageOperation, attemptHunkStageOperation, discardLines, undoLastDiscard, openCurrentFile; + let attemptLineStageOperation, attemptHunkStageOperation, attemptFileStageOperation, attemptSymlinkStageOperation; + let attemptModeStageOperation, discardLines, undoLastDiscard, openCurrentFile; let didSurfaceFile, didDiveIntoCorrespondingFilePatch, handleShowDiffClick; beforeEach(function() { @@ -19,6 +20,9 @@ describe('FilePatchView', function() { attemptLineStageOperation = sinon.spy(); attemptHunkStageOperation = sinon.spy(); + attemptModeStageOperation = sinon.spy(); + attemptFileStageOperation = sinon.spy(); + attemptSymlinkStageOperation = sinon.spy(); discardLines = sinon.spy(); undoLastDiscard = sinon.spy(); openCurrentFile = sinon.spy(); @@ -36,7 +40,10 @@ describe('FilePatchView', function() { isPartiallyStaged={false} hasUndoHistory={false} attemptLineStageOperation={attemptLineStageOperation} + attemptFileStageOperation={attemptFileStageOperation} + attemptModeStageOperation={attemptModeStageOperation} attemptHunkStageOperation={attemptHunkStageOperation} + attemptSymlinkStageOperation={attemptSymlinkStageOperation} discardLines={discardLines} undoLastDiscard={undoLastDiscard} openCurrentFile={openCurrentFile} From e10bc9981eff172a4698494bbfa8cdf2f530c940 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Tue, 9 Jan 2018 17:55:31 -0800 Subject: [PATCH 0151/5882] Implement GSOS#stageFileSymlinkChange --- lib/controllers/file-patch-controller.js | 4 ++-- lib/git-shell-out-strategy.js | 4 ++++ lib/models/repository-states/present.js | 5 +++++ lib/models/repository.js | 1 + 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/controllers/file-patch-controller.js b/lib/controllers/file-patch-controller.js index 10488fb80d..1c910f7e1b 100644 --- a/lib/controllers/file-patch-controller.js +++ b/lib/controllers/file-patch-controller.js @@ -349,7 +349,7 @@ export default class FilePatchController extends React.Component { const filePatch = this.state.filePatch; if (filePatch.hasTypechange() && filePatch.getStatus() === 'added') { - await this.repositoryObserver.getActiveModel().git.exec(['rm', '--cached', this.props.filePath]); + await this.repositoryObserver.getActiveModel().stageFileSymlinkChange(this.props.filePath); } else { await this.repositoryObserver.getActiveModel().stageFiles([this.props.filePath]); } @@ -361,7 +361,7 @@ export default class FilePatchController extends React.Component { const filePatch = this.state.filePatch; if (filePatch.hasTypechange() && filePatch.getStatus() === 'deleted') { - await this.repositoryObserver.getActiveModel().git.exec(['rm', '--cached', this.props.filePath]); + await this.repositoryObserver.getActiveModel().stageFileSymlinkChange(this.props.filePath); } else { await this.repositoryObserver.getActiveModel().unstageFiles([this.props.filePath]); } diff --git a/lib/git-shell-out-strategy.js b/lib/git-shell-out-strategy.js index dda57b90b8..c24835251a 100644 --- a/lib/git-shell-out-strategy.js +++ b/lib/git-shell-out-strategy.js @@ -393,6 +393,10 @@ export default class GitShellOutStrategy { }); } + stageFileSymlinkChange(filename) { + return this.exec(['rm', '--cached', filename], {writeOperation: true}); + } + applyPatch(patch, {index} = {}) { const args = ['apply', '-']; if (index) { args.splice(1, 0, '--cached'); } diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js index 89a8ad8a46..a834c7c249 100644 --- a/lib/models/repository-states/present.js +++ b/lib/models/repository-states/present.js @@ -228,6 +228,11 @@ export default class Present extends State { return this.git().stageFileModeChange(filePath, fileMode); } + @invalidate(filePath => Keys.cacheOperationKeys([filePath])) + stageFileSymlinkChange(filePath) { + return this.git().stageFileSymlinkChange(filePath); + } + @invalidate(filePatch => Keys.cacheOperationKeys([filePatch.getOldPath(), filePatch.getNewPath()])) applyPatchToIndex(filePatch) { const patchStr = filePatch.getHeaderString() + filePatch.toString(); diff --git a/lib/models/repository.js b/lib/models/repository.js index 9a5e6399f4..630ab82f57 100644 --- a/lib/models/repository.js +++ b/lib/models/repository.js @@ -251,6 +251,7 @@ const delegates = [ 'unstageFiles', 'stageFilesFromParentCommit', 'stageFileModeChange', + 'stageFileSymlinkChange', 'applyPatchToIndex', 'applyPatchToWorkdir', From af459428596eadd07c5561ff48289d619687d68e Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Tue, 9 Jan 2018 23:39:26 -0800 Subject: [PATCH 0152/5882] Add test for staging symlink changes without staging file contents --- test/fixtures/repo-symlinks/a.txt | 4 + .../repo-symlinks/dot-git/COMMIT_EDITMSG | 1 + test/fixtures/repo-symlinks/dot-git/HEAD | 1 + test/fixtures/repo-symlinks/dot-git/config | 7 + .../repo-symlinks/dot-git/description | 1 + .../dot-git/hooks/applypatch-msg.sample | 15 ++ .../dot-git/hooks/commit-msg.sample | 24 +++ .../dot-git/hooks/post-update.sample | 8 + .../dot-git/hooks/pre-applypatch.sample | 14 ++ .../dot-git/hooks/pre-commit.sample | 49 +++++ .../dot-git/hooks/pre-push.sample | 53 ++++++ .../dot-git/hooks/pre-rebase.sample | 169 ++++++++++++++++++ .../dot-git/hooks/pre-receive.sample | 24 +++ .../dot-git/hooks/prepare-commit-msg.sample | 36 ++++ .../repo-symlinks/dot-git/hooks/update.sample | 128 +++++++++++++ test/fixtures/repo-symlinks/dot-git/index | Bin 0 -> 297 bytes .../repo-symlinks/dot-git/info/exclude | 6 + test/fixtures/repo-symlinks/dot-git/logs/HEAD | 1 + .../dot-git/logs/refs/heads/master | 1 + .../2c/d053449a642aa9757d0d7d4c7dfffa0fcb98fa | Bin 0 -> 34 bytes .../47/1a7b87278fd8d1ab0f5eac2eba0f4b04bcff9e | Bin 0 -> 27 bytes .../9d/4352700bb7a3dedbd7e8f2a72c4005fa109530 | Bin 0 -> 21 bytes .../be/6f0daf2888b24155a4b81fdfdfd62ec3f75672 | Bin 0 -> 124 bytes .../c9/dbab82b91c53e70a872a4f1f793c275790abc8 | 2 + .../repo-symlinks/dot-git/refs/heads/master | 1 + test/fixtures/repo-symlinks/regular-file.txt | 1 + test/fixtures/repo-symlinks/symlink.txt | 1 + test/models/repository.test.js | 41 +++++ 28 files changed, 588 insertions(+) create mode 100644 test/fixtures/repo-symlinks/a.txt create mode 100644 test/fixtures/repo-symlinks/dot-git/COMMIT_EDITMSG create mode 100644 test/fixtures/repo-symlinks/dot-git/HEAD create mode 100644 test/fixtures/repo-symlinks/dot-git/config create mode 100644 test/fixtures/repo-symlinks/dot-git/description create mode 100755 test/fixtures/repo-symlinks/dot-git/hooks/applypatch-msg.sample create mode 100755 test/fixtures/repo-symlinks/dot-git/hooks/commit-msg.sample create mode 100755 test/fixtures/repo-symlinks/dot-git/hooks/post-update.sample create mode 100755 test/fixtures/repo-symlinks/dot-git/hooks/pre-applypatch.sample create mode 100755 test/fixtures/repo-symlinks/dot-git/hooks/pre-commit.sample create mode 100755 test/fixtures/repo-symlinks/dot-git/hooks/pre-push.sample create mode 100755 test/fixtures/repo-symlinks/dot-git/hooks/pre-rebase.sample create mode 100755 test/fixtures/repo-symlinks/dot-git/hooks/pre-receive.sample create mode 100755 test/fixtures/repo-symlinks/dot-git/hooks/prepare-commit-msg.sample create mode 100755 test/fixtures/repo-symlinks/dot-git/hooks/update.sample create mode 100644 test/fixtures/repo-symlinks/dot-git/index create mode 100644 test/fixtures/repo-symlinks/dot-git/info/exclude create mode 100644 test/fixtures/repo-symlinks/dot-git/logs/HEAD create mode 100644 test/fixtures/repo-symlinks/dot-git/logs/refs/heads/master create mode 100644 test/fixtures/repo-symlinks/dot-git/objects/2c/d053449a642aa9757d0d7d4c7dfffa0fcb98fa create mode 100644 test/fixtures/repo-symlinks/dot-git/objects/47/1a7b87278fd8d1ab0f5eac2eba0f4b04bcff9e create mode 100644 test/fixtures/repo-symlinks/dot-git/objects/9d/4352700bb7a3dedbd7e8f2a72c4005fa109530 create mode 100644 test/fixtures/repo-symlinks/dot-git/objects/be/6f0daf2888b24155a4b81fdfdfd62ec3f75672 create mode 100644 test/fixtures/repo-symlinks/dot-git/objects/c9/dbab82b91c53e70a872a4f1f793c275790abc8 create mode 100644 test/fixtures/repo-symlinks/dot-git/refs/heads/master create mode 100644 test/fixtures/repo-symlinks/regular-file.txt create mode 120000 test/fixtures/repo-symlinks/symlink.txt diff --git a/test/fixtures/repo-symlinks/a.txt b/test/fixtures/repo-symlinks/a.txt new file mode 100644 index 0000000000..471a7b8727 --- /dev/null +++ b/test/fixtures/repo-symlinks/a.txt @@ -0,0 +1,4 @@ +foo +bar +baz + diff --git a/test/fixtures/repo-symlinks/dot-git/COMMIT_EDITMSG b/test/fixtures/repo-symlinks/dot-git/COMMIT_EDITMSG new file mode 100644 index 0000000000..5852f44639 --- /dev/null +++ b/test/fixtures/repo-symlinks/dot-git/COMMIT_EDITMSG @@ -0,0 +1 @@ +Initial commit diff --git a/test/fixtures/repo-symlinks/dot-git/HEAD b/test/fixtures/repo-symlinks/dot-git/HEAD new file mode 100644 index 0000000000..cb089cd89a --- /dev/null +++ b/test/fixtures/repo-symlinks/dot-git/HEAD @@ -0,0 +1 @@ +ref: refs/heads/master diff --git a/test/fixtures/repo-symlinks/dot-git/config b/test/fixtures/repo-symlinks/dot-git/config new file mode 100644 index 0000000000..6c9406b7d9 --- /dev/null +++ b/test/fixtures/repo-symlinks/dot-git/config @@ -0,0 +1,7 @@ +[core] + repositoryformatversion = 0 + filemode = true + bare = false + logallrefupdates = true + ignorecase = true + precomposeunicode = true diff --git a/test/fixtures/repo-symlinks/dot-git/description b/test/fixtures/repo-symlinks/dot-git/description new file mode 100644 index 0000000000..498b267a8c --- /dev/null +++ b/test/fixtures/repo-symlinks/dot-git/description @@ -0,0 +1 @@ +Unnamed repository; edit this file 'description' to name the repository. diff --git a/test/fixtures/repo-symlinks/dot-git/hooks/applypatch-msg.sample b/test/fixtures/repo-symlinks/dot-git/hooks/applypatch-msg.sample new file mode 100755 index 0000000000..a5d7b84a67 --- /dev/null +++ b/test/fixtures/repo-symlinks/dot-git/hooks/applypatch-msg.sample @@ -0,0 +1,15 @@ +#!/bin/sh +# +# An example hook script to check the commit log message taken by +# applypatch from an e-mail message. +# +# The hook should exit with non-zero status after issuing an +# appropriate message if it wants to stop the commit. The hook is +# allowed to edit the commit message file. +# +# To enable this hook, rename this file to "applypatch-msg". + +. git-sh-setup +commitmsg="$(git rev-parse --git-path hooks/commit-msg)" +test -x "$commitmsg" && exec "$commitmsg" ${1+"$@"} +: diff --git a/test/fixtures/repo-symlinks/dot-git/hooks/commit-msg.sample b/test/fixtures/repo-symlinks/dot-git/hooks/commit-msg.sample new file mode 100755 index 0000000000..b58d1184a9 --- /dev/null +++ b/test/fixtures/repo-symlinks/dot-git/hooks/commit-msg.sample @@ -0,0 +1,24 @@ +#!/bin/sh +# +# An example hook script to check the commit log message. +# Called by "git commit" with one argument, the name of the file +# that has the commit message. The hook should exit with non-zero +# status after issuing an appropriate message if it wants to stop the +# commit. The hook is allowed to edit the commit message file. +# +# To enable this hook, rename this file to "commit-msg". + +# Uncomment the below to add a Signed-off-by line to the message. +# Doing this in a hook is a bad idea in general, but the prepare-commit-msg +# hook is more suited to it. +# +# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') +# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1" + +# This example catches duplicate Signed-off-by lines. + +test "" = "$(grep '^Signed-off-by: ' "$1" | + sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || { + echo >&2 Duplicate Signed-off-by lines. + exit 1 +} diff --git a/test/fixtures/repo-symlinks/dot-git/hooks/post-update.sample b/test/fixtures/repo-symlinks/dot-git/hooks/post-update.sample new file mode 100755 index 0000000000..ec17ec1939 --- /dev/null +++ b/test/fixtures/repo-symlinks/dot-git/hooks/post-update.sample @@ -0,0 +1,8 @@ +#!/bin/sh +# +# An example hook script to prepare a packed repository for use over +# dumb transports. +# +# To enable this hook, rename this file to "post-update". + +exec git update-server-info diff --git a/test/fixtures/repo-symlinks/dot-git/hooks/pre-applypatch.sample b/test/fixtures/repo-symlinks/dot-git/hooks/pre-applypatch.sample new file mode 100755 index 0000000000..4142082bcb --- /dev/null +++ b/test/fixtures/repo-symlinks/dot-git/hooks/pre-applypatch.sample @@ -0,0 +1,14 @@ +#!/bin/sh +# +# An example hook script to verify what is about to be committed +# by applypatch from an e-mail message. +# +# The hook should exit with non-zero status after issuing an +# appropriate message if it wants to stop the commit. +# +# To enable this hook, rename this file to "pre-applypatch". + +. git-sh-setup +precommit="$(git rev-parse --git-path hooks/pre-commit)" +test -x "$precommit" && exec "$precommit" ${1+"$@"} +: diff --git a/test/fixtures/repo-symlinks/dot-git/hooks/pre-commit.sample b/test/fixtures/repo-symlinks/dot-git/hooks/pre-commit.sample new file mode 100755 index 0000000000..68d62d5446 --- /dev/null +++ b/test/fixtures/repo-symlinks/dot-git/hooks/pre-commit.sample @@ -0,0 +1,49 @@ +#!/bin/sh +# +# An example hook script to verify what is about to be committed. +# Called by "git commit" with no arguments. The hook should +# exit with non-zero status after issuing an appropriate message if +# it wants to stop the commit. +# +# To enable this hook, rename this file to "pre-commit". + +if git rev-parse --verify HEAD >/dev/null 2>&1 +then + against=HEAD +else + # Initial commit: diff against an empty tree object + against=4b825dc642cb6eb9a060e54bf8d69288fbee4904 +fi + +# If you want to allow non-ASCII filenames set this variable to true. +allownonascii=$(git config --bool hooks.allownonascii) + +# Redirect output to stderr. +exec 1>&2 + +# Cross platform projects tend to avoid non-ASCII filenames; prevent +# them from being added to the repository. We exploit the fact that the +# printable range starts at the space character and ends with tilde. +if [ "$allownonascii" != "true" ] && + # Note that the use of brackets around a tr range is ok here, (it's + # even required, for portability to Solaris 10's /usr/bin/tr), since + # the square bracket bytes happen to fall in the designated range. + test $(git diff --cached --name-only --diff-filter=A -z $against | + LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0 +then + cat <<\EOF +Error: Attempt to add a non-ASCII file name. + +This can cause problems if you want to work with people on other platforms. + +To be portable it is advisable to rename the file. + +If you know what you are doing you can disable this check using: + + git config hooks.allownonascii true +EOF + exit 1 +fi + +# If there are whitespace errors, print the offending file names and fail. +exec git diff-index --check --cached $against -- diff --git a/test/fixtures/repo-symlinks/dot-git/hooks/pre-push.sample b/test/fixtures/repo-symlinks/dot-git/hooks/pre-push.sample new file mode 100755 index 0000000000..6187dbf439 --- /dev/null +++ b/test/fixtures/repo-symlinks/dot-git/hooks/pre-push.sample @@ -0,0 +1,53 @@ +#!/bin/sh + +# An example hook script to verify what is about to be pushed. Called by "git +# push" after it has checked the remote status, but before anything has been +# pushed. If this script exits with a non-zero status nothing will be pushed. +# +# This hook is called with the following parameters: +# +# $1 -- Name of the remote to which the push is being done +# $2 -- URL to which the push is being done +# +# If pushing without using a named remote those arguments will be equal. +# +# Information about the commits which are being pushed is supplied as lines to +# the standard input in the form: +# +# +# +# This sample shows how to prevent push of commits where the log message starts +# with "WIP" (work in progress). + +remote="$1" +url="$2" + +z40=0000000000000000000000000000000000000000 + +while read local_ref local_sha remote_ref remote_sha +do + if [ "$local_sha" = $z40 ] + then + # Handle delete + : + else + if [ "$remote_sha" = $z40 ] + then + # New branch, examine all commits + range="$local_sha" + else + # Update to existing branch, examine new commits + range="$remote_sha..$local_sha" + fi + + # Check for WIP commit + commit=`git rev-list -n 1 --grep '^WIP' "$range"` + if [ -n "$commit" ] + then + echo >&2 "Found WIP commit in $local_ref, not pushing" + exit 1 + fi + fi +done + +exit 0 diff --git a/test/fixtures/repo-symlinks/dot-git/hooks/pre-rebase.sample b/test/fixtures/repo-symlinks/dot-git/hooks/pre-rebase.sample new file mode 100755 index 0000000000..9773ed4cb2 --- /dev/null +++ b/test/fixtures/repo-symlinks/dot-git/hooks/pre-rebase.sample @@ -0,0 +1,169 @@ +#!/bin/sh +# +# Copyright (c) 2006, 2008 Junio C Hamano +# +# The "pre-rebase" hook is run just before "git rebase" starts doing +# its job, and can prevent the command from running by exiting with +# non-zero status. +# +# The hook is called with the following parameters: +# +# $1 -- the upstream the series was forked from. +# $2 -- the branch being rebased (or empty when rebasing the current branch). +# +# This sample shows how to prevent topic branches that are already +# merged to 'next' branch from getting rebased, because allowing it +# would result in rebasing already published history. + +publish=next +basebranch="$1" +if test "$#" = 2 +then + topic="refs/heads/$2" +else + topic=`git symbolic-ref HEAD` || + exit 0 ;# we do not interrupt rebasing detached HEAD +fi + +case "$topic" in +refs/heads/??/*) + ;; +*) + exit 0 ;# we do not interrupt others. + ;; +esac + +# Now we are dealing with a topic branch being rebased +# on top of master. Is it OK to rebase it? + +# Does the topic really exist? +git show-ref -q "$topic" || { + echo >&2 "No such branch $topic" + exit 1 +} + +# Is topic fully merged to master? +not_in_master=`git rev-list --pretty=oneline ^master "$topic"` +if test -z "$not_in_master" +then + echo >&2 "$topic is fully merged to master; better remove it." + exit 1 ;# we could allow it, but there is no point. +fi + +# Is topic ever merged to next? If so you should not be rebasing it. +only_next_1=`git rev-list ^master "^$topic" ${publish} | sort` +only_next_2=`git rev-list ^master ${publish} | sort` +if test "$only_next_1" = "$only_next_2" +then + not_in_topic=`git rev-list "^$topic" master` + if test -z "$not_in_topic" + then + echo >&2 "$topic is already up-to-date with master" + exit 1 ;# we could allow it, but there is no point. + else + exit 0 + fi +else + not_in_next=`git rev-list --pretty=oneline ^${publish} "$topic"` + /usr/bin/perl -e ' + my $topic = $ARGV[0]; + my $msg = "* $topic has commits already merged to public branch:\n"; + my (%not_in_next) = map { + /^([0-9a-f]+) /; + ($1 => 1); + } split(/\n/, $ARGV[1]); + for my $elem (map { + /^([0-9a-f]+) (.*)$/; + [$1 => $2]; + } split(/\n/, $ARGV[2])) { + if (!exists $not_in_next{$elem->[0]}) { + if ($msg) { + print STDERR $msg; + undef $msg; + } + print STDERR " $elem->[1]\n"; + } + } + ' "$topic" "$not_in_next" "$not_in_master" + exit 1 +fi + +exit 0 + +################################################################ + +This sample hook safeguards topic branches that have been +published from being rewound. + +The workflow assumed here is: + + * Once a topic branch forks from "master", "master" is never + merged into it again (either directly or indirectly). + + * Once a topic branch is fully cooked and merged into "master", + it is deleted. If you need to build on top of it to correct + earlier mistakes, a new topic branch is created by forking at + the tip of the "master". This is not strictly necessary, but + it makes it easier to keep your history simple. + + * Whenever you need to test or publish your changes to topic + branches, merge them into "next" branch. + +The script, being an example, hardcodes the publish branch name +to be "next", but it is trivial to make it configurable via +$GIT_DIR/config mechanism. + +With this workflow, you would want to know: + +(1) ... if a topic branch has ever been merged to "next". Young + topic branches can have stupid mistakes you would rather + clean up before publishing, and things that have not been + merged into other branches can be easily rebased without + affecting other people. But once it is published, you would + not want to rewind it. + +(2) ... if a topic branch has been fully merged to "master". + Then you can delete it. More importantly, you should not + build on top of it -- other people may already want to + change things related to the topic as patches against your + "master", so if you need further changes, it is better to + fork the topic (perhaps with the same name) afresh from the + tip of "master". + +Let's look at this example: + + o---o---o---o---o---o---o---o---o---o "next" + / / / / + / a---a---b A / / + / / / / + / / c---c---c---c B / + / / / \ / + / / / b---b C \ / + / / / / \ / + ---o---o---o---o---o---o---o---o---o---o---o "master" + + +A, B and C are topic branches. + + * A has one fix since it was merged up to "next". + + * B has finished. It has been fully merged up to "master" and "next", + and is ready to be deleted. + + * C has not merged to "next" at all. + +We would want to allow C to be rebased, refuse A, and encourage +B to be deleted. + +To compute (1): + + git rev-list ^master ^topic next + git rev-list ^master next + + if these match, topic has not merged in next at all. + +To compute (2): + + git rev-list master..topic + + if this is empty, it is fully merged to "master". diff --git a/test/fixtures/repo-symlinks/dot-git/hooks/pre-receive.sample b/test/fixtures/repo-symlinks/dot-git/hooks/pre-receive.sample new file mode 100755 index 0000000000..a1fd29ec14 --- /dev/null +++ b/test/fixtures/repo-symlinks/dot-git/hooks/pre-receive.sample @@ -0,0 +1,24 @@ +#!/bin/sh +# +# An example hook script to make use of push options. +# The example simply echoes all push options that start with 'echoback=' +# and rejects all pushes when the "reject" push option is used. +# +# To enable this hook, rename this file to "pre-receive". + +if test -n "$GIT_PUSH_OPTION_COUNT" +then + i=0 + while test "$i" -lt "$GIT_PUSH_OPTION_COUNT" + do + eval "value=\$GIT_PUSH_OPTION_$i" + case "$value" in + echoback=*) + echo "echo from the pre-receive-hook: ${value#*=}" >&2 + ;; + reject) + exit 1 + esac + i=$((i + 1)) + done +fi diff --git a/test/fixtures/repo-symlinks/dot-git/hooks/prepare-commit-msg.sample b/test/fixtures/repo-symlinks/dot-git/hooks/prepare-commit-msg.sample new file mode 100755 index 0000000000..f093a02ec4 --- /dev/null +++ b/test/fixtures/repo-symlinks/dot-git/hooks/prepare-commit-msg.sample @@ -0,0 +1,36 @@ +#!/bin/sh +# +# An example hook script to prepare the commit log message. +# Called by "git commit" with the name of the file that has the +# commit message, followed by the description of the commit +# message's source. The hook's purpose is to edit the commit +# message file. If the hook fails with a non-zero status, +# the commit is aborted. +# +# To enable this hook, rename this file to "prepare-commit-msg". + +# This hook includes three examples. The first comments out the +# "Conflicts:" part of a merge commit. +# +# The second includes the output of "git diff --name-status -r" +# into the message, just before the "git status" output. It is +# commented because it doesn't cope with --amend or with squashed +# commits. +# +# The third example adds a Signed-off-by line to the message, that can +# still be edited. This is rarely a good idea. + +case "$2,$3" in + merge,) + /usr/bin/perl -i.bak -ne 's/^/# /, s/^# #/#/ if /^Conflicts/ .. /#/; print' "$1" ;; + +# ,|template,) +# /usr/bin/perl -i.bak -pe ' +# print "\n" . `git diff --cached --name-status -r` +# if /^#/ && $first++ == 0' "$1" ;; + + *) ;; +esac + +# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') +# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1" diff --git a/test/fixtures/repo-symlinks/dot-git/hooks/update.sample b/test/fixtures/repo-symlinks/dot-git/hooks/update.sample new file mode 100755 index 0000000000..80ba94135c --- /dev/null +++ b/test/fixtures/repo-symlinks/dot-git/hooks/update.sample @@ -0,0 +1,128 @@ +#!/bin/sh +# +# An example hook script to block unannotated tags from entering. +# Called by "git receive-pack" with arguments: refname sha1-old sha1-new +# +# To enable this hook, rename this file to "update". +# +# Config +# ------ +# hooks.allowunannotated +# This boolean sets whether unannotated tags will be allowed into the +# repository. By default they won't be. +# hooks.allowdeletetag +# This boolean sets whether deleting tags will be allowed in the +# repository. By default they won't be. +# hooks.allowmodifytag +# This boolean sets whether a tag may be modified after creation. By default +# it won't be. +# hooks.allowdeletebranch +# This boolean sets whether deleting branches will be allowed in the +# repository. By default they won't be. +# hooks.denycreatebranch +# This boolean sets whether remotely creating branches will be denied +# in the repository. By default this is allowed. +# + +# --- Command line +refname="$1" +oldrev="$2" +newrev="$3" + +# --- Safety check +if [ -z "$GIT_DIR" ]; then + echo "Don't run this script from the command line." >&2 + echo " (if you want, you could supply GIT_DIR then run" >&2 + echo " $0 )" >&2 + exit 1 +fi + +if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then + echo "usage: $0 " >&2 + exit 1 +fi + +# --- Config +allowunannotated=$(git config --bool hooks.allowunannotated) +allowdeletebranch=$(git config --bool hooks.allowdeletebranch) +denycreatebranch=$(git config --bool hooks.denycreatebranch) +allowdeletetag=$(git config --bool hooks.allowdeletetag) +allowmodifytag=$(git config --bool hooks.allowmodifytag) + +# check for no description +projectdesc=$(sed -e '1q' "$GIT_DIR/description") +case "$projectdesc" in +"Unnamed repository"* | "") + echo "*** Project description file hasn't been set" >&2 + exit 1 + ;; +esac + +# --- Check types +# if $newrev is 0000...0000, it's a commit to delete a ref. +zero="0000000000000000000000000000000000000000" +if [ "$newrev" = "$zero" ]; then + newrev_type=delete +else + newrev_type=$(git cat-file -t $newrev) +fi + +case "$refname","$newrev_type" in + refs/tags/*,commit) + # un-annotated tag + short_refname=${refname##refs/tags/} + if [ "$allowunannotated" != "true" ]; then + echo "*** The un-annotated tag, $short_refname, is not allowed in this repository" >&2 + echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2 + exit 1 + fi + ;; + refs/tags/*,delete) + # delete tag + if [ "$allowdeletetag" != "true" ]; then + echo "*** Deleting a tag is not allowed in this repository" >&2 + exit 1 + fi + ;; + refs/tags/*,tag) + # annotated tag + if [ "$allowmodifytag" != "true" ] && git rev-parse $refname > /dev/null 2>&1 + then + echo "*** Tag '$refname' already exists." >&2 + echo "*** Modifying a tag is not allowed in this repository." >&2 + exit 1 + fi + ;; + refs/heads/*,commit) + # branch + if [ "$oldrev" = "$zero" -a "$denycreatebranch" = "true" ]; then + echo "*** Creating a branch is not allowed in this repository" >&2 + exit 1 + fi + ;; + refs/heads/*,delete) + # delete branch + if [ "$allowdeletebranch" != "true" ]; then + echo "*** Deleting a branch is not allowed in this repository" >&2 + exit 1 + fi + ;; + refs/remotes/*,commit) + # tracking branch + ;; + refs/remotes/*,delete) + # delete tracking branch + if [ "$allowdeletebranch" != "true" ]; then + echo "*** Deleting a tracking branch is not allowed in this repository" >&2 + exit 1 + fi + ;; + *) + # Anything else (is there anything else?) + echo "*** Update hook: unknown type of update to ref $refname of type $newrev_type" >&2 + exit 1 + ;; +esac + +# --- Finished +exit 0 diff --git a/test/fixtures/repo-symlinks/dot-git/index b/test/fixtures/repo-symlinks/dot-git/index new file mode 100644 index 0000000000000000000000000000000000000000..02d7882ea89913d4748b1f5138ac76f566802dc0 GIT binary patch literal 297 zcmZ?q402{*U|<4b=BUu(gFu=AMl&)nu&}IpWyZkJxP*a$@hebD1c-UvrK;Q2`)^!a z%^$Z$Zx_Ee%bx%97+4eaN-9b~+Clo?BhyfG_|VK@o9i4@z`cF(z1!Dcd|IyK!1_yI zssV#QQEGZ=PGXU6T4qiv#7Lld_o3#2X{dQ_3=9i^!f0AhQnU$iovb2`B)~ELW zFaFarelc(tSLWtq=4HcO$G{L0 1515549807 -0800 commit (initial): Initial commit diff --git a/test/fixtures/repo-symlinks/dot-git/logs/refs/heads/master b/test/fixtures/repo-symlinks/dot-git/logs/refs/heads/master new file mode 100644 index 0000000000..0b60ea082f --- /dev/null +++ b/test/fixtures/repo-symlinks/dot-git/logs/refs/heads/master @@ -0,0 +1 @@ +0000000000000000000000000000000000000000 c9dbab82b91c53e70a872a4f1f793c275790abc8 Katrina Uychaco 1515549807 -0800 commit (initial): Initial commit diff --git a/test/fixtures/repo-symlinks/dot-git/objects/2c/d053449a642aa9757d0d7d4c7dfffa0fcb98fa b/test/fixtures/repo-symlinks/dot-git/objects/2c/d053449a642aa9757d0d7d4c7dfffa0fcb98fa new file mode 100644 index 0000000000000000000000000000000000000000..95246a57743b8c340a22e62d0e4967c20dad8dfc GIT binary patch literal 34 qcmb=U!eqOpmnm^cdXC*q8v8NC`Rs literal 0 HcmV?d00001 diff --git a/test/fixtures/repo-symlinks/dot-git/objects/9d/4352700bb7a3dedbd7e8f2a72c4005fa109530 b/test/fixtures/repo-symlinks/dot-git/objects/9d/4352700bb7a3dedbd7e8f2a72c4005fa109530 new file mode 100644 index 0000000000000000000000000000000000000000..c96ab61db7d55d94468f9cde10db5cabd9c71172 GIT binary patch literal 21 ccmbG!`!N%P>?9Tr08Rh~BLDyZ literal 0 HcmV?d00001 diff --git a/test/fixtures/repo-symlinks/dot-git/objects/be/6f0daf2888b24155a4b81fdfdfd62ec3f75672 b/test/fixtures/repo-symlinks/dot-git/objects/be/6f0daf2888b24155a4b81fdfdfd62ec3f75672 new file mode 100644 index 0000000000000000000000000000000000000000..0e31c6a9c39e1018469b9b5914b81ed4ba272bc0 GIT binary patch literal 124 zcmV-?0E7Q{0V^p=O;s>7G-EI{FfcPQQApG)sVHG^m#S`8@4s0AhQnU$iovb2`B)~ELWFaFaregOdf5HO~2cRXJJ literal 0 HcmV?d00001 diff --git a/test/fixtures/repo-symlinks/dot-git/objects/c9/dbab82b91c53e70a872a4f1f793c275790abc8 b/test/fixtures/repo-symlinks/dot-git/objects/c9/dbab82b91c53e70a872a4f1f793c275790abc8 new file mode 100644 index 0000000000..ac1f43b4d8 --- /dev/null +++ b/test/fixtures/repo-symlinks/dot-git/objects/c9/dbab82b91c53e70a872a4f1f793c275790abc8 @@ -0,0 +1,2 @@ +xM +0]s%[q&ibMeFzyǃ{Z)AH.ˑFĠL@_Nu^ xj+<ײ3 \ No newline at end of file diff --git a/test/fixtures/repo-symlinks/dot-git/refs/heads/master b/test/fixtures/repo-symlinks/dot-git/refs/heads/master new file mode 100644 index 0000000000..ef16ef8fbf --- /dev/null +++ b/test/fixtures/repo-symlinks/dot-git/refs/heads/master @@ -0,0 +1 @@ +c9dbab82b91c53e70a872a4f1f793c275790abc8 diff --git a/test/fixtures/repo-symlinks/regular-file.txt b/test/fixtures/repo-symlinks/regular-file.txt new file mode 100644 index 0000000000..9d4352700b --- /dev/null +++ b/test/fixtures/repo-symlinks/regular-file.txt @@ -0,0 +1 @@ +Stuff diff --git a/test/fixtures/repo-symlinks/symlink.txt b/test/fixtures/repo-symlinks/symlink.txt new file mode 120000 index 0000000000..2cd053449a --- /dev/null +++ b/test/fixtures/repo-symlinks/symlink.txt @@ -0,0 +1 @@ +./regular-file.txt \ No newline at end of file diff --git a/test/models/repository.test.js b/test/models/repository.test.js index 4e7501bd97..5b86b31ffa 100644 --- a/test/models/repository.test.js +++ b/test/models/repository.test.js @@ -283,6 +283,47 @@ describe('Repository', function() { await repo.stageFileModeChange(filePath, '100644'); assert.deepEqual(await indexModeAndOid(filePath), {mode: '100644', oid}); }); + + it('can stage and unstage symlink changes without staging file contents', async function() { + const workingDirPath = await cloneRepository('symlinks'); + const repo = new Repository(workingDirPath); + await repo.getLoadPromise(); + + async function indexModeAndOid(filename) { + const output = await repo.git.exec(['ls-files', '-s', '--', filename]); + if (output) { + const parts = output.split(' '); + return {mode: parts[0], oid: parts[1]}; + } else { + return null; + } + } + + const deletedSymlinkAddedFilePath = 'symlink.txt'; + fs.unlinkSync(path.join(workingDirPath, deletedSymlinkAddedFilePath)); + fs.writeFileSync(path.join(workingDirPath, deletedSymlinkAddedFilePath), 'qux\nfoo\nbar\n', 'utf8'); + + const deletedFileAddedSymlinkPath = 'a.txt'; + fs.unlinkSync(path.join(workingDirPath, deletedFileAddedSymlinkPath)); + fs.symlinkSync(path.join(workingDirPath, 'regular-file.txt'), path.join(workingDirPath, deletedFileAddedSymlinkPath)); + + // Stage symlink change, leaving added file unstaged + assert.equal((await indexModeAndOid(deletedSymlinkAddedFilePath)).mode, '120000'); + await repo.stageFileSymlinkChange(deletedSymlinkAddedFilePath); + assert.isNull(await indexModeAndOid(deletedSymlinkAddedFilePath)); + const unstagedFilePatch = await repo.getFilePatchForPath(deletedSymlinkAddedFilePath, {staged: false}); + assert.equal(unstagedFilePatch.getStatus(), 'added'); + assert.equal(unstagedFilePatch.toString(), '@@ -0,0 +1,3 @@\n+qux\n+foo\n+bar\n'); + + // Unstage symlink change, leaving deleted file staged + await repo.stageFiles([deletedFileAddedSymlinkPath]); + assert.equal((await indexModeAndOid(deletedFileAddedSymlinkPath)).mode, '120000'); + await repo.stageFileSymlinkChange(deletedFileAddedSymlinkPath); + assert.isNull(await indexModeAndOid(deletedFileAddedSymlinkPath)); + const stagedFilePatch = await repo.getFilePatchForPath(deletedFileAddedSymlinkPath, {staged: true}); + assert.equal(stagedFilePatch.getStatus(), 'deleted'); + assert.equal(stagedFilePatch.toString(), '@@ -1,4 +0,0 @@\n-foo\n-bar\n-baz\n-\n'); + }); }); describe('getFilePatchForPath', function() { From e99deffa94c2dd01b06c5fe50f0b2fc60cf489c7 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Wed, 10 Jan 2018 14:22:40 -0800 Subject: [PATCH 0153/5882] WIP --- lib/models/file-patch.js | 13 +++++++++-- lib/models/repository-states/present.js | 4 ++-- test/models/file-patch.test.js | 9 ++++++- test/models/repository.test.js | 31 +++++++++++++++++++++---- 4 files changed, 48 insertions(+), 9 deletions(-) diff --git a/lib/models/file-patch.js b/lib/models/file-patch.js index 697d91015f..3681c675c9 100644 --- a/lib/models/file-patch.js +++ b/lib/models/file-patch.js @@ -275,11 +275,20 @@ export default class FilePatch { } toString() { - return this.getHunks().map(h => h.toString()).join(''); + return this.getHeaderString() + this.getHunks().map(h => h.toString()).join(''); } getHeaderString() { - let header = this.getOldPath() ? `--- a/${toGitPathSep(this.getOldPath())}` : '--- /dev/null'; + let header = `diff --git a/${toGitPathSep(this.getPath())} b/${toGitPathSep(this.getPath())}`; + header += '\n'; + if (this.getStatus() === 'added') { + header += `new file mode ${this.getNewMode()}`; + header += '\n'; + } else if (this.getStatus() === 'deleted') { + header += `deleted file mode ${this.getOldMode()}`; + header += '\n'; + } + header += this.getOldPath() ? `--- a/${toGitPathSep(this.getOldPath())}` : '--- /dev/null'; header += '\n'; header += this.getNewPath() ? `+++ b/${toGitPathSep(this.getNewPath())}` : '+++ /dev/null'; header += '\n'; diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js index a834c7c249..61791c0571 100644 --- a/lib/models/repository-states/present.js +++ b/lib/models/repository-states/present.js @@ -235,13 +235,13 @@ export default class Present extends State { @invalidate(filePatch => Keys.cacheOperationKeys([filePatch.getOldPath(), filePatch.getNewPath()])) applyPatchToIndex(filePatch) { - const patchStr = filePatch.getHeaderString() + filePatch.toString(); + const patchStr = filePatch.toString(); return this.git().applyPatch(patchStr, {index: true}); } @invalidate(filePatch => Keys.workdirOperationKeys([filePatch.getOldPath(), filePatch.getNewPath()])) applyPatchToWorkdir(filePatch) { - const patchStr = filePatch.getHeaderString() + filePatch.toString(); + const patchStr = filePatch.toString(); return this.git().applyPatch(patchStr); } diff --git a/test/models/file-patch.test.js b/test/models/file-patch.test.js index a6a8f6da48..b6537cbd4b 100644 --- a/test/models/file-patch.test.js +++ b/test/models/file-patch.test.js @@ -251,6 +251,9 @@ describe('FilePatch', function() { const patch = await repository.getFilePatchForPath('sample.js'); assert.equal(patch.toString(), dedent` + diff --git a/sample.js b/sample.js + --- a/sample.js + +++ b/sample.js @@ -1,4 +1,5 @@ -var quicksort = function () { +this is a modified line @@ -277,6 +280,10 @@ describe('FilePatch', function() { const patch = await repo.getFilePatchForPath('e.txt'); assert.equal(patch.toString(), dedent` + diff --git a/e.txt b/e.txt + new file mode 100644 + --- /dev/null + +++ b/e.txt @@ -0,0 +1,1 @@ +qux \\ No newline at end of file @@ -287,7 +294,7 @@ describe('FilePatch', function() { if (process.platform === 'win32') { describe('getHeaderString()', function() { - it('formats paths with git line endings', function() { + it('formats paths with git path separators', function() { const oldPath = path.join('foo', 'bar', 'old.js'); const newPath = path.join('baz', 'qux', 'new.js'); diff --git a/test/models/repository.test.js b/test/models/repository.test.js index 5b86b31ffa..2e3ad5be29 100644 --- a/test/models/repository.test.js +++ b/test/models/repository.test.js @@ -313,7 +313,18 @@ describe('Repository', function() { assert.isNull(await indexModeAndOid(deletedSymlinkAddedFilePath)); const unstagedFilePatch = await repo.getFilePatchForPath(deletedSymlinkAddedFilePath, {staged: false}); assert.equal(unstagedFilePatch.getStatus(), 'added'); - assert.equal(unstagedFilePatch.toString(), '@@ -0,0 +1,3 @@\n+qux\n+foo\n+bar\n'); + // assert.equal(unstagedFilePatch.toString(), '@@ -0,0 +1,3 @@\n+qux\n+foo\n+bar\n'); + assert.equal(unstagedFilePatch.toString(), dedent` + diff --git a/symlink.txt b/symlink.txt + new file mode 100644 + --- /dev/null + +++ b/symlink.txt + @@ -0,0 +1,3 @@ + +qux + +foo + +bar + + `); // Unstage symlink change, leaving deleted file staged await repo.stageFiles([deletedFileAddedSymlinkPath]); @@ -322,7 +333,19 @@ describe('Repository', function() { assert.isNull(await indexModeAndOid(deletedFileAddedSymlinkPath)); const stagedFilePatch = await repo.getFilePatchForPath(deletedFileAddedSymlinkPath, {staged: true}); assert.equal(stagedFilePatch.getStatus(), 'deleted'); - assert.equal(stagedFilePatch.toString(), '@@ -1,4 +0,0 @@\n-foo\n-bar\n-baz\n-\n'); + // assert.equal(stagedFilePatch.toString(), '@@ -1,4 +0,0 @@\n-foo\n-bar\n-baz\n-\n'); + assert.equal(stagedFilePatch.toString(), dedent` + diff --git a/a.txt b/a.txt + deleted file mode 100644 + --- a/a.txt + +++ /dev/null + @@ -1,4 +0,0 @@ + -foo + -bar + -baz + - + + `); }); }); @@ -1488,7 +1511,7 @@ describe('Repository', function() { await assertCorrectInvalidation({repository}, async () => { await observer.start(); - await repository.git.applyPatch(patch.getHeaderString() + patch.toString(), {index: true}); + await repository.git.applyPatch(patch.toString(), {index: true}); await expectEvents( repository, path.join('.git', 'index'), @@ -1504,7 +1527,7 @@ describe('Repository', function() { await assertCorrectInvalidation({repository}, async () => { await observer.start(); - await repository.git.applyPatch(patch.getHeaderString() + patch.toString()); + await repository.git.applyPatch(patch.toString()); await expectEvents( repository, 'a.txt', From f4de48d2f3ada2aab213cca267b417a30d09037a Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Wed, 10 Jan 2018 16:13:44 -0800 Subject: [PATCH 0154/5882] Fix FilePatch#getHeaderString() test on windows --- test/models/file-patch.test.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/models/file-patch.test.js b/test/models/file-patch.test.js index b6537cbd4b..7504d8279d 100644 --- a/test/models/file-patch.test.js +++ b/test/models/file-patch.test.js @@ -300,6 +300,7 @@ describe('FilePatch', function() { const patch = createFilePatch(oldPath, newPath, 'modified', []); assert.equal(patch.getHeaderString(), dedent` + diff --git a/foo/bar/old.js b/baz/qux/new.js --- a/foo/bar/old.js +++ b/baz/qux/new.js From d0cc9f33c4b130b7c1da0d0919014cb2d8070e3e Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Wed, 10 Jan 2018 16:16:57 -0800 Subject: [PATCH 0155/5882] Teach FilePatch to stringify typechange patches --- lib/controllers/file-patch-controller.js | 2 +- lib/models/file-patch.js | 53 +++++++++++++++++---- lib/models/repository-states/present.js | 2 +- test/models/file-patch.test.js | 59 ++++++++++++++++++++++++ 4 files changed, 106 insertions(+), 10 deletions(-) diff --git a/lib/controllers/file-patch-controller.js b/lib/controllers/file-patch-controller.js index 1c910f7e1b..c8e35db2c8 100644 --- a/lib/controllers/file-patch-controller.js +++ b/lib/controllers/file-patch-controller.js @@ -138,7 +138,7 @@ export default class FilePatchController extends React.Component { filePatch = oldFilePatch.clone({ oldFile: oldFilePatch.oldFile.clone({mode: null, symlink: null}), newFile: oldFilePatch.newFile.clone({mode: null, symlink: null}), - patch: oldFilePatch.getPatch().clone({hunks: [], typechange: null}), + patch: oldFilePatch.getPatch().clone({hunks: []}), }); if (!this.destroyed) { this.setState({filePatch, isPartiallyStaged}); } } diff --git a/lib/models/file-patch.js b/lib/models/file-patch.js index 3681c675c9..52ef713b96 100644 --- a/lib/models/file-patch.js +++ b/lib/models/file-patch.js @@ -2,6 +2,10 @@ import Hunk from './hunk'; import {toGitPathSep} from '../helpers'; class File { + static empty() { + return new File({path: null, mode: null, symlink: null}); + } + constructor({path, mode, symlink}) { this.path = path; this.mode = mode; @@ -16,6 +20,14 @@ class File { return this.mode; } + isSymlink() { + return this.getMode() === '120000'; + } + + isRegularFile() { + return this.getMode() === '100644' || this.getMode() === '100755'; + } + getSymlink() { return this.symlink; } @@ -30,10 +42,9 @@ class File { } class Patch { - constructor({status, hunks, typechange}) { + constructor({status, hunks}) { this.status = status; this.hunks = hunks; - this.typechange = !!typechange; } getStatus() { @@ -44,10 +55,6 @@ class Patch { return this.hunks; } - hasTypechange() { - return this.typechange; - } - clone(opts = {}) { return new Patch({ status: opts.status !== undefined ? opts.status : this.status, @@ -105,6 +112,14 @@ export default class FilePatch { return this.getNewFile().getMode(); } + getOldSymlink() { + return this.getOldFile().getSymlink(); + } + + getNewSymlink() { + return this.getNewFile().getSymlink(); + } + didChangeExecutableMode() { const oldMode = this.getOldMode(); const newMode = this.getNewMode(); @@ -124,7 +139,10 @@ export default class FilePatch { } hasTypechange() { - return this.getPatch().hasTypechange(); + const oldFile = this.getOldFile(); + const newFile = this.getNewFile(); + return (oldFile.isSymlink() && newFile.isRegularFile()) || + (newFile.isSymlink() && oldFile.isRegularFile()); } getPath() { @@ -275,7 +293,26 @@ export default class FilePatch { } toString() { - return this.getHeaderString() + this.getHunks().map(h => h.toString()).join(''); + if (this.hasTypechange()) { + const left = this.clone({ + newFile: File.empty(), + patch: this.getOldSymlink() ? new Patch({status: 'deleted', hunks: []}) : this.getPatch(), + }); + const right = this.clone({ + oldFile: File.empty(), + patch: this.getNewSymlink() ? new Patch({status: 'added', hunks: []}) : this.getPatch(), + }); + + return left.toString() + right.toString(); + } else if (this.getStatus() === 'added' && this.getNewFile().isSymlink()) { + const symlinkPath = this.getNewSymlink(); + return this.getHeaderString() + `@@ -0,0 +1 @@\n+${symlinkPath}\n\\ No newline at end of file\n`; + } else if (this.getStatus() === 'deleted' && this.getOldFile().isSymlink()) { + const symlinkPath = this.getOldSymlink(); + return this.getHeaderString() + `@@ -1 +0,0 @@\n-${symlinkPath}\n\\ No newline at end of file\n`; + } else { + return this.getHeaderString() + this.getHunks().map(h => h.toString()).join(''); + } } getHeaderString() { diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js index 61791c0571..d8fbf34af6 100644 --- a/lib/models/repository-states/present.js +++ b/lib/models/repository-states/present.js @@ -787,7 +787,7 @@ function buildFilePatchFromDualDiffs(diff1, diff2) { } else { throw new Error(`Invalid mode change diff status: ${modeChangeDiff.status}`); } - const patch = new FilePatch.Patch({status, hunks, typechange: true}); + const patch = new FilePatch.Patch({status, hunks}); return new FilePatch(oldFile, newFile, patch); } diff --git a/test/models/file-patch.test.js b/test/models/file-patch.test.js index 7504d8279d..4a0dad9f34 100644 --- a/test/models/file-patch.test.js +++ b/test/models/file-patch.test.js @@ -290,6 +290,65 @@ describe('FilePatch', function() { `); }); + + it('handles typechange patches for a symlink replaced with a file', async function() { + const workdirPath = await cloneRepository('symlinks'); + const repository = await buildRepository(workdirPath); + + const deletedSymlinkAddedFilePath = 'symlink.txt'; + fs.unlinkSync(path.join(workdirPath, deletedSymlinkAddedFilePath)); + fs.writeFileSync(path.join(workdirPath, deletedSymlinkAddedFilePath), 'qux\nfoo\nbar\n', 'utf8'); + + const patch = await repository.getFilePatchForPath(deletedSymlinkAddedFilePath); + assert.equal(patch.toString(), dedent` + diff --git a/symlink.txt b/symlink.txt + deleted file mode 120000 + --- a/symlink.txt + +++ /dev/null + @@ -1 +0,0 @@ + -./regular-file.txt + \\ No newline at end of file + diff --git a/symlink.txt b/symlink.txt + new file mode 100644 + --- /dev/null + +++ b/symlink.txt + @@ -0,0 +1,3 @@ + +qux + +foo + +bar + + `); + }); + + it('handles typechange patches for a file replaced with a symlink', async function() { + const workdirPath = await cloneRepository('symlinks'); + const repository = await buildRepository(workdirPath); + + const deletedFileAddedSymlinkPath = 'a.txt'; + fs.unlinkSync(path.join(workdirPath, deletedFileAddedSymlinkPath)); + fs.symlinkSync(path.join(workdirPath, 'regular-file.txt'), path.join(workdirPath, deletedFileAddedSymlinkPath)); + + const patch = await repository.getFilePatchForPath(deletedFileAddedSymlinkPath); + assert.equal(patch.toString(), dedent` + diff --git a/a.txt b/a.txt + deleted file mode 100644 + --- a/a.txt + +++ /dev/null + @@ -1,4 +0,0 @@ + -foo + -bar + -baz + - + diff --git a/a.txt b/a.txt + new file mode 120000 + --- /dev/null + +++ b/a.txt + @@ -0,0 +1 @@ + +${path.join(workdirPath, 'regular-file.txt')} + \\ No newline at end of file + + `); + }); }); if (process.platform === 'win32') { From 5e3b074bb8a325f4dd3f875a49d4bb1cfeb1577e Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Wed, 10 Jan 2018 17:28:10 -0800 Subject: [PATCH 0156/5882] Respect the Law of Demeter --- lib/controllers/file-patch-controller.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/controllers/file-patch-controller.js b/lib/controllers/file-patch-controller.js index c8e35db2c8..6563ef26cf 100644 --- a/lib/controllers/file-patch-controller.js +++ b/lib/controllers/file-patch-controller.js @@ -173,8 +173,8 @@ export default class FilePatchController extends React.Component { null; const symlinkChange = fp && fp.hasSymlink() ? { - oldSymlink: fp.getOldFile().getSymlink(), - newSymlink: fp.getNewFile().getSymlink(), + oldSymlink: fp.getOldSymlink(), + newSymlink: fp.getNewSymlink(), typechange: fp.hasTypechange(), filePatchStatus: fp.getStatus(), } : null; From bdfd549f1bc3ba888cd998e6a4137f69ceb43b8b Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Wed, 10 Jan 2018 17:36:34 -0800 Subject: [PATCH 0157/5882] Add test for staging lines that require symlink change to be staged And don't call stageOrUnstageSymlinkChange since the FilePatch creates a patch that includes the symlink change --- lib/controllers/file-patch-controller.js | 16 +-- lib/views/file-patch-view.js | 17 +-- .../controllers/file-patch-controller.test.js | 122 +++++++++++++++++- 3 files changed, 130 insertions(+), 25 deletions(-) diff --git a/lib/controllers/file-patch-controller.js b/lib/controllers/file-patch-controller.js index 6563ef26cf..43f8193397 100644 --- a/lib/controllers/file-patch-controller.js +++ b/lib/controllers/file-patch-controller.js @@ -422,19 +422,17 @@ export default class FilePatchController extends React.Component { } @autobind - attemptSymlinkStageOperation({noSwitchboard} = {}) { + attemptSymlinkStageOperation() { if (this.stagingOperationInProgress) { - return null; + return; } - if (!noSwitchboard) { - this.stagingOperationInProgress = true; - this.props.switchboard.getChangePatchPromise().then(() => { - this.stagingOperationInProgress = false; - }); - } + this.stagingOperationInProgress = true; + this.props.switchboard.getChangePatchPromise().then(() => { + this.stagingOperationInProgress = false; + }); - return this.stageOrUnstageSymlinkChange(); + this.stageOrUnstageSymlinkChange(); } async stageLines(lines) { diff --git a/lib/views/file-patch-view.js b/lib/views/file-patch-view.js index 54568ca881..4904004c17 100644 --- a/lib/views/file-patch-view.js +++ b/lib/views/file-patch-view.js @@ -560,18 +560,7 @@ export default class FilePatchView extends React.Component { return this.state.selection.getNextUpdatePromise(); } - async didClickStageButtonForHunk(hunk) { - // For the case when there is a symlink change AND added/deleted file, we must first stage/unstage the symlink - // before staging/unstaging the lines - const {symlinkChange, stagingStatus} = this.props; - const unstagedSymlinkChangeWithAddedLines = symlinkChange && symlinkChange.filePatchStatus === 'added' && - stagingStatus === 'unstaged'; - const stagedSymlinkChangeWithDeletedLines = symlinkChange && symlinkChange.filePatchStatus === 'deleted' && - stagingStatus === 'staged'; - if (unstagedSymlinkChangeWithAddedLines || stagedSymlinkChangeWithDeletedLines) { - await this.stageOrUnstageSymlinkChange({noSwitchboard: true}); - } - + didClickStageButtonForHunk(hunk) { if (this.state.selection.getSelectedHunks().has(hunk)) { this.props.attemptLineStageOperation(this.state.selection.getSelectedLines()); } else { @@ -632,8 +621,8 @@ export default class FilePatchView extends React.Component { } @autobind - stageOrUnstageSymlinkChange(options) { - return this.props.attemptSymlinkStageOperation(options); + stageOrUnstageSymlinkChange() { + this.props.attemptSymlinkStageOperation(); } @autobind diff --git a/test/controllers/file-patch-controller.test.js b/test/controllers/file-patch-controller.test.js index a77f67ab57..ec6cb8ed58 100644 --- a/test/controllers/file-patch-controller.test.js +++ b/test/controllers/file-patch-controller.test.js @@ -25,7 +25,7 @@ describe('FilePatchController', function() { let atomEnv, commandRegistry, tooltips, deserializers; let component, switchboard, repository, filePath, getFilePatchForPath, workdirPath; let discardLines, didSurfaceFile, didDiveIntoFilePath, quietlySelectItem, undoLastDiscard, openFiles, getRepositoryForWorkdir; - let getSelectedStagingViewItems; + let getSelectedStagingViewItems, resolutionProgress; beforeEach(async function() { atomEnv = global.buildAtomEnvironment(); @@ -47,7 +47,7 @@ describe('FilePatchController', function() { repository = await buildRepository(workdirPath); getRepositoryForWorkdir = () => repository; - const resolutionProgress = new ResolutionProgress(); + resolutionProgress = new ResolutionProgress(); FilePatchController.resetConfirmedLargeFilePatches(); @@ -461,6 +461,124 @@ describe('FilePatchController', function() { assert.autocrlfEqual(await repository.readFileFromIndex('sample.js'), originalLines.join('\n')); }); + it('stages symlink change when staging added lines that depend on change', async function() { + const workingDirPath = await cloneRepository('symlinks'); + const repository = await buildRepository(workingDirPath); + await repository.getLoadPromise(); + + async function indexModeAndOid(filename) { + const output = await repository.git.exec(['ls-files', '-s', '--', filename]); + if (output) { + const parts = output.split(' '); + return {mode: parts[0], oid: parts[1]}; + } else { + return null; + } + } + + const deletedSymlinkAddedFilePath = 'symlink.txt'; + fs.unlinkSync(path.join(workingDirPath, deletedSymlinkAddedFilePath)); + fs.writeFileSync(path.join(workingDirPath, deletedSymlinkAddedFilePath), 'qux\nfoo\nbar\nbaz\nzoo\n', 'utf8'); + + const component = ( + repository} + workingDirectoryPath={repository.getWorkingDirectoryPath()} + getSelectedStagingViewItems={getSelectedStagingViewItems} + uri={'some/uri'} + /> + ); + + const wrapper = mount(React.cloneElement(component, {filePath: deletedSymlinkAddedFilePath, initialStagingStatus: 'unstaged'})); + + assert.equal((await indexModeAndOid(deletedSymlinkAddedFilePath)).mode, '120000'); + + await assert.async.isTrue(wrapper.find('HunkView').exists()); + const opPromise0 = switchboard.getFinishStageOperationPromise(); + const hunkView0 = wrapper.find('HunkView').at(0); + hunkView0.find('LineView').at(1).find('.github-HunkView-line').simulate('mousedown', {button: 0, detail: 1}); + hunkView0.find('LineView').at(2).find('.github-HunkView-line').simulate('mousemove', {}); + window.dispatchEvent(new MouseEvent('mouseup')); + hunkView0.find('button.github-HunkView-stageButton').simulate('click'); + await opPromise0; + + repository.refresh(); + assert.autocrlfEqual(await repository.readFileFromIndex(deletedSymlinkAddedFilePath), 'foo\nbar\n'); + assert.equal((await indexModeAndOid(deletedSymlinkAddedFilePath)).mode, '100644'); + }); + + it('stages symlink change when staging deleted lines that depend on change', async function() { + const workingDirPath = await cloneRepository('symlinks'); + const repository = await buildRepository(workingDirPath); + await repository.getLoadPromise(); + + async function indexModeAndOid(filename) { + const output = await repository.git.exec(['ls-files', '-s', '--', filename]); + if (output) { + const parts = output.split(' '); + return {mode: parts[0], oid: parts[1]}; + } else { + return null; + } + } + + const deletedFileAddedSymlinkPath = 'a.txt'; + fs.unlinkSync(path.join(workingDirPath, deletedFileAddedSymlinkPath)); + fs.symlinkSync(path.join(workingDirPath, 'regular-file.txt'), path.join(workingDirPath, deletedFileAddedSymlinkPath)); + await repository.stageFiles([deletedFileAddedSymlinkPath]); + + const component = ( + repository} + workingDirectoryPath={repository.getWorkingDirectoryPath()} + getSelectedStagingViewItems={getSelectedStagingViewItems} + uri={'some/uri'} + /> + ); + + assert.equal((await indexModeAndOid(deletedFileAddedSymlinkPath)).mode, '120000'); + const wrapper = mount(React.cloneElement(component, {filePath: deletedFileAddedSymlinkPath, initialStagingStatus: 'staged'})); + + await assert.async.isTrue(wrapper.find('HunkView').exists()); + const opPromise0 = switchboard.getFinishStageOperationPromise(); + const hunkView0 = wrapper.find('HunkView').at(0); + hunkView0.find('LineView').at(1).find('.github-HunkView-line').simulate('mousedown', {button: 0, detail: 1}); + hunkView0.find('LineView').at(2).find('.github-HunkView-line').simulate('mousemove', {}); + window.dispatchEvent(new MouseEvent('mouseup')); + hunkView0.find('button.github-HunkView-stageButton').simulate('click'); + await opPromise0; + + repository.refresh(); + assert.autocrlfEqual(await repository.readFileFromIndex(deletedFileAddedSymlinkPath), 'bar\nbaz\n'); + assert.equal((await indexModeAndOid(deletedFileAddedSymlinkPath)).mode, '100644'); + }); + // https://github.com/atom/github/issues/417 describe('when unstaging the last lines/hunks from a file', function() { it('removes added files from index when last hunk is unstaged', async function() { From e8cc72303561060c67ce9cc2e437b7b13254ee5b Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Wed, 10 Jan 2018 21:35:10 -0800 Subject: [PATCH 0158/5882] :art: FilePatchController tests. Extract `createComponent` helper --- .../controllers/file-patch-controller.test.js | 788 +++++++++--------- 1 file changed, 376 insertions(+), 412 deletions(-) diff --git a/test/controllers/file-patch-controller.test.js b/test/controllers/file-patch-controller.test.js index ec6cb8ed58..f531f6cb96 100644 --- a/test/controllers/file-patch-controller.test.js +++ b/test/controllers/file-patch-controller.test.js @@ -21,69 +21,71 @@ function createFilePatch(oldFilePath, newFilePath, status, hunks) { return new FilePatch(oldFile, newFile, patch); } -describe('FilePatchController', function() { - let atomEnv, commandRegistry, tooltips, deserializers; - let component, switchboard, repository, filePath, getFilePatchForPath, workdirPath; - let discardLines, didSurfaceFile, didDiveIntoFilePath, quietlySelectItem, undoLastDiscard, openFiles, getRepositoryForWorkdir; - let getSelectedStagingViewItems, resolutionProgress; - - beforeEach(async function() { - atomEnv = global.buildAtomEnvironment(); - commandRegistry = atomEnv.commands; - deserializers = atomEnv.deserializers; - tooltips = atomEnv.tooltips; - - switchboard = new Switchboard(); - - discardLines = sinon.spy(); - didSurfaceFile = sinon.spy(); - didDiveIntoFilePath = sinon.spy(); - quietlySelectItem = sinon.spy(); - undoLastDiscard = sinon.spy(); - openFiles = sinon.spy(); - getSelectedStagingViewItems = sinon.spy(); - - workdirPath = await cloneRepository('multi-line-file'); - repository = await buildRepository(workdirPath); - - getRepositoryForWorkdir = () => repository; - resolutionProgress = new ResolutionProgress(); - - FilePatchController.resetConfirmedLargeFilePatches(); - - filePath = 'sample.js'; - - component = ( - - ); - }); +let atomEnv, commandRegistry, tooltips, deserializers; +let switchboard, getFilePatchForPath; +let discardLines, didSurfaceFile, didDiveIntoFilePath, quietlySelectItem, undoLastDiscard, openFiles, getRepositoryForWorkdir; +let getSelectedStagingViewItems, resolutionProgress; + +function createComponent(repository, filePath) { + atomEnv = global.buildAtomEnvironment(); + commandRegistry = atomEnv.commands; + deserializers = atomEnv.deserializers; + tooltips = atomEnv.tooltips; + + switchboard = new Switchboard(); + + discardLines = sinon.spy(); + didSurfaceFile = sinon.spy(); + didDiveIntoFilePath = sinon.spy(); + quietlySelectItem = sinon.spy(); + undoLastDiscard = sinon.spy(); + openFiles = sinon.spy(); + getSelectedStagingViewItems = sinon.spy(); + + + getRepositoryForWorkdir = () => repository; + resolutionProgress = new ResolutionProgress(); + + FilePatchController.resetConfirmedLargeFilePatches(); + + return ( + + ); +} +describe('FilePatchController', function() { afterEach(function() { atomEnv.destroy(); }); describe('unit tests', function() { - beforeEach(function() { + let workdirPath, repository, filePath, component; + beforeEach(async function() { + workdirPath = await cloneRepository('multi-line-file'); + repository = await buildRepository(workdirPath); + filePath = 'sample.js'; + component = createComponent(repository, filePath); + getFilePatchForPath = sinon.stub(repository, 'getFilePatchForPath'); }); @@ -315,158 +317,8 @@ describe('FilePatchController', function() { }); describe('integration tests', function() { - it('stages and unstages hunks when the stage button is clicked on hunk views with no individual lines selected', async function() { - const absFilePath = path.join(workdirPath, filePath); - const originalLines = fs.readFileSync(absFilePath, 'utf8').split('\n'); - const unstagedLines = originalLines.slice(); - unstagedLines.splice(1, 1, - 'this is a modified line', - 'this is a new line', - 'this is another new line', - ); - unstagedLines.splice(11, 2, 'this is a modified line'); - fs.writeFileSync(absFilePath, unstagedLines.join('\n')); - - const wrapper = mount(React.cloneElement(component, {filePath})); - - // selectNext() - await assert.async.isTrue(wrapper.find('HunkView').exists()); - commandRegistry.dispatch(wrapper.find('FilePatchView').getDOMNode(), 'core:move-down'); - - await assert.async.isTrue(wrapper.find('HunkView').exists()); - const hunkView0 = wrapper.find('HunkView').at(0); - assert.isFalse(hunkView0.prop('isSelected')); - const opPromise0 = switchboard.getFinishStageOperationPromise(); - hunkView0.find('button.github-HunkView-stageButton').simulate('click'); - await opPromise0; - - const expectedStagedLines = originalLines.slice(); - expectedStagedLines.splice(1, 1, - 'this is a modified line', - 'this is a new line', - 'this is another new line', - ); - assert.autocrlfEqual(await repository.readFileFromIndex('sample.js'), expectedStagedLines.join('\n')); - const updatePromise0 = switchboard.getChangePatchPromise(); - const stagedFilePatch = await repository.getFilePatchForPath('sample.js', {staged: true}); - wrapper.setState({ - stagingStatus: 'staged', - filePatch: stagedFilePatch, - }); - await updatePromise0; - const hunkView1 = wrapper.find('HunkView').at(0); - const opPromise1 = switchboard.getFinishStageOperationPromise(); - hunkView1.find('button.github-HunkView-stageButton').simulate('click'); - await opPromise1; - assert.autocrlfEqual(await repository.readFileFromIndex('sample.js'), originalLines.join('\n')); - }); - - it('stages and unstages individual lines when the stage button is clicked on a hunk with selected lines', async function() { - const absFilePath = path.join(workdirPath, filePath); - - const originalLines = fs.readFileSync(absFilePath, 'utf8').split('\n'); - - // write some unstaged changes - const unstagedLines = originalLines.slice(); - unstagedLines.splice(1, 1, - 'this is a modified line', - 'this is a new line', - 'this is another new line', - ); - unstagedLines.splice(11, 2, 'this is a modified line'); - fs.writeFileSync(absFilePath, unstagedLines.join('\n')); - - // stage a subset of lines from first hunk - const wrapper = mount(React.cloneElement(component, {filePath})); - - await assert.async.isTrue(wrapper.find('HunkView').exists()); - const opPromise0 = switchboard.getFinishStageOperationPromise(); - const hunkView0 = wrapper.find('HunkView').at(0); - hunkView0.find('LineView').at(1).find('.github-HunkView-line').simulate('mousedown', {button: 0, detail: 1}); - hunkView0.find('LineView').at(3).find('.github-HunkView-line').simulate('mousemove', {}); - window.dispatchEvent(new MouseEvent('mouseup')); - hunkView0.find('button.github-HunkView-stageButton').simulate('click'); - await opPromise0; - - repository.refresh(); - const expectedLines0 = originalLines.slice(); - expectedLines0.splice(1, 1, - 'this is a modified line', - 'this is a new line', - ); - assert.autocrlfEqual(await repository.readFileFromIndex('sample.js'), expectedLines0.join('\n')); - - // stage remaining lines in hunk - const updatePromise1 = switchboard.getChangePatchPromise(); - const unstagedFilePatch1 = await repository.getFilePatchForPath('sample.js'); - wrapper.setState({filePatch: unstagedFilePatch1}); - await updatePromise1; - - const opPromise1 = switchboard.getFinishStageOperationPromise(); - wrapper.find('HunkView').at(0).find('button.github-HunkView-stageButton').simulate('click'); - await opPromise1; - - repository.refresh(); - const expectedLines1 = originalLines.slice(); - expectedLines1.splice(1, 1, - 'this is a modified line', - 'this is a new line', - 'this is another new line', - ); - assert.autocrlfEqual(await repository.readFileFromIndex('sample.js'), expectedLines1.join('\n')); - - // unstage a subset of lines from the first hunk - const updatePromise2 = switchboard.getChangePatchPromise(); - const stagedFilePatch2 = await repository.getFilePatchForPath('sample.js', {staged: true}); - wrapper.setState({ - filePatch: stagedFilePatch2, - stagingStatus: 'staged', - }); - await updatePromise2; - - const hunkView2 = wrapper.find('HunkView').at(0); - hunkView2.find('LineView').at(1).find('.github-HunkView-line') - .simulate('mousedown', {button: 0, detail: 1}); - window.dispatchEvent(new MouseEvent('mouseup')); - hunkView2.find('LineView').at(2).find('.github-HunkView-line') - .simulate('mousedown', {button: 0, detail: 1, metaKey: true}); - window.dispatchEvent(new MouseEvent('mouseup')); - - const opPromise2 = switchboard.getFinishStageOperationPromise(); - hunkView2.find('button.github-HunkView-stageButton').simulate('click'); - await opPromise2; - - repository.refresh(); - const expectedLines2 = originalLines.slice(); - expectedLines2.splice(2, 0, - 'this is a new line', - 'this is another new line', - ); - assert.autocrlfEqual(await repository.readFileFromIndex('sample.js'), expectedLines2.join('\n')); - - // unstage the rest of the hunk - const updatePromise3 = switchboard.getChangePatchPromise(); - const stagedFilePatch3 = await repository.getFilePatchForPath('sample.js', {staged: true}); - wrapper.setState({ - filePatch: stagedFilePatch3, - }); - await updatePromise3; - - commandRegistry.dispatch(wrapper.find('FilePatchView').getDOMNode(), 'github:toggle-patch-selection-mode'); - - const opPromise3 = switchboard.getFinishStageOperationPromise(); - wrapper.find('HunkView').at(0).find('button.github-HunkView-stageButton').simulate('click'); - await opPromise3; - - assert.autocrlfEqual(await repository.readFileFromIndex('sample.js'), originalLines.join('\n')); - }); - - it('stages symlink change when staging added lines that depend on change', async function() { - const workingDirPath = await cloneRepository('symlinks'); - const repository = await buildRepository(workingDirPath); - await repository.getLoadPromise(); - - async function indexModeAndOid(filename) { + describe('handling symlink files', function() { + async function indexModeAndOid(repository, filename) { const output = await repository.git.exec(['ls-files', '-s', '--', filename]); if (output) { const parts = output.split(' '); @@ -476,167 +328,79 @@ describe('FilePatchController', function() { } } - const deletedSymlinkAddedFilePath = 'symlink.txt'; - fs.unlinkSync(path.join(workingDirPath, deletedSymlinkAddedFilePath)); - fs.writeFileSync(path.join(workingDirPath, deletedSymlinkAddedFilePath), 'qux\nfoo\nbar\nbaz\nzoo\n', 'utf8'); - - const component = ( - repository} - workingDirectoryPath={repository.getWorkingDirectoryPath()} - getSelectedStagingViewItems={getSelectedStagingViewItems} - uri={'some/uri'} - /> - ); - - const wrapper = mount(React.cloneElement(component, {filePath: deletedSymlinkAddedFilePath, initialStagingStatus: 'unstaged'})); - - assert.equal((await indexModeAndOid(deletedSymlinkAddedFilePath)).mode, '120000'); - - await assert.async.isTrue(wrapper.find('HunkView').exists()); - const opPromise0 = switchboard.getFinishStageOperationPromise(); - const hunkView0 = wrapper.find('HunkView').at(0); - hunkView0.find('LineView').at(1).find('.github-HunkView-line').simulate('mousedown', {button: 0, detail: 1}); - hunkView0.find('LineView').at(2).find('.github-HunkView-line').simulate('mousemove', {}); - window.dispatchEvent(new MouseEvent('mouseup')); - hunkView0.find('button.github-HunkView-stageButton').simulate('click'); - await opPromise0; - - repository.refresh(); - assert.autocrlfEqual(await repository.readFileFromIndex(deletedSymlinkAddedFilePath), 'foo\nbar\n'); - assert.equal((await indexModeAndOid(deletedSymlinkAddedFilePath)).mode, '100644'); - }); + it('stages symlink change when staging added lines that depend on change', async function() { + const workingDirPath = await cloneRepository('symlinks'); + const repository = await buildRepository(workingDirPath); - it('stages symlink change when staging deleted lines that depend on change', async function() { - const workingDirPath = await cloneRepository('symlinks'); - const repository = await buildRepository(workingDirPath); - await repository.getLoadPromise(); - async function indexModeAndOid(filename) { - const output = await repository.git.exec(['ls-files', '-s', '--', filename]); - if (output) { - const parts = output.split(' '); - return {mode: parts[0], oid: parts[1]}; - } else { - return null; - } - } + const deletedSymlinkAddedFilePath = 'symlink.txt'; + fs.unlinkSync(path.join(workingDirPath, deletedSymlinkAddedFilePath)); + fs.writeFileSync(path.join(workingDirPath, deletedSymlinkAddedFilePath), 'qux\nfoo\nbar\nbaz\nzoo\n', 'utf8'); - const deletedFileAddedSymlinkPath = 'a.txt'; - fs.unlinkSync(path.join(workingDirPath, deletedFileAddedSymlinkPath)); - fs.symlinkSync(path.join(workingDirPath, 'regular-file.txt'), path.join(workingDirPath, deletedFileAddedSymlinkPath)); - await repository.stageFiles([deletedFileAddedSymlinkPath]); - - const component = ( - repository} - workingDirectoryPath={repository.getWorkingDirectoryPath()} - getSelectedStagingViewItems={getSelectedStagingViewItems} - uri={'some/uri'} - /> - ); - - assert.equal((await indexModeAndOid(deletedFileAddedSymlinkPath)).mode, '120000'); - const wrapper = mount(React.cloneElement(component, {filePath: deletedFileAddedSymlinkPath, initialStagingStatus: 'staged'})); + const component = createComponent(repository, deletedSymlinkAddedFilePath); - await assert.async.isTrue(wrapper.find('HunkView').exists()); - const opPromise0 = switchboard.getFinishStageOperationPromise(); - const hunkView0 = wrapper.find('HunkView').at(0); - hunkView0.find('LineView').at(1).find('.github-HunkView-line').simulate('mousedown', {button: 0, detail: 1}); - hunkView0.find('LineView').at(2).find('.github-HunkView-line').simulate('mousemove', {}); - window.dispatchEvent(new MouseEvent('mouseup')); - hunkView0.find('button.github-HunkView-stageButton').simulate('click'); - await opPromise0; - - repository.refresh(); - assert.autocrlfEqual(await repository.readFileFromIndex(deletedFileAddedSymlinkPath), 'bar\nbaz\n'); - assert.equal((await indexModeAndOid(deletedFileAddedSymlinkPath)).mode, '100644'); - }); + const wrapper = mount(React.cloneElement(component, {filePath: deletedSymlinkAddedFilePath, initialStagingStatus: 'unstaged'})); - // https://github.com/atom/github/issues/417 - describe('when unstaging the last lines/hunks from a file', function() { - it('removes added files from index when last hunk is unstaged', async function() { - const absFilePath = path.join(workdirPath, 'new-file.txt'); - - fs.writeFileSync(absFilePath, 'foo\n'); - await repository.stageFiles(['new-file.txt']); - - const wrapper = mount(React.cloneElement(component, { - filePath: 'new-file.txt', - initialStagingStatus: 'staged', - })); + assert.equal((await indexModeAndOid(repository, deletedSymlinkAddedFilePath)).mode, '120000'); await assert.async.isTrue(wrapper.find('HunkView').exists()); + const opPromise0 = switchboard.getFinishStageOperationPromise(); + const hunkView0 = wrapper.find('HunkView').at(0); + hunkView0.find('LineView').at(1).find('.github-HunkView-line').simulate('mousedown', {button: 0, detail: 1}); + hunkView0.find('LineView').at(2).find('.github-HunkView-line').simulate('mousemove', {}); + window.dispatchEvent(new MouseEvent('mouseup')); + hunkView0.find('button.github-HunkView-stageButton').simulate('click'); + await opPromise0; - const opPromise = switchboard.getFinishStageOperationPromise(); - wrapper.find('HunkView').at(0).find('button.github-HunkView-stageButton').simulate('click'); - await opPromise; - - const stagedChanges = await repository.getStagedChanges(); - assert.equal(stagedChanges.length, 0); + repository.refresh(); + assert.autocrlfEqual(await repository.readFileFromIndex(deletedSymlinkAddedFilePath), 'foo\nbar\n'); + assert.equal((await indexModeAndOid(repository, deletedSymlinkAddedFilePath)).mode, '100644'); }); - it('removes added files from index when last lines are unstaged', async function() { - const absFilePath = path.join(workdirPath, 'new-file.txt'); - - fs.writeFileSync(absFilePath, 'foo\n'); - await repository.stageFiles(['new-file.txt']); + it('stages symlink change when staging deleted lines that depend on change', async function() { + const workingDirPath = await cloneRepository('symlinks'); + const repository = await buildRepository(workingDirPath); + await repository.getLoadPromise(); - const wrapper = mount(React.cloneElement(component, { - filePath: 'new-file.txt', - initialStagingStatus: 'staged', - })); + const deletedFileAddedSymlinkPath = 'a.txt'; + fs.unlinkSync(path.join(workingDirPath, deletedFileAddedSymlinkPath)); + fs.symlinkSync(path.join(workingDirPath, 'regular-file.txt'), path.join(workingDirPath, deletedFileAddedSymlinkPath)); + await repository.stageFiles([deletedFileAddedSymlinkPath]); - await assert.async.isTrue(wrapper.find('HunkView').exists()); + const component = createComponent(repository, deletedFileAddedSymlinkPath); - const viewNode = wrapper.find('FilePatchView').getDOMNode(); - commandRegistry.dispatch(viewNode, 'github:toggle-patch-selection-mode'); - commandRegistry.dispatch(viewNode, 'core:select-all'); + assert.equal((await indexModeAndOid(repository, deletedFileAddedSymlinkPath)).mode, '120000'); + const wrapper = mount(React.cloneElement(component, {filePath: deletedFileAddedSymlinkPath, initialStagingStatus: 'staged'})); - const opPromise = switchboard.getFinishStageOperationPromise(); - wrapper.find('HunkView').at(0).find('button.github-HunkView-stageButton').simulate('click'); - await opPromise; + await assert.async.isTrue(wrapper.find('HunkView').exists()); + const opPromise0 = switchboard.getFinishStageOperationPromise(); + const hunkView0 = wrapper.find('HunkView').at(0); + hunkView0.find('LineView').at(1).find('.github-HunkView-line').simulate('mousedown', {button: 0, detail: 1}); + hunkView0.find('LineView').at(2).find('.github-HunkView-line').simulate('mousemove', {}); + window.dispatchEvent(new MouseEvent('mouseup')); + hunkView0.find('button.github-HunkView-stageButton').simulate('click'); + await opPromise0; - const stagedChanges = await repository.getStagedChanges(); - assert.lengthOf(stagedChanges, 0); + repository.refresh(); + assert.autocrlfEqual(await repository.readFileFromIndex(deletedFileAddedSymlinkPath), 'bar\nbaz\n'); + assert.equal((await indexModeAndOid(repository, deletedFileAddedSymlinkPath)).mode, '100644'); }); }); - // https://github.com/atom/github/issues/341 - describe('when duplicate staging occurs', function() { - it('avoids patch conflicts with pending line staging operations', async function() { + describe('handling non-symlink changes', function() { + let workdirPath, repository, filePath, component; + beforeEach(async function() { + workdirPath = await cloneRepository('multi-line-file'); + repository = await buildRepository(workdirPath); + filePath = 'sample.js'; + component = createComponent(repository, filePath); + }); + + it('stages and unstages hunks when the stage button is clicked on hunk views with no individual lines selected', async function() { const absFilePath = path.join(workdirPath, filePath); const originalLines = fs.readFileSync(absFilePath, 'utf8').split('\n'); - - // write some unstaged changes const unstagedLines = originalLines.slice(); - unstagedLines.splice(1, 0, + unstagedLines.splice(1, 1, 'this is a modified line', 'this is a new line', 'this is another new line', @@ -646,56 +410,46 @@ describe('FilePatchController', function() { const wrapper = mount(React.cloneElement(component, {filePath})); + // selectNext() await assert.async.isTrue(wrapper.find('HunkView').exists()); - const hunkView0 = wrapper.find('HunkView').at(0); - hunkView0.find('LineView').at(1).find('.github-HunkView-line') - .simulate('mousedown', {button: 0, detail: 1}); - window.dispatchEvent(new MouseEvent('mouseup')); + commandRegistry.dispatch(wrapper.find('FilePatchView').getDOMNode(), 'core:move-down'); - // stage lines in rapid succession - // second stage action is a no-op since the first staging operation is in flight - const line1StagingPromise = switchboard.getFinishStageOperationPromise(); - hunkView0.find('.github-HunkView-stageButton').simulate('click'); - hunkView0.find('.github-HunkView-stageButton').simulate('click'); - await line1StagingPromise; - - const changePatchPromise = switchboard.getChangePatchPromise(); - - // assert that only line 1 has been staged - repository.refresh(); // clear the cached file patches - let expectedLines = originalLines.slice(); - expectedLines.splice(1, 0, - 'this is a modified line', - ); - let actualLines = await repository.readFileFromIndex(filePath); - assert.autocrlfEqual(actualLines, expectedLines.join('\n')); - await changePatchPromise; - - const hunkView1 = wrapper.find('HunkView').at(0); - hunkView1.find('LineView').at(2).find('.github-HunkView-line') - .simulate('mousedown', {button: 0, detail: 1}); - window.dispatchEvent(new MouseEvent('mouseup')); - const line2StagingPromise = switchboard.getFinishStageOperationPromise(); - hunkView1.find('.github-HunkView-stageButton').simulate('click'); - await line2StagingPromise; + await assert.async.isTrue(wrapper.find('HunkView').exists()); + const hunkView0 = wrapper.find('HunkView').at(0); + assert.isFalse(hunkView0.prop('isSelected')); + const opPromise0 = switchboard.getFinishStageOperationPromise(); + hunkView0.find('button.github-HunkView-stageButton').simulate('click'); + await opPromise0; - // assert that line 2 has now been staged - expectedLines = originalLines.slice(); - expectedLines.splice(1, 0, + const expectedStagedLines = originalLines.slice(); + expectedStagedLines.splice(1, 1, 'this is a modified line', 'this is a new line', + 'this is another new line', ); - actualLines = await repository.readFileFromIndex(filePath); - assert.autocrlfEqual(actualLines, expectedLines.join('\n')); + assert.autocrlfEqual(await repository.readFileFromIndex('sample.js'), expectedStagedLines.join('\n')); + const updatePromise0 = switchboard.getChangePatchPromise(); + const stagedFilePatch = await repository.getFilePatchForPath('sample.js', {staged: true}); + wrapper.setState({ + stagingStatus: 'staged', + filePatch: stagedFilePatch, + }); + await updatePromise0; + const hunkView1 = wrapper.find('HunkView').at(0); + const opPromise1 = switchboard.getFinishStageOperationPromise(); + hunkView1.find('button.github-HunkView-stageButton').simulate('click'); + await opPromise1; + assert.autocrlfEqual(await repository.readFileFromIndex('sample.js'), originalLines.join('\n')); }); - it('avoids patch conflicts with pending hunk staging operations', async function() { + it('stages and unstages individual lines when the stage button is clicked on a hunk with selected lines', async function() { const absFilePath = path.join(workdirPath, filePath); + const originalLines = fs.readFileSync(absFilePath, 'utf8').split('\n'); // write some unstaged changes const unstagedLines = originalLines.slice(); - unstagedLines.splice(1, 0, + unstagedLines.splice(1, 1, 'this is a modified line', 'this is a new line', 'this is another new line', @@ -703,45 +457,255 @@ describe('FilePatchController', function() { unstagedLines.splice(11, 2, 'this is a modified line'); fs.writeFileSync(absFilePath, unstagedLines.join('\n')); + // stage a subset of lines from first hunk const wrapper = mount(React.cloneElement(component, {filePath})); await assert.async.isTrue(wrapper.find('HunkView').exists()); + const opPromise0 = switchboard.getFinishStageOperationPromise(); + const hunkView0 = wrapper.find('HunkView').at(0); + hunkView0.find('LineView').at(1).find('.github-HunkView-line').simulate('mousedown', {button: 0, detail: 1}); + hunkView0.find('LineView').at(3).find('.github-HunkView-line').simulate('mousemove', {}); + window.dispatchEvent(new MouseEvent('mouseup')); + hunkView0.find('button.github-HunkView-stageButton').simulate('click'); + await opPromise0; - // ensure staging the same hunk twice does not cause issues - // second stage action is a no-op since the first staging operation is in flight - const hunk1StagingPromise = switchboard.getFinishStageOperationPromise(); - wrapper.find('HunkView').at(0).find('.github-HunkView-stageButton').simulate('click'); - wrapper.find('HunkView').at(0).find('.github-HunkView-stageButton').simulate('click'); - await hunk1StagingPromise; - - const patchPromise0 = switchboard.getChangePatchPromise(); - repository.refresh(); // clear the cached file patches - const modifiedFilePatch = await repository.getFilePatchForPath(filePath); - wrapper.setState({filePatch: modifiedFilePatch}); - await patchPromise0; - - let expectedLines = originalLines.slice(); - expectedLines.splice(1, 0, + repository.refresh(); + const expectedLines0 = originalLines.slice(); + expectedLines0.splice(1, 1, 'this is a modified line', 'this is a new line', - 'this is another new line', ); - let actualLines = await repository.readFileFromIndex(filePath); - assert.autocrlfEqual(actualLines, expectedLines.join('\n')); + assert.autocrlfEqual(await repository.readFileFromIndex('sample.js'), expectedLines0.join('\n')); - const hunk2StagingPromise = switchboard.getFinishStageOperationPromise(); - wrapper.find('HunkView').at(0).find('.github-HunkView-stageButton').simulate('click'); - await hunk2StagingPromise; + // stage remaining lines in hunk + const updatePromise1 = switchboard.getChangePatchPromise(); + const unstagedFilePatch1 = await repository.getFilePatchForPath('sample.js'); + wrapper.setState({filePatch: unstagedFilePatch1}); + await updatePromise1; - expectedLines = originalLines.slice(); - expectedLines.splice(1, 0, + const opPromise1 = switchboard.getFinishStageOperationPromise(); + wrapper.find('HunkView').at(0).find('button.github-HunkView-stageButton').simulate('click'); + await opPromise1; + + repository.refresh(); + const expectedLines1 = originalLines.slice(); + expectedLines1.splice(1, 1, 'this is a modified line', 'this is a new line', 'this is another new line', ); - expectedLines.splice(11, 2, 'this is a modified line'); - actualLines = await repository.readFileFromIndex(filePath); - assert.autocrlfEqual(actualLines, expectedLines.join('\n')); + assert.autocrlfEqual(await repository.readFileFromIndex('sample.js'), expectedLines1.join('\n')); + + // unstage a subset of lines from the first hunk + const updatePromise2 = switchboard.getChangePatchPromise(); + const stagedFilePatch2 = await repository.getFilePatchForPath('sample.js', {staged: true}); + wrapper.setState({ + filePatch: stagedFilePatch2, + stagingStatus: 'staged', + }); + await updatePromise2; + + const hunkView2 = wrapper.find('HunkView').at(0); + hunkView2.find('LineView').at(1).find('.github-HunkView-line') + .simulate('mousedown', {button: 0, detail: 1}); + window.dispatchEvent(new MouseEvent('mouseup')); + hunkView2.find('LineView').at(2).find('.github-HunkView-line') + .simulate('mousedown', {button: 0, detail: 1, metaKey: true}); + window.dispatchEvent(new MouseEvent('mouseup')); + + const opPromise2 = switchboard.getFinishStageOperationPromise(); + hunkView2.find('button.github-HunkView-stageButton').simulate('click'); + await opPromise2; + + repository.refresh(); + const expectedLines2 = originalLines.slice(); + expectedLines2.splice(2, 0, + 'this is a new line', + 'this is another new line', + ); + assert.autocrlfEqual(await repository.readFileFromIndex('sample.js'), expectedLines2.join('\n')); + + // unstage the rest of the hunk + const updatePromise3 = switchboard.getChangePatchPromise(); + const stagedFilePatch3 = await repository.getFilePatchForPath('sample.js', {staged: true}); + wrapper.setState({ + filePatch: stagedFilePatch3, + }); + await updatePromise3; + + commandRegistry.dispatch(wrapper.find('FilePatchView').getDOMNode(), 'github:toggle-patch-selection-mode'); + + const opPromise3 = switchboard.getFinishStageOperationPromise(); + wrapper.find('HunkView').at(0).find('button.github-HunkView-stageButton').simulate('click'); + await opPromise3; + + assert.autocrlfEqual(await repository.readFileFromIndex('sample.js'), originalLines.join('\n')); + }); + + // https://github.com/atom/github/issues/417 + describe('when unstaging the last lines/hunks from a file', function() { + it('removes added files from index when last hunk is unstaged', async function() { + const absFilePath = path.join(workdirPath, 'new-file.txt'); + + fs.writeFileSync(absFilePath, 'foo\n'); + await repository.stageFiles(['new-file.txt']); + + const wrapper = mount(React.cloneElement(component, { + filePath: 'new-file.txt', + initialStagingStatus: 'staged', + })); + + await assert.async.isTrue(wrapper.find('HunkView').exists()); + + const opPromise = switchboard.getFinishStageOperationPromise(); + wrapper.find('HunkView').at(0).find('button.github-HunkView-stageButton').simulate('click'); + await opPromise; + + const stagedChanges = await repository.getStagedChanges(); + assert.equal(stagedChanges.length, 0); + }); + + it('removes added files from index when last lines are unstaged', async function() { + const absFilePath = path.join(workdirPath, 'new-file.txt'); + + fs.writeFileSync(absFilePath, 'foo\n'); + await repository.stageFiles(['new-file.txt']); + + const wrapper = mount(React.cloneElement(component, { + filePath: 'new-file.txt', + initialStagingStatus: 'staged', + })); + + await assert.async.isTrue(wrapper.find('HunkView').exists()); + + const viewNode = wrapper.find('FilePatchView').getDOMNode(); + commandRegistry.dispatch(viewNode, 'github:toggle-patch-selection-mode'); + commandRegistry.dispatch(viewNode, 'core:select-all'); + + const opPromise = switchboard.getFinishStageOperationPromise(); + wrapper.find('HunkView').at(0).find('button.github-HunkView-stageButton').simulate('click'); + await opPromise; + + const stagedChanges = await repository.getStagedChanges(); + assert.lengthOf(stagedChanges, 0); + }); + }); + + // https://github.com/atom/github/issues/341 + describe('when duplicate staging occurs', function() { + it('avoids patch conflicts with pending line staging operations', async function() { + const absFilePath = path.join(workdirPath, filePath); + const originalLines = fs.readFileSync(absFilePath, 'utf8').split('\n'); + + // write some unstaged changes + const unstagedLines = originalLines.slice(); + unstagedLines.splice(1, 0, + 'this is a modified line', + 'this is a new line', + 'this is another new line', + ); + unstagedLines.splice(11, 2, 'this is a modified line'); + fs.writeFileSync(absFilePath, unstagedLines.join('\n')); + + const wrapper = mount(React.cloneElement(component, {filePath})); + + await assert.async.isTrue(wrapper.find('HunkView').exists()); + const hunkView0 = wrapper.find('HunkView').at(0); + hunkView0.find('LineView').at(1).find('.github-HunkView-line') + .simulate('mousedown', {button: 0, detail: 1}); + window.dispatchEvent(new MouseEvent('mouseup')); + + // stage lines in rapid succession + // second stage action is a no-op since the first staging operation is in flight + const line1StagingPromise = switchboard.getFinishStageOperationPromise(); + hunkView0.find('.github-HunkView-stageButton').simulate('click'); + hunkView0.find('.github-HunkView-stageButton').simulate('click'); + await line1StagingPromise; + + const changePatchPromise = switchboard.getChangePatchPromise(); + + // assert that only line 1 has been staged + repository.refresh(); // clear the cached file patches + let expectedLines = originalLines.slice(); + expectedLines.splice(1, 0, + 'this is a modified line', + ); + let actualLines = await repository.readFileFromIndex(filePath); + assert.autocrlfEqual(actualLines, expectedLines.join('\n')); + await changePatchPromise; + + const hunkView1 = wrapper.find('HunkView').at(0); + hunkView1.find('LineView').at(2).find('.github-HunkView-line') + .simulate('mousedown', {button: 0, detail: 1}); + window.dispatchEvent(new MouseEvent('mouseup')); + const line2StagingPromise = switchboard.getFinishStageOperationPromise(); + hunkView1.find('.github-HunkView-stageButton').simulate('click'); + await line2StagingPromise; + + // assert that line 2 has now been staged + expectedLines = originalLines.slice(); + expectedLines.splice(1, 0, + 'this is a modified line', + 'this is a new line', + ); + actualLines = await repository.readFileFromIndex(filePath); + assert.autocrlfEqual(actualLines, expectedLines.join('\n')); + }); + + it('avoids patch conflicts with pending hunk staging operations', async function() { + const absFilePath = path.join(workdirPath, filePath); + const originalLines = fs.readFileSync(absFilePath, 'utf8').split('\n'); + + // write some unstaged changes + const unstagedLines = originalLines.slice(); + unstagedLines.splice(1, 0, + 'this is a modified line', + 'this is a new line', + 'this is another new line', + ); + unstagedLines.splice(11, 2, 'this is a modified line'); + fs.writeFileSync(absFilePath, unstagedLines.join('\n')); + + const wrapper = mount(React.cloneElement(component, {filePath})); + + await assert.async.isTrue(wrapper.find('HunkView').exists()); + + // ensure staging the same hunk twice does not cause issues + // second stage action is a no-op since the first staging operation is in flight + const hunk1StagingPromise = switchboard.getFinishStageOperationPromise(); + wrapper.find('HunkView').at(0).find('.github-HunkView-stageButton').simulate('click'); + wrapper.find('HunkView').at(0).find('.github-HunkView-stageButton').simulate('click'); + await hunk1StagingPromise; + + const patchPromise0 = switchboard.getChangePatchPromise(); + repository.refresh(); // clear the cached file patches + const modifiedFilePatch = await repository.getFilePatchForPath(filePath); + wrapper.setState({filePatch: modifiedFilePatch}); + await patchPromise0; + + let expectedLines = originalLines.slice(); + expectedLines.splice(1, 0, + 'this is a modified line', + 'this is a new line', + 'this is another new line', + ); + let actualLines = await repository.readFileFromIndex(filePath); + assert.autocrlfEqual(actualLines, expectedLines.join('\n')); + + const hunk2StagingPromise = switchboard.getFinishStageOperationPromise(); + wrapper.find('HunkView').at(0).find('.github-HunkView-stageButton').simulate('click'); + await hunk2StagingPromise; + + expectedLines = originalLines.slice(); + expectedLines.splice(1, 0, + 'this is a modified line', + 'this is a new line', + 'this is another new line', + ); + expectedLines.splice(11, 2, 'this is a modified line'); + actualLines = await repository.readFileFromIndex(filePath); + assert.autocrlfEqual(actualLines, expectedLines.join('\n')); + }); }); }); }); From b0a0847e529d9b1d69b29173960cc023e094e6fd Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Wed, 10 Jan 2018 23:17:22 -0800 Subject: [PATCH 0159/5882] Fix FilePatchView props for symlinkChange --- 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 4904004c17..faa467619e 100644 --- a/lib/views/file-patch-view.js +++ b/lib/views/file-patch-view.js @@ -25,7 +25,8 @@ export default class FilePatchView extends React.Component { symlinkChange: PropTypes.shape({ oldSymlink: PropTypes.string, newSymlink: PropTypes.string, - status: PropTypes.string.isRequired, + typechange: PropTypes.bool, + filePatchStatus: PropTypes.string, }), stagingStatus: PropTypes.oneOf(['unstaged', 'staged']).isRequired, isPartiallyStaged: PropTypes.bool.isRequired, From e830800ab64372fcfe4825ee095013ae0a6db2a8 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Wed, 10 Jan 2018 23:19:57 -0800 Subject: [PATCH 0160/5882] Test file creation/deletion when staging all lines in typechange file --- .../controllers/file-patch-controller.test.js | 58 ++++++++++++++++++- 1 file changed, 56 insertions(+), 2 deletions(-) diff --git a/test/controllers/file-patch-controller.test.js b/test/controllers/file-patch-controller.test.js index f531f6cb96..11509b8f3f 100644 --- a/test/controllers/file-patch-controller.test.js +++ b/test/controllers/file-patch-controller.test.js @@ -332,7 +332,6 @@ describe('FilePatchController', function() { const workingDirPath = await cloneRepository('symlinks'); const repository = await buildRepository(workingDirPath); - const deletedSymlinkAddedFilePath = 'symlink.txt'; fs.unlinkSync(path.join(workingDirPath, deletedSymlinkAddedFilePath)); fs.writeFileSync(path.join(workingDirPath, deletedSymlinkAddedFilePath), 'qux\nfoo\nbar\nbaz\nzoo\n', 'utf8'); @@ -360,7 +359,6 @@ describe('FilePatchController', function() { it('stages symlink change when staging deleted lines that depend on change', async function() { const workingDirPath = await cloneRepository('symlinks'); const repository = await buildRepository(workingDirPath); - await repository.getLoadPromise(); const deletedFileAddedSymlinkPath = 'a.txt'; fs.unlinkSync(path.join(workingDirPath, deletedFileAddedSymlinkPath)); @@ -385,6 +383,62 @@ describe('FilePatchController', function() { assert.autocrlfEqual(await repository.readFileFromIndex(deletedFileAddedSymlinkPath), 'bar\nbaz\n'); assert.equal((await indexModeAndOid(repository, deletedFileAddedSymlinkPath)).mode, '100644'); }); + + it('stages file deletion when all deleted files are staged', async function() { + const workingDirPath = await cloneRepository('symlinks'); + const repository = await buildRepository(workingDirPath); + await repository.getLoadPromise(); + + const deletedFileAddedSymlinkPath = 'a.txt'; + fs.unlinkSync(path.join(workingDirPath, deletedFileAddedSymlinkPath)); + fs.symlinkSync(path.join(workingDirPath, 'regular-file.txt'), path.join(workingDirPath, deletedFileAddedSymlinkPath)); + + const component = createComponent(repository, deletedFileAddedSymlinkPath); + const wrapper = mount(React.cloneElement(component, {filePath: deletedFileAddedSymlinkPath, initialStagingStatus: 'unstaged'})); + + assert.equal((await indexModeAndOid(repository, deletedFileAddedSymlinkPath)).mode, '100644'); + + await assert.async.isTrue(wrapper.find('HunkView').exists()); + const opPromise0 = switchboard.getFinishStageOperationPromise(); + const hunkView0 = wrapper.find('HunkView').at(0); + hunkView0.find('.github-HunkView-title').simulate('click'); + hunkView0.find('button.github-HunkView-stageButton').simulate('click'); + await opPromise0; + + repository.refresh(); + assert.isNull(await indexModeAndOid(repository, deletedFileAddedSymlinkPath)); // File is not on index, file deletion has been staged + const {stagedFiles, unstagedFiles} = await repository.getStatusesForChangedFiles(); + assert.equal(unstagedFiles[deletedFileAddedSymlinkPath], 'added'); + assert.equal(stagedFiles[deletedFileAddedSymlinkPath], 'deleted'); + }); + + it('unstages file creation when all added files are unstaged', async function() { + const workingDirPath = await cloneRepository('symlinks'); + const repository = await buildRepository(workingDirPath); + + const deletedSymlinkAddedFilePath = 'symlink.txt'; + fs.unlinkSync(path.join(workingDirPath, deletedSymlinkAddedFilePath)); + fs.writeFileSync(path.join(workingDirPath, deletedSymlinkAddedFilePath), 'qux\nfoo\nbar\nbaz\nzoo\n', 'utf8'); + await repository.stageFiles([deletedSymlinkAddedFilePath]); + + const component = createComponent(repository, deletedSymlinkAddedFilePath); + const wrapper = mount(React.cloneElement(component, {filePath: deletedSymlinkAddedFilePath, initialStagingStatus: 'staged'})); + + assert.equal((await indexModeAndOid(repository, deletedSymlinkAddedFilePath)).mode, '100644'); + + await assert.async.isTrue(wrapper.find('HunkView').exists()); + const opPromise0 = switchboard.getFinishStageOperationPromise(); + const hunkView0 = wrapper.find('HunkView').at(0); + hunkView0.find('.github-HunkView-title').simulate('click'); + hunkView0.find('button.github-HunkView-stageButton').simulate('click'); + await opPromise0; + + repository.refresh(); + assert.isNull(await indexModeAndOid(repository, deletedSymlinkAddedFilePath)); // File is not on index, file creation has been unstaged + const {stagedFiles, unstagedFiles} = await repository.getStatusesForChangedFiles(); + assert.equal(unstagedFiles[deletedSymlinkAddedFilePath], 'added'); + assert.equal(stagedFiles[deletedSymlinkAddedFilePath], 'deleted'); + }); }); describe('handling non-symlink changes', function() { From 2774bb9dc1d95d5371e6369b521298b1244956fd Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 11 Jan 2018 09:30:34 -0800 Subject: [PATCH 0161/5882] Fix tests on Travis (#1279) --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index ea0cc36c84..4de0cdab4f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: node_js -node_js: '6.9.4' -sudo: false +node_js: '8' +sudo: true os: linux dist: trusty From ccbacdbb11f2ceaefb97ae5974f2694b6c28c008 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Thu, 11 Jan 2018 14:28:38 -0800 Subject: [PATCH 0162/5882] Fix test descriptions --- test/controllers/file-patch-controller.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/controllers/file-patch-controller.test.js b/test/controllers/file-patch-controller.test.js index 11509b8f3f..df40da19b1 100644 --- a/test/controllers/file-patch-controller.test.js +++ b/test/controllers/file-patch-controller.test.js @@ -384,7 +384,7 @@ describe('FilePatchController', function() { assert.equal((await indexModeAndOid(repository, deletedFileAddedSymlinkPath)).mode, '100644'); }); - it('stages file deletion when all deleted files are staged', async function() { + it('stages file deletion when all deleted lines are staged', async function() { const workingDirPath = await cloneRepository('symlinks'); const repository = await buildRepository(workingDirPath); await repository.getLoadPromise(); @@ -412,7 +412,7 @@ describe('FilePatchController', function() { assert.equal(stagedFiles[deletedFileAddedSymlinkPath], 'deleted'); }); - it('unstages file creation when all added files are unstaged', async function() { + it('unstages file creation when all added lines are unstaged', async function() { const workingDirPath = await cloneRepository('symlinks'); const repository = await buildRepository(workingDirPath); From 31d2c08041f3469bdb25e0b36d6abd1fc916d206 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Thu, 11 Jan 2018 14:37:45 -0800 Subject: [PATCH 0163/5882] Fix patch headers when file names are different --- lib/models/file-patch.js | 4 +++- test/models/file-patch.test.js | 24 +++++++++++------------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/lib/models/file-patch.js b/lib/models/file-patch.js index 52ef713b96..a6d20f57c0 100644 --- a/lib/models/file-patch.js +++ b/lib/models/file-patch.js @@ -316,7 +316,9 @@ export default class FilePatch { } getHeaderString() { - let header = `diff --git a/${toGitPathSep(this.getPath())} b/${toGitPathSep(this.getPath())}`; + const fromPath = this.getOldPath() || this.getNewPath(); + const toPath = this.getNewPath() || this.getOldPath(); + let header = `diff --git a/${toGitPathSep(fromPath)} b/${toGitPathSep(toPath)}`; header += '\n'; if (this.getStatus() === 'added') { header += `new file mode ${this.getNewMode()}`; diff --git a/test/models/file-patch.test.js b/test/models/file-patch.test.js index 4a0dad9f34..30667570d3 100644 --- a/test/models/file-patch.test.js +++ b/test/models/file-patch.test.js @@ -351,20 +351,18 @@ describe('FilePatch', function() { }); }); - if (process.platform === 'win32') { - describe('getHeaderString()', function() { - it('formats paths with git path separators', function() { - const oldPath = path.join('foo', 'bar', 'old.js'); - const newPath = path.join('baz', 'qux', 'new.js'); + describe('getHeaderString()', function() { + it('formats paths with git path separators', function() { + const oldPath = path.join('foo', 'bar', 'old.js'); + const newPath = path.join('baz', 'qux', 'new.js'); - const patch = createFilePatch(oldPath, newPath, 'modified', []); - assert.equal(patch.getHeaderString(), dedent` - diff --git a/foo/bar/old.js b/baz/qux/new.js - --- a/foo/bar/old.js - +++ b/baz/qux/new.js + const patch = createFilePatch(oldPath, newPath, 'modified', []); + assert.equal(patch.getHeaderString(), dedent` + diff --git a/foo/bar/old.js b/baz/qux/new.js + --- a/foo/bar/old.js + +++ b/baz/qux/new.js - `); - }); + `); }); - } + }); }); From 85d8ec98b80ba428f9703a6d81253c662bc09c36 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Thu, 11 Jan 2018 14:56:50 -0800 Subject: [PATCH 0164/5882] WIP fix tests on appveyor --- test/controllers/file-patch-controller.test.js | 12 ++++++++---- test/helpers.js | 2 ++ test/models/file-patch.test.js | 12 +++++++----- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/test/controllers/file-patch-controller.test.js b/test/controllers/file-patch-controller.test.js index df40da19b1..d8793d6023 100644 --- a/test/controllers/file-patch-controller.test.js +++ b/test/controllers/file-patch-controller.test.js @@ -73,7 +73,7 @@ function createComponent(repository, filePath) { ); } -describe('FilePatchController', function() { +describe.only('FilePatchController', function() { afterEach(function() { atomEnv.destroy(); }); @@ -317,7 +317,7 @@ describe('FilePatchController', function() { }); describe('integration tests', function() { - describe('handling symlink files', function() { + describe.only('handling symlink files', function() { async function indexModeAndOid(repository, filename) { const output = await repository.git.exec(['ls-files', '-s', '--', filename]); if (output) { @@ -328,10 +328,12 @@ describe('FilePatchController', function() { } } - it('stages symlink change when staging added lines that depend on change', async function() { + it.only('stages symlink change when staging added lines that depend on change', async function() { const workingDirPath = await cloneRepository('symlinks'); const repository = await buildRepository(workingDirPath); + repository.git.exec(['config', 'core.symlinks', 'true']); + const deletedSymlinkAddedFilePath = 'symlink.txt'; fs.unlinkSync(path.join(workingDirPath, deletedSymlinkAddedFilePath)); fs.writeFileSync(path.join(workingDirPath, deletedSymlinkAddedFilePath), 'qux\nfoo\nbar\nbaz\nzoo\n', 'utf8'); @@ -412,10 +414,12 @@ describe('FilePatchController', function() { assert.equal(stagedFiles[deletedFileAddedSymlinkPath], 'deleted'); }); - it('unstages file creation when all added lines are unstaged', async function() { + it.only('unstages file creation when all added lines are unstaged', async function() { const workingDirPath = await cloneRepository('symlinks'); const repository = await buildRepository(workingDirPath); + repository.git.exec(['config', 'core.symlinks', 'true']); + const deletedSymlinkAddedFilePath = 'symlink.txt'; fs.unlinkSync(path.join(workingDirPath, deletedSymlinkAddedFilePath)); fs.writeFileSync(path.join(workingDirPath, deletedSymlinkAddedFilePath), 'qux\nfoo\nbar\nbaz\nzoo\n', 'utf8'); diff --git a/test/helpers.js b/test/helpers.js index 79dca63ac1..f6edf07068 100644 --- a/test/helpers.js +++ b/test/helpers.js @@ -12,6 +12,8 @@ import WorkerManager from '../lib/worker-manager'; import ContextMenuInterceptor from '../lib/context-menu-interceptor'; import getRepoPipelineManager from '../lib/get-repo-pipeline-manager'; +export {toGitPathSep} from '../lib/helpers'; + assert.autocrlfEqual = (actual, expected, ...args) => { const newActual = actual.replace(/\r\n/g, '\n'); const newExpected = expected.replace(/\r\n/g, '\n'); diff --git a/test/models/file-patch.test.js b/test/models/file-patch.test.js index 30667570d3..07d7387ec1 100644 --- a/test/models/file-patch.test.js +++ b/test/models/file-patch.test.js @@ -1,4 +1,4 @@ -import {cloneRepository, buildRepository} from '../helpers'; +import {cloneRepository, buildRepository, toGitPathSep} from '../helpers'; import path from 'path'; import fs from 'fs'; import dedent from 'dedent-js'; @@ -291,10 +291,12 @@ describe('FilePatch', function() { `); }); - it('handles typechange patches for a symlink replaced with a file', async function() { + it.only('handles typechange patches for a symlink replaced with a file', async function() { const workdirPath = await cloneRepository('symlinks'); const repository = await buildRepository(workdirPath); + repository.git.exec(['config', 'core.symlinks', 'true']); + const deletedSymlinkAddedFilePath = 'symlink.txt'; fs.unlinkSync(path.join(workdirPath, deletedSymlinkAddedFilePath)); fs.writeFileSync(path.join(workdirPath, deletedSymlinkAddedFilePath), 'qux\nfoo\nbar\n', 'utf8'); @@ -320,7 +322,7 @@ describe('FilePatch', function() { `); }); - it('handles typechange patches for a file replaced with a symlink', async function() { + it.only('handles typechange patches for a file replaced with a symlink', async function() { const workdirPath = await cloneRepository('symlinks'); const repository = await buildRepository(workdirPath); @@ -344,14 +346,14 @@ describe('FilePatch', function() { --- /dev/null +++ b/a.txt @@ -0,0 +1 @@ - +${path.join(workdirPath, 'regular-file.txt')} + +${toGitPathSep(path.join(workdirPath, 'regular-file.txt'))} \\ No newline at end of file `); }); }); - describe('getHeaderString()', function() { + describe.only('getHeaderString()', function() { it('formats paths with git path separators', function() { const oldPath = path.join('foo', 'bar', 'old.js'); const newPath = path.join('baz', 'qux', 'new.js'); From 2894ec93bc32ce08dcdd73f0207bb7be2e829914 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Thu, 11 Jan 2018 15:02:49 -0800 Subject: [PATCH 0165/5882] Remove `.only`s --- test/controllers/file-patch-controller.test.js | 8 ++++---- test/models/file-patch.test.js | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/test/controllers/file-patch-controller.test.js b/test/controllers/file-patch-controller.test.js index d8793d6023..16e61bef0c 100644 --- a/test/controllers/file-patch-controller.test.js +++ b/test/controllers/file-patch-controller.test.js @@ -73,7 +73,7 @@ function createComponent(repository, filePath) { ); } -describe.only('FilePatchController', function() { +describe('FilePatchController', function() { afterEach(function() { atomEnv.destroy(); }); @@ -317,7 +317,7 @@ describe.only('FilePatchController', function() { }); describe('integration tests', function() { - describe.only('handling symlink files', function() { + describe('handling symlink files', function() { async function indexModeAndOid(repository, filename) { const output = await repository.git.exec(['ls-files', '-s', '--', filename]); if (output) { @@ -328,7 +328,7 @@ describe.only('FilePatchController', function() { } } - it.only('stages symlink change when staging added lines that depend on change', async function() { + it('stages symlink change when staging added lines that depend on change', async function() { const workingDirPath = await cloneRepository('symlinks'); const repository = await buildRepository(workingDirPath); @@ -414,7 +414,7 @@ describe.only('FilePatchController', function() { assert.equal(stagedFiles[deletedFileAddedSymlinkPath], 'deleted'); }); - it.only('unstages file creation when all added lines are unstaged', async function() { + it('unstages file creation when all added lines are unstaged', async function() { const workingDirPath = await cloneRepository('symlinks'); const repository = await buildRepository(workingDirPath); diff --git a/test/models/file-patch.test.js b/test/models/file-patch.test.js index 07d7387ec1..89ae666f16 100644 --- a/test/models/file-patch.test.js +++ b/test/models/file-patch.test.js @@ -291,7 +291,7 @@ describe('FilePatch', function() { `); }); - it.only('handles typechange patches for a symlink replaced with a file', async function() { + it('handles typechange patches for a symlink replaced with a file', async function() { const workdirPath = await cloneRepository('symlinks'); const repository = await buildRepository(workdirPath); @@ -322,7 +322,7 @@ describe('FilePatch', function() { `); }); - it.only('handles typechange patches for a file replaced with a symlink', async function() { + it('handles typechange patches for a file replaced with a symlink', async function() { const workdirPath = await cloneRepository('symlinks'); const repository = await buildRepository(workdirPath); @@ -353,7 +353,7 @@ describe('FilePatch', function() { }); }); - describe.only('getHeaderString()', function() { + describe('getHeaderString()', function() { it('formats paths with git path separators', function() { const oldPath = path.join('foo', 'bar', 'old.js'); const newPath = path.join('baz', 'qux', 'new.js'); From 0f87db52856bbd0fee8c8146ff1936b9fe553316 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Thu, 11 Jan 2018 15:08:13 -0800 Subject: [PATCH 0166/5882] Ensure git path separators for symlink real path --- lib/git-shell-out-strategy.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/git-shell-out-strategy.js b/lib/git-shell-out-strategy.js index c24835251a..4d88645da4 100644 --- a/lib/git-shell-out-strategy.js +++ b/lib/git-shell-out-strategy.js @@ -794,8 +794,7 @@ function buildAddedFilePatch(filePath, contents, mode, realpath) { const noNewLine = contents[contents.length - 1] !== '\n'; let lines; if (mode === '120000') { - // TODO: normalize path separators? - lines = [`+${realpath}`, '\\ No newline at end of file']; + lines = [`+${toGitPathSep(realpath)}`, '\\ No newline at end of file']; } else { lines = contents.trim().split(LINE_ENDING_REGEX).map(line => `+${line}`); } From 192bd0822e4723167515f269d57b3e006e58825e Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Thu, 11 Jan 2018 15:09:06 -0800 Subject: [PATCH 0167/5882] :fire: commented out lines in tests --- test/models/repository.test.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/models/repository.test.js b/test/models/repository.test.js index 2e3ad5be29..c8e7a7bc70 100644 --- a/test/models/repository.test.js +++ b/test/models/repository.test.js @@ -313,7 +313,6 @@ describe('Repository', function() { assert.isNull(await indexModeAndOid(deletedSymlinkAddedFilePath)); const unstagedFilePatch = await repo.getFilePatchForPath(deletedSymlinkAddedFilePath, {staged: false}); assert.equal(unstagedFilePatch.getStatus(), 'added'); - // assert.equal(unstagedFilePatch.toString(), '@@ -0,0 +1,3 @@\n+qux\n+foo\n+bar\n'); assert.equal(unstagedFilePatch.toString(), dedent` diff --git a/symlink.txt b/symlink.txt new file mode 100644 @@ -333,7 +332,6 @@ describe('Repository', function() { assert.isNull(await indexModeAndOid(deletedFileAddedSymlinkPath)); const stagedFilePatch = await repo.getFilePatchForPath(deletedFileAddedSymlinkPath, {staged: true}); assert.equal(stagedFilePatch.getStatus(), 'deleted'); - // assert.equal(stagedFilePatch.toString(), '@@ -1,4 +0,0 @@\n-foo\n-bar\n-baz\n-\n'); assert.equal(stagedFilePatch.toString(), dedent` diff --git a/a.txt b/a.txt deleted file mode 100644 From 5795121dea2bdedbf5ef9a3d59c783353e405506 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Thu, 11 Jan 2018 15:39:16 -0800 Subject: [PATCH 0168/5882] Remove unnecessary git files from new `repo-symlinks` fixture --- .../repo-symlinks/dot-git/COMMIT_EDITMSG | 1 - .../repo-symlinks/dot-git/description | 1 - .../dot-git/hooks/applypatch-msg.sample | 15 -- .../dot-git/hooks/commit-msg.sample | 24 --- .../dot-git/hooks/post-update.sample | 8 - .../dot-git/hooks/pre-applypatch.sample | 14 -- .../dot-git/hooks/pre-commit.sample | 49 ----- .../dot-git/hooks/pre-push.sample | 53 ------ .../dot-git/hooks/pre-rebase.sample | 169 ------------------ .../dot-git/hooks/pre-receive.sample | 24 --- .../dot-git/hooks/prepare-commit-msg.sample | 36 ---- .../repo-symlinks/dot-git/hooks/update.sample | 128 ------------- .../repo-symlinks/dot-git/info/exclude | 6 - test/fixtures/repo-symlinks/dot-git/logs/HEAD | 1 - .../dot-git/logs/refs/heads/master | 1 - 15 files changed, 530 deletions(-) delete mode 100644 test/fixtures/repo-symlinks/dot-git/COMMIT_EDITMSG delete mode 100644 test/fixtures/repo-symlinks/dot-git/description delete mode 100755 test/fixtures/repo-symlinks/dot-git/hooks/applypatch-msg.sample delete mode 100755 test/fixtures/repo-symlinks/dot-git/hooks/commit-msg.sample delete mode 100755 test/fixtures/repo-symlinks/dot-git/hooks/post-update.sample delete mode 100755 test/fixtures/repo-symlinks/dot-git/hooks/pre-applypatch.sample delete mode 100755 test/fixtures/repo-symlinks/dot-git/hooks/pre-commit.sample delete mode 100755 test/fixtures/repo-symlinks/dot-git/hooks/pre-push.sample delete mode 100755 test/fixtures/repo-symlinks/dot-git/hooks/pre-rebase.sample delete mode 100755 test/fixtures/repo-symlinks/dot-git/hooks/pre-receive.sample delete mode 100755 test/fixtures/repo-symlinks/dot-git/hooks/prepare-commit-msg.sample delete mode 100755 test/fixtures/repo-symlinks/dot-git/hooks/update.sample delete mode 100644 test/fixtures/repo-symlinks/dot-git/info/exclude delete mode 100644 test/fixtures/repo-symlinks/dot-git/logs/HEAD delete mode 100644 test/fixtures/repo-symlinks/dot-git/logs/refs/heads/master diff --git a/test/fixtures/repo-symlinks/dot-git/COMMIT_EDITMSG b/test/fixtures/repo-symlinks/dot-git/COMMIT_EDITMSG deleted file mode 100644 index 5852f44639..0000000000 --- a/test/fixtures/repo-symlinks/dot-git/COMMIT_EDITMSG +++ /dev/null @@ -1 +0,0 @@ -Initial commit diff --git a/test/fixtures/repo-symlinks/dot-git/description b/test/fixtures/repo-symlinks/dot-git/description deleted file mode 100644 index 498b267a8c..0000000000 --- a/test/fixtures/repo-symlinks/dot-git/description +++ /dev/null @@ -1 +0,0 @@ -Unnamed repository; edit this file 'description' to name the repository. diff --git a/test/fixtures/repo-symlinks/dot-git/hooks/applypatch-msg.sample b/test/fixtures/repo-symlinks/dot-git/hooks/applypatch-msg.sample deleted file mode 100755 index a5d7b84a67..0000000000 --- a/test/fixtures/repo-symlinks/dot-git/hooks/applypatch-msg.sample +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/sh -# -# An example hook script to check the commit log message taken by -# applypatch from an e-mail message. -# -# The hook should exit with non-zero status after issuing an -# appropriate message if it wants to stop the commit. The hook is -# allowed to edit the commit message file. -# -# To enable this hook, rename this file to "applypatch-msg". - -. git-sh-setup -commitmsg="$(git rev-parse --git-path hooks/commit-msg)" -test -x "$commitmsg" && exec "$commitmsg" ${1+"$@"} -: diff --git a/test/fixtures/repo-symlinks/dot-git/hooks/commit-msg.sample b/test/fixtures/repo-symlinks/dot-git/hooks/commit-msg.sample deleted file mode 100755 index b58d1184a9..0000000000 --- a/test/fixtures/repo-symlinks/dot-git/hooks/commit-msg.sample +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/sh -# -# An example hook script to check the commit log message. -# Called by "git commit" with one argument, the name of the file -# that has the commit message. The hook should exit with non-zero -# status after issuing an appropriate message if it wants to stop the -# commit. The hook is allowed to edit the commit message file. -# -# To enable this hook, rename this file to "commit-msg". - -# Uncomment the below to add a Signed-off-by line to the message. -# Doing this in a hook is a bad idea in general, but the prepare-commit-msg -# hook is more suited to it. -# -# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') -# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1" - -# This example catches duplicate Signed-off-by lines. - -test "" = "$(grep '^Signed-off-by: ' "$1" | - sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || { - echo >&2 Duplicate Signed-off-by lines. - exit 1 -} diff --git a/test/fixtures/repo-symlinks/dot-git/hooks/post-update.sample b/test/fixtures/repo-symlinks/dot-git/hooks/post-update.sample deleted file mode 100755 index ec17ec1939..0000000000 --- a/test/fixtures/repo-symlinks/dot-git/hooks/post-update.sample +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/sh -# -# An example hook script to prepare a packed repository for use over -# dumb transports. -# -# To enable this hook, rename this file to "post-update". - -exec git update-server-info diff --git a/test/fixtures/repo-symlinks/dot-git/hooks/pre-applypatch.sample b/test/fixtures/repo-symlinks/dot-git/hooks/pre-applypatch.sample deleted file mode 100755 index 4142082bcb..0000000000 --- a/test/fixtures/repo-symlinks/dot-git/hooks/pre-applypatch.sample +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/sh -# -# An example hook script to verify what is about to be committed -# by applypatch from an e-mail message. -# -# The hook should exit with non-zero status after issuing an -# appropriate message if it wants to stop the commit. -# -# To enable this hook, rename this file to "pre-applypatch". - -. git-sh-setup -precommit="$(git rev-parse --git-path hooks/pre-commit)" -test -x "$precommit" && exec "$precommit" ${1+"$@"} -: diff --git a/test/fixtures/repo-symlinks/dot-git/hooks/pre-commit.sample b/test/fixtures/repo-symlinks/dot-git/hooks/pre-commit.sample deleted file mode 100755 index 68d62d5446..0000000000 --- a/test/fixtures/repo-symlinks/dot-git/hooks/pre-commit.sample +++ /dev/null @@ -1,49 +0,0 @@ -#!/bin/sh -# -# An example hook script to verify what is about to be committed. -# Called by "git commit" with no arguments. The hook should -# exit with non-zero status after issuing an appropriate message if -# it wants to stop the commit. -# -# To enable this hook, rename this file to "pre-commit". - -if git rev-parse --verify HEAD >/dev/null 2>&1 -then - against=HEAD -else - # Initial commit: diff against an empty tree object - against=4b825dc642cb6eb9a060e54bf8d69288fbee4904 -fi - -# If you want to allow non-ASCII filenames set this variable to true. -allownonascii=$(git config --bool hooks.allownonascii) - -# Redirect output to stderr. -exec 1>&2 - -# Cross platform projects tend to avoid non-ASCII filenames; prevent -# them from being added to the repository. We exploit the fact that the -# printable range starts at the space character and ends with tilde. -if [ "$allownonascii" != "true" ] && - # Note that the use of brackets around a tr range is ok here, (it's - # even required, for portability to Solaris 10's /usr/bin/tr), since - # the square bracket bytes happen to fall in the designated range. - test $(git diff --cached --name-only --diff-filter=A -z $against | - LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0 -then - cat <<\EOF -Error: Attempt to add a non-ASCII file name. - -This can cause problems if you want to work with people on other platforms. - -To be portable it is advisable to rename the file. - -If you know what you are doing you can disable this check using: - - git config hooks.allownonascii true -EOF - exit 1 -fi - -# If there are whitespace errors, print the offending file names and fail. -exec git diff-index --check --cached $against -- diff --git a/test/fixtures/repo-symlinks/dot-git/hooks/pre-push.sample b/test/fixtures/repo-symlinks/dot-git/hooks/pre-push.sample deleted file mode 100755 index 6187dbf439..0000000000 --- a/test/fixtures/repo-symlinks/dot-git/hooks/pre-push.sample +++ /dev/null @@ -1,53 +0,0 @@ -#!/bin/sh - -# An example hook script to verify what is about to be pushed. Called by "git -# push" after it has checked the remote status, but before anything has been -# pushed. If this script exits with a non-zero status nothing will be pushed. -# -# This hook is called with the following parameters: -# -# $1 -- Name of the remote to which the push is being done -# $2 -- URL to which the push is being done -# -# If pushing without using a named remote those arguments will be equal. -# -# Information about the commits which are being pushed is supplied as lines to -# the standard input in the form: -# -# -# -# This sample shows how to prevent push of commits where the log message starts -# with "WIP" (work in progress). - -remote="$1" -url="$2" - -z40=0000000000000000000000000000000000000000 - -while read local_ref local_sha remote_ref remote_sha -do - if [ "$local_sha" = $z40 ] - then - # Handle delete - : - else - if [ "$remote_sha" = $z40 ] - then - # New branch, examine all commits - range="$local_sha" - else - # Update to existing branch, examine new commits - range="$remote_sha..$local_sha" - fi - - # Check for WIP commit - commit=`git rev-list -n 1 --grep '^WIP' "$range"` - if [ -n "$commit" ] - then - echo >&2 "Found WIP commit in $local_ref, not pushing" - exit 1 - fi - fi -done - -exit 0 diff --git a/test/fixtures/repo-symlinks/dot-git/hooks/pre-rebase.sample b/test/fixtures/repo-symlinks/dot-git/hooks/pre-rebase.sample deleted file mode 100755 index 9773ed4cb2..0000000000 --- a/test/fixtures/repo-symlinks/dot-git/hooks/pre-rebase.sample +++ /dev/null @@ -1,169 +0,0 @@ -#!/bin/sh -# -# Copyright (c) 2006, 2008 Junio C Hamano -# -# The "pre-rebase" hook is run just before "git rebase" starts doing -# its job, and can prevent the command from running by exiting with -# non-zero status. -# -# The hook is called with the following parameters: -# -# $1 -- the upstream the series was forked from. -# $2 -- the branch being rebased (or empty when rebasing the current branch). -# -# This sample shows how to prevent topic branches that are already -# merged to 'next' branch from getting rebased, because allowing it -# would result in rebasing already published history. - -publish=next -basebranch="$1" -if test "$#" = 2 -then - topic="refs/heads/$2" -else - topic=`git symbolic-ref HEAD` || - exit 0 ;# we do not interrupt rebasing detached HEAD -fi - -case "$topic" in -refs/heads/??/*) - ;; -*) - exit 0 ;# we do not interrupt others. - ;; -esac - -# Now we are dealing with a topic branch being rebased -# on top of master. Is it OK to rebase it? - -# Does the topic really exist? -git show-ref -q "$topic" || { - echo >&2 "No such branch $topic" - exit 1 -} - -# Is topic fully merged to master? -not_in_master=`git rev-list --pretty=oneline ^master "$topic"` -if test -z "$not_in_master" -then - echo >&2 "$topic is fully merged to master; better remove it." - exit 1 ;# we could allow it, but there is no point. -fi - -# Is topic ever merged to next? If so you should not be rebasing it. -only_next_1=`git rev-list ^master "^$topic" ${publish} | sort` -only_next_2=`git rev-list ^master ${publish} | sort` -if test "$only_next_1" = "$only_next_2" -then - not_in_topic=`git rev-list "^$topic" master` - if test -z "$not_in_topic" - then - echo >&2 "$topic is already up-to-date with master" - exit 1 ;# we could allow it, but there is no point. - else - exit 0 - fi -else - not_in_next=`git rev-list --pretty=oneline ^${publish} "$topic"` - /usr/bin/perl -e ' - my $topic = $ARGV[0]; - my $msg = "* $topic has commits already merged to public branch:\n"; - my (%not_in_next) = map { - /^([0-9a-f]+) /; - ($1 => 1); - } split(/\n/, $ARGV[1]); - for my $elem (map { - /^([0-9a-f]+) (.*)$/; - [$1 => $2]; - } split(/\n/, $ARGV[2])) { - if (!exists $not_in_next{$elem->[0]}) { - if ($msg) { - print STDERR $msg; - undef $msg; - } - print STDERR " $elem->[1]\n"; - } - } - ' "$topic" "$not_in_next" "$not_in_master" - exit 1 -fi - -exit 0 - -################################################################ - -This sample hook safeguards topic branches that have been -published from being rewound. - -The workflow assumed here is: - - * Once a topic branch forks from "master", "master" is never - merged into it again (either directly or indirectly). - - * Once a topic branch is fully cooked and merged into "master", - it is deleted. If you need to build on top of it to correct - earlier mistakes, a new topic branch is created by forking at - the tip of the "master". This is not strictly necessary, but - it makes it easier to keep your history simple. - - * Whenever you need to test or publish your changes to topic - branches, merge them into "next" branch. - -The script, being an example, hardcodes the publish branch name -to be "next", but it is trivial to make it configurable via -$GIT_DIR/config mechanism. - -With this workflow, you would want to know: - -(1) ... if a topic branch has ever been merged to "next". Young - topic branches can have stupid mistakes you would rather - clean up before publishing, and things that have not been - merged into other branches can be easily rebased without - affecting other people. But once it is published, you would - not want to rewind it. - -(2) ... if a topic branch has been fully merged to "master". - Then you can delete it. More importantly, you should not - build on top of it -- other people may already want to - change things related to the topic as patches against your - "master", so if you need further changes, it is better to - fork the topic (perhaps with the same name) afresh from the - tip of "master". - -Let's look at this example: - - o---o---o---o---o---o---o---o---o---o "next" - / / / / - / a---a---b A / / - / / / / - / / c---c---c---c B / - / / / \ / - / / / b---b C \ / - / / / / \ / - ---o---o---o---o---o---o---o---o---o---o---o "master" - - -A, B and C are topic branches. - - * A has one fix since it was merged up to "next". - - * B has finished. It has been fully merged up to "master" and "next", - and is ready to be deleted. - - * C has not merged to "next" at all. - -We would want to allow C to be rebased, refuse A, and encourage -B to be deleted. - -To compute (1): - - git rev-list ^master ^topic next - git rev-list ^master next - - if these match, topic has not merged in next at all. - -To compute (2): - - git rev-list master..topic - - if this is empty, it is fully merged to "master". diff --git a/test/fixtures/repo-symlinks/dot-git/hooks/pre-receive.sample b/test/fixtures/repo-symlinks/dot-git/hooks/pre-receive.sample deleted file mode 100755 index a1fd29ec14..0000000000 --- a/test/fixtures/repo-symlinks/dot-git/hooks/pre-receive.sample +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/sh -# -# An example hook script to make use of push options. -# The example simply echoes all push options that start with 'echoback=' -# and rejects all pushes when the "reject" push option is used. -# -# To enable this hook, rename this file to "pre-receive". - -if test -n "$GIT_PUSH_OPTION_COUNT" -then - i=0 - while test "$i" -lt "$GIT_PUSH_OPTION_COUNT" - do - eval "value=\$GIT_PUSH_OPTION_$i" - case "$value" in - echoback=*) - echo "echo from the pre-receive-hook: ${value#*=}" >&2 - ;; - reject) - exit 1 - esac - i=$((i + 1)) - done -fi diff --git a/test/fixtures/repo-symlinks/dot-git/hooks/prepare-commit-msg.sample b/test/fixtures/repo-symlinks/dot-git/hooks/prepare-commit-msg.sample deleted file mode 100755 index f093a02ec4..0000000000 --- a/test/fixtures/repo-symlinks/dot-git/hooks/prepare-commit-msg.sample +++ /dev/null @@ -1,36 +0,0 @@ -#!/bin/sh -# -# An example hook script to prepare the commit log message. -# Called by "git commit" with the name of the file that has the -# commit message, followed by the description of the commit -# message's source. The hook's purpose is to edit the commit -# message file. If the hook fails with a non-zero status, -# the commit is aborted. -# -# To enable this hook, rename this file to "prepare-commit-msg". - -# This hook includes three examples. The first comments out the -# "Conflicts:" part of a merge commit. -# -# The second includes the output of "git diff --name-status -r" -# into the message, just before the "git status" output. It is -# commented because it doesn't cope with --amend or with squashed -# commits. -# -# The third example adds a Signed-off-by line to the message, that can -# still be edited. This is rarely a good idea. - -case "$2,$3" in - merge,) - /usr/bin/perl -i.bak -ne 's/^/# /, s/^# #/#/ if /^Conflicts/ .. /#/; print' "$1" ;; - -# ,|template,) -# /usr/bin/perl -i.bak -pe ' -# print "\n" . `git diff --cached --name-status -r` -# if /^#/ && $first++ == 0' "$1" ;; - - *) ;; -esac - -# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') -# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1" diff --git a/test/fixtures/repo-symlinks/dot-git/hooks/update.sample b/test/fixtures/repo-symlinks/dot-git/hooks/update.sample deleted file mode 100755 index 80ba94135c..0000000000 --- a/test/fixtures/repo-symlinks/dot-git/hooks/update.sample +++ /dev/null @@ -1,128 +0,0 @@ -#!/bin/sh -# -# An example hook script to block unannotated tags from entering. -# Called by "git receive-pack" with arguments: refname sha1-old sha1-new -# -# To enable this hook, rename this file to "update". -# -# Config -# ------ -# hooks.allowunannotated -# This boolean sets whether unannotated tags will be allowed into the -# repository. By default they won't be. -# hooks.allowdeletetag -# This boolean sets whether deleting tags will be allowed in the -# repository. By default they won't be. -# hooks.allowmodifytag -# This boolean sets whether a tag may be modified after creation. By default -# it won't be. -# hooks.allowdeletebranch -# This boolean sets whether deleting branches will be allowed in the -# repository. By default they won't be. -# hooks.denycreatebranch -# This boolean sets whether remotely creating branches will be denied -# in the repository. By default this is allowed. -# - -# --- Command line -refname="$1" -oldrev="$2" -newrev="$3" - -# --- Safety check -if [ -z "$GIT_DIR" ]; then - echo "Don't run this script from the command line." >&2 - echo " (if you want, you could supply GIT_DIR then run" >&2 - echo " $0 )" >&2 - exit 1 -fi - -if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then - echo "usage: $0 " >&2 - exit 1 -fi - -# --- Config -allowunannotated=$(git config --bool hooks.allowunannotated) -allowdeletebranch=$(git config --bool hooks.allowdeletebranch) -denycreatebranch=$(git config --bool hooks.denycreatebranch) -allowdeletetag=$(git config --bool hooks.allowdeletetag) -allowmodifytag=$(git config --bool hooks.allowmodifytag) - -# check for no description -projectdesc=$(sed -e '1q' "$GIT_DIR/description") -case "$projectdesc" in -"Unnamed repository"* | "") - echo "*** Project description file hasn't been set" >&2 - exit 1 - ;; -esac - -# --- Check types -# if $newrev is 0000...0000, it's a commit to delete a ref. -zero="0000000000000000000000000000000000000000" -if [ "$newrev" = "$zero" ]; then - newrev_type=delete -else - newrev_type=$(git cat-file -t $newrev) -fi - -case "$refname","$newrev_type" in - refs/tags/*,commit) - # un-annotated tag - short_refname=${refname##refs/tags/} - if [ "$allowunannotated" != "true" ]; then - echo "*** The un-annotated tag, $short_refname, is not allowed in this repository" >&2 - echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2 - exit 1 - fi - ;; - refs/tags/*,delete) - # delete tag - if [ "$allowdeletetag" != "true" ]; then - echo "*** Deleting a tag is not allowed in this repository" >&2 - exit 1 - fi - ;; - refs/tags/*,tag) - # annotated tag - if [ "$allowmodifytag" != "true" ] && git rev-parse $refname > /dev/null 2>&1 - then - echo "*** Tag '$refname' already exists." >&2 - echo "*** Modifying a tag is not allowed in this repository." >&2 - exit 1 - fi - ;; - refs/heads/*,commit) - # branch - if [ "$oldrev" = "$zero" -a "$denycreatebranch" = "true" ]; then - echo "*** Creating a branch is not allowed in this repository" >&2 - exit 1 - fi - ;; - refs/heads/*,delete) - # delete branch - if [ "$allowdeletebranch" != "true" ]; then - echo "*** Deleting a branch is not allowed in this repository" >&2 - exit 1 - fi - ;; - refs/remotes/*,commit) - # tracking branch - ;; - refs/remotes/*,delete) - # delete tracking branch - if [ "$allowdeletebranch" != "true" ]; then - echo "*** Deleting a tracking branch is not allowed in this repository" >&2 - exit 1 - fi - ;; - *) - # Anything else (is there anything else?) - echo "*** Update hook: unknown type of update to ref $refname of type $newrev_type" >&2 - exit 1 - ;; -esac - -# --- Finished -exit 0 diff --git a/test/fixtures/repo-symlinks/dot-git/info/exclude b/test/fixtures/repo-symlinks/dot-git/info/exclude deleted file mode 100644 index a5196d1be8..0000000000 --- a/test/fixtures/repo-symlinks/dot-git/info/exclude +++ /dev/null @@ -1,6 +0,0 @@ -# git ls-files --others --exclude-from=.git/info/exclude -# Lines that start with '#' are comments. -# For a project mostly in C, the following would be a good set of -# exclude patterns (uncomment them if you want to use them): -# *.[oa] -# *~ diff --git a/test/fixtures/repo-symlinks/dot-git/logs/HEAD b/test/fixtures/repo-symlinks/dot-git/logs/HEAD deleted file mode 100644 index 0b60ea082f..0000000000 --- a/test/fixtures/repo-symlinks/dot-git/logs/HEAD +++ /dev/null @@ -1 +0,0 @@ -0000000000000000000000000000000000000000 c9dbab82b91c53e70a872a4f1f793c275790abc8 Katrina Uychaco 1515549807 -0800 commit (initial): Initial commit diff --git a/test/fixtures/repo-symlinks/dot-git/logs/refs/heads/master b/test/fixtures/repo-symlinks/dot-git/logs/refs/heads/master deleted file mode 100644 index 0b60ea082f..0000000000 --- a/test/fixtures/repo-symlinks/dot-git/logs/refs/heads/master +++ /dev/null @@ -1 +0,0 @@ -0000000000000000000000000000000000000000 c9dbab82b91c53e70a872a4f1f793c275790abc8 Katrina Uychaco 1515549807 -0800 commit (initial): Initial commit From 79383d54e0de5738d179e2b538e2679dd01b384e Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Thu, 11 Jan 2018 15:46:32 -0800 Subject: [PATCH 0169/5882] :fire: unnecessary React.cloneElement calls --- .../controllers/file-patch-controller.test.js | 37 ++++++++----------- 1 file changed, 15 insertions(+), 22 deletions(-) diff --git a/test/controllers/file-patch-controller.test.js b/test/controllers/file-patch-controller.test.js index 16e61bef0c..395752a02f 100644 --- a/test/controllers/file-patch-controller.test.js +++ b/test/controllers/file-patch-controller.test.js @@ -90,7 +90,7 @@ describe('FilePatchController', function() { }); it('bases its tab title on the staging status', function() { - const wrapper = mount(React.cloneElement(component, {filePath})); + const wrapper = mount(component); assert.equal(wrapper.instance().getTitle(), `Unstaged Changes: ${filePath}`); @@ -119,9 +119,7 @@ describe('FilePatchController', function() { getFilePatchForPath.returns(filePatch); - const wrapper = mount(React.cloneElement(component, { - filePath, largeDiffLineThreshold: 5, - })); + const wrapper = mount(React.cloneElement(component, {largeDiffLineThreshold: 5})); await assert.async.match(wrapper.text(), /large diff/); }); @@ -138,9 +136,7 @@ describe('FilePatchController', function() { const filePatch = createFilePatch(filePath, filePath, 'modified', [hunk]); getFilePatchForPath.returns(filePatch); - const wrapper = mount(React.cloneElement(component, { - filePath, largeDiffLineThreshold: 5, - })); + const wrapper = mount(React.cloneElement(component, {largeDiffLineThreshold: 5})); await assert.async.isTrue(wrapper.find('.large-file-patch').exists()); @@ -185,7 +181,7 @@ describe('FilePatchController', function() { repository.getFilePatchForPath.restore(); fs.writeFileSync(path.join(workdirPath, filePath), 'change', 'utf8'); - const wrapper = mount(React.cloneElement(component, {filePath, initialStagingStatus: 'unstaged'})); + const wrapper = mount(component); await assert.async.isNotNull(wrapper.state('filePatch')); @@ -204,7 +200,7 @@ describe('FilePatchController', function() { // https://github.com/atom/github/issues/505 describe('getFilePatchForPath(filePath, staged, isAmending)', function() { it('calls repository.getFilePatchForPath with amending: true only if staged is true', async () => { - const wrapper = mount(React.cloneElement(component, {filePath, initialStagingStatus: 'unstaged'})); + const wrapper = mount(component); await wrapper.instance().repositoryObserver.getLastModelDataRefreshPromise(); repository.getFilePatchForPath.reset(); @@ -219,7 +215,7 @@ describe('FilePatchController', function() { const emptyFilePatch = createFilePatch(filePath, filePath, 'modified', []); getFilePatchForPath.returns(emptyFilePatch); - const wrapper = mount(React.cloneElement(component, {filePath})); + const wrapper = mount(component); assert.isTrue(wrapper.find('FilePatchView').exists()); assert.isTrue(wrapper.find('FilePatchView').text().includes('File has no contents')); @@ -239,7 +235,7 @@ describe('FilePatchController', function() { const filePatch0 = createFilePatch(filePath, filePath, 'modified', [hunk1, hunk2]); getFilePatchForPath.returns(filePatch0); - const wrapper = shallow(React.cloneElement(component, {filePath})); + const wrapper = shallow(component); let view0; await until(() => { @@ -266,7 +262,7 @@ describe('FilePatchController', function() { const filePatch = createFilePatch(filePath, filePath, 'modified', [new Hunk(1, 1, 1, 3, '', [])]); getFilePatchForPath.returns(filePatch); - const wrapper = mount(React.cloneElement(component, {filePath})); + const wrapper = mount(component); await assert.async.isTrue(wrapper.find('FilePatchView').exists()); commandRegistry.dispatch(wrapper.find('FilePatchView').getDOMNode(), 'core:move-right'); @@ -291,10 +287,7 @@ describe('FilePatchController', function() { const filePatch = createFilePatch(filePath, filePath, 'modified', [hunk]); getFilePatchForPath.returns(filePatch); - const wrapper = mount(React.cloneElement(component, { - filePath, - openFiles: openFilesStub, - })); + const wrapper = mount(React.cloneElement(component, {openFiles: openFilesStub})); await assert.async.isTrue(wrapper.find('HunkView').exists()); @@ -340,7 +333,7 @@ describe('FilePatchController', function() { const component = createComponent(repository, deletedSymlinkAddedFilePath); - const wrapper = mount(React.cloneElement(component, {filePath: deletedSymlinkAddedFilePath, initialStagingStatus: 'unstaged'})); + const wrapper = mount(React.cloneElement(component, {filePath: deletedSymlinkAddedFilePath})); assert.equal((await indexModeAndOid(repository, deletedSymlinkAddedFilePath)).mode, '120000'); @@ -396,7 +389,7 @@ describe('FilePatchController', function() { fs.symlinkSync(path.join(workingDirPath, 'regular-file.txt'), path.join(workingDirPath, deletedFileAddedSymlinkPath)); const component = createComponent(repository, deletedFileAddedSymlinkPath); - const wrapper = mount(React.cloneElement(component, {filePath: deletedFileAddedSymlinkPath, initialStagingStatus: 'unstaged'})); + const wrapper = mount(React.cloneElement(component, {filePath: deletedFileAddedSymlinkPath})); assert.equal((await indexModeAndOid(repository, deletedFileAddedSymlinkPath)).mode, '100644'); @@ -466,7 +459,7 @@ describe('FilePatchController', function() { unstagedLines.splice(11, 2, 'this is a modified line'); fs.writeFileSync(absFilePath, unstagedLines.join('\n')); - const wrapper = mount(React.cloneElement(component, {filePath})); + const wrapper = mount(component); // selectNext() await assert.async.isTrue(wrapper.find('HunkView').exists()); @@ -516,7 +509,7 @@ describe('FilePatchController', function() { fs.writeFileSync(absFilePath, unstagedLines.join('\n')); // stage a subset of lines from first hunk - const wrapper = mount(React.cloneElement(component, {filePath})); + const wrapper = mount(component); await assert.async.isTrue(wrapper.find('HunkView').exists()); const opPromise0 = switchboard.getFinishStageOperationPromise(); @@ -665,7 +658,7 @@ describe('FilePatchController', function() { unstagedLines.splice(11, 2, 'this is a modified line'); fs.writeFileSync(absFilePath, unstagedLines.join('\n')); - const wrapper = mount(React.cloneElement(component, {filePath})); + const wrapper = mount(component); await assert.async.isTrue(wrapper.find('HunkView').exists()); const hunkView0 = wrapper.find('HunkView').at(0); @@ -724,7 +717,7 @@ describe('FilePatchController', function() { unstagedLines.splice(11, 2, 'this is a modified line'); fs.writeFileSync(absFilePath, unstagedLines.join('\n')); - const wrapper = mount(React.cloneElement(component, {filePath})); + const wrapper = mount(component); await assert.async.isTrue(wrapper.find('HunkView').exists()); From 4fc208c8ab5fbcb85d4ed180ade48575067e6e84 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Thu, 11 Jan 2018 15:54:25 -0800 Subject: [PATCH 0170/5882] Add comments to clarify symlink tests --- .../controllers/file-patch-controller.test.js | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/test/controllers/file-patch-controller.test.js b/test/controllers/file-patch-controller.test.js index 395752a02f..29e4e54d6a 100644 --- a/test/controllers/file-patch-controller.test.js +++ b/test/controllers/file-patch-controller.test.js @@ -325,6 +325,7 @@ describe('FilePatchController', function() { const workingDirPath = await cloneRepository('symlinks'); const repository = await buildRepository(workingDirPath); + // correctly handle symlinks on Windows repository.git.exec(['config', 'core.symlinks', 'true']); const deletedSymlinkAddedFilePath = 'symlink.txt'; @@ -332,11 +333,12 @@ describe('FilePatchController', function() { fs.writeFileSync(path.join(workingDirPath, deletedSymlinkAddedFilePath), 'qux\nfoo\nbar\nbaz\nzoo\n', 'utf8'); const component = createComponent(repository, deletedSymlinkAddedFilePath); - const wrapper = mount(React.cloneElement(component, {filePath: deletedSymlinkAddedFilePath})); + // index shows file is symlink assert.equal((await indexModeAndOid(repository, deletedSymlinkAddedFilePath)).mode, '120000'); + // Stage a couple added lines, but not all await assert.async.isTrue(wrapper.find('HunkView').exists()); const opPromise0 = switchboard.getFinishStageOperationPromise(); const hunkView0 = wrapper.find('HunkView').at(0); @@ -347,11 +349,12 @@ describe('FilePatchController', function() { await opPromise0; repository.refresh(); - assert.autocrlfEqual(await repository.readFileFromIndex(deletedSymlinkAddedFilePath), 'foo\nbar\n'); + // index no longer shows file is symlink (symlink has been deleted), now a regular file with contents assert.equal((await indexModeAndOid(repository, deletedSymlinkAddedFilePath)).mode, '100644'); + assert.autocrlfEqual(await repository.readFileFromIndex(deletedSymlinkAddedFilePath), 'foo\nbar\n'); }); - it('stages symlink change when staging deleted lines that depend on change', async function() { + it('unstages symlink change when unstaging deleted lines that depend on change', async function() { const workingDirPath = await cloneRepository('symlinks'); const repository = await buildRepository(workingDirPath); @@ -361,10 +364,12 @@ describe('FilePatchController', function() { await repository.stageFiles([deletedFileAddedSymlinkPath]); const component = createComponent(repository, deletedFileAddedSymlinkPath); + const wrapper = mount(React.cloneElement(component, {filePath: deletedFileAddedSymlinkPath, initialStagingStatus: 'staged'})); + // index shows file is symlink assert.equal((await indexModeAndOid(repository, deletedFileAddedSymlinkPath)).mode, '120000'); - const wrapper = mount(React.cloneElement(component, {filePath: deletedFileAddedSymlinkPath, initialStagingStatus: 'staged'})); + // unstage a couple of lines, but not all await assert.async.isTrue(wrapper.find('HunkView').exists()); const opPromise0 = switchboard.getFinishStageOperationPromise(); const hunkView0 = wrapper.find('HunkView').at(0); @@ -375,8 +380,9 @@ describe('FilePatchController', function() { await opPromise0; repository.refresh(); - assert.autocrlfEqual(await repository.readFileFromIndex(deletedFileAddedSymlinkPath), 'bar\nbaz\n'); + // index no longer shows file is symlink (symlink creation has been unstaged), shows contents of file that existed prior to symlink assert.equal((await indexModeAndOid(repository, deletedFileAddedSymlinkPath)).mode, '100644'); + assert.autocrlfEqual(await repository.readFileFromIndex(deletedFileAddedSymlinkPath), 'bar\nbaz\n'); }); it('stages file deletion when all deleted lines are staged', async function() { @@ -393,6 +399,7 @@ describe('FilePatchController', function() { assert.equal((await indexModeAndOid(repository, deletedFileAddedSymlinkPath)).mode, '100644'); + // stage all deleted lines await assert.async.isTrue(wrapper.find('HunkView').exists()); const opPromise0 = switchboard.getFinishStageOperationPromise(); const hunkView0 = wrapper.find('HunkView').at(0); @@ -401,7 +408,8 @@ describe('FilePatchController', function() { await opPromise0; repository.refresh(); - assert.isNull(await indexModeAndOid(repository, deletedFileAddedSymlinkPath)); // File is not on index, file deletion has been staged + // File is not on index, file deletion has been staged + assert.isNull(await indexModeAndOid(repository, deletedFileAddedSymlinkPath)); const {stagedFiles, unstagedFiles} = await repository.getStatusesForChangedFiles(); assert.equal(unstagedFiles[deletedFileAddedSymlinkPath], 'added'); assert.equal(stagedFiles[deletedFileAddedSymlinkPath], 'deleted'); @@ -423,6 +431,7 @@ describe('FilePatchController', function() { assert.equal((await indexModeAndOid(repository, deletedSymlinkAddedFilePath)).mode, '100644'); + // unstage all added lines await assert.async.isTrue(wrapper.find('HunkView').exists()); const opPromise0 = switchboard.getFinishStageOperationPromise(); const hunkView0 = wrapper.find('HunkView').at(0); @@ -431,7 +440,8 @@ describe('FilePatchController', function() { await opPromise0; repository.refresh(); - assert.isNull(await indexModeAndOid(repository, deletedSymlinkAddedFilePath)); // File is not on index, file creation has been unstaged + // File is not on index, file creation has been unstaged + assert.isNull(await indexModeAndOid(repository, deletedSymlinkAddedFilePath)); const {stagedFiles, unstagedFiles} = await repository.getStatusesForChangedFiles(); assert.equal(unstagedFiles[deletedSymlinkAddedFilePath], 'added'); assert.equal(stagedFiles[deletedSymlinkAddedFilePath], 'deleted'); From 1ce953b7da5ac4cdf3f7aae3854c942baaf1b40f Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Thu, 11 Jan 2018 17:25:39 -0800 Subject: [PATCH 0171/5882] Focus tests for appveyor --- test/controllers/file-patch-controller.test.js | 2 +- test/models/file-patch.test.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/controllers/file-patch-controller.test.js b/test/controllers/file-patch-controller.test.js index 29e4e54d6a..a6bad95f66 100644 --- a/test/controllers/file-patch-controller.test.js +++ b/test/controllers/file-patch-controller.test.js @@ -321,7 +321,7 @@ describe('FilePatchController', function() { } } - it('stages symlink change when staging added lines that depend on change', async function() { + it.only('stages symlink change when staging added lines that depend on change', async function() { const workingDirPath = await cloneRepository('symlinks'); const repository = await buildRepository(workingDirPath); diff --git a/test/models/file-patch.test.js b/test/models/file-patch.test.js index 89ae666f16..6639a9f0a7 100644 --- a/test/models/file-patch.test.js +++ b/test/models/file-patch.test.js @@ -291,7 +291,7 @@ describe('FilePatch', function() { `); }); - it('handles typechange patches for a symlink replaced with a file', async function() { + it.only('handles typechange patches for a symlink replaced with a file', async function() { const workdirPath = await cloneRepository('symlinks'); const repository = await buildRepository(workdirPath); From c22842fa4ee5fea565c26bce0be203d34674c24e Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Thu, 11 Jan 2018 17:33:32 -0800 Subject: [PATCH 0172/5882] Add for loop to suss out flaky tests --- .../controllers/file-patch-controller.test.js | 198 +++++++++--------- test/models/file-patch.test.js | 108 +++++----- 2 files changed, 156 insertions(+), 150 deletions(-) diff --git a/test/controllers/file-patch-controller.test.js b/test/controllers/file-patch-controller.test.js index a6bad95f66..fdf30b38a0 100644 --- a/test/controllers/file-patch-controller.test.js +++ b/test/controllers/file-patch-controller.test.js @@ -310,143 +310,145 @@ describe('FilePatchController', function() { }); describe('integration tests', function() { - describe('handling symlink files', function() { - async function indexModeAndOid(repository, filename) { - const output = await repository.git.exec(['ls-files', '-s', '--', filename]); - if (output) { - const parts = output.split(' '); - return {mode: parts[0], oid: parts[1]}; - } else { - return null; + for (let i = 0; i < 10; i++) { + describe.only('handling symlink files', function() { + async function indexModeAndOid(repository, filename) { + const output = await repository.git.exec(['ls-files', '-s', '--', filename]); + if (output) { + const parts = output.split(' '); + return {mode: parts[0], oid: parts[1]}; + } else { + return null; + } } - } - it.only('stages symlink change when staging added lines that depend on change', async function() { - const workingDirPath = await cloneRepository('symlinks'); - const repository = await buildRepository(workingDirPath); + it('stages symlink change when staging added lines that depend on change', async function() { + const workingDirPath = await cloneRepository('symlinks'); + const repository = await buildRepository(workingDirPath); // correctly handle symlinks on Windows - repository.git.exec(['config', 'core.symlinks', 'true']); + repository.git.exec(['config', 'core.symlinks', 'true']); - const deletedSymlinkAddedFilePath = 'symlink.txt'; - fs.unlinkSync(path.join(workingDirPath, deletedSymlinkAddedFilePath)); - fs.writeFileSync(path.join(workingDirPath, deletedSymlinkAddedFilePath), 'qux\nfoo\nbar\nbaz\nzoo\n', 'utf8'); + const deletedSymlinkAddedFilePath = 'symlink.txt'; + fs.unlinkSync(path.join(workingDirPath, deletedSymlinkAddedFilePath)); + fs.writeFileSync(path.join(workingDirPath, deletedSymlinkAddedFilePath), 'qux\nfoo\nbar\nbaz\nzoo\n', 'utf8'); - const component = createComponent(repository, deletedSymlinkAddedFilePath); - const wrapper = mount(React.cloneElement(component, {filePath: deletedSymlinkAddedFilePath})); + const component = createComponent(repository, deletedSymlinkAddedFilePath); + const wrapper = mount(React.cloneElement(component, {filePath: deletedSymlinkAddedFilePath})); // index shows file is symlink - assert.equal((await indexModeAndOid(repository, deletedSymlinkAddedFilePath)).mode, '120000'); + assert.equal((await indexModeAndOid(repository, deletedSymlinkAddedFilePath)).mode, '120000'); // Stage a couple added lines, but not all - await assert.async.isTrue(wrapper.find('HunkView').exists()); - const opPromise0 = switchboard.getFinishStageOperationPromise(); - const hunkView0 = wrapper.find('HunkView').at(0); - hunkView0.find('LineView').at(1).find('.github-HunkView-line').simulate('mousedown', {button: 0, detail: 1}); - hunkView0.find('LineView').at(2).find('.github-HunkView-line').simulate('mousemove', {}); - window.dispatchEvent(new MouseEvent('mouseup')); - hunkView0.find('button.github-HunkView-stageButton').simulate('click'); - await opPromise0; + await assert.async.isTrue(wrapper.find('HunkView').exists()); + const opPromise0 = switchboard.getFinishStageOperationPromise(); + const hunkView0 = wrapper.find('HunkView').at(0); + hunkView0.find('LineView').at(1).find('.github-HunkView-line').simulate('mousedown', {button: 0, detail: 1}); + hunkView0.find('LineView').at(2).find('.github-HunkView-line').simulate('mousemove', {}); + window.dispatchEvent(new MouseEvent('mouseup')); + hunkView0.find('button.github-HunkView-stageButton').simulate('click'); + await opPromise0; - repository.refresh(); + repository.refresh(); // index no longer shows file is symlink (symlink has been deleted), now a regular file with contents - assert.equal((await indexModeAndOid(repository, deletedSymlinkAddedFilePath)).mode, '100644'); - assert.autocrlfEqual(await repository.readFileFromIndex(deletedSymlinkAddedFilePath), 'foo\nbar\n'); - }); + assert.equal((await indexModeAndOid(repository, deletedSymlinkAddedFilePath)).mode, '100644'); + assert.autocrlfEqual(await repository.readFileFromIndex(deletedSymlinkAddedFilePath), 'foo\nbar\n'); + }); - it('unstages symlink change when unstaging deleted lines that depend on change', async function() { - const workingDirPath = await cloneRepository('symlinks'); - const repository = await buildRepository(workingDirPath); + it('unstages symlink change when unstaging deleted lines that depend on change', async function() { + const workingDirPath = await cloneRepository('symlinks'); + const repository = await buildRepository(workingDirPath); - const deletedFileAddedSymlinkPath = 'a.txt'; - fs.unlinkSync(path.join(workingDirPath, deletedFileAddedSymlinkPath)); - fs.symlinkSync(path.join(workingDirPath, 'regular-file.txt'), path.join(workingDirPath, deletedFileAddedSymlinkPath)); - await repository.stageFiles([deletedFileAddedSymlinkPath]); + const deletedFileAddedSymlinkPath = 'a.txt'; + fs.unlinkSync(path.join(workingDirPath, deletedFileAddedSymlinkPath)); + fs.symlinkSync(path.join(workingDirPath, 'regular-file.txt'), path.join(workingDirPath, deletedFileAddedSymlinkPath)); + await repository.stageFiles([deletedFileAddedSymlinkPath]); - const component = createComponent(repository, deletedFileAddedSymlinkPath); - const wrapper = mount(React.cloneElement(component, {filePath: deletedFileAddedSymlinkPath, initialStagingStatus: 'staged'})); + const component = createComponent(repository, deletedFileAddedSymlinkPath); + const wrapper = mount(React.cloneElement(component, {filePath: deletedFileAddedSymlinkPath, initialStagingStatus: 'staged'})); // index shows file is symlink - assert.equal((await indexModeAndOid(repository, deletedFileAddedSymlinkPath)).mode, '120000'); + assert.equal((await indexModeAndOid(repository, deletedFileAddedSymlinkPath)).mode, '120000'); // unstage a couple of lines, but not all - await assert.async.isTrue(wrapper.find('HunkView').exists()); - const opPromise0 = switchboard.getFinishStageOperationPromise(); - const hunkView0 = wrapper.find('HunkView').at(0); - hunkView0.find('LineView').at(1).find('.github-HunkView-line').simulate('mousedown', {button: 0, detail: 1}); - hunkView0.find('LineView').at(2).find('.github-HunkView-line').simulate('mousemove', {}); - window.dispatchEvent(new MouseEvent('mouseup')); - hunkView0.find('button.github-HunkView-stageButton').simulate('click'); - await opPromise0; + await assert.async.isTrue(wrapper.find('HunkView').exists()); + const opPromise0 = switchboard.getFinishStageOperationPromise(); + const hunkView0 = wrapper.find('HunkView').at(0); + hunkView0.find('LineView').at(1).find('.github-HunkView-line').simulate('mousedown', {button: 0, detail: 1}); + hunkView0.find('LineView').at(2).find('.github-HunkView-line').simulate('mousemove', {}); + window.dispatchEvent(new MouseEvent('mouseup')); + hunkView0.find('button.github-HunkView-stageButton').simulate('click'); + await opPromise0; - repository.refresh(); + repository.refresh(); // index no longer shows file is symlink (symlink creation has been unstaged), shows contents of file that existed prior to symlink - assert.equal((await indexModeAndOid(repository, deletedFileAddedSymlinkPath)).mode, '100644'); - assert.autocrlfEqual(await repository.readFileFromIndex(deletedFileAddedSymlinkPath), 'bar\nbaz\n'); - }); + assert.equal((await indexModeAndOid(repository, deletedFileAddedSymlinkPath)).mode, '100644'); + assert.autocrlfEqual(await repository.readFileFromIndex(deletedFileAddedSymlinkPath), 'bar\nbaz\n'); + }); - it('stages file deletion when all deleted lines are staged', async function() { - const workingDirPath = await cloneRepository('symlinks'); - const repository = await buildRepository(workingDirPath); - await repository.getLoadPromise(); + it('stages file deletion when all deleted lines are staged', async function() { + const workingDirPath = await cloneRepository('symlinks'); + const repository = await buildRepository(workingDirPath); + await repository.getLoadPromise(); - const deletedFileAddedSymlinkPath = 'a.txt'; - fs.unlinkSync(path.join(workingDirPath, deletedFileAddedSymlinkPath)); - fs.symlinkSync(path.join(workingDirPath, 'regular-file.txt'), path.join(workingDirPath, deletedFileAddedSymlinkPath)); + const deletedFileAddedSymlinkPath = 'a.txt'; + fs.unlinkSync(path.join(workingDirPath, deletedFileAddedSymlinkPath)); + fs.symlinkSync(path.join(workingDirPath, 'regular-file.txt'), path.join(workingDirPath, deletedFileAddedSymlinkPath)); - const component = createComponent(repository, deletedFileAddedSymlinkPath); - const wrapper = mount(React.cloneElement(component, {filePath: deletedFileAddedSymlinkPath})); + const component = createComponent(repository, deletedFileAddedSymlinkPath); + const wrapper = mount(React.cloneElement(component, {filePath: deletedFileAddedSymlinkPath})); - assert.equal((await indexModeAndOid(repository, deletedFileAddedSymlinkPath)).mode, '100644'); + assert.equal((await indexModeAndOid(repository, deletedFileAddedSymlinkPath)).mode, '100644'); // stage all deleted lines - await assert.async.isTrue(wrapper.find('HunkView').exists()); - const opPromise0 = switchboard.getFinishStageOperationPromise(); - const hunkView0 = wrapper.find('HunkView').at(0); - hunkView0.find('.github-HunkView-title').simulate('click'); - hunkView0.find('button.github-HunkView-stageButton').simulate('click'); - await opPromise0; + await assert.async.isTrue(wrapper.find('HunkView').exists()); + const opPromise0 = switchboard.getFinishStageOperationPromise(); + const hunkView0 = wrapper.find('HunkView').at(0); + hunkView0.find('.github-HunkView-title').simulate('click'); + hunkView0.find('button.github-HunkView-stageButton').simulate('click'); + await opPromise0; - repository.refresh(); + repository.refresh(); // File is not on index, file deletion has been staged - assert.isNull(await indexModeAndOid(repository, deletedFileAddedSymlinkPath)); - const {stagedFiles, unstagedFiles} = await repository.getStatusesForChangedFiles(); - assert.equal(unstagedFiles[deletedFileAddedSymlinkPath], 'added'); - assert.equal(stagedFiles[deletedFileAddedSymlinkPath], 'deleted'); - }); + assert.isNull(await indexModeAndOid(repository, deletedFileAddedSymlinkPath)); + const {stagedFiles, unstagedFiles} = await repository.getStatusesForChangedFiles(); + assert.equal(unstagedFiles[deletedFileAddedSymlinkPath], 'added'); + assert.equal(stagedFiles[deletedFileAddedSymlinkPath], 'deleted'); + }); - it('unstages file creation when all added lines are unstaged', async function() { - const workingDirPath = await cloneRepository('symlinks'); - const repository = await buildRepository(workingDirPath); + it('unstages file creation when all added lines are unstaged', async function() { + const workingDirPath = await cloneRepository('symlinks'); + const repository = await buildRepository(workingDirPath); - repository.git.exec(['config', 'core.symlinks', 'true']); + repository.git.exec(['config', 'core.symlinks', 'true']); - const deletedSymlinkAddedFilePath = 'symlink.txt'; - fs.unlinkSync(path.join(workingDirPath, deletedSymlinkAddedFilePath)); - fs.writeFileSync(path.join(workingDirPath, deletedSymlinkAddedFilePath), 'qux\nfoo\nbar\nbaz\nzoo\n', 'utf8'); - await repository.stageFiles([deletedSymlinkAddedFilePath]); + const deletedSymlinkAddedFilePath = 'symlink.txt'; + fs.unlinkSync(path.join(workingDirPath, deletedSymlinkAddedFilePath)); + fs.writeFileSync(path.join(workingDirPath, deletedSymlinkAddedFilePath), 'qux\nfoo\nbar\nbaz\nzoo\n', 'utf8'); + await repository.stageFiles([deletedSymlinkAddedFilePath]); - const component = createComponent(repository, deletedSymlinkAddedFilePath); - const wrapper = mount(React.cloneElement(component, {filePath: deletedSymlinkAddedFilePath, initialStagingStatus: 'staged'})); + const component = createComponent(repository, deletedSymlinkAddedFilePath); + const wrapper = mount(React.cloneElement(component, {filePath: deletedSymlinkAddedFilePath, initialStagingStatus: 'staged'})); - assert.equal((await indexModeAndOid(repository, deletedSymlinkAddedFilePath)).mode, '100644'); + assert.equal((await indexModeAndOid(repository, deletedSymlinkAddedFilePath)).mode, '100644'); // unstage all added lines - await assert.async.isTrue(wrapper.find('HunkView').exists()); - const opPromise0 = switchboard.getFinishStageOperationPromise(); - const hunkView0 = wrapper.find('HunkView').at(0); - hunkView0.find('.github-HunkView-title').simulate('click'); - hunkView0.find('button.github-HunkView-stageButton').simulate('click'); - await opPromise0; + await assert.async.isTrue(wrapper.find('HunkView').exists()); + const opPromise0 = switchboard.getFinishStageOperationPromise(); + const hunkView0 = wrapper.find('HunkView').at(0); + hunkView0.find('.github-HunkView-title').simulate('click'); + hunkView0.find('button.github-HunkView-stageButton').simulate('click'); + await opPromise0; - repository.refresh(); + repository.refresh(); // File is not on index, file creation has been unstaged - assert.isNull(await indexModeAndOid(repository, deletedSymlinkAddedFilePath)); - const {stagedFiles, unstagedFiles} = await repository.getStatusesForChangedFiles(); - assert.equal(unstagedFiles[deletedSymlinkAddedFilePath], 'added'); - assert.equal(stagedFiles[deletedSymlinkAddedFilePath], 'deleted'); + assert.isNull(await indexModeAndOid(repository, deletedSymlinkAddedFilePath)); + const {stagedFiles, unstagedFiles} = await repository.getStatusesForChangedFiles(); + assert.equal(unstagedFiles[deletedSymlinkAddedFilePath], 'added'); + assert.equal(stagedFiles[deletedSymlinkAddedFilePath], 'deleted'); + }); }); - }); + } describe('handling non-symlink changes', function() { let workdirPath, repository, filePath, component; diff --git a/test/models/file-patch.test.js b/test/models/file-patch.test.js index 6639a9f0a7..7c4d1477c1 100644 --- a/test/models/file-patch.test.js +++ b/test/models/file-patch.test.js @@ -291,66 +291,70 @@ describe('FilePatch', function() { `); }); - it.only('handles typechange patches for a symlink replaced with a file', async function() { - const workdirPath = await cloneRepository('symlinks'); - const repository = await buildRepository(workdirPath); + for (let i = 0; i < 10; i++) { + describe.only('typechange file patches', function() { + it('handles typechange patches for a symlink replaced with a file', async function() { + const workdirPath = await cloneRepository('symlinks'); + const repository = await buildRepository(workdirPath); - repository.git.exec(['config', 'core.symlinks', 'true']); + repository.git.exec(['config', 'core.symlinks', 'true']); - const deletedSymlinkAddedFilePath = 'symlink.txt'; - fs.unlinkSync(path.join(workdirPath, deletedSymlinkAddedFilePath)); - fs.writeFileSync(path.join(workdirPath, deletedSymlinkAddedFilePath), 'qux\nfoo\nbar\n', 'utf8'); + const deletedSymlinkAddedFilePath = 'symlink.txt'; + fs.unlinkSync(path.join(workdirPath, deletedSymlinkAddedFilePath)); + fs.writeFileSync(path.join(workdirPath, deletedSymlinkAddedFilePath), 'qux\nfoo\nbar\n', 'utf8'); - const patch = await repository.getFilePatchForPath(deletedSymlinkAddedFilePath); - assert.equal(patch.toString(), dedent` - diff --git a/symlink.txt b/symlink.txt - deleted file mode 120000 - --- a/symlink.txt - +++ /dev/null - @@ -1 +0,0 @@ - -./regular-file.txt - \\ No newline at end of file - diff --git a/symlink.txt b/symlink.txt - new file mode 100644 - --- /dev/null - +++ b/symlink.txt - @@ -0,0 +1,3 @@ - +qux - +foo - +bar + const patch = await repository.getFilePatchForPath(deletedSymlinkAddedFilePath); + assert.equal(patch.toString(), dedent` + diff --git a/symlink.txt b/symlink.txt + deleted file mode 120000 + --- a/symlink.txt + +++ /dev/null + @@ -1 +0,0 @@ + -./regular-file.txt + \\ No newline at end of file + diff --git a/symlink.txt b/symlink.txt + new file mode 100644 + --- /dev/null + +++ b/symlink.txt + @@ -0,0 +1,3 @@ + +qux + +foo + +bar - `); - }); + `); + }); - it('handles typechange patches for a file replaced with a symlink', async function() { - const workdirPath = await cloneRepository('symlinks'); - const repository = await buildRepository(workdirPath); + it('handles typechange patches for a file replaced with a symlink', async function() { + const workdirPath = await cloneRepository('symlinks'); + const repository = await buildRepository(workdirPath); - const deletedFileAddedSymlinkPath = 'a.txt'; - fs.unlinkSync(path.join(workdirPath, deletedFileAddedSymlinkPath)); - fs.symlinkSync(path.join(workdirPath, 'regular-file.txt'), path.join(workdirPath, deletedFileAddedSymlinkPath)); + const deletedFileAddedSymlinkPath = 'a.txt'; + fs.unlinkSync(path.join(workdirPath, deletedFileAddedSymlinkPath)); + fs.symlinkSync(path.join(workdirPath, 'regular-file.txt'), path.join(workdirPath, deletedFileAddedSymlinkPath)); - const patch = await repository.getFilePatchForPath(deletedFileAddedSymlinkPath); - assert.equal(patch.toString(), dedent` - diff --git a/a.txt b/a.txt - deleted file mode 100644 - --- a/a.txt - +++ /dev/null - @@ -1,4 +0,0 @@ - -foo - -bar - -baz - - - diff --git a/a.txt b/a.txt - new file mode 120000 - --- /dev/null - +++ b/a.txt - @@ -0,0 +1 @@ - +${toGitPathSep(path.join(workdirPath, 'regular-file.txt'))} - \\ No newline at end of file + const patch = await repository.getFilePatchForPath(deletedFileAddedSymlinkPath); + assert.equal(patch.toString(), dedent` + diff --git a/a.txt b/a.txt + deleted file mode 100644 + --- a/a.txt + +++ /dev/null + @@ -1,4 +0,0 @@ + -foo + -bar + -baz + - + diff --git a/a.txt b/a.txt + new file mode 120000 + --- /dev/null + +++ b/a.txt + @@ -0,0 +1 @@ + +${toGitPathSep(path.join(workdirPath, 'regular-file.txt'))} + \\ No newline at end of file - `); - }); + `); + }); + }); + } }); describe('getHeaderString()', function() { From 2f04381cb7c0c172c7e1b49d80c24a9752c0cbc0 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 12 Jan 2018 15:13:09 -0800 Subject: [PATCH 0173/5882] Scroll to selected diff line only if head of selection changes Previously, `componentDidUpdate` was being called when anonymous function props were updated, triggering scrolling at undesired times. Instead, now we specifically check to see if the relevant props have changed before scrolling. Admittedly, I'm not sure what the purpose of the `selectedLine` branch of the original conditional was for. I traced its addition back to the "open diff from editor" feature I added 8 months ago, but manual testing of that feature seems to work without it. So I'm removing it and hoping that no regression is introduced, and taking a mental note to write more informative commit messages in the future. --- lib/views/hunk-view.js | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/lib/views/hunk-view.js b/lib/views/hunk-view.js index c0bf62bc83..fd0231f442 100644 --- a/lib/views/hunk-view.js +++ b/lib/views/hunk-view.js @@ -94,18 +94,17 @@ export default class HunkView extends React.Component { this.lineElements.set(line, element); } - componentDidUpdate() { - const selectedLine = Array.from(this.props.selectedLines)[0]; - if (selectedLine && this.lineElements.get(selectedLine)) { - // QUESTION: why is this setTimeout needed? - const element = this.lineElements.get(selectedLine); - setTimeout(() => { - element.scrollIntoViewIfNeeded(); - }, 0); - } else if (this.props.headHunk === this.props.hunk) { - this.element.scrollIntoViewIfNeeded(); - } else if (this.props.headLine && this.lineElements.has(this.props.headLine)) { - this.lineElements.get(this.props.headLine).scrollIntoViewIfNeeded(); + componentDidUpdate(prevProps) { + if (prevProps.headLine !== this.props.headLine) { + if (this.props.headLine && this.lineElements.has(this.props.headLine)) { + this.lineElements.get(this.props.headLine).scrollIntoViewIfNeeded(); + } + } + + if (prevProps.headHunk !== this.props.headHunk) { + if (this.props.headHunk === this.props.hunk) { + this.element.scrollIntoViewIfNeeded(); + } } } } From d68a26d5ab397d11d124ad0038b25fcda6c44713 Mon Sep 17 00:00:00 2001 From: simurai Date: Mon, 15 Jan 2018 11:16:09 +0900 Subject: [PATCH 0174/5882] Make is-blank take up available space if there is something else in the container --- styles/file-patch-view.less | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/styles/file-patch-view.less b/styles/file-patch-view.less index 29693d022f..2c94647a09 100644 --- a/styles/file-patch-view.less +++ b/styles/file-patch-view.less @@ -62,10 +62,13 @@ &-container { flex: 1; overflow-y: auto; + display: flex; + flex-direction: column; - .is-blank, .large-file-patch { + .is-blank, + .large-file-patch { + flex: 1; display: flex; - height: 100%; align-items: center; justify-content: center; text-align: center; From 4dd85fedb19a2ff010d665fac8b7f1986790efa0 Mon Sep 17 00:00:00 2001 From: simurai Date: Mon, 15 Jan 2018 17:21:35 +0900 Subject: [PATCH 0175/5882] Restyle meta section --- lib/views/file-patch-view.js | 126 +++++++++++++++++++++++++---------- styles/file-patch-view.less | 106 ++++++++++++++++++++++------- 2 files changed, 170 insertions(+), 62 deletions(-) diff --git a/lib/views/file-patch-view.js b/lib/views/file-patch-view.js index faa467619e..f914f8e930 100644 --- a/lib/views/file-patch-view.js +++ b/lib/views/file-patch-view.js @@ -283,16 +283,28 @@ export default class FilePatchView extends React.Component { renderExecutableModeChange(unstaged) { const {executableModeChange} = this.props; return ( -
- - File changed mode from {executableModeChange.oldMode} - to {executableModeChange.newMode} - - +
+
+
+

Mode change

+
+ +
+
+
+ File changed mode + + from {executableModeChange.oldMode} + + + to {executableModeChange.newMode} + +
+
); } @@ -304,41 +316,81 @@ export default class FilePatchView extends React.Component { if (oldSymlink && !newSymlink) { return ( -
- - Symlink to {oldSymlink} deleted. - - +
+
+
+

Symlink deleted

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

Symlink added

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

Symlink changed

+
+ +
+
+
+ + from {oldSymlink} + + + to {newSymlink} + +
+
); } else { diff --git a/styles/file-patch-view.less b/styles/file-patch-view.less index 2c94647a09..224f1bb133 100644 --- a/styles/file-patch-view.less +++ b/styles/file-patch-view.less @@ -26,31 +26,6 @@ } } - &-mode-change { - padding: @component-padding/2; - padding-left: @component-padding; - display: flex; - flex-direction: row; - border-bottom: 1px solid @base-border-color; - - span { - flex: 1; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - margin-right: 10px; - } - - .btn { - font-size: .9em; - &.icon-move-up::before, - &.icon-move-down::before { - font-size: 1em; - margin-right: .5em; - } - } - } - &-title { flex: 1; overflow: hidden; @@ -80,4 +55,85 @@ flex-direction: column; } } + + + // Meta section + + &-meta { + padding: @component-padding; + border-bottom: 1px solid @base-border-color; + } + + &-metaContainer { + border: 1px solid @base-border-color; + border-radius: @component-border-radius; + } + + &-metaHeader { + display: flex; + align-items: center; + padding: @component-padding; + background-color: @background-color-highlight; + } + + &-metaTitle { + flex: 1; + margin: 0; + font-size: 1.25em; + line-height: 1.5; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + &-metaControls { + margin-left: @component-padding; + + .btn { + &.icon-move-up::before, + &.icon-move-down::before { + font-size: 1em; + margin-right: .5em; + vertical-align: baseline; + } + } + } + + &-metaDetails { + padding: @component-padding; + } + + &-metaDiff { + margin: 0 .2em; + padding: .2em .4em; + line-height: 1.6; + border-radius: @component-border-radius; + &--added { + color: mix(@text-color-highlight, @text-color-success, 40%); + background-color: mix(@base-background-color, @background-color-success, 80%); + } + &--removed { + color: mix(@text-color-highlight, @text-color-error, 40%); + background-color: mix(@base-background-color, @background-color-error, 80%); + } + &--fullWidth { + display: block; + margin: @component-padding/2 0 0 0; + &:first-child { + margin-top: 0; + } + } + + code { + margin: 0 .2em; + padding: 0 .2em; + font-size: .9em; + color: inherit; + line-height: 1; + vertical-align: middle; + word-break: break-all; + background-color: @base-background-color; + } + } + } From 5d3a78c7b811054e8cf1abe54b69219d4cd514fd Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Tue, 16 Jan 2018 10:43:52 -0800 Subject: [PATCH 0176/5882] :arrow_up: atom-babel6-transpiler@1.1.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3cf420dc10..5bf7422a2a 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ ], "dependencies": { "@binarymuse/relay-compiler": "^1.2.0", - "atom-babel6-transpiler": "1.1.1", + "atom-babel6-transpiler": "1.1.3", "babel-generator": "^6.25.0", "babel-plugin-chai-assert-async": "^0.1.0", "babel-plugin-relay": "^1.2.0-rc.1", From 17d719275ee87caf07d3423ff7bf8a98662702bd Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Tue, 16 Jan 2018 10:53:01 -0800 Subject: [PATCH 0177/5882] Add command to install React devTools in dev mode --- lib/controllers/root-controller.js | 9 +++++++++ package.json | 1 + 2 files changed, 10 insertions(+) diff --git a/lib/controllers/root-controller.js b/lib/controllers/root-controller.js index cfd3380fa8..b3de460592 100644 --- a/lib/controllers/root-controller.js +++ b/lib/controllers/root-controller.js @@ -106,9 +106,12 @@ export default class RootController extends React.Component { } render() { + const devMode = global.atom && global.atom.inDevMode(); + return (
+ {devMode && } @@ -329,6 +332,12 @@ export default class RootController extends React.Component { }); } + @autobind + installReactDevTools() { + const devTools = require('electron-devtools-installer'); + devTools.default(devTools.REACT_DEVELOPER_TOOLS); + } + @autobind getRepositoryForWorkdir(workdir) { return this.props.getRepositoryForWorkdir(workdir); diff --git a/package.json b/package.json index 3cf420dc10..95f734e5f1 100644 --- a/package.json +++ b/package.json @@ -78,6 +78,7 @@ "chai": "^3.5.0", "chai-as-promised": "^5.3.0", "dedent-js": "^1.0.1", + "electron-devtools-installer": "^2.2.3", "enzyme": "^2.9.1", "eslint": "^3.0.0", "eslint-config-fbjs": "2.0.0-alpha.1", From 74130e46eaaa8fb64b646723a6b3f90d73ebb832 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Wed, 17 Jan 2018 12:14:54 -0800 Subject: [PATCH 0178/5882] Fix staging/unstaging lines without symlink changes, and add tests --- lib/models/file-patch.js | 37 +++++++--- .../controllers/file-patch-controller.test.js | 68 +++++++++++++++++++ 2 files changed, 97 insertions(+), 8 deletions(-) diff --git a/lib/models/file-patch.js b/lib/models/file-patch.js index a6d20f57c0..d9a0ef702f 100644 --- a/lib/models/file-patch.js +++ b/lib/models/file-patch.js @@ -205,10 +205,20 @@ export default class FilePatch { delta += newRowCount - hunk.getNewRowCount(); } - return this.clone({ - newFile: this.getNewPath() ? this.getNewFile() : this.getOldFile(), - patch: this.getPatch().clone({hunks}), - }); + if (this.hasTypechange() && this.getStatus() === 'deleted') { + // handle special case when symlink is created where a file was deleted. + // In order to stage the line deletion, we must ensure that the created file patch a modified status + // and that new file is not a symlink + return this.clone({ + newFile: this.getNewFile().clone({mode: null, symlink: null}), + patch: this.getPatch().clone({hunks, status: 'modified'}), + }); + } else { + return this.clone({ + newFile: this.getNewPath() ? this.getNewFile() : this.getOldFile(), + patch: this.getPatch().clone({hunks, status: 'modified'}), + }); + } } getUnstagePatch() { @@ -286,10 +296,21 @@ export default class FilePatch { delta += oldRowCount - hunk.getOldRowCount(); } - return this.clone({ - oldFile: this.getOldPath() ? this.getOldFile() : this.getNewFile(), - patch: this.getPatch().clone({hunks}), - }).getUnstagePatch(); + if (this.hasTypechange() && this.getStatus() === 'added') { + // handle special case when symlink is created where a file was deleted. + // In order to unstage the line addition, we must ensure that the created file patch a modified status + // and that oldFile is not a symlink + const oldFile = this.getOldPath() ? this.getOldFile() : this.getNewFile(); + return this.clone({ + oldFile: oldFile.clone({mode: null, symlink: null}), + patch: this.getPatch().clone({hunks, status: 'modified'}), + }).getUnstagePatch(); + } else { + return this.clone({ + oldFile: this.getOldPath() ? this.getOldFile() : this.getNewFile(), + patch: this.getPatch().clone({hunks, status: 'modified'}), + }).getUnstagePatch(); + } } toString() { diff --git a/test/controllers/file-patch-controller.test.js b/test/controllers/file-patch-controller.test.js index fdf30b38a0..4392b8ff6b 100644 --- a/test/controllers/file-patch-controller.test.js +++ b/test/controllers/file-patch-controller.test.js @@ -322,6 +322,74 @@ describe('FilePatchController', function() { } } + it('unstages added lines that don\'t require symlink change', async function() { + const workingDirPath = await cloneRepository('symlinks'); + const repository = await buildRepository(workingDirPath); + + // correctly handle symlinks on Windows + repository.git.exec(['config', 'core.symlinks', 'true']); + + const deletedSymlinkAddedFilePath = 'symlink.txt'; + fs.unlinkSync(path.join(workingDirPath, deletedSymlinkAddedFilePath)); + fs.writeFileSync(path.join(workingDirPath, deletedSymlinkAddedFilePath), 'qux\nfoo\nbar\nbaz\nzoo\n', 'utf8'); + + // Stage whole file + await repository.stageFiles([deletedSymlinkAddedFilePath]); + + const component = createComponent(repository, deletedSymlinkAddedFilePath); + const wrapper = mount(React.cloneElement(component, {filePath: deletedSymlinkAddedFilePath, initialStagingStatus: 'staged'})); + + // index shows symlink deltion and added lines + assert.autocrlfEqual(await repository.readFileFromIndex(deletedSymlinkAddedFilePath), 'qux\nfoo\nbar\nbaz\nzoo\n'); + assert.equal((await indexModeAndOid(repository, deletedSymlinkAddedFilePath)).mode, '100644'); + + // Unstage a couple added lines, but not all + await assert.async.isTrue(wrapper.find('HunkView').exists()); + const opPromise0 = switchboard.getFinishStageOperationPromise(); + const hunkView0 = wrapper.find('HunkView').at(0); + hunkView0.find('LineView').at(1).find('.github-HunkView-line').simulate('mousedown', {button: 0, detail: 1}); + hunkView0.find('LineView').at(2).find('.github-HunkView-line').simulate('mousemove', {}); + window.dispatchEvent(new MouseEvent('mouseup')); + hunkView0.find('button.github-HunkView-stageButton').simulate('click'); + await opPromise0; + + repository.refresh(); + // index shows symlink deletions still staged, only a couple of lines have been unstaged + assert.equal((await indexModeAndOid(repository, deletedSymlinkAddedFilePath)).mode, '100644'); + assert.autocrlfEqual(await repository.readFileFromIndex(deletedSymlinkAddedFilePath), 'qux\nbaz\nzoo\n'); + }); + + it('stages deleted lines that don\'t require symlink change', async function() { + const workingDirPath = await cloneRepository('symlinks'); + const repository = await buildRepository(workingDirPath); + + const deletedFileAddedSymlinkPath = 'a.txt'; + fs.unlinkSync(path.join(workingDirPath, deletedFileAddedSymlinkPath)); + fs.symlinkSync(path.join(workingDirPath, 'regular-file.txt'), path.join(workingDirPath, deletedFileAddedSymlinkPath)); + + const component = createComponent(repository, deletedFileAddedSymlinkPath); + const wrapper = mount(React.cloneElement(component, {filePath: deletedFileAddedSymlinkPath, initialStagingStatus: 'unstaged'})); + + // index shows file is not a symlink, no deleted lines + assert.equal((await indexModeAndOid(repository, deletedFileAddedSymlinkPath)).mode, '100644'); + assert.autocrlfEqual(await repository.readFileFromIndex(deletedFileAddedSymlinkPath), 'foo\nbar\nbaz\n\n'); + + // stage a couple of lines, but not all + await assert.async.isTrue(wrapper.find('HunkView').exists()); + const opPromise0 = switchboard.getFinishStageOperationPromise(); + const hunkView0 = wrapper.find('HunkView').at(0); + hunkView0.find('LineView').at(1).find('.github-HunkView-line').simulate('mousedown', {button: 0, detail: 1}); + hunkView0.find('LineView').at(2).find('.github-HunkView-line').simulate('mousemove', {}); + window.dispatchEvent(new MouseEvent('mouseup')); + hunkView0.find('button.github-HunkView-stageButton').simulate('click'); + await opPromise0; + + repository.refresh(); + // index shows symlink change has not been staged, a couple of lines have been deleted + assert.equal((await indexModeAndOid(repository, deletedFileAddedSymlinkPath)).mode, '100644'); + assert.autocrlfEqual(await repository.readFileFromIndex(deletedFileAddedSymlinkPath), 'foo\n\n'); + }); + it('stages symlink change when staging added lines that depend on change', async function() { const workingDirPath = await cloneRepository('symlinks'); const repository = await buildRepository(workingDirPath); From 07bd63c211b93718e68ead0d715c2040523920b1 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Wed, 17 Jan 2018 13:48:34 -0800 Subject: [PATCH 0179/5882] Unfocus tests --- .../controllers/file-patch-controller.test.js | 290 +++++++++--------- test/models/file-patch.test.js | 42 ++- 2 files changed, 164 insertions(+), 168 deletions(-) diff --git a/test/controllers/file-patch-controller.test.js b/test/controllers/file-patch-controller.test.js index 4392b8ff6b..a22635d5e4 100644 --- a/test/controllers/file-patch-controller.test.js +++ b/test/controllers/file-patch-controller.test.js @@ -310,213 +310,211 @@ describe('FilePatchController', function() { }); describe('integration tests', function() { - for (let i = 0; i < 10; i++) { - describe.only('handling symlink files', function() { - async function indexModeAndOid(repository, filename) { - const output = await repository.git.exec(['ls-files', '-s', '--', filename]); - if (output) { - const parts = output.split(' '); - return {mode: parts[0], oid: parts[1]}; - } else { - return null; - } + describe('handling symlink files', function() { + async function indexModeAndOid(repository, filename) { + const output = await repository.git.exec(['ls-files', '-s', '--', filename]); + if (output) { + const parts = output.split(' '); + return {mode: parts[0], oid: parts[1]}; + } else { + return null; } + } - it('unstages added lines that don\'t require symlink change', async function() { - const workingDirPath = await cloneRepository('symlinks'); - const repository = await buildRepository(workingDirPath); + it('unstages added lines that don\'t require symlink change', async function() { + const workingDirPath = await cloneRepository('symlinks'); + const repository = await buildRepository(workingDirPath); // correctly handle symlinks on Windows - repository.git.exec(['config', 'core.symlinks', 'true']); + repository.git.exec(['config', 'core.symlinks', 'true']); - const deletedSymlinkAddedFilePath = 'symlink.txt'; - fs.unlinkSync(path.join(workingDirPath, deletedSymlinkAddedFilePath)); - fs.writeFileSync(path.join(workingDirPath, deletedSymlinkAddedFilePath), 'qux\nfoo\nbar\nbaz\nzoo\n', 'utf8'); + const deletedSymlinkAddedFilePath = 'symlink.txt'; + fs.unlinkSync(path.join(workingDirPath, deletedSymlinkAddedFilePath)); + fs.writeFileSync(path.join(workingDirPath, deletedSymlinkAddedFilePath), 'qux\nfoo\nbar\nbaz\nzoo\n', 'utf8'); // Stage whole file - await repository.stageFiles([deletedSymlinkAddedFilePath]); + await repository.stageFiles([deletedSymlinkAddedFilePath]); - const component = createComponent(repository, deletedSymlinkAddedFilePath); - const wrapper = mount(React.cloneElement(component, {filePath: deletedSymlinkAddedFilePath, initialStagingStatus: 'staged'})); + const component = createComponent(repository, deletedSymlinkAddedFilePath); + const wrapper = mount(React.cloneElement(component, {filePath: deletedSymlinkAddedFilePath, initialStagingStatus: 'staged'})); // index shows symlink deltion and added lines - assert.autocrlfEqual(await repository.readFileFromIndex(deletedSymlinkAddedFilePath), 'qux\nfoo\nbar\nbaz\nzoo\n'); - assert.equal((await indexModeAndOid(repository, deletedSymlinkAddedFilePath)).mode, '100644'); + assert.autocrlfEqual(await repository.readFileFromIndex(deletedSymlinkAddedFilePath), 'qux\nfoo\nbar\nbaz\nzoo\n'); + assert.equal((await indexModeAndOid(repository, deletedSymlinkAddedFilePath)).mode, '100644'); // Unstage a couple added lines, but not all - await assert.async.isTrue(wrapper.find('HunkView').exists()); - const opPromise0 = switchboard.getFinishStageOperationPromise(); - const hunkView0 = wrapper.find('HunkView').at(0); - hunkView0.find('LineView').at(1).find('.github-HunkView-line').simulate('mousedown', {button: 0, detail: 1}); - hunkView0.find('LineView').at(2).find('.github-HunkView-line').simulate('mousemove', {}); - window.dispatchEvent(new MouseEvent('mouseup')); - hunkView0.find('button.github-HunkView-stageButton').simulate('click'); - await opPromise0; + await assert.async.isTrue(wrapper.find('HunkView').exists()); + const opPromise0 = switchboard.getFinishStageOperationPromise(); + const hunkView0 = wrapper.find('HunkView').at(0); + hunkView0.find('LineView').at(1).find('.github-HunkView-line').simulate('mousedown', {button: 0, detail: 1}); + hunkView0.find('LineView').at(2).find('.github-HunkView-line').simulate('mousemove', {}); + window.dispatchEvent(new MouseEvent('mouseup')); + hunkView0.find('button.github-HunkView-stageButton').simulate('click'); + await opPromise0; - repository.refresh(); + repository.refresh(); // index shows symlink deletions still staged, only a couple of lines have been unstaged - assert.equal((await indexModeAndOid(repository, deletedSymlinkAddedFilePath)).mode, '100644'); - assert.autocrlfEqual(await repository.readFileFromIndex(deletedSymlinkAddedFilePath), 'qux\nbaz\nzoo\n'); - }); + assert.equal((await indexModeAndOid(repository, deletedSymlinkAddedFilePath)).mode, '100644'); + assert.autocrlfEqual(await repository.readFileFromIndex(deletedSymlinkAddedFilePath), 'qux\nbaz\nzoo\n'); + }); - it('stages deleted lines that don\'t require symlink change', async function() { - const workingDirPath = await cloneRepository('symlinks'); - const repository = await buildRepository(workingDirPath); + it('stages deleted lines that don\'t require symlink change', async function() { + const workingDirPath = await cloneRepository('symlinks'); + const repository = await buildRepository(workingDirPath); - const deletedFileAddedSymlinkPath = 'a.txt'; - fs.unlinkSync(path.join(workingDirPath, deletedFileAddedSymlinkPath)); - fs.symlinkSync(path.join(workingDirPath, 'regular-file.txt'), path.join(workingDirPath, deletedFileAddedSymlinkPath)); + const deletedFileAddedSymlinkPath = 'a.txt'; + fs.unlinkSync(path.join(workingDirPath, deletedFileAddedSymlinkPath)); + fs.symlinkSync(path.join(workingDirPath, 'regular-file.txt'), path.join(workingDirPath, deletedFileAddedSymlinkPath)); - const component = createComponent(repository, deletedFileAddedSymlinkPath); - const wrapper = mount(React.cloneElement(component, {filePath: deletedFileAddedSymlinkPath, initialStagingStatus: 'unstaged'})); + const component = createComponent(repository, deletedFileAddedSymlinkPath); + const wrapper = mount(React.cloneElement(component, {filePath: deletedFileAddedSymlinkPath, initialStagingStatus: 'unstaged'})); // index shows file is not a symlink, no deleted lines - assert.equal((await indexModeAndOid(repository, deletedFileAddedSymlinkPath)).mode, '100644'); - assert.autocrlfEqual(await repository.readFileFromIndex(deletedFileAddedSymlinkPath), 'foo\nbar\nbaz\n\n'); + assert.equal((await indexModeAndOid(repository, deletedFileAddedSymlinkPath)).mode, '100644'); + assert.autocrlfEqual(await repository.readFileFromIndex(deletedFileAddedSymlinkPath), 'foo\nbar\nbaz\n\n'); // stage a couple of lines, but not all - await assert.async.isTrue(wrapper.find('HunkView').exists()); - const opPromise0 = switchboard.getFinishStageOperationPromise(); - const hunkView0 = wrapper.find('HunkView').at(0); - hunkView0.find('LineView').at(1).find('.github-HunkView-line').simulate('mousedown', {button: 0, detail: 1}); - hunkView0.find('LineView').at(2).find('.github-HunkView-line').simulate('mousemove', {}); - window.dispatchEvent(new MouseEvent('mouseup')); - hunkView0.find('button.github-HunkView-stageButton').simulate('click'); - await opPromise0; + await assert.async.isTrue(wrapper.find('HunkView').exists()); + const opPromise0 = switchboard.getFinishStageOperationPromise(); + const hunkView0 = wrapper.find('HunkView').at(0); + hunkView0.find('LineView').at(1).find('.github-HunkView-line').simulate('mousedown', {button: 0, detail: 1}); + hunkView0.find('LineView').at(2).find('.github-HunkView-line').simulate('mousemove', {}); + window.dispatchEvent(new MouseEvent('mouseup')); + hunkView0.find('button.github-HunkView-stageButton').simulate('click'); + await opPromise0; - repository.refresh(); + repository.refresh(); // index shows symlink change has not been staged, a couple of lines have been deleted - assert.equal((await indexModeAndOid(repository, deletedFileAddedSymlinkPath)).mode, '100644'); - assert.autocrlfEqual(await repository.readFileFromIndex(deletedFileAddedSymlinkPath), 'foo\n\n'); - }); + assert.equal((await indexModeAndOid(repository, deletedFileAddedSymlinkPath)).mode, '100644'); + assert.autocrlfEqual(await repository.readFileFromIndex(deletedFileAddedSymlinkPath), 'foo\n\n'); + }); - it('stages symlink change when staging added lines that depend on change', async function() { - const workingDirPath = await cloneRepository('symlinks'); - const repository = await buildRepository(workingDirPath); + it('stages symlink change when staging added lines that depend on change', async function() { + const workingDirPath = await cloneRepository('symlinks'); + const repository = await buildRepository(workingDirPath); // correctly handle symlinks on Windows - repository.git.exec(['config', 'core.symlinks', 'true']); + repository.git.exec(['config', 'core.symlinks', 'true']); - const deletedSymlinkAddedFilePath = 'symlink.txt'; - fs.unlinkSync(path.join(workingDirPath, deletedSymlinkAddedFilePath)); - fs.writeFileSync(path.join(workingDirPath, deletedSymlinkAddedFilePath), 'qux\nfoo\nbar\nbaz\nzoo\n', 'utf8'); + const deletedSymlinkAddedFilePath = 'symlink.txt'; + fs.unlinkSync(path.join(workingDirPath, deletedSymlinkAddedFilePath)); + fs.writeFileSync(path.join(workingDirPath, deletedSymlinkAddedFilePath), 'qux\nfoo\nbar\nbaz\nzoo\n', 'utf8'); - const component = createComponent(repository, deletedSymlinkAddedFilePath); - const wrapper = mount(React.cloneElement(component, {filePath: deletedSymlinkAddedFilePath})); + const component = createComponent(repository, deletedSymlinkAddedFilePath); + const wrapper = mount(React.cloneElement(component, {filePath: deletedSymlinkAddedFilePath})); // index shows file is symlink - assert.equal((await indexModeAndOid(repository, deletedSymlinkAddedFilePath)).mode, '120000'); + assert.equal((await indexModeAndOid(repository, deletedSymlinkAddedFilePath)).mode, '120000'); // Stage a couple added lines, but not all - await assert.async.isTrue(wrapper.find('HunkView').exists()); - const opPromise0 = switchboard.getFinishStageOperationPromise(); - const hunkView0 = wrapper.find('HunkView').at(0); - hunkView0.find('LineView').at(1).find('.github-HunkView-line').simulate('mousedown', {button: 0, detail: 1}); - hunkView0.find('LineView').at(2).find('.github-HunkView-line').simulate('mousemove', {}); - window.dispatchEvent(new MouseEvent('mouseup')); - hunkView0.find('button.github-HunkView-stageButton').simulate('click'); - await opPromise0; + await assert.async.isTrue(wrapper.find('HunkView').exists()); + const opPromise0 = switchboard.getFinishStageOperationPromise(); + const hunkView0 = wrapper.find('HunkView').at(0); + hunkView0.find('LineView').at(1).find('.github-HunkView-line').simulate('mousedown', {button: 0, detail: 1}); + hunkView0.find('LineView').at(2).find('.github-HunkView-line').simulate('mousemove', {}); + window.dispatchEvent(new MouseEvent('mouseup')); + hunkView0.find('button.github-HunkView-stageButton').simulate('click'); + await opPromise0; - repository.refresh(); + repository.refresh(); // index no longer shows file is symlink (symlink has been deleted), now a regular file with contents - assert.equal((await indexModeAndOid(repository, deletedSymlinkAddedFilePath)).mode, '100644'); - assert.autocrlfEqual(await repository.readFileFromIndex(deletedSymlinkAddedFilePath), 'foo\nbar\n'); - }); + assert.equal((await indexModeAndOid(repository, deletedSymlinkAddedFilePath)).mode, '100644'); + assert.autocrlfEqual(await repository.readFileFromIndex(deletedSymlinkAddedFilePath), 'foo\nbar\n'); + }); - it('unstages symlink change when unstaging deleted lines that depend on change', async function() { - const workingDirPath = await cloneRepository('symlinks'); - const repository = await buildRepository(workingDirPath); + it('unstages symlink change when unstaging deleted lines that depend on change', async function() { + const workingDirPath = await cloneRepository('symlinks'); + const repository = await buildRepository(workingDirPath); - const deletedFileAddedSymlinkPath = 'a.txt'; - fs.unlinkSync(path.join(workingDirPath, deletedFileAddedSymlinkPath)); - fs.symlinkSync(path.join(workingDirPath, 'regular-file.txt'), path.join(workingDirPath, deletedFileAddedSymlinkPath)); - await repository.stageFiles([deletedFileAddedSymlinkPath]); + const deletedFileAddedSymlinkPath = 'a.txt'; + fs.unlinkSync(path.join(workingDirPath, deletedFileAddedSymlinkPath)); + fs.symlinkSync(path.join(workingDirPath, 'regular-file.txt'), path.join(workingDirPath, deletedFileAddedSymlinkPath)); + await repository.stageFiles([deletedFileAddedSymlinkPath]); - const component = createComponent(repository, deletedFileAddedSymlinkPath); - const wrapper = mount(React.cloneElement(component, {filePath: deletedFileAddedSymlinkPath, initialStagingStatus: 'staged'})); + const component = createComponent(repository, deletedFileAddedSymlinkPath); + const wrapper = mount(React.cloneElement(component, {filePath: deletedFileAddedSymlinkPath, initialStagingStatus: 'staged'})); // index shows file is symlink - assert.equal((await indexModeAndOid(repository, deletedFileAddedSymlinkPath)).mode, '120000'); + assert.equal((await indexModeAndOid(repository, deletedFileAddedSymlinkPath)).mode, '120000'); // unstage a couple of lines, but not all - await assert.async.isTrue(wrapper.find('HunkView').exists()); - const opPromise0 = switchboard.getFinishStageOperationPromise(); - const hunkView0 = wrapper.find('HunkView').at(0); - hunkView0.find('LineView').at(1).find('.github-HunkView-line').simulate('mousedown', {button: 0, detail: 1}); - hunkView0.find('LineView').at(2).find('.github-HunkView-line').simulate('mousemove', {}); - window.dispatchEvent(new MouseEvent('mouseup')); - hunkView0.find('button.github-HunkView-stageButton').simulate('click'); - await opPromise0; + await assert.async.isTrue(wrapper.find('HunkView').exists()); + const opPromise0 = switchboard.getFinishStageOperationPromise(); + const hunkView0 = wrapper.find('HunkView').at(0); + hunkView0.find('LineView').at(1).find('.github-HunkView-line').simulate('mousedown', {button: 0, detail: 1}); + hunkView0.find('LineView').at(2).find('.github-HunkView-line').simulate('mousemove', {}); + window.dispatchEvent(new MouseEvent('mouseup')); + hunkView0.find('button.github-HunkView-stageButton').simulate('click'); + await opPromise0; - repository.refresh(); + repository.refresh(); // index no longer shows file is symlink (symlink creation has been unstaged), shows contents of file that existed prior to symlink - assert.equal((await indexModeAndOid(repository, deletedFileAddedSymlinkPath)).mode, '100644'); - assert.autocrlfEqual(await repository.readFileFromIndex(deletedFileAddedSymlinkPath), 'bar\nbaz\n'); - }); + assert.equal((await indexModeAndOid(repository, deletedFileAddedSymlinkPath)).mode, '100644'); + assert.autocrlfEqual(await repository.readFileFromIndex(deletedFileAddedSymlinkPath), 'bar\nbaz\n'); + }); - it('stages file deletion when all deleted lines are staged', async function() { - const workingDirPath = await cloneRepository('symlinks'); - const repository = await buildRepository(workingDirPath); - await repository.getLoadPromise(); + it('stages file deletion when all deleted lines are staged', async function() { + const workingDirPath = await cloneRepository('symlinks'); + const repository = await buildRepository(workingDirPath); + await repository.getLoadPromise(); - const deletedFileAddedSymlinkPath = 'a.txt'; - fs.unlinkSync(path.join(workingDirPath, deletedFileAddedSymlinkPath)); - fs.symlinkSync(path.join(workingDirPath, 'regular-file.txt'), path.join(workingDirPath, deletedFileAddedSymlinkPath)); + const deletedFileAddedSymlinkPath = 'a.txt'; + fs.unlinkSync(path.join(workingDirPath, deletedFileAddedSymlinkPath)); + fs.symlinkSync(path.join(workingDirPath, 'regular-file.txt'), path.join(workingDirPath, deletedFileAddedSymlinkPath)); - const component = createComponent(repository, deletedFileAddedSymlinkPath); - const wrapper = mount(React.cloneElement(component, {filePath: deletedFileAddedSymlinkPath})); + const component = createComponent(repository, deletedFileAddedSymlinkPath); + const wrapper = mount(React.cloneElement(component, {filePath: deletedFileAddedSymlinkPath})); - assert.equal((await indexModeAndOid(repository, deletedFileAddedSymlinkPath)).mode, '100644'); + assert.equal((await indexModeAndOid(repository, deletedFileAddedSymlinkPath)).mode, '100644'); // stage all deleted lines - await assert.async.isTrue(wrapper.find('HunkView').exists()); - const opPromise0 = switchboard.getFinishStageOperationPromise(); - const hunkView0 = wrapper.find('HunkView').at(0); - hunkView0.find('.github-HunkView-title').simulate('click'); - hunkView0.find('button.github-HunkView-stageButton').simulate('click'); - await opPromise0; + await assert.async.isTrue(wrapper.find('HunkView').exists()); + const opPromise0 = switchboard.getFinishStageOperationPromise(); + const hunkView0 = wrapper.find('HunkView').at(0); + hunkView0.find('.github-HunkView-title').simulate('click'); + hunkView0.find('button.github-HunkView-stageButton').simulate('click'); + await opPromise0; - repository.refresh(); + repository.refresh(); // File is not on index, file deletion has been staged - assert.isNull(await indexModeAndOid(repository, deletedFileAddedSymlinkPath)); - const {stagedFiles, unstagedFiles} = await repository.getStatusesForChangedFiles(); - assert.equal(unstagedFiles[deletedFileAddedSymlinkPath], 'added'); - assert.equal(stagedFiles[deletedFileAddedSymlinkPath], 'deleted'); - }); + assert.isNull(await indexModeAndOid(repository, deletedFileAddedSymlinkPath)); + const {stagedFiles, unstagedFiles} = await repository.getStatusesForChangedFiles(); + assert.equal(unstagedFiles[deletedFileAddedSymlinkPath], 'added'); + assert.equal(stagedFiles[deletedFileAddedSymlinkPath], 'deleted'); + }); - it('unstages file creation when all added lines are unstaged', async function() { - const workingDirPath = await cloneRepository('symlinks'); - const repository = await buildRepository(workingDirPath); + it('unstages file creation when all added lines are unstaged', async function() { + const workingDirPath = await cloneRepository('symlinks'); + const repository = await buildRepository(workingDirPath); - repository.git.exec(['config', 'core.symlinks', 'true']); + repository.git.exec(['config', 'core.symlinks', 'true']); - const deletedSymlinkAddedFilePath = 'symlink.txt'; - fs.unlinkSync(path.join(workingDirPath, deletedSymlinkAddedFilePath)); - fs.writeFileSync(path.join(workingDirPath, deletedSymlinkAddedFilePath), 'qux\nfoo\nbar\nbaz\nzoo\n', 'utf8'); - await repository.stageFiles([deletedSymlinkAddedFilePath]); + const deletedSymlinkAddedFilePath = 'symlink.txt'; + fs.unlinkSync(path.join(workingDirPath, deletedSymlinkAddedFilePath)); + fs.writeFileSync(path.join(workingDirPath, deletedSymlinkAddedFilePath), 'qux\nfoo\nbar\nbaz\nzoo\n', 'utf8'); + await repository.stageFiles([deletedSymlinkAddedFilePath]); - const component = createComponent(repository, deletedSymlinkAddedFilePath); - const wrapper = mount(React.cloneElement(component, {filePath: deletedSymlinkAddedFilePath, initialStagingStatus: 'staged'})); + const component = createComponent(repository, deletedSymlinkAddedFilePath); + const wrapper = mount(React.cloneElement(component, {filePath: deletedSymlinkAddedFilePath, initialStagingStatus: 'staged'})); - assert.equal((await indexModeAndOid(repository, deletedSymlinkAddedFilePath)).mode, '100644'); + assert.equal((await indexModeAndOid(repository, deletedSymlinkAddedFilePath)).mode, '100644'); // unstage all added lines - await assert.async.isTrue(wrapper.find('HunkView').exists()); - const opPromise0 = switchboard.getFinishStageOperationPromise(); - const hunkView0 = wrapper.find('HunkView').at(0); - hunkView0.find('.github-HunkView-title').simulate('click'); - hunkView0.find('button.github-HunkView-stageButton').simulate('click'); - await opPromise0; + await assert.async.isTrue(wrapper.find('HunkView').exists()); + const opPromise0 = switchboard.getFinishStageOperationPromise(); + const hunkView0 = wrapper.find('HunkView').at(0); + hunkView0.find('.github-HunkView-title').simulate('click'); + hunkView0.find('button.github-HunkView-stageButton').simulate('click'); + await opPromise0; - repository.refresh(); + repository.refresh(); // File is not on index, file creation has been unstaged - assert.isNull(await indexModeAndOid(repository, deletedSymlinkAddedFilePath)); - const {stagedFiles, unstagedFiles} = await repository.getStatusesForChangedFiles(); - assert.equal(unstagedFiles[deletedSymlinkAddedFilePath], 'added'); - assert.equal(stagedFiles[deletedSymlinkAddedFilePath], 'deleted'); - }); + assert.isNull(await indexModeAndOid(repository, deletedSymlinkAddedFilePath)); + const {stagedFiles, unstagedFiles} = await repository.getStatusesForChangedFiles(); + assert.equal(unstagedFiles[deletedSymlinkAddedFilePath], 'added'); + assert.equal(stagedFiles[deletedSymlinkAddedFilePath], 'deleted'); }); - } + }); describe('handling non-symlink changes', function() { let workdirPath, repository, filePath, component; diff --git a/test/models/file-patch.test.js b/test/models/file-patch.test.js index 7c4d1477c1..916d68c79e 100644 --- a/test/models/file-patch.test.js +++ b/test/models/file-patch.test.js @@ -291,20 +291,19 @@ describe('FilePatch', function() { `); }); - for (let i = 0; i < 10; i++) { - describe.only('typechange file patches', function() { - it('handles typechange patches for a symlink replaced with a file', async function() { - const workdirPath = await cloneRepository('symlinks'); - const repository = await buildRepository(workdirPath); + describe('typechange file patches', function() { + it('handles typechange patches for a symlink replaced with a file', async function() { + const workdirPath = await cloneRepository('symlinks'); + const repository = await buildRepository(workdirPath); - repository.git.exec(['config', 'core.symlinks', 'true']); + repository.git.exec(['config', 'core.symlinks', 'true']); - const deletedSymlinkAddedFilePath = 'symlink.txt'; - fs.unlinkSync(path.join(workdirPath, deletedSymlinkAddedFilePath)); - fs.writeFileSync(path.join(workdirPath, deletedSymlinkAddedFilePath), 'qux\nfoo\nbar\n', 'utf8'); + const deletedSymlinkAddedFilePath = 'symlink.txt'; + fs.unlinkSync(path.join(workdirPath, deletedSymlinkAddedFilePath)); + fs.writeFileSync(path.join(workdirPath, deletedSymlinkAddedFilePath), 'qux\nfoo\nbar\n', 'utf8'); - const patch = await repository.getFilePatchForPath(deletedSymlinkAddedFilePath); - assert.equal(patch.toString(), dedent` + const patch = await repository.getFilePatchForPath(deletedSymlinkAddedFilePath); + assert.equal(patch.toString(), dedent` diff --git a/symlink.txt b/symlink.txt deleted file mode 120000 --- a/symlink.txt @@ -322,18 +321,18 @@ describe('FilePatch', function() { +bar `); - }); + }); - it('handles typechange patches for a file replaced with a symlink', async function() { - const workdirPath = await cloneRepository('symlinks'); - const repository = await buildRepository(workdirPath); + it('handles typechange patches for a file replaced with a symlink', async function() { + const workdirPath = await cloneRepository('symlinks'); + const repository = await buildRepository(workdirPath); - const deletedFileAddedSymlinkPath = 'a.txt'; - fs.unlinkSync(path.join(workdirPath, deletedFileAddedSymlinkPath)); - fs.symlinkSync(path.join(workdirPath, 'regular-file.txt'), path.join(workdirPath, deletedFileAddedSymlinkPath)); + const deletedFileAddedSymlinkPath = 'a.txt'; + fs.unlinkSync(path.join(workdirPath, deletedFileAddedSymlinkPath)); + fs.symlinkSync(path.join(workdirPath, 'regular-file.txt'), path.join(workdirPath, deletedFileAddedSymlinkPath)); - const patch = await repository.getFilePatchForPath(deletedFileAddedSymlinkPath); - assert.equal(patch.toString(), dedent` + const patch = await repository.getFilePatchForPath(deletedFileAddedSymlinkPath); + assert.equal(patch.toString(), dedent` diff --git a/a.txt b/a.txt deleted file mode 100644 --- a/a.txt @@ -352,9 +351,8 @@ describe('FilePatch', function() { \\ No newline at end of file `); - }); }); - } + }); }); describe('getHeaderString()', function() { From 4fd8a733a72d9193292e81ce2b12af6c15626d86 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Wed, 17 Jan 2018 14:54:19 -0800 Subject: [PATCH 0180/5882] :art: and fix logic for getting un/stagePatchForLines --- lib/models/file-patch.js | 75 +++++++++++++++++----------------- test/models/file-patch.test.js | 6 +-- 2 files changed, 41 insertions(+), 40 deletions(-) diff --git a/lib/models/file-patch.js b/lib/models/file-patch.js index d9a0ef702f..909289bdc1 100644 --- a/lib/models/file-patch.js +++ b/lib/models/file-patch.js @@ -162,18 +162,34 @@ export default class FilePatch { } getStagePatchForLines(selectedLines) { - if (this.changedLineCount === [...selectedLines].filter(line => line.isChanged()).length) { + const wholeFileSelected = this.changedLineCount === [...selectedLines].filter(line => line.isChanged()).length; + if (wholeFileSelected) { if (this.hasTypechange() && this.getStatus() === 'deleted') { // handle special case when symlink is created where a file was deleted. In order to stage the file deletion, // we must ensure that the created file patch has no new file return this.clone({ - newFile: new File({path: null, mode: null, symlink: null}), + newFile: File.empty(), }); } else { return this; } + } else { + const hunks = this.getStagePatchHunks(selectedLines); + if (this.getStatus() === 'deleted') { + // Set status to modified + return this.clone({ + newFile: this.getOldFile(), + patch: this.getPatch().clone({hunks, status: 'modified'}), + }); + } else { + return this.clone({ + patch: this.getPatch().clone({hunks}), + }); + } } + } + getStagePatchHunks(selectedLines) { let delta = 0; const hunks = []; for (const hunk of this.getHunks()) { @@ -199,26 +215,12 @@ export default class FilePatch { } const newRowCount = newLineNumber - newStartRow; if (hunkContainsSelectedLines) { - // eslint-disable-next-line max-len + // eslint-disable-next-line max-len hunks.push(new Hunk(hunk.getOldStartRow(), newStartRow, hunk.getOldRowCount(), newRowCount, hunk.getSectionHeading(), lines)); } delta += newRowCount - hunk.getNewRowCount(); } - - if (this.hasTypechange() && this.getStatus() === 'deleted') { - // handle special case when symlink is created where a file was deleted. - // In order to stage the line deletion, we must ensure that the created file patch a modified status - // and that new file is not a symlink - return this.clone({ - newFile: this.getNewFile().clone({mode: null, symlink: null}), - patch: this.getPatch().clone({hunks, status: 'modified'}), - }); - } else { - return this.clone({ - newFile: this.getNewPath() ? this.getNewFile() : this.getOldFile(), - patch: this.getPatch().clone({hunks, status: 'modified'}), - }); - } + return hunks; } getUnstagePatch() { @@ -257,14 +259,28 @@ export default class FilePatch { // handle special case when a file was created after a symlink was deleted. // In order to unstage the file creation, we must ensure that the unstage patch has no new file, // so when the patch is applied to the index, there file will be removed from the index - return this.getUnstagePatch().clone({ - newFile: new File({path: null, mode: null, symlink: null}), - }); + return this.clone({ + oldFile: File.empty(), + }).getUnstagePatch(); } else { return this.getUnstagePatch(); } } + const hunks = this.getUnstagePatchHunks(selectedLines); + if (this.getStatus() === 'added') { + return this.clone({ + oldFile: this.getNewFile(), + patch: this.getPatch().clone({hunks, status: 'modified'}), + }).getUnstagePatch(); + } else { + return this.clone({ + patch: this.getPatch().clone({hunks}), + }).getUnstagePatch(); + } + } + + getUnstagePatchHunks(selectedLines) { let delta = 0; const hunks = []; for (const hunk of this.getHunks()) { @@ -295,22 +311,7 @@ export default class FilePatch { } delta += oldRowCount - hunk.getOldRowCount(); } - - if (this.hasTypechange() && this.getStatus() === 'added') { - // handle special case when symlink is created where a file was deleted. - // In order to unstage the line addition, we must ensure that the created file patch a modified status - // and that oldFile is not a symlink - const oldFile = this.getOldPath() ? this.getOldFile() : this.getNewFile(); - return this.clone({ - oldFile: oldFile.clone({mode: null, symlink: null}), - patch: this.getPatch().clone({hunks, status: 'modified'}), - }).getUnstagePatch(); - } else { - return this.clone({ - oldFile: this.getOldPath() ? this.getOldFile() : this.getNewFile(), - patch: this.getPatch().clone({hunks, status: 'modified'}), - }).getUnstagePatch(); - } + return hunks; } toString() { diff --git a/test/models/file-patch.test.js b/test/models/file-patch.test.js index 916d68c79e..5160c70973 100644 --- a/test/models/file-patch.test.js +++ b/test/models/file-patch.test.js @@ -94,7 +94,7 @@ describe('FilePatch', function() { ]); const linesFromHunk = filePatch.getHunks()[0].getLines().slice(0, 2); assert.deepEqual(filePatch.getStagePatchForLines(new Set(linesFromHunk)), createFilePatch( - 'a.txt', 'a.txt', 'deleted', [ + 'a.txt', 'a.txt', 'modified', [ new Hunk(1, 1, 3, 1, '', [ new HunkLine('line-1', 'deleted', 1, -1), new HunkLine('line-2', 'deleted', 2, -1), @@ -184,7 +184,7 @@ describe('FilePatch', function() { ]); const linesFromHunk = filePatch.getHunks()[0].getLines().slice(0, 2); assert.deepEqual(filePatch.getUnstagePatchForLines(new Set(linesFromHunk)), createFilePatch( - 'a.txt', 'a.txt', 'deleted', [ + 'a.txt', 'a.txt', 'modified', [ new Hunk(1, 1, 3, 1, '', [ new HunkLine('line-1', 'deleted', 1, -1), new HunkLine('line-2', 'deleted', 2, -1), @@ -227,7 +227,7 @@ describe('FilePatch', function() { ]); const linesFromHunk = filePatch.getHunks()[0].getLines().slice(0, 2); assert.deepEqual(filePatch.getUnstagePatchForLines(new Set(linesFromHunk)), createFilePatch( - 'a.txt', 'a.txt', 'deleted', [ + 'a.txt', 'a.txt', 'modified', [ new Hunk(1, 1, 3, 1, '', [ new HunkLine('line-1', 'deleted', 1, -1), new HunkLine('line-2', 'deleted', 2, -1), From 2a153de89fe3aef9849a1936281a0ce9e1a94c1c Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Wed, 17 Jan 2018 15:16:17 -0800 Subject: [PATCH 0181/5882] Make executable mode change message more user-friendly --- lib/views/file-patch-view.js | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/lib/views/file-patch-view.js b/lib/views/file-patch-view.js index f914f8e930..ee658b23c0 100644 --- a/lib/views/file-patch-view.js +++ b/lib/views/file-patch-view.js @@ -12,6 +12,11 @@ import Commands, {Command} from './commands'; import FilePatchSelection from './file-patch-selection'; import Switchboard from '../switchboard'; +const executableText = { + 100644: 'non executable 100644', + 100755: 'executable 100755', +}; + export default class FilePatchView extends React.Component { static propTypes = { commandRegistry: PropTypes.object.isRequired, @@ -297,12 +302,12 @@ export default class FilePatchView extends React.Component {
File changed mode - - from {executableModeChange.oldMode} - - - to {executableModeChange.newMode} - + +
From edc313dc09cadf48e9c9ad99822421c852292c77 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Wed, 17 Jan 2018 16:01:19 -0800 Subject: [PATCH 0182/5882] Focus tests failing on windows --- test/controllers/file-patch-controller.test.js | 8 +++++++- test/models/file-patch.test.js | 13 ++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/test/controllers/file-patch-controller.test.js b/test/controllers/file-patch-controller.test.js index a22635d5e4..d659ff0035 100644 --- a/test/controllers/file-patch-controller.test.js +++ b/test/controllers/file-patch-controller.test.js @@ -389,7 +389,7 @@ describe('FilePatchController', function() { assert.autocrlfEqual(await repository.readFileFromIndex(deletedFileAddedSymlinkPath), 'foo\n\n'); }); - it('stages symlink change when staging added lines that depend on change', async function() { + it.only('stages symlink change when staging added lines that depend on change', async function() { const workingDirPath = await cloneRepository('symlinks'); const repository = await buildRepository(workingDirPath); @@ -400,6 +400,12 @@ describe('FilePatchController', function() { fs.unlinkSync(path.join(workingDirPath, deletedSymlinkAddedFilePath)); fs.writeFileSync(path.join(workingDirPath, deletedSymlinkAddedFilePath), 'qux\nfoo\nbar\nbaz\nzoo\n', 'utf8'); + console.log(workingDirPath); + debugger; + // cd into directory and view in atom. do hunks show up? + + // try adding 'initialStagingStatus: 'unstaged'' + const component = createComponent(repository, deletedSymlinkAddedFilePath); const wrapper = mount(React.cloneElement(component, {filePath: deletedSymlinkAddedFilePath})); diff --git a/test/models/file-patch.test.js b/test/models/file-patch.test.js index 5160c70973..ed36c1de9f 100644 --- a/test/models/file-patch.test.js +++ b/test/models/file-patch.test.js @@ -292,7 +292,7 @@ describe('FilePatch', function() { }); describe('typechange file patches', function() { - it('handles typechange patches for a symlink replaced with a file', async function() { + it.only('handles typechange patches for a symlink replaced with a file', async function() { const workdirPath = await cloneRepository('symlinks'); const repository = await buildRepository(workdirPath); @@ -302,6 +302,17 @@ describe('FilePatch', function() { fs.unlinkSync(path.join(workdirPath, deletedSymlinkAddedFilePath)); fs.writeFileSync(path.join(workdirPath, deletedSymlinkAddedFilePath), 'qux\nfoo\nbar\n', 'utf8'); + console.log(workdirPath); + debugger; + // cd into folder and see what diff looks like + // if not then perhaps symlinks are not being treated correctly on windows. + // is mode changing correctly? should we manually change it? + // should we ignore test? is it even relevant? + // if it matches below there's app logic error + // file isn't being marked as deleted + // header for patch added is missing entirely + // check toString method + const patch = await repository.getFilePatchForPath(deletedSymlinkAddedFilePath); assert.equal(patch.toString(), dedent` diff --git a/symlink.txt b/symlink.txt From 06c00b52e4f8ae628c641d18e8855ac1d4fa78c1 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Wed, 17 Jan 2018 17:07:19 -0800 Subject: [PATCH 0183/5882] Remove focused tests, add await to exec calls --- test/controllers/file-patch-controller.test.js | 18 ++++++------------ test/models/file-patch.test.js | 15 ++------------- 2 files changed, 8 insertions(+), 25 deletions(-) diff --git a/test/controllers/file-patch-controller.test.js b/test/controllers/file-patch-controller.test.js index d659ff0035..3e39aeb7a4 100644 --- a/test/controllers/file-patch-controller.test.js +++ b/test/controllers/file-patch-controller.test.js @@ -325,8 +325,8 @@ describe('FilePatchController', function() { const workingDirPath = await cloneRepository('symlinks'); const repository = await buildRepository(workingDirPath); - // correctly handle symlinks on Windows - repository.git.exec(['config', 'core.symlinks', 'true']); + // correctly handle symlinks on Windows + await repository.git.exec(['config', 'core.symlinks', 'true']); const deletedSymlinkAddedFilePath = 'symlink.txt'; fs.unlinkSync(path.join(workingDirPath, deletedSymlinkAddedFilePath)); @@ -389,23 +389,17 @@ describe('FilePatchController', function() { assert.autocrlfEqual(await repository.readFileFromIndex(deletedFileAddedSymlinkPath), 'foo\n\n'); }); - it.only('stages symlink change when staging added lines that depend on change', async function() { + it('stages symlink change when staging added lines that depend on change', async function() { const workingDirPath = await cloneRepository('symlinks'); const repository = await buildRepository(workingDirPath); // correctly handle symlinks on Windows - repository.git.exec(['config', 'core.symlinks', 'true']); + await repository.git.exec(['config', 'core.symlinks', 'true']); const deletedSymlinkAddedFilePath = 'symlink.txt'; fs.unlinkSync(path.join(workingDirPath, deletedSymlinkAddedFilePath)); fs.writeFileSync(path.join(workingDirPath, deletedSymlinkAddedFilePath), 'qux\nfoo\nbar\nbaz\nzoo\n', 'utf8'); - - console.log(workingDirPath); - debugger; - // cd into directory and view in atom. do hunks show up? - - // try adding 'initialStagingStatus: 'unstaged'' - + const component = createComponent(repository, deletedSymlinkAddedFilePath); const wrapper = mount(React.cloneElement(component, {filePath: deletedSymlinkAddedFilePath})); @@ -493,7 +487,7 @@ describe('FilePatchController', function() { const workingDirPath = await cloneRepository('symlinks'); const repository = await buildRepository(workingDirPath); - repository.git.exec(['config', 'core.symlinks', 'true']); + await repository.git.exec(['config', 'core.symlinks', 'true']); const deletedSymlinkAddedFilePath = 'symlink.txt'; fs.unlinkSync(path.join(workingDirPath, deletedSymlinkAddedFilePath)); diff --git a/test/models/file-patch.test.js b/test/models/file-patch.test.js index ed36c1de9f..59a160f985 100644 --- a/test/models/file-patch.test.js +++ b/test/models/file-patch.test.js @@ -292,27 +292,16 @@ describe('FilePatch', function() { }); describe('typechange file patches', function() { - it.only('handles typechange patches for a symlink replaced with a file', async function() { + it('handles typechange patches for a symlink replaced with a file', async function() { const workdirPath = await cloneRepository('symlinks'); const repository = await buildRepository(workdirPath); - repository.git.exec(['config', 'core.symlinks', 'true']); + await repository.git.exec(['config', 'core.symlinks', 'true']); const deletedSymlinkAddedFilePath = 'symlink.txt'; fs.unlinkSync(path.join(workdirPath, deletedSymlinkAddedFilePath)); fs.writeFileSync(path.join(workdirPath, deletedSymlinkAddedFilePath), 'qux\nfoo\nbar\n', 'utf8'); - console.log(workdirPath); - debugger; - // cd into folder and see what diff looks like - // if not then perhaps symlinks are not being treated correctly on windows. - // is mode changing correctly? should we manually change it? - // should we ignore test? is it even relevant? - // if it matches below there's app logic error - // file isn't being marked as deleted - // header for patch added is missing entirely - // check toString method - const patch = await repository.getFilePatchForPath(deletedSymlinkAddedFilePath); assert.equal(patch.toString(), dedent` diff --git a/symlink.txt b/symlink.txt From e15a9eec778b01f1161505f412be1010d17dd63a Mon Sep 17 00:00:00 2001 From: David Wilson Date: Wed, 17 Jan 2018 17:10:00 -0800 Subject: [PATCH 0184/5882] Remove trailing space --- test/controllers/file-patch-controller.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/controllers/file-patch-controller.test.js b/test/controllers/file-patch-controller.test.js index 3e39aeb7a4..69370af5b1 100644 --- a/test/controllers/file-patch-controller.test.js +++ b/test/controllers/file-patch-controller.test.js @@ -399,7 +399,7 @@ describe('FilePatchController', function() { const deletedSymlinkAddedFilePath = 'symlink.txt'; fs.unlinkSync(path.join(workingDirPath, deletedSymlinkAddedFilePath)); fs.writeFileSync(path.join(workingDirPath, deletedSymlinkAddedFilePath), 'qux\nfoo\nbar\nbaz\nzoo\n', 'utf8'); - + const component = createComponent(repository, deletedSymlinkAddedFilePath); const wrapper = mount(React.cloneElement(component, {filePath: deletedSymlinkAddedFilePath})); From 2b61baed7adad1ef28b79828ff70741494c93cf8 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Wed, 17 Jan 2018 18:54:35 -0800 Subject: [PATCH 0185/5882] Stop event propagation even when there's no previous list This prevents the Diagnostics panel from being focused --- lib/views/git-tab-view.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/views/git-tab-view.js b/lib/views/git-tab-view.js index cb5b71bee8..e6b3bb084b 100644 --- a/lib/views/git-tab-view.js +++ b/lib/views/git-tab-view.js @@ -217,9 +217,8 @@ export default class GitTabView { evt.stopPropagation(); } } else { - if (stagingView.activatePreviousList()) { - evt.stopPropagation(); - } + stagingView.activatePreviousList(); + evt.stopPropagation(); } } From a3597e3c39fc25629752c70d9576d6fb1571f170 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 18 Jan 2018 10:59:15 -0800 Subject: [PATCH 0186/5882] Require keytar lazily --- lib/models/github-login-model.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/models/github-login-model.js b/lib/models/github-login-model.js index f7d2de7bc2..efb09ace05 100644 --- a/lib/models/github-login-model.js +++ b/lib/models/github-login-model.js @@ -1,13 +1,17 @@ import {execFile} from 'child_process'; -import keytar from 'keytar'; - import {Emitter} from 'event-kit'; export const UNAUTHENTICATED = Symbol('UNAUTHENTICATED'); export class KeytarStrategy { + static get keytar() { + return require('keytar'); + } + static async isValid() { + const keytar = this.keytar; + try { const rand = Math.floor(Math.random() * 10e20).toString(16); await keytar.setPassword('atom-test-service', rand, rand); @@ -21,15 +25,15 @@ export class KeytarStrategy { } getPassword(service, account) { - return keytar.getPassword(service, account); + return this.constructor.keytar.getPassword(service, account); } replacePassword(service, account, password) { - return keytar.setPassword(service, account, password); + return this.constructor.keytar.setPassword(service, account, password); } deletePassword(service, account) { - return keytar.deletePassword(service, account); + return this.constructor.keytar.deletePassword(service, account); } } From 900330d2918836f8606eb4d0e2c6ca85c18f363d Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 18 Jan 2018 11:01:02 -0800 Subject: [PATCH 0187/5882] Skip Keytar strategy if ATOM_GITHUB_DISABLE_KEYTAR is set --- lib/models/github-login-model.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/models/github-login-model.js b/lib/models/github-login-model.js index efb09ace05..21c4362b62 100644 --- a/lib/models/github-login-model.js +++ b/lib/models/github-login-model.js @@ -10,6 +10,11 @@ export class KeytarStrategy { } static async isValid() { + // Allow for disabling Keytar on problematic CI environments + if (process.env.ATOM_GITHUB_DISABLE_KEYTAR) { + return false; + } + const keytar = this.keytar; try { From 0ed4ec0a1ef60090a6c6e97c5601f3b26b162755 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 18 Jan 2018 11:01:10 -0800 Subject: [PATCH 0188/5882] Don't use Keytar on Travis --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 4de0cdab4f..4002cd1cb8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -27,6 +27,7 @@ env: - MOCHA_TIMEOUT=60000 - UNTIL_TIMEOUT=30000 - TRAVIS_BUILD_JOB=runtests + - ATOM_GITHUB_DISABLE_KEYTAR=1 matrix: - ATOM_CHANNEL=stable - ATOM_CHANNEL=beta From c1fb9cb56ef5177d6e465170f0c57ad1b5b26ca9 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 18 Jan 2018 11:24:09 -0800 Subject: [PATCH 0189/5882] Add all the logs --- test/github-package.test.js | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/test/github-package.test.js b/test/github-package.test.js index 46446a7b0c..04977c7429 100644 --- a/test/github-package.test.js +++ b/test/github-package.test.js @@ -13,6 +13,7 @@ describe('GithubPackage', function() { let githubPackage, contextPool; beforeEach(function() { + console.log('be: 1'); atomEnv = global.buildAtomEnvironment(); workspace = atomEnv.workspace; project = atomEnv.project; @@ -26,22 +27,29 @@ describe('GithubPackage', function() { grammars = atomEnv.grammars; getLoadSettings = atomEnv.getLoadSettings.bind(atomEnv); configDirPath = path.join(__dirname, 'fixtures', 'atomenv-config'); + console.log('be: 2'); githubPackage = new GithubPackage( workspace, project, commandRegistry, notificationManager, tooltips, styles, grammars, confirm, config, deserializers, configDirPath, getLoadSettings, ); + console.log('be: 3'); sinon.stub(githubPackage, 'rerender').callsFake(callback => { callback && setTimeout(callback); }); + console.log('be: 4'); contextPool = githubPackage.getContextPool(); + console.log('be: 5'); }); afterEach(async function() { + console.log('ae: 1'); await githubPackage.deactivate(); + console.log('ae: 2'); atomEnv.destroy(); + console.log('ae: 3'); }); async function contextUpdateAfter(chunk) { @@ -131,34 +139,52 @@ describe('GithubPackage', function() { }); it('uses models from preexisting projects', async function() { + console.log('1a'); const [workdirPath1, workdirPath2, nonRepositoryPath] = await Promise.all([ cloneRepository('three-files'), cloneRepository('three-files'), getTempDir(), ]); + console.log('2a'); project.setPaths([workdirPath1, workdirPath2, nonRepositoryPath]); + console.log('3a'); await contextUpdateAfter(() => githubPackage.activate()); + console.log('4a'); + console.log('5a'); assert.isTrue(contextPool.getContext(workdirPath1).isPresent()); + console.log('6a'); assert.isTrue(contextPool.getContext(workdirPath2).isPresent()); + console.log('7a'); assert.isTrue(contextPool.getContext(nonRepositoryPath).isPresent()); + console.log('8a'); assert.isTrue(githubPackage.getActiveRepository().isUndetermined()); + console.log('9a'); }); it('uses an active model from a single preexisting project', async function() { + console.log('1b'); const workdirPath = await cloneRepository('three-files'); + console.log('2b'); project.setPaths([workdirPath]); + console.log('3b'); await contextUpdateAfter(() => githubPackage.activate()); + console.log('4b'); const context = contextPool.getContext(workdirPath); + console.log('5b'); assert.isTrue(context.isPresent()); + console.log('6b'); assert.strictEqual(context.getRepository(), githubPackage.getActiveRepository()); + console.log('7b'); assert.strictEqual(context.getResolutionProgress(), githubPackage.getActiveResolutionProgress()); + console.log('8b'); assert.equal(githubPackage.getActiveWorkdir(), workdirPath); + console.log('9b'); }); it('uses an active model from a preexisting active pane item', async function() { From 04418e39f2820e3fcfa4e93246f1d3189be410d4 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 18 Jan 2018 11:33:52 -0800 Subject: [PATCH 0190/5882] More log --- test/github-package.test.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/github-package.test.js b/test/github-package.test.js index 04977c7429..89f9628320 100644 --- a/test/github-package.test.js +++ b/test/github-package.test.js @@ -53,9 +53,13 @@ describe('GithubPackage', function() { }); async function contextUpdateAfter(chunk) { + console.log('cae: 1'); const updatePromise = githubPackage.getSwitchboard().getFinishActiveContextUpdatePromise(); + console.log('cae: 2'); await chunk(); + console.log('cae: 3'); return updatePromise; + console.log('cae: 4'); } describe('construction', function() { From 241766828e6c31cfeab9576d2522ad8264511918 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 18 Jan 2018 11:51:09 -0800 Subject: [PATCH 0191/5882] Guess what? That's right, MORE LOGS --- lib/github-package.js | 6 ++++++ test/github-package.test.js | 1 - 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/github-package.js b/lib/github-package.js index ae080ab5e3..574b7f58ad 100644 --- a/lib/github-package.js +++ b/lib/github-package.js @@ -539,19 +539,25 @@ export default class GithubPackage { } setActiveContext(nextActiveContext) { + console.log('> START setActiveContext'); if (nextActiveContext !== this.activeContext) { if (this.activeContext === this.guessedContext) { this.guessedContext.destroy(); this.guessedContext = null; } + cosnole.log('activeContext set, rerendering'); this.activeContext = nextActiveContext; + console.log('starting rerender'); this.rerender(() => { + console.log('finished rerender, triggering didFinishActiveContextUpdate'); this.switchboard.didFinishContextChangeRender(); this.switchboard.didFinishActiveContextUpdate(); }); } else { + console.log('got into the else case, triggering didFinishActiveContextUpdate'); this.switchboard.didFinishActiveContextUpdate(); } + console.log('> END setActiveContext'); } async updateActiveContext(savedState = {}) { diff --git a/test/github-package.test.js b/test/github-package.test.js index 89f9628320..5bf396b4e0 100644 --- a/test/github-package.test.js +++ b/test/github-package.test.js @@ -59,7 +59,6 @@ describe('GithubPackage', function() { await chunk(); console.log('cae: 3'); return updatePromise; - console.log('cae: 4'); } describe('construction', function() { From fb5bee6bf1413308bd9d4181e36704ce459793e3 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 18 Jan 2018 11:55:55 -0800 Subject: [PATCH 0192/5882] Typing is hard --- lib/github-package.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/github-package.js b/lib/github-package.js index 574b7f58ad..fd5436f473 100644 --- a/lib/github-package.js +++ b/lib/github-package.js @@ -545,7 +545,7 @@ export default class GithubPackage { this.guessedContext.destroy(); this.guessedContext = null; } - cosnole.log('activeContext set, rerendering'); + console.log('activeContext set, rerendering'); this.activeContext = nextActiveContext; console.log('starting rerender'); this.rerender(() => { From 4f4c21f325f5e86001126b8b32fd54854640794b Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 18 Jan 2018 12:12:15 -0800 Subject: [PATCH 0193/5882] Log all the GitHub package function calls --- lib/github-package.js | 46 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/lib/github-package.js b/lib/github-package.js index fd5436f473..01a743e6e5 100644 --- a/lib/github-package.js +++ b/lib/github-package.js @@ -23,6 +23,25 @@ import AsyncQueue from './async-queue'; import WorkerManager from './worker-manager'; import getRepoPipelineManager from './get-repo-pipeline-manager'; +function logFn(target, key, descriptor) { + const orig = descriptor.value; + descriptor.value = function(...args) { + let name; + if (target && target.name) { + name = `${target.name}.${key}`; + } else if (this.constructor && this.constructor.name) { + name = `${this.constructor.name}#${key}`; + } else { + name = `#${key}`; + } + console.log(`> START: ${name}`); + const value = orig.apply(this, args); + console.log(`> END: ${name}`); + return value; + }; +} + + const defaultState = { }; @@ -86,6 +105,7 @@ export default class GithubPackage { this.filePatchItems = []; } + @logFn setupYardstick() { const stagingSeries = ['stageLine', 'stageHunk', 'unstageLine', 'unstageHunk']; @@ -127,6 +147,7 @@ export default class GithubPackage { ); } + @logFn async activate(state = {}) { this.savedState = {...defaultState, ...state}; @@ -238,6 +259,7 @@ export default class GithubPackage { this.rerender(); } + @logFn serialize() { const activeRepository = this.getActiveRepository(); const activeRepositoryPath = activeRepository ? activeRepository.getWorkingDirectoryPath() : null; @@ -248,6 +270,7 @@ export default class GithubPackage { }; } + @logFn rerender(callback) { if (this.workspace.isDestroyed()) { return; @@ -295,6 +318,7 @@ export default class GithubPackage { ); } + @logFn async deactivate() { this.subscriptions.dispose(); this.contextPool.clear(); @@ -307,22 +331,26 @@ export default class GithubPackage { } @autobind + @logFn consumeStatusBar(statusBar) { this.statusBar = statusBar; this.rerender(); } @autobind + @logFn createGitTimingsView() { return GitTimingsView.createPaneItem(); } @autobind + @logFn createIssueishPaneItem({uri}) { return IssueishPaneItem.opener(uri); } @autobind + @logFn createDockItemStub({uri}) { let item; switch (uri) { @@ -347,12 +375,14 @@ export default class GithubPackage { return item; } + @logFn createGitTabControllerStub(uri) { return StubItem.create('git-tab-controller', { title: 'Git', }, uri); } + @logFn createGithubTabControllerStub(uri) { return StubItem.create('github-tab-controller', { title: 'GitHub (preview)', @@ -360,6 +390,7 @@ export default class GithubPackage { } @autobind + @logFn createFilePatchControllerStub({uri} = {}) { const item = StubItem.create('git-file-patch-controller', { title: 'Diff', @@ -372,6 +403,7 @@ export default class GithubPackage { } @autobind + @logFn destroyGitTabItem() { if (this.gitTabStubItem) { this.gitTabStubItem.destroy(); @@ -383,6 +415,7 @@ export default class GithubPackage { } @autobind + @logFn destroyGithubTabItem() { if (this.githubTabStubItem) { this.githubTabStubItem.destroy(); @@ -402,6 +435,7 @@ export default class GithubPackage { } @autobind + @logFn async createRepositoryForProjectPath(projectPath) { await mkdirs(projectPath); @@ -417,6 +451,7 @@ export default class GithubPackage { } @autobind + @logFn async cloneRepositoryForProjectPath(remoteUrl, projectPath) { const context = this.contextPool.getContext(projectPath); let repository; @@ -437,32 +472,39 @@ export default class GithubPackage { } @autobind + @logFn getRepositoryForWorkdir(projectPath) { const loadingGuessRepo = Repository.loadingGuess({pipelineManager: this.pipelineManager}); return this.guessedContext ? loadingGuessRepo : this.contextPool.getContext(projectPath).getRepository(); } + @logFn getActiveWorkdir() { return this.activeContext.getWorkingDirectory(); } + @logFn getActiveRepository() { return this.activeContext.getRepository(); } + @logFn getActiveResolutionProgress() { return this.activeContext.getResolutionProgress(); } + @logFn getContextPool() { return this.contextPool; } + @logFn getSwitchboard() { return this.switchboard; } @autobind + @logFn async scheduleActiveContextUpdate(savedState = {}) { this.switchboard.didScheduleActiveContextUpdate(); await this.activeContextQueue.push(this.updateActiveContext.bind(this, savedState), {parallel: false}); @@ -481,6 +523,7 @@ export default class GithubPackage { * First updates the pool of resident contexts to match all git working directories that correspond to open * projects and pane items. */ + @logFn async getNextContext(savedState) { const workdirs = new Set( await Promise.all( @@ -538,6 +581,7 @@ export default class GithubPackage { return this.activeContext; } + @logFn setActiveContext(nextActiveContext) { console.log('> START setActiveContext'); if (nextActiveContext !== this.activeContext) { @@ -560,6 +604,7 @@ export default class GithubPackage { console.log('> END setActiveContext'); } + @logFn async updateActiveContext(savedState = {}) { if (this.workspace.isDestroyed()) { return; @@ -571,6 +616,7 @@ export default class GithubPackage { this.setActiveContext(nextActiveContext); } + @logFn refreshAtomGitRepository(workdir) { const atomGitRepo = this.project.getRepositories().find(repo => { return repo && path.normalize(repo.getWorkingDirectory()) === workdir; From 6711887ac7c50816ba3e18d49fe315e5bc2ad2c7 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 18 Jan 2018 12:23:20 -0800 Subject: [PATCH 0194/5882] MOAR --- lib/github-package.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/github-package.js b/lib/github-package.js index 01a743e6e5..e362dc196e 100644 --- a/lib/github-package.js +++ b/lib/github-package.js @@ -607,12 +607,16 @@ export default class GithubPackage { @logFn async updateActiveContext(savedState = {}) { if (this.workspace.isDestroyed()) { + console.log('returning early from here'); return; } + console.log('triggering didBeginActiveContextUpdate'); this.switchboard.didBeginActiveContextUpdate(); + console.log('getting the next context'); const nextActiveContext = await this.getNextContext(savedState); + console.log('about to call setActiveContext with', nextActiveContext); this.setActiveContext(nextActiveContext); } From b64725fc2c05765cde94dfb5199f2a1da118154c Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 18 Jan 2018 12:33:09 -0800 Subject: [PATCH 0195/5882] Even. More. --- lib/github-package.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/lib/github-package.js b/lib/github-package.js index e362dc196e..6cfea4cc68 100644 --- a/lib/github-package.js +++ b/lib/github-package.js @@ -533,41 +533,52 @@ export default class GithubPackage { }), ), ); + console.log('got all the workdirs'); const fromPaneItem = async maybeItem => { const itemPath = pathForPaneItem(maybeItem); if (!itemPath) { + console.log('no item path, returning early'); return {}; } + console.log('getting itemWorkdir from the workdirCache'); const itemWorkdir = await this.workdirCache.find(itemPath); + console.log('maybe adding to workdirs set'); if (itemWorkdir && !this.project.contains(itemPath)) { workdirs.add(itemWorkdir); } + console.log('got an item in fromPaneItem! returning', itemPath, itemWorkdir); return {itemPath, itemWorkdir}; }; + console.log('getting active item'); const active = await fromPaneItem(this.workspace.getCenter().getActivePaneItem()); + console.log('setting contextPool'); this.contextPool.set(workdirs, savedState); if (active.itemPath) { // Prefer an active item + console.log('returning context from contextPool (active.itemPath is truthy)'); return this.contextPool.getContext(active.itemWorkdir || active.itemPath); } if (this.project.getPaths().length === 1) { + console.log('inside getPaths().length === 1'); // Single project const projectPath = this.project.getPaths()[0]; const activeWorkingDir = await this.workdirCache.find(projectPath); + console.log('returning context from contextPool (getPaths().length === 1)'); return this.contextPool.getContext(activeWorkingDir || projectPath); } if (this.project.getPaths().length === 0 && !this.activeContext.getRepository().isUndetermined()) { // No projects. Revert to the absent context unless we've guessed that more projects are on the way. + console.log('no paths and repo is undetermined; return absent'); return WorkdirContext.absent({pipelineManager: this.pipelineManager}); } @@ -575,9 +586,11 @@ export default class GithubPackage { // resident in the pool. const savedWorkingDir = savedState.activeRepositoryPath; if (savedWorkingDir) { + console.log('returning savedWorkingDir'); return this.contextPool.getContext(savedWorkingDir); } + console.log('returning this.activeContext', this.activeContext); return this.activeContext; } From 17be81a9ec90c7ed17fa2cbf2ec06dc5b052a5ff Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 18 Jan 2018 12:39:45 -0800 Subject: [PATCH 0196/5882] ok stop logs --- lib/github-package.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/github-package.js b/lib/github-package.js index 6cfea4cc68..b7a4264de9 100644 --- a/lib/github-package.js +++ b/lib/github-package.js @@ -590,7 +590,7 @@ export default class GithubPackage { return this.contextPool.getContext(savedWorkingDir); } - console.log('returning this.activeContext', this.activeContext); + console.log('returning this.activeContext', !!this.activeContext); return this.activeContext; } @@ -629,7 +629,7 @@ export default class GithubPackage { console.log('getting the next context'); const nextActiveContext = await this.getNextContext(savedState); - console.log('about to call setActiveContext with', nextActiveContext); + console.log('about to call setActiveContext with', !!nextActiveContext); this.setActiveContext(nextActiveContext); } From 971f6aa888127b1fd65d530dc81b9625b9217a0c Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 18 Jan 2018 12:48:59 -0800 Subject: [PATCH 0197/5882] Again with the logs? Yes. --- lib/models/workdir-cache.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/lib/models/workdir-cache.js b/lib/models/workdir-cache.js index c26a9e3e46..b67e16dd0e 100644 --- a/lib/models/workdir-cache.js +++ b/lib/models/workdir-cache.js @@ -13,22 +13,34 @@ export default class WorkdirCache { } async find(startDir) { + console.log(`> START: find ${startDir}`); try { + console.log('resolving startDir'); const resolvedDir = await this.resolvePath(startDir); + console.log('checking cached'); const cached = this.known.get(resolvedDir); if (cached !== undefined) { + console.log('RETURNING cached', cached); return cached; } + console.log('walking to root'); const workDir = await this.walkToRoot(resolvedDir); + console.log('clearing known if it is too large..'); if (this.known.size >= this.maxSize) { + console.log('IT IS! clearing'); this.known.clear(); } + console.log('setting', resolvedDir, workDir); this.known.set(resolvedDir, workDir); + console.log('RETURNING', workDir); return workDir; } catch (e) { + console.log('OMG GOT AN ERROR'); + console.log(e); if (e.code === 'ENOENT') { + console.log('ENOENT, returning null'); return null; } From 8d7d086ce05fd2532ffacfe8d515ce4057375fc2 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 18 Jan 2018 12:58:23 -0800 Subject: [PATCH 0198/5882] Almost --- lib/models/workdir-cache.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/models/workdir-cache.js b/lib/models/workdir-cache.js index b67e16dd0e..29937bec48 100644 --- a/lib/models/workdir-cache.js +++ b/lib/models/workdir-cache.js @@ -58,8 +58,20 @@ export default class WorkdirCache { } resolvePath(unresolvedPath) { + console.log('in resolvePath', unresolvedPath); return new Promise((resolve, reject) => { - fs.realpath(unresolvedPath, (err, resolved) => (err ? reject(err) : resolve(resolved))); + console.log('in promise callback, calling realpath'); + fs.realpath(unresolvedPath, (err, resolved) => { + console.log('realpath returned'); + if (err) { + console.log('Rejecting with err'); + console.log(err); + reject(err); + } else { + console.log('resolving with', resolved); + resolve(resolved); + } + }); }); } From 4e6ed76d9945056facb5f3f717b205ec2b3b52b2 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 18 Jan 2018 13:13:57 -0800 Subject: [PATCH 0199/5882] Let's try using the native version --- lib/models/workdir-cache.js | 3 ++- package.json | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/models/workdir-cache.js b/lib/models/workdir-cache.js index 29937bec48..f5e2496084 100644 --- a/lib/models/workdir-cache.js +++ b/lib/models/workdir-cache.js @@ -1,5 +1,6 @@ import path from 'path'; import fs from 'fs'; +import realpath from 'realpath-native'; import {readFile, isValidWorkdir} from '../helpers'; @@ -61,7 +62,7 @@ export default class WorkdirCache { console.log('in resolvePath', unresolvedPath); return new Promise((resolve, reject) => { console.log('in promise callback, calling realpath'); - fs.realpath(unresolvedPath, (err, resolved) => { + realpath(unresolvedPath, (err, resolved) => { console.log('realpath returned'); if (err) { console.log('Rejecting with err'); diff --git a/package.json b/package.json index 5bf7422a2a..877e83a0f8 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "react": "^15.6.1", "react-dom": "^15.6.1", "react-relay": "^1.2.0-rc.1", + "realpath-native": "^1.0.0", "relay-runtime": "^1.2.0-rc.1", "temp": "^0.8.3", "tinycolor2": "^1.4.1", From 5c67c6c23e4502aa9a13d32aa699a51c0a6c5cc3 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 18 Jan 2018 13:23:11 -0800 Subject: [PATCH 0200/5882] Oh it's a promise --- lib/models/workdir-cache.js | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/lib/models/workdir-cache.js b/lib/models/workdir-cache.js index f5e2496084..76312c8c1f 100644 --- a/lib/models/workdir-cache.js +++ b/lib/models/workdir-cache.js @@ -58,22 +58,26 @@ export default class WorkdirCache { } } - resolvePath(unresolvedPath) { - console.log('in resolvePath', unresolvedPath); - return new Promise((resolve, reject) => { - console.log('in promise callback, calling realpath'); - realpath(unresolvedPath, (err, resolved) => { - console.log('realpath returned'); - if (err) { - console.log('Rejecting with err'); - console.log(err); - reject(err); - } else { - console.log('resolving with', resolved); - resolve(resolved); - } - }); - }); + async resolvePath(unresolvedPath) { + console.log('START realpath'); + const resolvedPath = await realpath(unresolvedPath); + console.log('RETURNING from realpath'); + return resolvedPath; + // console.log('in resolvePath', unresolvedPath); + // return new Promise((resolve, reject) => { + // console.log('in promise callback, calling realpath'); + // realpath(unresolvedPath, (err, resolved) => { + // console.log('realpath returned'); + // if (err) { + // console.log('Rejecting with err'); + // console.log(err); + // reject(err); + // } else { + // console.log('resolving with', resolved); + // resolve(resolved); + // } + // }); + // }); } walkToRoot(initialDir) { From dcddec3301ecc3649724f15d44b012ac3a33c021 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 18 Jan 2018 13:33:52 -0800 Subject: [PATCH 0201/5882] Lawgs --- lib/github-package.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/github-package.js b/lib/github-package.js index b7a4264de9..f08afca821 100644 --- a/lib/github-package.js +++ b/lib/github-package.js @@ -272,15 +272,19 @@ export default class GithubPackage { @logFn rerender(callback) { + console.log('> inside rerender... callback??', !!callback); if (this.workspace.isDestroyed()) { + console.log('workspace is destroyed, returning early'); return; } if (!this.activated) { + console.log('package is not activated, returning early'); return; } if (!this.element) { + console.log('no element, creating it'); this.element = document.createElement('div'); this.subscriptions.add(new Disposable(() => { ReactDom.unmountComponentAtNode(this.element); @@ -288,6 +292,7 @@ export default class GithubPackage { })); } + console.log('finally calling ReactDom.render()'); ReactDom.render( { this.controller = c; }} @@ -316,6 +321,7 @@ export default class GithubPackage { getRepositoryForWorkdir={this.getRepositoryForWorkdir} />, this.element, callback, ); + console.log('Done with rerender!'); } @logFn From d6f5b6b9f47900758c8464cc71dd9e5a93b32192 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 18 Jan 2018 13:48:25 -0800 Subject: [PATCH 0202/5882] use realPath helper everywhere --- lib/helpers.js | 11 +++++++---- lib/models/workdir-cache.js | 5 ++--- test/github-package.test.js | 8 ++++---- test/helpers.js | 5 +++-- test/models/repository.test.js | 8 ++++---- test/models/workdir-cache.test.js | 3 ++- 6 files changed, 22 insertions(+), 18 deletions(-) diff --git a/lib/helpers.js b/lib/helpers.js index 13a1469b3b..1d6ac9b71f 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -3,6 +3,7 @@ import fs from 'fs-extra'; import os from 'os'; import temp from 'temp'; import {ncp} from 'ncp'; +import realpathNative from 'realpath-native'; import FilePatchController from './controllers/file-patch-controller'; @@ -204,16 +205,18 @@ export function getTempDir(options = {}) { if (options.symlinkOk) { resolve(folder); } else { - fs.realpath(folder, (realError, rpath) => (realError ? reject(realError) : resolve(rpath))); + resolve(realpathNative(folder)); + // fs.realpath(folder, (realError, rpath) => (realError ? reject(realError) : resolve(rpath))); } }); }); } export function realPath(folder) { - return new Promise((resolve, reject) => { - fs.realpath(folder, (err, rpath) => (err ? reject(err) : resolve(rpath))); - }); + return realpathNative(folder); + // return new Promise((resolve, reject) => { + // fs.realpath(folder, (err, rpath) => (err ? reject(err) : resolve(rpath))); + // }); } export function fsStat(absoluteFilePath) { diff --git a/lib/models/workdir-cache.js b/lib/models/workdir-cache.js index 76312c8c1f..334a5f2ced 100644 --- a/lib/models/workdir-cache.js +++ b/lib/models/workdir-cache.js @@ -1,8 +1,7 @@ import path from 'path'; import fs from 'fs'; -import realpath from 'realpath-native'; -import {readFile, isValidWorkdir} from '../helpers'; +import {readFile, realPath, isValidWorkdir} from '../helpers'; /** * Locate the nearest git working directory above a given starting point, caching results. @@ -60,7 +59,7 @@ export default class WorkdirCache { async resolvePath(unresolvedPath) { console.log('START realpath'); - const resolvedPath = await realpath(unresolvedPath); + const resolvedPath = await realPath(unresolvedPath); console.log('RETURNING from realpath'); return resolvedPath; // console.log('in resolvePath', unresolvedPath); diff --git a/test/github-package.test.js b/test/github-package.test.js index 5bf396b4e0..af7b9c9bb9 100644 --- a/test/github-package.test.js +++ b/test/github-package.test.js @@ -4,7 +4,7 @@ import temp from 'temp'; import until from 'test-until'; import {cloneRepository} from './helpers'; -import {writeFile, deleteFileOrFolder, fileExists, getTempDir} from '../lib/helpers'; +import {writeFile, deleteFileOrFolder, fileExists, getTempDir, realPath} from '../lib/helpers'; import GithubPackage from '../lib/github-package'; describe('GithubPackage', function() { @@ -267,7 +267,7 @@ describe('GithubPackage', function() { // Repository with a merge conflict, repository without a merge conflict, path without a repository const workdirMergeConflict = await cloneRepository('merge-conflict'); const workdirNoConflict = await cloneRepository('three-files'); - const nonRepositoryPath = fs.realpathSync(temp.mkdirSync()); + const nonRepositoryPath = await realPath(temp.mkdirSync()); fs.writeFileSync(path.join(nonRepositoryPath, 'c.txt')); project.setPaths([workdirMergeConflict, workdirNoConflict, nonRepositoryPath]); @@ -485,7 +485,7 @@ describe('GithubPackage', function() { }); it('uses an absent context when the active item is not in a git repository', async function() { - const nonRepositoryPath = fs.realpathSync(temp.mkdirSync()); + const nonRepositoryPath = await realPath(temp.mkdirSync()); const workdir = await cloneRepository('three-files'); project.setPaths([nonRepositoryPath, workdir]); await writeFile(path.join(nonRepositoryPath, 'a.txt'), 'stuff'); @@ -600,7 +600,7 @@ describe('GithubPackage', function() { if (process.platform !== 'win32') { it('handles symlinked project paths', async function() { const workdirPath = await cloneRepository('three-files'); - const symlinkPath = fs.realpathSync(temp.mkdirSync()) + '-symlink'; + const symlinkPath = await realPath(temp.mkdirSync()) + '-symlink'; fs.symlinkSync(workdirPath, symlinkPath); project.setPaths([symlinkPath]); await workspace.open(path.join(symlinkPath, 'a.txt')); diff --git a/test/helpers.js b/test/helpers.js index 79dca63ac1..a35aa833be 100644 --- a/test/helpers.js +++ b/test/helpers.js @@ -11,6 +11,7 @@ import GitShellOutStrategy from '../lib/git-shell-out-strategy'; import WorkerManager from '../lib/worker-manager'; import ContextMenuInterceptor from '../lib/context-menu-interceptor'; import getRepoPipelineManager from '../lib/get-repo-pipeline-manager'; +import {realPath} from '../lib/helpers'; assert.autocrlfEqual = (actual, expected, ...args) => { const newActual = actual.replace(/\r\n/g, '\n'); @@ -25,7 +26,7 @@ const cachedClonedRepos = {}; function copyCachedRepo(repoName) { const workingDirPath = temp.mkdirSync('git-fixture-'); fs.copySync(cachedClonedRepos[repoName], workingDirPath); - return fs.realpathSync(workingDirPath); + return realPath(workingDirPath); } export async function cloneRepository(repoName = 'three-files') { @@ -58,7 +59,7 @@ export async function initRepository(repoName) { await git.exec(['init']); await git.exec(['config', '--local', 'user.email', 'nope@nah.com']); await git.exec(['config', '--local', 'user.name', 'Someone']); - return fs.realpathSync(workingDirPath); + return realPath(workingDirPath); } export async function setUpLocalAndRemoteRepositories(repoName = 'multiple-commits', options = {}) { diff --git a/test/models/repository.test.js b/test/models/repository.test.js index 1c8083917a..a6d717f67a 100644 --- a/test/models/repository.test.js +++ b/test/models/repository.test.js @@ -15,7 +15,7 @@ import { cloneRepository, setUpLocalAndRemoteRepositories, getHeadCommitOnRemote, assertDeepPropertyVals, assertEqualSortedArraysByKey, } from '../helpers'; -import {getPackageRoot, writeFile, copyFile, fsStat, getTempDir} from '../../lib/helpers'; +import {getPackageRoot, writeFile, copyFile, fsStat, getTempDir, realPath} from '../../lib/helpers'; describe('Repository', function() { it('delegates all state methods', function() { @@ -89,7 +89,7 @@ describe('Repository', function() { describe('init', function() { it('creates a repository in the given dir and returns the repository', async function() { - const soonToBeRepositoryPath = fs.realpathSync(temp.mkdirSync()); + const soonToBeRepositoryPath = await realPath(temp.mkdirSync()); const repo = new Repository(soonToBeRepositoryPath); assert.isTrue(repo.isLoading()); @@ -106,7 +106,7 @@ describe('Repository', function() { describe('clone', function() { it('clones a repository from a URL to a directory and returns the repository', async function() { const upstreamPath = await cloneRepository('three-files'); - const destDir = fs.realpathSync(temp.mkdirSync()); + const destDir = await realPath(temp.mkdirSync()); const repo = new Repository(destDir); const clonePromise = repo.clone(upstreamPath); @@ -118,7 +118,7 @@ describe('Repository', function() { it('clones a repository when the directory does not exist yet', async function() { const upstreamPath = await cloneRepository('three-files'); - const parentDir = fs.realpathSync(temp.mkdirSync()); + const parentDir = await realPath(temp.mkdirSync()); const destDir = path.join(parentDir, 'subdir'); const repo = new Repository(destDir); diff --git a/test/models/workdir-cache.test.js b/test/models/workdir-cache.test.js index 524fc80154..9e120a7cc9 100644 --- a/test/models/workdir-cache.test.js +++ b/test/models/workdir-cache.test.js @@ -5,6 +5,7 @@ import fs from 'fs'; import {cloneRepository} from '../helpers'; import WorkdirCache from '../../lib/models/workdir-cache'; +import {realPath} from '../../lib/helpers'; describe('WorkdirCache', function() { let cache; @@ -29,7 +30,7 @@ describe('WorkdirCache', function() { it('finds a workdir from a gitdir file', async function() { const repoDir = await cloneRepository('three-files'); - const expectedDir = fs.realpathSync(temp.mkdirSync()); + const expectedDir = await realPath(temp.mkdirSync()); fs.writeFileSync(path.join(expectedDir, '.git'), `gitdir: ${path.join(repoDir, '.git')}`, 'utf8'); const actualDir = await cache.find(expectedDir); From 56a64cdb56883454e54fd9bdb408f70245480c82 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 18 Jan 2018 13:55:52 -0800 Subject: [PATCH 0203/5882] Maybe this decorator is problematic --- lib/github-package.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/github-package.js b/lib/github-package.js index f08afca821..ebd9c8f19f 100644 --- a/lib/github-package.js +++ b/lib/github-package.js @@ -270,7 +270,6 @@ export default class GithubPackage { }; } - @logFn rerender(callback) { console.log('> inside rerender... callback??', !!callback); if (this.workspace.isDestroyed()) { From b8a9baba3d941fffdf5451e6eb37abc964185209 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 18 Jan 2018 14:08:33 -0800 Subject: [PATCH 0204/5882] You'll never guess --- test/github-package.test.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/test/github-package.test.js b/test/github-package.test.js index af7b9c9bb9..bcb33f4ccf 100644 --- a/test/github-package.test.js +++ b/test/github-package.test.js @@ -36,7 +36,14 @@ describe('GithubPackage', function() { console.log('be: 3'); sinon.stub(githubPackage, 'rerender').callsFake(callback => { - callback && setTimeout(callback); + console.log('calling the FAKE rerender!'); + if (callback) { + console.log('there is a callback to call... initiiating a setTimeout'); + setTimeout(() => { + console.log('Calling that callback from the setTimeout!'); + callback(); + }); + } }); console.log('be: 4'); From 8ef8de187ead05a5aa777331de695c6e968a7234 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 18 Jan 2018 14:14:59 -0800 Subject: [PATCH 0205/5882] =?UTF-8?q?=F0=9F=91=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/github-package.test.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/github-package.test.js b/test/github-package.test.js index bcb33f4ccf..fa0dc41352 100644 --- a/test/github-package.test.js +++ b/test/github-package.test.js @@ -39,10 +39,11 @@ describe('GithubPackage', function() { console.log('calling the FAKE rerender!'); if (callback) { console.log('there is a callback to call... initiiating a setTimeout'); - setTimeout(() => { + const handle = setTimeout(() => { console.log('Calling that callback from the setTimeout!'); callback(); - }); + }, 0); + console.log('the setTimeout call returned a handle:', handle); } }); console.log('be: 4'); From b5128e8fa2e4d72450c53405f7a24b2b4ab92e51 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 18 Jan 2018 14:22:51 -0800 Subject: [PATCH 0206/5882] All the cool kids use process.nextTick --- test/github-package.test.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/test/github-package.test.js b/test/github-package.test.js index fa0dc41352..f0e83ca9e9 100644 --- a/test/github-package.test.js +++ b/test/github-package.test.js @@ -39,10 +39,14 @@ describe('GithubPackage', function() { console.log('calling the FAKE rerender!'); if (callback) { console.log('there is a callback to call... initiiating a setTimeout'); - const handle = setTimeout(() => { - console.log('Calling that callback from the setTimeout!'); + process.nextTick(() => { + console.log('Calling that callback from the nextTick!'); callback(); - }, 0); + }); + // const handle = setTimeout(() => { + // console.log('Calling that callback from the setTimeout!'); + // callback(); + // }, 0); console.log('the setTimeout call returned a handle:', handle); } }); From abcf2f1cdbacec67e6df87f2b04c7fad9c90ad18 Mon Sep 17 00:00:00 2001 From: simurai Date: Fri, 19 Jan 2018 17:06:31 +0900 Subject: [PATCH 0207/5882] Add priority to stylesheet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit So that themes and user’s styles.less come after. --- lib/models/style-calculator.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/models/style-calculator.js b/lib/models/style-calculator.js index 434500e408..9c72022ed0 100644 --- a/lib/models/style-calculator.js +++ b/lib/models/style-calculator.js @@ -25,6 +25,6 @@ export default class StyleCalculator { @autobind updateStyles(sourcePath, getStylesheetFn) { const stylesheet = getStylesheetFn(this.config); - this.styles.addStyleSheet(stylesheet, {sourcePath}); + this.styles.addStyleSheet(stylesheet, {sourcePath, priority: 0}); } } From bbba3f2466382ad7e06c65b4bb7b225e80a384ba Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 19 Jan 2018 00:43:31 -0800 Subject: [PATCH 0208/5882] Update test to have priority --- test/models/style-calculator.test.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/models/style-calculator.test.js b/test/models/style-calculator.test.js index 02a3760187..c88adeb224 100644 --- a/test/models/style-calculator.test.js +++ b/test/models/style-calculator.test.js @@ -44,19 +44,19 @@ describe('StyleCalculator', function(done) { assert.deepEqual(Object.keys(configChangeCallbacks), ['config1', 'config2']); assert.equal(stylesMock.addStyleSheet.callCount, 1); assert.deepEqual(stylesMock.addStyleSheet.getCall(0).args, [ - expectedCss, {sourcePath: 'my-source-path'}, + expectedCss, {sourcePath: 'my-source-path', priority: 0}, ]); configChangeCallbacks.config1(); assert.equal(stylesMock.addStyleSheet.callCount, 2); assert.deepEqual(stylesMock.addStyleSheet.getCall(1).args, [ - expectedCss, {sourcePath: 'my-source-path'}, + expectedCss, {sourcePath: 'my-source-path', priority: 0}, ]); configChangeCallbacks.config2(); assert.equal(stylesMock.addStyleSheet.callCount, 3); assert.deepEqual(stylesMock.addStyleSheet.getCall(2).args, [ - expectedCss, {sourcePath: 'my-source-path'}, + expectedCss, {sourcePath: 'my-source-path', priority: 0}, ]); }); }); From d312c4526fe0eb5d64ad4be4219365f483b7660e Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Tue, 6 Feb 2018 23:52:38 -0800 Subject: [PATCH 0209/5882] Use repo fixture with multiple commits for tests that check HEAD~ --- test/models/repository.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/models/repository.test.js b/test/models/repository.test.js index c8e7a7bc70..08727423c9 100644 --- a/test/models/repository.test.js +++ b/test/models/repository.test.js @@ -1297,7 +1297,7 @@ describe('Repository', function() { }); it('when writing a merge conflict to the index', async function() { - const workdir = await cloneRepository('three-files'); + const workdir = await cloneRepository('multi-commits-files'); const repository = new Repository(workdir); await repository.getLoadPromise(); @@ -1376,7 +1376,7 @@ describe('Repository', function() { }); it('when setting a config option', async function() { - const workdir = await cloneRepository('three-files'); + const workdir = await cloneRepository('multi-commits-files'); const repository = new Repository(workdir); await repository.getLoadPromise(); From 0a83f6edf02db6e4028bee1c6d73351cfe8321cc Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 7 Feb 2018 08:32:46 -0500 Subject: [PATCH 0210/5882] Prepare 0.10.0 release --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 750e864bd9..dfd90eda3a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "github", "main": "./lib/index", - "version": "0.9.1", + "version": "0.10.0", "description": "GitHub integration", "repository": "https://github.com/atom/github", "license": "MIT", From dcf4722096139b3a8bc657f1ed1adffc90454ba7 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 7 Feb 2018 08:53:09 -0500 Subject: [PATCH 0211/5882] Trick electron-link --- lib/controllers/root-controller.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/controllers/root-controller.js b/lib/controllers/root-controller.js index b3de460592..738e578161 100644 --- a/lib/controllers/root-controller.js +++ b/lib/controllers/root-controller.js @@ -334,7 +334,10 @@ export default class RootController extends React.Component { @autobind installReactDevTools() { - const devTools = require('electron-devtools-installer'); + // Prevent electron-link from attempting to descend into electron-devtools-installer, which is not available + // when we're bundled in Atom. + const devToolsName = 'electron-devtools-installer'; + const devTools = require(devToolsName); devTools.default(devTools.REACT_DEVELOPER_TOOLS); } From d480fc4adb04530a7ada5fa1bc40929c6a7f1cdd Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 7 Feb 2018 09:31:44 -0500 Subject: [PATCH 0212/5882] Prepare 0.10.1 release --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index dfd90eda3a..e303e5c417 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "github", "main": "./lib/index", - "version": "0.10.0", + "version": "0.10.1", "description": "GitHub integration", "repository": "https://github.com/atom/github", "license": "MIT", From 8199d6581faaef7d39fca039304c84bcf27fd96b Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 7 Feb 2018 15:15:31 -0500 Subject: [PATCH 0213/5882] Unit test to ensure ResolutionConflictController can be disabled --- .../repository-conflict-controller.test.js | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/test/controllers/repository-conflict-controller.test.js b/test/controllers/repository-conflict-controller.test.js index f6c473d429..56c0724bdb 100644 --- a/test/controllers/repository-conflict-controller.test.js +++ b/test/controllers/repository-conflict-controller.test.js @@ -67,4 +67,26 @@ describe('RepositoryConflictController', () => { await assert.async.equal(wrapper.find(EditorConflictController).length, 2); }); }); + + describe('with the configuration option disabled', function() { + beforeEach(function() { + atomEnv.config.set('github.graphicalConflictResolution', false); + }); + + it('renders no children', async function() { + const workdirPath = await cloneRepository('merge-conflict'); + const repository = await buildRepository(workdirPath); + + await assert.isRejected(repository.git.merge('origin/branch')); + + await Promise.all(['modified-on-both-ours.txt', 'modified-on-both-theirs.txt'].map(basename => { + return workspace.open(path.join(workdirPath, basename)); + })); + + app = React.cloneElement(app, {repository}); + const wrapper = mount(app); + + await assert.async.lengthOf(wrapper.find(EditorConflictController), 0); + }); + }); }); From 366fdf20aae4a8e8bda0dc3a51fccb0eaa94449a Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 7 Feb 2018 15:17:13 -0500 Subject: [PATCH 0214/5882] Add the config option to package.json --- package.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/package.json b/package.json index e303e5c417..03d7292471 100644 --- a/package.json +++ b/package.json @@ -166,6 +166,11 @@ "type": "boolean", "default": true, "description": "Hard wrap commit message body in commit box to 72 characters. Does not apply to expanded commit editors, where message formatting is preserved." + }, + "graphicalConflictResolution": { + "type": "boolean", + "default": true, + "description": "Resolve merge conflicts with in-editor controls" } }, "deserializers": { From 4e5eb8a3c8c6d56cb30a4d67ff98ef632ca20a93 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 7 Feb 2018 15:24:06 -0500 Subject: [PATCH 0215/5882] Pass the Atom config to ResolutionConflictController --- lib/controllers/root-controller.js | 1 + test/controllers/repository-conflict-controller.test.js | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/controllers/root-controller.js b/lib/controllers/root-controller.js index 738e578161..a645a86954 100644 --- a/lib/controllers/root-controller.js +++ b/lib/controllers/root-controller.js @@ -291,6 +291,7 @@ export default class RootController extends React.Component { return ( { beforeEach(() => { atomEnv = global.buildAtomEnvironment(); + atomEnv.config.set('github.graphicalConflictResolution', true); workspace = atomEnv.workspace; const commandRegistry = atomEnv.commands; - app = ; + app = ; }); afterEach(() => atomEnv.destroy()); From 7eda5497a6a8aeb24058fc87baa8b9830ba28bb1 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 7 Feb 2018 15:24:21 -0500 Subject: [PATCH 0216/5882] Check the config setting before rendering EditorConflictControllers --- lib/controllers/repository-conflict-controller.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/controllers/repository-conflict-controller.js b/lib/controllers/repository-conflict-controller.js index ae788beeb3..7cdce07680 100644 --- a/lib/controllers/repository-conflict-controller.js +++ b/lib/controllers/repository-conflict-controller.js @@ -25,6 +25,7 @@ export default class RepositoryConflictController extends React.Component { static propTypes = { workspace: PropTypes.object.isRequired, commandRegistry: PropTypes.object.isRequired, + config: PropTypes.object.isRequired, resolutionProgress: PropTypes.object.isRequired, repository: PropTypes.object.isRequired, mergeConflictPaths: PropTypes.arrayOf(PropTypes.string), @@ -81,7 +82,8 @@ export default class RepositoryConflictController extends React.Component { getConflictingEditors() { if ( this.props.mergeConflictPaths.length === 0 || - this.state.openEditors.length === 0 + this.state.openEditors.length === 0 || + !this.props.config.get('github.graphicalConflictResolution') ) { return []; } From 05427980e4ff72d9b05f9d2111a1fcb4024046ab Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 7 Feb 2018 15:27:35 -0500 Subject: [PATCH 0217/5882] Re-render the RepositoryConflictController if the config changes --- lib/controllers/repository-conflict-controller.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/controllers/repository-conflict-controller.js b/lib/controllers/repository-conflict-controller.js index 7cdce07680..8f2b5a5b66 100644 --- a/lib/controllers/repository-conflict-controller.js +++ b/lib/controllers/repository-conflict-controller.js @@ -57,6 +57,7 @@ export default class RepositoryConflictController extends React.Component { this.subscriptions.add( this.props.workspace.observeTextEditors(updateState), this.props.workspace.onDidDestroyPaneItem(updateState), + this.props.config.observe('github.graphicalConflictResolution', () => this.forceUpdate()), ); } From 8475e4ac08cf3fec8a3e2bd7ed34aca8e7ec4af7 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Fri, 9 Feb 2018 16:38:44 -0500 Subject: [PATCH 0218/5882] Align changed file count in status bar --- lib/views/changed-files-count-view.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/views/changed-files-count-view.js b/lib/views/changed-files-count-view.js index ee4cb97e4a..dc1c95c6cc 100644 --- a/lib/views/changed-files-count-view.js +++ b/lib/views/changed-files-count-view.js @@ -23,8 +23,9 @@ export default class ChangedFilesCountView extends React.Component { return ( + {label} {this.props.mergeConflictsPresent && } From a65d1d126c0b32fa54a61bd256162c85af6d9d59 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 14 Feb 2018 14:31:39 -0500 Subject: [PATCH 0219/5882] console.logify the next spec too just for good measure --- test/github-package.test.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/github-package.test.js b/test/github-package.test.js index f0e83ca9e9..6b8aa146ce 100644 --- a/test/github-package.test.js +++ b/test/github-package.test.js @@ -203,20 +203,30 @@ describe('GithubPackage', function() { }); it('uses an active model from a preexisting active pane item', async function() { + console.log('1c') const [workdirPath1, workdirPath2] = await Promise.all([ cloneRepository('three-files'), cloneRepository('three-files'), ]); + console.log('2c') project.setPaths([workdirPath1, workdirPath2]); + console.log('3c') await workspace.open(path.join(workdirPath2, 'a.txt')); + console.log('4c') await contextUpdateAfter(() => githubPackage.activate()); + console.log('5c') const context = contextPool.getContext(workdirPath2); + console.log('6c') assert.isTrue(context.isPresent()); + console.log('7c') assert.strictEqual(context.getRepository(), githubPackage.getActiveRepository()); + console.log('8c') assert.strictEqual(context.getResolutionProgress(), githubPackage.getActiveResolutionProgress()); + console.log('9c') assert.equal(githubPackage.getActiveWorkdir(), workdirPath2); + console.log('10c') }); it('uses an active model from serialized state', async function() { From 18c27e427ba5f8fdea4a9002a22463e040cfb2a6 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 14 Feb 2018 14:47:24 -0500 Subject: [PATCH 0220/5882] Let's use actual mocha-stress --- package-lock.json | 168 +++++++++------------------------------------- package.json | 1 + test/runner.js | 9 +-- 3 files changed, 36 insertions(+), 142 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9b63daef87..277fc7f0c9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "github", - "version": "0.8.0", + "version": "0.10.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -234,7 +234,7 @@ "arr-flatten": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==" + "integrity": "sha1-NgSLv/TntH4TZkQxbJlmnqWukfE=" }, "arr-union": { "version": "3.1.0", @@ -308,17 +308,10 @@ "resolved": "https://registry.npmjs.org/atob/-/atob-2.0.3.tgz", "integrity": "sha1-GcenYEc3dEaPILLS0DNyrX1Mv10=" }, - "atom-babel6-transpiler": { - "version": "https://registry.npmjs.org/atom-babel6-transpiler/-/atom-babel6-transpiler-1.1.1.tgz", - "integrity": "sha1-y32Rl5sNz+TfIbJKtTiL0hn/IXQ=", - "requires": { - "babel-core": "6.25.0" - } - }, "atom-mocha-test-runner": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/atom-mocha-test-runner/-/atom-mocha-test-runner-1.2.0.tgz", - "integrity": "sha512-HVbx7cAvySjVfVNKpb2go9RO890Xs6yigWWAwoISOz4l2X5oMTMs1rIw04geuEQeTTmW3ob3nj6YN1KWf2cBHg==", + "integrity": "sha1-qPZQm40pqAn8tv9H8FiEthLNxqk=", "dev": true, "requires": { "etch": "0.8.0", @@ -366,32 +359,6 @@ "js-tokens": "3.0.2" } }, - "babel-core": { - "version": "6.25.0", - "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.25.0.tgz", - "integrity": "sha1-fdQrBGPHQunVKW3rPsZ6kyLa1yk=", - "requires": { - "babel-code-frame": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.22.0.tgz", - "babel-generator": "6.26.0", - "babel-helpers": "https://registry.npmjs.org/babel-helpers/-/babel-helpers-6.24.1.tgz", - "babel-messages": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", - "babel-register": "https://registry.npmjs.org/babel-register/-/babel-register-6.24.1.tgz", - "babel-runtime": "6.25.0", - "babel-template": "6.25.0", - "babel-traverse": "6.25.0", - "babel-types": "6.25.0", - "babylon": "6.17.4", - "convert-source-map": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.5.0.tgz", - "debug": "2.6.8", - "json5": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", - "lodash": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", - "minimatch": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "path-is-absolute": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "private": "https://registry.npmjs.org/private/-/private-0.1.7.tgz", - "slash": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", - "source-map": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz" - } - }, "babel-eslint": { "version": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-7.2.3.tgz", "integrity": "sha1-sv4tgBJkcPXBlELcdXJTqJdxCCc=", @@ -508,14 +475,6 @@ "babel-types": "6.25.0" } }, - "babel-helpers": { - "version": "https://registry.npmjs.org/babel-helpers/-/babel-helpers-6.24.1.tgz", - "integrity": "sha1-NHHenK7DiOXIUOWX5Yom3fN2ArI=", - "requires": { - "babel-runtime": "6.25.0", - "babel-template": "6.25.0" - } - }, "babel-messages": { "version": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=", @@ -827,19 +786,6 @@ "babel-preset-flow": "https://registry.npmjs.org/babel-preset-flow/-/babel-preset-flow-6.23.0.tgz" } }, - "babel-register": { - "version": "https://registry.npmjs.org/babel-register/-/babel-register-6.24.1.tgz", - "integrity": "sha1-fhDhOi9xBlvfrVoXh7pFvKbe118=", - "requires": { - "babel-core": "6.25.0", - "babel-runtime": "6.25.0", - "core-js": "2.5.0", - "home-or-tmp": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz", - "lodash": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", - "mkdirp": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "source-map-support": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.15.tgz" - } - }, "babel-runtime": { "version": "6.25.0", "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.25.0.tgz", @@ -1310,10 +1256,6 @@ "integrity": "sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo=", "dev": true }, - "convert-source-map": { - "version": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.5.0.tgz", - "integrity": "sha1-ms1whRxtXf3ZPZKC5e35SgP/RrU=" - }, "copy-descriptor": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", @@ -1355,7 +1297,7 @@ "lru-cache": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.1.tgz", - "integrity": "sha512-q4spe4KTfsAS1SUHLO0wz8Qiyf1+vMIAgpRYioFYDMNqKfHQbg+AVDH3i4fvpl71/P1L0dBl+fQi+P37UYf0ew==", + "integrity": "sha1-Yi4y6CSItJJ5EUpPns9F581rulU=", "requires": { "pseudomap": "1.0.2", "yallist": "2.1.2" @@ -1568,7 +1510,7 @@ "dugite": { "version": "1.49.0", "resolved": "https://registry.npmjs.org/dugite/-/dugite-1.49.0.tgz", - "integrity": "sha512-zPjTVInNS+t3x0e7+hdkICDgIe1vYvXm/bde0BNBP3/eId/iWPn/Oi1z9mGRZySScRKTXdOJ7ITHzUfaNxIfCg==", + "integrity": "sha1-b1zkraF0jwyL+bCOYj9foZKYA0w=", "requires": { "checksum": "0.1.1", "mkdirp": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", @@ -1618,7 +1560,7 @@ "boom": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", - "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==", + "integrity": "sha1-XdnabuOl8wIHdDYpDLcX0/SlTgI=", "requires": { "hoek": "4.2.0" } @@ -1652,7 +1594,7 @@ "hawk": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz", - "integrity": "sha512-miowhl2+U7Qle4vdLqDdPt9m09K6yZhkLDTWGoUiUzrQCn+mHHSmfJgAyGaLRZbPmTqfFFjRV1QWCW0VWUJBbQ==", + "integrity": "sha1-r02RTrBl+bXOTZ0RwcshJu7MMDg=", "requires": { "boom": "4.3.1", "cryptiles": "3.1.2", @@ -1663,7 +1605,7 @@ "hoek": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.0.tgz", - "integrity": "sha512-v0XCLxICi9nPfYrS9RL8HbYnXi9obYAeLbSP00BmnZwCK9+Ih9WOjoZ8YoHCoav2csqn4FOz4Orldsy2dmDwmQ==" + "integrity": "sha1-ctnQdU9/4lyi0BrY+PmpRJqJUm0=" }, "http-signature": { "version": "1.2.0", @@ -1696,12 +1638,12 @@ "qs": { "version": "6.5.1", "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", - "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" + "integrity": "sha1-NJzfbu+J7EXBLX1es/wMhwNDptg=" }, "request": { "version": "2.83.0", "resolved": "https://registry.npmjs.org/request/-/request-2.83.0.tgz", - "integrity": "sha512-lR3gD69osqm6EYLk9wB/G1W/laGWjzH90t1vEa2xuxHD5KUrSzp9pUSfTm+YC5Nxt2T8nMPEvKlhbQayU7bgFw==", + "integrity": "sha1-ygtl2gLtYpNYh4COb1EDgQNOM1Y=", "requires": { "aws-sign2": "0.7.0", "aws4": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", @@ -1738,7 +1680,7 @@ "sntp": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/sntp/-/sntp-2.1.0.tgz", - "integrity": "sha512-FL1b58BDrqS3A11lJ0zEdnJ3UOKqVxawAkF3k7F0CVN7VQ34aZrV+G8BZ1WC9ZL7NyrwsW0oviwsWDgRuVYtJg==", + "integrity": "sha1-LGzsFP7cIiJznK+bXD2F0cxaLMg=", "requires": { "hoek": "4.2.0" } @@ -2251,7 +2193,7 @@ "is-descriptor": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "integrity": "sha1-Nm2CQN3kh8pRgjsaufB6EKeCUco=", "requires": { "is-accessor-descriptor": "0.1.6", "is-data-descriptor": "0.1.4", @@ -2352,7 +2294,7 @@ "fast-glob": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-1.0.1.tgz", - "integrity": "sha512-C2VHbdBwSkaQDyavjQZDflZzmZKrsUK3fTdJtsOnED0L0vtHCw+NL0h8pRcydbpRHlNJLZ4/LbOfEdJKspK91A==", + "integrity": "sha1-MPmxEg/Ven8XI2SmRY+9vZgYezw=", "requires": { "bash-glob": "1.0.1", "glob-parent": "3.1.0", @@ -2730,14 +2672,6 @@ "version": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-1.2.0.tgz", "integrity": "sha1-qkSM8JhtVcxAdzsXF0t90GbLfPs=" }, - "home-or-tmp": { - "version": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz", - "integrity": "sha1-42w/LSyufXRqhX440Y1fMqeILbg=", - "requires": { - "os-homedir": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "os-tmpdir": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz" - } - }, "hosted-git-info": { "version": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.5.0.tgz", "integrity": "sha1-bWDjSzq7yDEwYsO3mO+NkBoHrzw=" @@ -3029,7 +2963,7 @@ "is-plain-object": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "integrity": "sha1-LBY7P6+xtgbZ0Xko8FwqHDjgdnc=", "requires": { "isobject": "3.0.1" } @@ -3188,10 +3122,6 @@ "integrity": "sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE=", "dev": true }, - "json5": { - "version": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", - "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=" - }, "jsonfile": { "version": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", "integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=", @@ -3233,13 +3163,6 @@ "integrity": "sha1-OGchPo3Xm/Ho8jAMDPwe+xgsDfE=", "dev": true }, - "keytar": { - "version": "https://registry.npmjs.org/keytar/-/keytar-4.0.4.tgz", - "integrity": "sha1-WaMG9EihxqMJzWjLKRKQlajIsds=", - "requires": { - "nan": "2.5.1" - } - }, "kind-of": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", @@ -3275,7 +3198,7 @@ "less": { "version": "2.7.3", "resolved": "https://registry.npmjs.org/less/-/less-2.7.3.tgz", - "integrity": "sha512-KPdIJKWcEAb02TuJtaLrhue0krtRLoRoo7x6BNJIBelO00t/CCdJQUnHW5V34OnHMWzIktSalJxRO+FvytQlCQ==", + "integrity": "sha1-zBJg9RyQCp7A2R+2mYE54CUHtjs=", "dev": true, "requires": { "errno": "0.1.4", @@ -3936,6 +3859,12 @@ } } }, + "mocha-stress": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mocha-stress/-/mocha-stress-1.0.0.tgz", + "integrity": "sha512-AeEgizAGFl+V6Bp3qzvjr9PcErlBw6qQOemDXKpbYIQaUvwKQscZmoBxq38ey0sLW9o3wwidbvaFSRQ3VApd+Q==", + "dev": true + }, "moment": { "version": "https://registry.npmjs.org/moment/-/moment-2.18.1.tgz", "integrity": "sha1-w2GT3Tzhwu7SrbfIAtu8d6gbHA8=" @@ -4105,7 +4034,7 @@ "is-descriptor": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "integrity": "sha1-Nm2CQN3kh8pRgjsaufB6EKeCUco=", "requires": { "is-accessor-descriptor": "0.1.6", "is-data-descriptor": "0.1.4", @@ -4278,7 +4207,8 @@ }, "os-homedir": { "version": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "dev": true }, "os-locale": { "version": "1.4.0", @@ -4404,10 +4334,6 @@ "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", "dev": true }, - "private": { - "version": "https://registry.npmjs.org/private/-/private-0.1.7.tgz", - "integrity": "sha1-aM5eih7woju1cMwoU3tTMqumPvE=" - }, "process": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/process/-/process-0.5.2.tgz", @@ -4889,10 +4815,6 @@ } } }, - "slash": { - "version": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", - "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=" - }, "slice-ansi": { "version": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz", "integrity": "sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=", @@ -4932,7 +4854,7 @@ "is-descriptor": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "integrity": "sha1-Nm2CQN3kh8pRgjsaufB6EKeCUco=", "requires": { "is-accessor-descriptor": "0.1.6", "is-data-descriptor": "0.1.4", @@ -4954,7 +4876,7 @@ "snapdragon-node": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", - "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "integrity": "sha1-bBdfhv8UvbByRWPo88GwIaKGhTs=", "requires": { "define-property": "1.0.0", "isobject": "3.0.1", @@ -4964,7 +4886,7 @@ "snapdragon-util": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", - "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "integrity": "sha1-+VZHlIbyrNeXAGk/b3uAXkWrVuI=", "requires": { "kind-of": "3.2.2" }, @@ -5002,13 +4924,6 @@ "urix": "0.1.0" } }, - "source-map-support": { - "version": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.15.tgz", - "integrity": "sha1-AyAt9lwG0r2MfsI2KhkwVv7407E=", - "requires": { - "source-map": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz" - } - }, "source-map-url": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", @@ -5029,13 +4944,6 @@ "version": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz", "integrity": "sha1-yd96NCRZSt5r0RkA1ZZpbcBrrFc=" }, - "split": { - "version": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", - "integrity": "sha1-YFvZvjA6pZ+zX5Ip++oN3snqB9k=", - "requires": { - "through": "https://registry.npmjs.org/through/-/through-2.3.8.tgz" - } - }, "split-string": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/split-string/-/split-string-2.1.1.tgz", @@ -5091,7 +4999,7 @@ "is-descriptor": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "integrity": "sha1-Nm2CQN3kh8pRgjsaufB6EKeCUco=", "requires": { "is-accessor-descriptor": "0.1.6", "is-data-descriptor": "0.1.4", @@ -5240,7 +5148,8 @@ }, "through": { "version": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true }, "tinycolor2": { "version": "1.4.1", @@ -5305,7 +5214,7 @@ "is-descriptor": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "integrity": "sha1-Nm2CQN3kh8pRgjsaufB6EKeCUco=", "requires": { "is-accessor-descriptor": "0.1.6", "is-data-descriptor": "0.1.4", @@ -5433,7 +5342,7 @@ "is-descriptor": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "integrity": "sha1-Nm2CQN3kh8pRgjsaufB6EKeCUco=", "requires": { "is-accessor-descriptor": "0.1.6", "is-data-descriptor": "0.1.4", @@ -5506,17 +5415,6 @@ "x-is-string": "0.1.0" } }, - "what-the-diff": { - "version": "https://registry.npmjs.org/what-the-diff/-/what-the-diff-0.3.0.tgz", - "integrity": "sha1-F/DZFm9lL4/p9Jl/LFOkCt5bVQ8=" - }, - "what-the-status": { - "version": "https://registry.npmjs.org/what-the-status/-/what-the-status-1.0.2.tgz", - "integrity": "sha1-e7SJ+zUuM01+GnNoZAlj7qD3Qtw=", - "requires": { - "split": "https://registry.npmjs.org/split/-/split-1.0.1.tgz" - } - }, "whatwg-fetch": { "version": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz", "integrity": "sha1-nITsLc9oGH/wC8ZOEnS0QhduHIQ=" @@ -5524,7 +5422,7 @@ "which": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/which/-/which-1.3.0.tgz", - "integrity": "sha512-xcJpopdamTuY5duC/KnTTNBraPK54YwpenP4lzxU8H91GudWpFv38u0CKjclE1Wi2EH2EDz5LRcHcKbCIzqGyg==", + "integrity": "sha1-/wS9/AEO5UfXgL7DjhrBwnd9JTo=", "requires": { "isexe": "2.0.0" } diff --git a/package.json b/package.json index ff4c39dcfc..a9c62329ef 100644 --- a/package.json +++ b/package.json @@ -98,6 +98,7 @@ "mocha": "^3.4.2", "mocha-appveyor-reporter": "^0.4.0", "mocha-junit-and-console-reporter": "^1.6.0", + "mocha-stress": "^1.0.0", "node-fetch": "^1.7.1", "react-test-renderer": "^15.6.1", "simulant": "^0.2.2", diff --git a/test/runner.js b/test/runner.js index e719efe702..40d8620a89 100644 --- a/test/runner.js +++ b/test/runner.js @@ -8,13 +8,6 @@ import until from 'test-until'; chai.use(chaiAsPromised); global.assert = chai.assert; -global.stress = function(count, ...args) { - const [description, ...rest] = args; - for (let i = 0; i < count; i++) { - it.only(`${description} #${i}`, ...rest); - } -}; - // Give tests that rely on filesystem event delivery lots of breathing room. until.setDefaultTimeout(parseInt(process.env.UNTIL_TIMEOUT || '3000', 10)); @@ -23,6 +16,8 @@ module.exports = createRunner({ reporter: process.env.MOCHA_REPORTER || 'spec', overrideTestPaths: [/spec$/, /test/], }, mocha => { + require('mocha-stress'); + mocha.timeout(parseInt(process.env.MOCHA_TIMEOUT || '5000', 10)); if (process.env.TEST_JUNIT_XML_PATH) { From 19df27e4a71c38a6881a34e109746ebd18dcbb05 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 14 Feb 2018 14:47:58 -0500 Subject: [PATCH 0221/5882] Stress the activate specs, let's see if they still freeze --- test/github-package.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/github-package.test.js b/test/github-package.test.js index 6b8aa146ce..3f1aba3979 100644 --- a/test/github-package.test.js +++ b/test/github-package.test.js @@ -146,7 +146,7 @@ describe('GithubPackage', function() { }); }); - describe('activate()', function() { + describe.stress(10, 'activate()', function() { it('begins with an undetermined repository context', async function() { await contextUpdateAfter(() => githubPackage.activate()); From 74ddf1f850952eec73a8e10bafcd71a39b80c5f4 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 14 Feb 2018 14:59:35 -0500 Subject: [PATCH 0222/5882] Does not repro under .only. interesting --- test/github-package.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/github-package.test.js b/test/github-package.test.js index 3f1aba3979..6b8aa146ce 100644 --- a/test/github-package.test.js +++ b/test/github-package.test.js @@ -146,7 +146,7 @@ describe('GithubPackage', function() { }); }); - describe.stress(10, 'activate()', function() { + describe('activate()', function() { it('begins with an undetermined repository context', async function() { await contextUpdateAfter(() => githubPackage.activate()); From 224dddfd158e2c4a5d04791b13dbcbca6ea29b7c Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 14 Feb 2018 15:03:34 -0500 Subject: [PATCH 0223/5882] Hypothesis: the lockup is in native code, so setIntervals will stop --- test/github-package.test.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/github-package.test.js b/test/github-package.test.js index 6b8aa146ce..7ea99c0003 100644 --- a/test/github-package.test.js +++ b/test/github-package.test.js @@ -11,8 +11,11 @@ describe('GithubPackage', function() { let atomEnv, workspace, project, commandRegistry, notificationManager, grammars, config, confirm, tooltips, styles; let getLoadSettings, configDirPath, deserializers; let githubPackage, contextPool; + let interval; beforeEach(function() { + interval = setInterval(() => console.log('(beat)'), 5000) + console.log('be: 1'); atomEnv = global.buildAtomEnvironment(); workspace = atomEnv.workspace; @@ -62,6 +65,7 @@ describe('GithubPackage', function() { console.log('ae: 2'); atomEnv.destroy(); console.log('ae: 3'); + clearInterval(interval) }); async function contextUpdateAfter(chunk) { From 5639e1f2a8692846d1c6334526e0d730aa2f6050 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 14 Feb 2018 15:06:45 -0500 Subject: [PATCH 0224/5882] Ah there's the unreferenced var --- test/github-package.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/github-package.test.js b/test/github-package.test.js index 7ea99c0003..7f461ac1ab 100644 --- a/test/github-package.test.js +++ b/test/github-package.test.js @@ -50,7 +50,7 @@ describe('GithubPackage', function() { // console.log('Calling that callback from the setTimeout!'); // callback(); // }, 0); - console.log('the setTimeout call returned a handle:', handle); + // console.log('the setTimeout call returned a handle:', handle); } }); console.log('be: 4'); From 4a9cc8cde2f515e573cc4e620e1b2d9e5aa850be Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 14 Feb 2018 15:12:18 -0500 Subject: [PATCH 0225/5882] Drop down that interval a bit to be more useful --- test/github-package.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/github-package.test.js b/test/github-package.test.js index 7f461ac1ab..7ab6816cff 100644 --- a/test/github-package.test.js +++ b/test/github-package.test.js @@ -14,7 +14,7 @@ describe('GithubPackage', function() { let interval; beforeEach(function() { - interval = setInterval(() => console.log('(beat)'), 5000) + interval = setInterval(() => console.log('(beat)'), 500) console.log('be: 1'); atomEnv = global.buildAtomEnvironment(); From 77524faafd0b970f459a239b2f0fbcf24d96a2af Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 14 Feb 2018 15:18:13 -0500 Subject: [PATCH 0226/5882] Even more why not --- test/github-package.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/github-package.test.js b/test/github-package.test.js index 7ab6816cff..d1933d77ce 100644 --- a/test/github-package.test.js +++ b/test/github-package.test.js @@ -14,7 +14,7 @@ describe('GithubPackage', function() { let interval; beforeEach(function() { - interval = setInterval(() => console.log('(beat)'), 500) + interval = setInterval(() => console.log('(beat)'), 50) console.log('be: 1'); atomEnv = global.buildAtomEnvironment(); From 0ec99b9d8a25039596d7360d709e8f401d70b458 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 14 Feb 2018 15:41:24 -0500 Subject: [PATCH 0227/5882] Dump a native stack with gdb after four minutes --- script/cibuild | 14 ++++++++++++++ test/runner.js | 3 +++ 2 files changed, 17 insertions(+) diff --git a/script/cibuild b/script/cibuild index ea6319d795..f2249b5557 100755 --- a/script/cibuild +++ b/script/cibuild @@ -1,5 +1,18 @@ #!/bin/bash +function waitAndDump { + # About four minutes + sleep 240 + + local NODE_PID=$(cat mocha.pid) + if [ -z "${NODE_PID}" ]; then + echo "Unable to read node pid from mocha.pid" + exit 1 + fi + + gdb -batch -ex "thread apply all bt" -p ${NODE_PID} +} + function updateStatus { STATUS=$1 CTX=$2 @@ -168,6 +181,7 @@ function runTests { if [ $LINT_RESULT -ne 0 ]; then echo ">>> LINTING FAILED! <<< Continuing on to tests..."; fi echo "Running specs..." + waitAndDump & "$ATOM_SCRIPT_PATH" --test test TEST_RESULT=$? diff --git a/test/runner.js b/test/runner.js index 40d8620a89..3ad337b774 100644 --- a/test/runner.js +++ b/test/runner.js @@ -2,9 +2,12 @@ import {createRunner} from 'atom-mocha-test-runner'; import chai from 'chai'; import chaiAsPromised from 'chai-as-promised'; import path from 'path'; +import fs from 'fs-extra'; import until from 'test-until'; +fs.writeFileSync('mocha.pid', process.pid) + chai.use(chaiAsPromised); global.assert = chai.assert; From 68fc39dca8b8cc774d191e2b3f0e6c437eb740c8 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 14 Feb 2018 17:30:10 -0500 Subject: [PATCH 0228/5882] Install gdb --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 4002cd1cb8..9c56c53610 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,6 +16,7 @@ addons: - gnome-keyring - libsecret-1-dev - python-gnomekeyring + - gdb env: global: From 805eae61a01493a6e977ca190a8bd3709a8c319e Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 15 Feb 2018 07:58:12 -0500 Subject: [PATCH 0229/5882] Hmm that needs a sudo --- script/cibuild | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/cibuild b/script/cibuild index f2249b5557..16528c37bb 100755 --- a/script/cibuild +++ b/script/cibuild @@ -10,7 +10,7 @@ function waitAndDump { exit 1 fi - gdb -batch -ex "thread apply all bt" -p ${NODE_PID} + sudo gdb -batch -ex "thread apply all bt" -p ${NODE_PID} } function updateStatus { From 55531eb884075f805ba8edfff6c33a571b72fa27 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 15 Feb 2018 08:22:26 -0500 Subject: [PATCH 0230/5882] Nothing obvious in gdb backtraces. Let's check strace --- script/cibuild | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/cibuild b/script/cibuild index 16528c37bb..4661557651 100755 --- a/script/cibuild +++ b/script/cibuild @@ -10,7 +10,7 @@ function waitAndDump { exit 1 fi - sudo gdb -batch -ex "thread apply all bt" -p ${NODE_PID} + sudo strace -p ${NODE_PID} } function updateStatus { From 1b1b6d5acc6b3cfbc95b03e6eed1717c12039af7 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 15 Feb 2018 08:39:19 -0500 Subject: [PATCH 0231/5882] Attach strace to all child threads and processes --- script/cibuild | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/cibuild b/script/cibuild index 4661557651..23e059766c 100755 --- a/script/cibuild +++ b/script/cibuild @@ -10,7 +10,7 @@ function waitAndDump { exit 1 fi - sudo strace -p ${NODE_PID} + sudo strace -p ${NODE_PID} -f } function updateStatus { From 06cfb443ea448669b1c548adc81de9564ff7027d Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 15 Feb 2018 09:27:16 -0500 Subject: [PATCH 0232/5882] Well nothing jumped out of the strace --- script/cibuild | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/cibuild b/script/cibuild index 23e059766c..03e54f8299 100755 --- a/script/cibuild +++ b/script/cibuild @@ -10,7 +10,7 @@ function waitAndDump { exit 1 fi - sudo strace -p ${NODE_PID} -f + # sudo strace -p ${NODE_PID} -f } function updateStatus { From 2c11b13a7a9064eb5917c76e532f767ceb15cdaa Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 15 Feb 2018 13:29:46 -0500 Subject: [PATCH 0233/5882] Let's try to isolate the freezing test again --- test/github-package.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/github-package.test.js b/test/github-package.test.js index d1933d77ce..79fdd5d323 100644 --- a/test/github-package.test.js +++ b/test/github-package.test.js @@ -183,7 +183,7 @@ describe('GithubPackage', function() { console.log('9a'); }); - it('uses an active model from a single preexisting project', async function() { + it.only('uses an active model from a single preexisting project', async function() { console.log('1b'); const workdirPath = await cloneRepository('three-files'); console.log('2b'); From c029155dab9ebd849781f87e6be9622ca6519c8f Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Wed, 14 Feb 2018 20:45:21 -0800 Subject: [PATCH 0234/5882] Fix logic for automatically opening diff views based on selection change If there are stale pending pane items corresponding to a file that no longer exists in the changed files list, update them to reflect the new selection. If the selection in the changed file list has changed due to keyboard nav and there is a pending diff view in the active pane, update the diff view to reflect the new selection. --- lib/views/staging-view.js | 72 +++++++++++++++++++++------------ test/views/staging-view.test.js | 12 +++--- 2 files changed, 52 insertions(+), 32 deletions(-) diff --git a/lib/views/staging-view.js b/lib/views/staging-view.js index a73196b977..0d224a4e20 100644 --- a/lib/views/staging-view.js +++ b/lib/views/staging-view.js @@ -391,20 +391,60 @@ export default class StagingView { await this.showMergeConflictFileForPath(selectedItem.filePath, {activate: true}); } } else { - const pendingItem = this.getPendingFilePatchItem(); - if (openNew || pendingItem) { - const activate = pendingItem === this.props.workspace.getActivePaneItem(); - await this.showFilePatchItem(selectedItem.filePath, this.selection.getActiveListKey(), {activate}); + if (openNew) { + // User explicitly asked to view diff, such as via click + await this.showFilePatchItem(selectedItem.filePath, this.selection.getActiveListKey(), {activate: false}); + } else { + const panesWithStaleItemsToUpdate = this.getPanesWithStalePendingFilePatchItem(); + if (panesWithStaleItemsToUpdate.length > 0) { + // Staging event caused selection to change, so we need to update all old diff views to reflect new selection + await Promise.all(panesWithStaleItemsToUpdate.map(async pane => { + await this.showFilePatchItem(selectedItem.filePath, this.selection.getActiveListKey(), { + activate: false, + pane, + }); + })); + } else { + // Selection was changed via keyboard navigation, update pending item in active pane + const activePane = this.props.workspace.getCenter().getActivePane(); + const activePendingItem = activePane.getPendingItem(); + const activePaneHasPendingFilePatchItem = activePendingItem && activePendingItem.getRealItem && + activePendingItem.getRealItem() instanceof FilePatchController; + if (activePaneHasPendingFilePatchItem) { + await this.showFilePatchItem(selectedItem.filePath, this.selection.getActiveListKey(), { + activate: false, + pane: activePane, + }); + } + } } } } - async showFilePatchItem(filePath, stagingStatus, {activate} = {activate: false}) { + getPanesWithStalePendingFilePatchItem() { + // "stale" meaning there is no longer a changed file associated with it due to contents being fully staged/unstaged + return this.props.workspace.getPanes().filter(pane => { + const pendingItem = pane.getPendingItem(); + if (!pendingItem) { return false; } + const isDiffViewItem = pendingItem.getRealItem && pendingItem.getRealItem() instanceof FilePatchController; + const isInActiveRepo = pendingItem.getWorkingDirectory() === this.props.workingDirectoryPath; + const isStale = !this.changedFileExists(pendingItem.getFilePath(), pendingItem.getStagingStatus()); + return isDiffViewItem && isInActiveRepo && isStale; + }); + } + + changedFileExists(filePath, stagingStatus) { + return this.selection.findItem((item, key) => { + return key === stagingStatus && item.filePath === filePath; + }); + } + + async showFilePatchItem(filePath, stagingStatus, {activate, pane} = {activate: false}) { const encodedFilePath = encodeURIComponent(filePath); const encodedWorkdir = encodeURIComponent(this.props.workingDirectoryPath); const filePatchItem = await this.props.workspace.open( `atom-github://file-patch/${encodedFilePath}?workdir=${encodedWorkdir}&stagingStatus=${stagingStatus}`, - {pending: true, activatePane: activate, activateItem: activate}, + {pending: true, activatePane: activate, activateItem: activate, pane}, ); if (activate) { filePatchItem.focus(); @@ -428,26 +468,6 @@ export default class StagingView { return new File(absolutePath).exists(); } - getPendingFilePatchItem() { - const activePane = this.props.workspace.getActivePane(); - const activeItem = activePane.getActiveItem(); - const activePendingItem = activePane.getPendingItem(); - const isFPC = activeItem && activeItem.getRealItem && activeItem.getRealItem() instanceof FilePatchController; - if (isFPC && activeItem === activePendingItem) { - return activeItem; - } - const panes = this.props.workspace.getPanes(); - for (let i = 0; i < panes.length; i++) { - const pane = panes[i]; - const item = pane.getActiveItem(); - const pendingItem = pane.getPendingItem(); - if (item && item.getRealItem && item.getRealItem() instanceof FilePatchController && item === pendingItem) { - return item; - } - } - return null; - } - @autobind dblclickOnItem(event, item) { return this.props.attemptFileStageOperation([item.filePath], this.selection.listKeyForItem(item)); diff --git a/test/views/staging-view.test.js b/test/views/staging-view.test.js index 7a794a88bd..ac4220b3fb 100644 --- a/test/views/staging-view.test.js +++ b/test/views/staging-view.test.js @@ -255,7 +255,7 @@ describe('StagingView', function() { assert.equal(workspace.open.callCount, 1); assert.deepEqual(workspace.open.args[0], [ `atom-github://file-patch/file.txt?workdir=${encodeURIComponent(workingDirectoryPath)}&stagingStatus=staged`, - {pending: true, activatePane: true, activateItem: true}, + {pending: true, activatePane: true, pane: undefined, activateItem: true}, ]); assert.isTrue(filePatchItem.focus.called); }); @@ -275,7 +275,7 @@ describe('StagingView', function() { assert.equal(workspace.open.callCount, 1); assert.deepEqual(workspace.open.args[0], [ `atom-github://file-patch/file.txt?workdir=${encodeURIComponent(workingDirectoryPath)}&stagingStatus=staged`, - {pending: true, activatePane: false, activateItem: false}, + {pending: true, activatePane: false, pane: undefined, activateItem: false}, ]); assert.isFalse(focus.called); assert.equal(activateItem.callCount, 1); @@ -355,11 +355,11 @@ describe('StagingView', function() { }); document.body.appendChild(view.element); - const getPendingFilePatchItem = sinon.stub(view, 'getPendingFilePatchItem').returns(false); + const getPanesWithStalePendingFilePatchItem = sinon.stub(view, 'getPanesWithStalePendingFilePatchItem').returns([]); await view.selectNext(); assert.isFalse(showFilePatchItem.called); - getPendingFilePatchItem.returns(true); + getPanesWithStalePendingFilePatchItem.returns(['pending-file-patch-item']); await view.selectPrevious(); assert.isTrue(showFilePatchItem.calledWith(filePatches[0].filePath)); await view.selectNext(); @@ -423,11 +423,11 @@ describe('StagingView', function() { }); document.body.appendChild(view.element); - const getPendingFilePatchItem = sinon.stub(view, 'getPendingFilePatchItem').returns(false); + const getPanesWithStalePendingFilePatchItem = sinon.stub(view, 'getPanesWithStalePendingFilePatchItem').returns([]); await view.selectNext(); assert.isFalse(showFilePatchItem.called); - getPendingFilePatchItem.returns(true); + getPanesWithStalePendingFilePatchItem.returns(['pending-file-patch-item']); await view.selectPrevious(); await assert.async.isTrue(showFilePatchItem.calledWith(filePatches[0].filePath)); await view.selectNext(); From e29a832e038c6058f85777da30557e3de2585071 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 15 Feb 2018 15:23:50 -0500 Subject: [PATCH 0235/5882] Yield the event loop in a beforeEach to see if it locks up elsewhere --- test/github-package.test.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/test/github-package.test.js b/test/github-package.test.js index 79fdd5d323..7274273d42 100644 --- a/test/github-package.test.js +++ b/test/github-package.test.js @@ -59,6 +59,12 @@ describe('GithubPackage', function() { console.log('be: 5'); }); + beforeEach(async function () { + console.log('be2: 0') + await new Promise(resolve => resolve()) + console.log('be2: 1') + }) + afterEach(async function() { console.log('ae: 1'); await githubPackage.deactivate(); @@ -183,7 +189,7 @@ describe('GithubPackage', function() { console.log('9a'); }); - it.only('uses an active model from a single preexisting project', async function() { + it('uses an active model from a single preexisting project', async function() { console.log('1b'); const workdirPath = await cloneRepository('three-files'); console.log('2b'); From 89240cf7c59398f832db3ff0a74c303d9abf1913 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 15 Feb 2018 15:33:51 -0500 Subject: [PATCH 0236/5882] Is it the event loop yield that's getting us in trouble... ? --- test/github-package.test.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/github-package.test.js b/test/github-package.test.js index 7274273d42..64bf93852a 100644 --- a/test/github-package.test.js +++ b/test/github-package.test.js @@ -210,6 +210,8 @@ describe('GithubPackage', function() { console.log('8b'); assert.equal(githubPackage.getActiveWorkdir(), workdirPath); console.log('9b'); + await new Promise(resolve => setTimeout(resolve, 10)) + console.log('10b') }); it('uses an active model from a preexisting active pane item', async function() { From 5106767ad009ddb0c0b6091b5d452f09998056e9 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 15 Feb 2018 15:46:30 -0500 Subject: [PATCH 0237/5882] Will just a nextTick do it --- test/github-package.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/github-package.test.js b/test/github-package.test.js index 64bf93852a..580640c674 100644 --- a/test/github-package.test.js +++ b/test/github-package.test.js @@ -210,7 +210,7 @@ describe('GithubPackage', function() { console.log('8b'); assert.equal(githubPackage.getActiveWorkdir(), workdirPath); console.log('9b'); - await new Promise(resolve => setTimeout(resolve, 10)) + await new Promise(resolve => process.nextTick(resolve)) console.log('10b') }); From 2a992d4cb98ae273780e5494e4fa8780cae03108 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 15 Feb 2018 15:58:14 -0500 Subject: [PATCH 0238/5882] the (beat) isn't helpful any more --- test/github-package.test.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/test/github-package.test.js b/test/github-package.test.js index 580640c674..e213577baa 100644 --- a/test/github-package.test.js +++ b/test/github-package.test.js @@ -11,11 +11,8 @@ describe('GithubPackage', function() { let atomEnv, workspace, project, commandRegistry, notificationManager, grammars, config, confirm, tooltips, styles; let getLoadSettings, configDirPath, deserializers; let githubPackage, contextPool; - let interval; beforeEach(function() { - interval = setInterval(() => console.log('(beat)'), 50) - console.log('be: 1'); atomEnv = global.buildAtomEnvironment(); workspace = atomEnv.workspace; @@ -71,7 +68,6 @@ describe('GithubPackage', function() { console.log('ae: 2'); atomEnv.destroy(); console.log('ae: 3'); - clearInterval(interval) }); async function contextUpdateAfter(chunk) { From 7fbf0ce1d5dc7ddd3b7ccbfb77d0cdb8c5a3c552 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 15 Feb 2018 15:58:31 -0500 Subject: [PATCH 0239/5882] resolve that promise on the next tick --- test/github-package.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/github-package.test.js b/test/github-package.test.js index e213577baa..7a960e93cf 100644 --- a/test/github-package.test.js +++ b/test/github-package.test.js @@ -58,7 +58,7 @@ describe('GithubPackage', function() { beforeEach(async function () { console.log('be2: 0') - await new Promise(resolve => resolve()) + await new Promise(resolve => process.nextTick(resolve)) console.log('be2: 1') }) From 4a6f9fa6ff98cb5a98f5e9f7f77801ae181dabae Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 15 Feb 2018 16:11:27 -0500 Subject: [PATCH 0240/5882] Nope, only on setTimeout(). Interesting --- test/github-package.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/github-package.test.js b/test/github-package.test.js index 7a960e93cf..f6a16f3470 100644 --- a/test/github-package.test.js +++ b/test/github-package.test.js @@ -206,7 +206,7 @@ describe('GithubPackage', function() { console.log('8b'); assert.equal(githubPackage.getActiveWorkdir(), workdirPath); console.log('9b'); - await new Promise(resolve => process.nextTick(resolve)) + await new Promise(resolve => setTimeout(resolve, 0)) console.log('10b') }); From 2053b414da30bb565b030409144c3478651d30ee Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 15 Feb 2018 16:28:13 -0500 Subject: [PATCH 0241/5882] Clean up your PathWatchers --- test/github-package.test.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/test/github-package.test.js b/test/github-package.test.js index f6a16f3470..a6c873c033 100644 --- a/test/github-package.test.js +++ b/test/github-package.test.js @@ -66,8 +66,18 @@ describe('GithubPackage', function() { console.log('ae: 1'); await githubPackage.deactivate(); console.log('ae: 2'); - atomEnv.destroy(); + + const promises = [] + for (let p in atomEnv.project.watcherPromisesByPath) { + promises.push(atom.project.watcherPromisesByPath[p]) + } console.log('ae: 3'); + await Promise.all( + promises.map(p => p.then(w => w.stop())) + ) + + atomEnv.destroy(); + console.log('ae: 4'); }); async function contextUpdateAfter(chunk) { From 807b5bdddbb21ef61199c0d86e0de9229e34f0ca Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Thu, 15 Feb 2018 16:24:09 -0800 Subject: [PATCH 0242/5882] WIP update selected item when focusing pane item --- lib/views/staging-view.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/views/staging-view.js b/lib/views/staging-view.js index 0d224a4e20..c6a1e44f1c 100644 --- a/lib/views/staging-view.js +++ b/lib/views/staging-view.js @@ -143,9 +143,17 @@ export default class StagingView { await this.resolutionProgressObserver.setActiveModel(this.props.resolutionProgress); } + const isRepoSame = oldProps.workingDirectoryPath === this.props.workingDirectoryPath; const selectionChanged = !isEqual(previouslySelectedItems, this.selection.getSelectedItems()); - if (selectionChanged) { - this.debouncedDidChangeSelectedItem(); + if (isRepoSame && selectionChanged) { + // Fixes bug when focusing diff view from non-active repo. refactor to make more readable + const item = this.props.workspace.getActivePaneItem(); + const isDiffViewItem = item.getRealItem && item.getRealItem() instanceof FilePatchController; + if (isDiffViewItem) { + this.quietlySelectItem(item.getFilePath(), item.getStagingStatus()); + } else { + this.debouncedDidChangeSelectedItem(); + } } return etch.update(this); From a22f8de39109a2d6f9e4020a9a5bca0bbcfb6e05 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 15 Feb 2018 21:30:48 -0500 Subject: [PATCH 0243/5882] Let's try using the emulated filesystem watcher --- test/github-package.test.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/test/github-package.test.js b/test/github-package.test.js index a6c873c033..f9814b4458 100644 --- a/test/github-package.test.js +++ b/test/github-package.test.js @@ -29,6 +29,8 @@ describe('GithubPackage', function() { configDirPath = path.join(__dirname, 'fixtures', 'atomenv-config'); console.log('be: 2'); + config.set('core.fileSystemWatcher', 'atom') + githubPackage = new GithubPackage( workspace, project, commandRegistry, notificationManager, tooltips, styles, grammars, confirm, config, deserializers, configDirPath, getLoadSettings, @@ -67,14 +69,14 @@ describe('GithubPackage', function() { await githubPackage.deactivate(); console.log('ae: 2'); - const promises = [] - for (let p in atomEnv.project.watcherPromisesByPath) { - promises.push(atom.project.watcherPromisesByPath[p]) - } - console.log('ae: 3'); - await Promise.all( - promises.map(p => p.then(w => w.stop())) - ) + // const promises = [] + // for (let p in atomEnv.project.watcherPromisesByPath) { + // promises.push(atom.project.watcherPromisesByPath[p]) + // } + // console.log('ae: 3'); + // await Promise.all( + // promises.map(p => p.then(w => w.stop())) + // ) atomEnv.destroy(); console.log('ae: 4'); From 6968ecb3952668370e661578141d327e9e405adf Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Sun, 18 Feb 2018 16:37:44 -0800 Subject: [PATCH 0244/5882] :art: logic for selecting corresponding file when diff view is active item --- lib/views/staging-view.js | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/lib/views/staging-view.js b/lib/views/staging-view.js index c6a1e44f1c..23643f731f 100644 --- a/lib/views/staging-view.js +++ b/lib/views/staging-view.js @@ -111,6 +111,7 @@ export default class StagingView { if (isFilePatchController && isMatch) { this.quietlySelectItem(item.getFilePath(), item.getStagingStatus()); } + this.activeFilePatch = isFilePatchController ? item : null; } }), ); @@ -143,15 +144,13 @@ export default class StagingView { await this.resolutionProgressObserver.setActiveModel(this.props.resolutionProgress); } - const isRepoSame = oldProps.workingDirectoryPath === this.props.workingDirectoryPath; - const selectionChanged = !isEqual(previouslySelectedItems, this.selection.getSelectedItems()); - if (isRepoSame && selectionChanged) { - // Fixes bug when focusing diff view from non-active repo. refactor to make more readable - const item = this.props.workspace.getActivePaneItem(); - const isDiffViewItem = item.getRealItem && item.getRealItem() instanceof FilePatchController; - if (isDiffViewItem) { - this.quietlySelectItem(item.getFilePath(), item.getStagingStatus()); - } else { + if (this.activeFilePatch) { + this.quietlySelectItem(this.activeFilePatch.getFilePath(), this.activeFilePatch.getStagingStatus()); + } else { + const isRepoSame = oldProps.workingDirectoryPath === this.props.workingDirectoryPath; + const selectionsPresent = previouslySelectedItems.size > 0 && this.selection.getSelectedItems() > 0; // ignore when data has not yet been fetched + const selectionChanged = selectionsPresent && !isEqual(previouslySelectedItems, this.selection.getSelectedItems()); + if (isRepoSame && selectionChanged) { this.debouncedDidChangeSelectedItem(); } } From e0627d2c23986f173c6d782a8de4551ba0f32435 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Sun, 18 Feb 2018 16:57:01 -0800 Subject: [PATCH 0245/5882] :art: comments --- lib/views/staging-view.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/views/staging-view.js b/lib/views/staging-view.js index 23643f731f..01170b5b93 100644 --- a/lib/views/staging-view.js +++ b/lib/views/staging-view.js @@ -404,7 +404,7 @@ export default class StagingView { } else { const panesWithStaleItemsToUpdate = this.getPanesWithStalePendingFilePatchItem(); if (panesWithStaleItemsToUpdate.length > 0) { - // Staging event caused selection to change, so we need to update all old diff views to reflect new selection + // Update stale items to reflect new selection await Promise.all(panesWithStaleItemsToUpdate.map(async pane => { await this.showFilePatchItem(selectedItem.filePath, this.selection.getActiveListKey(), { activate: false, @@ -429,7 +429,7 @@ export default class StagingView { } getPanesWithStalePendingFilePatchItem() { - // "stale" meaning there is no longer a changed file associated with it due to contents being fully staged/unstaged + // "stale" meaning there is no longer a changed file associated with it due to changes being fully staged/unstaged/stashed/deleted/etc return this.props.workspace.getPanes().filter(pane => { const pendingItem = pane.getPendingItem(); if (!pendingItem) { return false; } From 9a7b2b435d94828380fa30db49c01da030394244 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Sun, 18 Feb 2018 16:59:26 -0800 Subject: [PATCH 0246/5882] Test for multiple stale pending items --- test/views/staging-view.test.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/test/views/staging-view.test.js b/test/views/staging-view.test.js index ac4220b3fb..76e3e728ec 100644 --- a/test/views/staging-view.test.js +++ b/test/views/staging-view.test.js @@ -359,10 +359,13 @@ describe('StagingView', function() { await view.selectNext(); assert.isFalse(showFilePatchItem.called); - getPanesWithStalePendingFilePatchItem.returns(['pending-file-patch-item']); + getPanesWithStalePendingFilePatchItem.returns(['item1', 'item2']); await view.selectPrevious(); + assert.isTrue(showFilePatchItem.calledTwice); assert.isTrue(showFilePatchItem.calledWith(filePatches[0].filePath)); + showFilePatchItem.reset(); await view.selectNext(); + assert.isTrue(showFilePatchItem.calledTwice); assert.isTrue(showFilePatchItem.calledWith(filePatches[1].filePath)); view.element.remove(); @@ -427,11 +430,14 @@ describe('StagingView', function() { await view.selectNext(); assert.isFalse(showFilePatchItem.called); - getPanesWithStalePendingFilePatchItem.returns(['pending-file-patch-item']); + getPanesWithStalePendingFilePatchItem.returns(['item1', 'item2', 'item3']); await view.selectPrevious(); await assert.async.isTrue(showFilePatchItem.calledWith(filePatches[0].filePath)); + assert.isTrue(showFilePatchItem.calledThrice); + showFilePatchItem.reset(); await view.selectNext(); await assert.async.isTrue(showFilePatchItem.calledWith(filePatches[1].filePath)); + assert.isTrue(showFilePatchItem.calledThrice); view.element.remove(); }); From dda105abfffea6db4258aa8930b0a170ffe1b585 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Sun, 18 Feb 2018 17:21:56 -0800 Subject: [PATCH 0247/5882] :shirt: --- lib/views/staging-view.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/views/staging-view.js b/lib/views/staging-view.js index 01170b5b93..d40b1d95ab 100644 --- a/lib/views/staging-view.js +++ b/lib/views/staging-view.js @@ -139,6 +139,7 @@ export default class StagingView { conflicts: this.props.mergeConflicts || [], staged: this.props.stagedChanges, }); + const currentlySelectedItems = this.selection.getSelectedItems(); if (this.props.resolutionProgress !== oldProps.resolutionProgress) { await this.resolutionProgressObserver.setActiveModel(this.props.resolutionProgress); @@ -148,8 +149,9 @@ export default class StagingView { this.quietlySelectItem(this.activeFilePatch.getFilePath(), this.activeFilePatch.getStagingStatus()); } else { const isRepoSame = oldProps.workingDirectoryPath === this.props.workingDirectoryPath; - const selectionsPresent = previouslySelectedItems.size > 0 && this.selection.getSelectedItems() > 0; // ignore when data has not yet been fetched - const selectionChanged = selectionsPresent && !isEqual(previouslySelectedItems, this.selection.getSelectedItems()); + const selectionsPresent = previouslySelectedItems.size > 0 && currentlySelectedItems > 0; + // ignore when data has not yet been fetched and no selection is present + const selectionChanged = selectionsPresent && !isEqual(previouslySelectedItems, currentlySelectedItems); if (isRepoSame && selectionChanged) { this.debouncedDidChangeSelectedItem(); } @@ -429,7 +431,8 @@ export default class StagingView { } getPanesWithStalePendingFilePatchItem() { - // "stale" meaning there is no longer a changed file associated with it due to changes being fully staged/unstaged/stashed/deleted/etc + // "stale" meaning there is no longer a changed file associated with item + // due to changes being fully staged/unstaged/stashed/deleted/etc return this.props.workspace.getPanes().filter(pane => { const pendingItem = pane.getPendingItem(); if (!pendingItem) { return false; } From 3faf87bbb545d6595f7de0f0359671e990d0ed76 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 19 Feb 2018 11:25:28 -0800 Subject: [PATCH 0248/5882] Disable filesystem watchers rapidly changing project paths --- test/github-package.test.js | 6 ++++-- test/helpers.js | 17 +++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/test/github-package.test.js b/test/github-package.test.js index f9814b4458..f0ab69907a 100644 --- a/test/github-package.test.js +++ b/test/github-package.test.js @@ -3,7 +3,7 @@ import path from 'path'; import temp from 'temp'; import until from 'test-until'; -import {cloneRepository} from './helpers'; +import {cloneRepository, disableFilesystemWatchers} from './helpers'; import {writeFile, deleteFileOrFolder, fileExists, getTempDir, realPath} from '../lib/helpers'; import GithubPackage from '../lib/github-package'; @@ -12,9 +12,11 @@ describe('GithubPackage', function() { let getLoadSettings, configDirPath, deserializers; let githubPackage, contextPool; - beforeEach(function() { + beforeEach(async function() { console.log('be: 1'); atomEnv = global.buildAtomEnvironment(); + await disableFilesystemWatchers(atomEnv) + workspace = atomEnv.workspace; project = atomEnv.project; commandRegistry = atomEnv.commands; diff --git a/test/helpers.js b/test/helpers.js index 74399ce300..cb2332d025 100644 --- a/test/helpers.js +++ b/test/helpers.js @@ -12,6 +12,7 @@ import WorkerManager from '../lib/worker-manager'; import ContextMenuInterceptor from '../lib/context-menu-interceptor'; import getRepoPipelineManager from '../lib/get-repo-pipeline-manager'; import {realPath} from '../lib/helpers'; +import {Directory} from 'atom'; export {toGitPathSep} from '../lib/helpers'; @@ -193,6 +194,22 @@ export function isProcessAlive(pid) { return alive; } +class UnwatchedDirectory extends Directory { + onDidChangeFiles (callback) { + return {dispose: () => {}} + } +} + +export async function disableFilesystemWatchers (atomEnv) { + atomEnv.packages.serviceHub.provide('atom.directory-provider', '0.1.0', { + directoryForURISync (uri) { + return new UnwatchedDirectory(uri) + } + }) + + await until('directoryProvider is available', () => atom.project.directoryProviders.length > 0) +} + // eslint-disable-next-line jasmine/no-global-setup beforeEach(function() { global.sinon = sinon.sandbox.create(); From 3b69f6f0bc6e860b79d69009fd7c6923efe2146b Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 19 Feb 2018 11:32:52 -0800 Subject: [PATCH 0249/5882] `until` works better when you import it --- test/helpers.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/helpers.js b/test/helpers.js index cb2332d025..a429c84953 100644 --- a/test/helpers.js +++ b/test/helpers.js @@ -1,6 +1,7 @@ import fs from 'fs-extra'; import path from 'path'; import temp from 'temp'; +import until from 'test-until'; import React from 'react'; import ReactDom from 'react-dom'; From 86f963b17b8052c51b77e0e3e35912bd8898a82e Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 19 Feb 2018 11:45:50 -0800 Subject: [PATCH 0250/5882] Wait for atomEnv, not atom --- test/helpers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/helpers.js b/test/helpers.js index a429c84953..1abc6bb432 100644 --- a/test/helpers.js +++ b/test/helpers.js @@ -208,7 +208,7 @@ export async function disableFilesystemWatchers (atomEnv) { } }) - await until('directoryProvider is available', () => atom.project.directoryProviders.length > 0) + await until('directoryProvider is available', () => atomEnv.project.directoryProviders.length > 0) } // eslint-disable-next-line jasmine/no-global-setup From 4aad88a8ae825481ef86a737f2f232194ca196ed Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 19 Feb 2018 12:14:28 -0800 Subject: [PATCH 0251/5882] That isn't actually asynchronous --- test/github-package.test.js | 4 ++-- test/helpers.js | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/test/github-package.test.js b/test/github-package.test.js index f0ab69907a..2000b14de2 100644 --- a/test/github-package.test.js +++ b/test/github-package.test.js @@ -12,10 +12,10 @@ describe('GithubPackage', function() { let getLoadSettings, configDirPath, deserializers; let githubPackage, contextPool; - beforeEach(async function() { + beforeEach(function() { console.log('be: 1'); atomEnv = global.buildAtomEnvironment(); - await disableFilesystemWatchers(atomEnv) + disableFilesystemWatchers(atomEnv) workspace = atomEnv.workspace; project = atomEnv.project; diff --git a/test/helpers.js b/test/helpers.js index 1abc6bb432..96451b8666 100644 --- a/test/helpers.js +++ b/test/helpers.js @@ -201,14 +201,12 @@ class UnwatchedDirectory extends Directory { } } -export async function disableFilesystemWatchers (atomEnv) { +export function disableFilesystemWatchers (atomEnv) { atomEnv.packages.serviceHub.provide('atom.directory-provider', '0.1.0', { directoryForURISync (uri) { return new UnwatchedDirectory(uri) } }) - - await until('directoryProvider is available', () => atomEnv.project.directoryProviders.length > 0) } // eslint-disable-next-line jasmine/no-global-setup From 1117949aa57119543f7f53e3a81c74a3aacb2294 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 19 Feb 2018 12:30:21 -0800 Subject: [PATCH 0252/5882] Revert "That isn't actually asynchronous" This reverts commit 4aad88a8ae825481ef86a737f2f232194ca196ed. --- test/github-package.test.js | 4 ++-- test/helpers.js | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/test/github-package.test.js b/test/github-package.test.js index 2000b14de2..f0ab69907a 100644 --- a/test/github-package.test.js +++ b/test/github-package.test.js @@ -12,10 +12,10 @@ describe('GithubPackage', function() { let getLoadSettings, configDirPath, deserializers; let githubPackage, contextPool; - beforeEach(function() { + beforeEach(async function() { console.log('be: 1'); atomEnv = global.buildAtomEnvironment(); - disableFilesystemWatchers(atomEnv) + await disableFilesystemWatchers(atomEnv) workspace = atomEnv.workspace; project = atomEnv.project; diff --git a/test/helpers.js b/test/helpers.js index 96451b8666..1abc6bb432 100644 --- a/test/helpers.js +++ b/test/helpers.js @@ -201,12 +201,14 @@ class UnwatchedDirectory extends Directory { } } -export function disableFilesystemWatchers (atomEnv) { +export async function disableFilesystemWatchers (atomEnv) { atomEnv.packages.serviceHub.provide('atom.directory-provider', '0.1.0', { directoryForURISync (uri) { return new UnwatchedDirectory(uri) } }) + + await until('directoryProvider is available', () => atomEnv.project.directoryProviders.length > 0) } // eslint-disable-next-line jasmine/no-global-setup From 04baea1e238d12374996f18b6d50bcd009e507a0 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 19 Feb 2018 12:40:22 -0800 Subject: [PATCH 0253/5882] Ensure FileSystemChangeObservers are destroyed on test teardown --- .../file-system-change-observer.test.js | 54 ++++++++++--------- 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/test/models/file-system-change-observer.test.js b/test/models/file-system-change-observer.test.js index 1d26b5cc3e..10c290f8eb 100644 --- a/test/models/file-system-change-observer.test.js +++ b/test/models/file-system-change-observer.test.js @@ -6,13 +6,29 @@ import {cloneRepository, buildRepository, setUpLocalAndRemoteRepositories} from import FileSystemChangeObserver from '../../lib/models/file-system-change-observer'; describe('FileSystemChangeObserver', function() { + let observer, changeSpy; + + beforeEach(function() { + changeSpy = sinon.spy(); + }); + + function createObserver(repository) { + observer = new FileSystemChangeObserver(repository); + observer.onDidChange(changeSpy); + return observer; + } + + afterEach(async function() { + if (observer) { + await observer.destroy(); + } + }); + it('emits an event when a project file is modified, created, or deleted', async function() { const workdirPath = await cloneRepository('three-files'); const repository = await buildRepository(workdirPath); - const changeSpy = sinon.spy(); - const changeObserver = new FileSystemChangeObserver(repository); - changeObserver.onDidChange(changeSpy); - await changeObserver.start(); + observer = createObserver(repository) + await observer.start(); fs.writeFileSync(path.join(workdirPath, 'a.txt'), 'a change\n'); await assert.async.isTrue(changeSpy.called); @@ -29,10 +45,8 @@ describe('FileSystemChangeObserver', function() { it('emits an event when a file is staged or unstaged', async function() { const workdirPath = await cloneRepository('three-files'); const repository = await buildRepository(workdirPath); - const changeSpy = sinon.spy(); - const changeObserver = new FileSystemChangeObserver(repository); - changeObserver.onDidChange(changeSpy); - await changeObserver.start(); + observer = createObserver(repository); + await observer.start(); fs.writeFileSync(path.join(workdirPath, 'a.txt'), 'a change\n'); await repository.git.exec(['add', 'a.txt']); @@ -46,10 +60,8 @@ describe('FileSystemChangeObserver', function() { it('emits an event when a branch is checked out', async function() { const workdirPath = await cloneRepository('three-files'); const repository = await buildRepository(workdirPath); - const changeSpy = sinon.spy(); - const changeObserver = new FileSystemChangeObserver(repository); - changeObserver.onDidChange(changeSpy); - await changeObserver.start(); + observer = createObserver(repository); + await observer.start(); await repository.git.exec(['checkout', '-b', 'new-branch']); await assert.async.isTrue(changeSpy.called); @@ -58,10 +70,8 @@ describe('FileSystemChangeObserver', function() { it('emits an event when commits are pushed', async function() { const {localRepoPath} = await setUpLocalAndRemoteRepositories(); const repository = await buildRepository(localRepoPath); - const changeSpy = sinon.spy(); - const changeObserver = new FileSystemChangeObserver(repository); - changeObserver.onDidChange(changeSpy); - await changeObserver.start(); + observer = createObserver(repository); + await observer.start(); await repository.git.exec(['commit', '--allow-empty', '-m', 'new commit']); @@ -73,10 +83,8 @@ describe('FileSystemChangeObserver', function() { it('emits an event when a new tracking branch is added after pushing', async function() { const {localRepoPath} = await setUpLocalAndRemoteRepositories(); const repository = await buildRepository(localRepoPath); - const changeSpy = sinon.spy(); - const changeObserver = new FileSystemChangeObserver(repository); - changeObserver.onDidChange(changeSpy); - await changeObserver.start(); + observer = createObserver(repository); + await observer.start(); await repository.git.exec(['checkout', '-b', 'new-branch']); @@ -88,10 +96,8 @@ describe('FileSystemChangeObserver', function() { it('emits an event when commits have been fetched', async function() { const {localRepoPath} = await setUpLocalAndRemoteRepositories({remoteAhead: true}); const repository = await buildRepository(localRepoPath); - const changeSpy = sinon.spy(); - const changeObserver = new FileSystemChangeObserver(repository); - changeObserver.onDidChange(changeSpy); - await changeObserver.start(); + observer = createObserver(repository); + await observer.start(); await repository.git.exec(['fetch', 'origin', 'master']); await assert.async.isTrue(changeSpy.called); From 7a85cb6051b142015623e2f4f70678bf6be25157 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 19 Feb 2018 14:28:25 -0800 Subject: [PATCH 0254/5882] Use watchPath in WorkspaceChangeObserver --- lib/models/workspace-change-observer.js | 97 +++++++++---------- test/models/workspace-change-observer.test.js | 47 +++++---- 2 files changed, 72 insertions(+), 72 deletions(-) diff --git a/lib/models/workspace-change-observer.js b/lib/models/workspace-change-observer.js index f47a99d574..e62860e9d2 100644 --- a/lib/models/workspace-change-observer.js +++ b/lib/models/workspace-change-observer.js @@ -1,20 +1,13 @@ import path from 'path'; import {CompositeDisposable, Disposable, Emitter} from 'event-kit'; -import nsfw from 'nsfw'; +import {watchPath} from 'atom'; import {autobind} from 'core-decorators'; import EventLogger from './event-logger'; export const FOCUS = Symbol('focus'); -const actionText = new Map([ - [nsfw.actions.CREATED, 'created'], - [nsfw.actions.DELETED, 'deleted'], - [nsfw.actions.MODIFIED, 'modified'], - [nsfw.actions.RENAMED, 'renamed'], -]); - export default class WorkspaceChangeObserver { constructor(window, workspace, repository) { this.window = window; @@ -79,54 +72,56 @@ export default class WorkspaceChangeObserver { async watchActiveRepositoryGitDirectory() { const repository = this.getRepository(); const gitDirectoryPath = repository.getGitDirectoryPath(); - if (repository) { - this.currentFileWatcher = await nsfw( - gitDirectoryPath, - events => { - const filteredEvents = events.filter(e => { - return ['config', 'index', 'HEAD', 'MERGE_HEAD'].includes(e.file || e.newFile) || - e.directory.includes(path.join('.git', 'refs')); - }).map(event => { - const payload = { - action: actionText.get(event.action) || `(Unknown action: ${event.action})`, - path: path.join(event.directory, event.file || event.newFile), - }; - - if (event.oldFile) { - payload.oldPath = path.join(event.directory, event.oldFile); - } - - return payload; - }); - - if (filteredEvents.length) { - this.logger.showEvents(filteredEvents); - this.didChange(filteredEvents); - const workdirOrHeadEvent = filteredEvents.filter(e => !['config', 'index'].includes(path.basename(e.path))); - if (workdirOrHeadEvent) { - this.logger.showWorkdirOrHeadEvents(); - this.didChangeWorkdirOrHead(); - } - } - }, - { - debounceMS: 100, - errorCallback: errors => { - const workingDirectory = repository.getWorkingDirectoryPath(); - // eslint-disable-next-line no-console - console.warn(`Error in WorkspaceChangeObserver in ${workingDirectory}:`, errors); - this.stopCurrentFileWatcher(); - }, - }, - ); - await this.currentFileWatcher.start(); - this.logger.showStarted(gitDirectoryPath, 'workspace emulated'); + + const basenamesOfInterest = ['config', 'index', 'HEAD', 'MERGE_HEAD']; + const workdirOrHeadBasenames = ['config', 'index']; + + const eventPaths = event => { + const ps = [event.path]; + if (event.oldPath) { ps.push(event.oldPath); } + return ps; } + + const acceptEvent = event => { + return eventPaths(event).some(eventPath => { + return basenamesOfInterest.includes(path.basename(eventPath)) || + path.dirname(eventPath).includes(path.join('.git', 'refs')); + }) + }; + + const isWorkdirOrHeadEvent = event => { + return eventPaths(event).some(eventPath => workdirOrHeadBasenames.includes(path.basename(eventPath))); + }; + + console.log(`about to watch ${gitDirectoryPath}`) + this.currentFileWatcher = await watchPath(gitDirectoryPath, {}, events => { + const filteredEvents = events.filter(acceptEvent); + console.log({events, filteredEvents}) + + if (filteredEvents.length) { + this.logger.showEvents(filteredEvents); + this.didChange(filteredEvents); + if (filteredEvents.some(isWorkdirOrHeadEvent)) { + this.logger.showWorkdirOrHeadEvents(); + this.didChangeWorkdirOrHead(); + } + } + }) + console.log('watcher started', {currentFileWatcher: this.currentFileWatcher}) + + this.currentFileWatcher.onDidError(error => { + const workingDirectory = repository.getWorkingDirectoryPath(); + // eslint-disable-next-line no-console + console.warn(`Error in WorkspaceChangeObserver in ${workingDirectory}:`, errors); + this.stopCurrentFileWatcher(); + }); + + this.logger.showStarted(gitDirectoryPath, 'workspace emulated'); } async stopCurrentFileWatcher() { if (this.currentFileWatcher) { - await this.currentFileWatcher.stop(); + this.currentFileWatcher.dispose(); this.currentFileWatcher = null; this.logger.showStopped(); } diff --git a/test/models/workspace-change-observer.test.js b/test/models/workspace-change-observer.test.js index 733b7a0e23..50a521b167 100644 --- a/test/models/workspace-change-observer.test.js +++ b/test/models/workspace-change-observer.test.js @@ -8,29 +8,37 @@ import {writeFile} from '../../lib/helpers'; import WorkspaceChangeObserver from '../../lib/models/workspace-change-observer'; describe('WorkspaceChangeObserver', function() { - let atomEnv, workspace; + let atomEnv, workspace, observer, changeSpy; beforeEach(function() { atomEnv = global.buildAtomEnvironment(); + atomEnv.config.set('core.fileSystemWatcher', 'native') workspace = atomEnv.workspace; + changeSpy = sinon.spy(); }); - afterEach(function() { + function createObserver(repository) { + observer = new WorkspaceChangeObserver(window, workspace, repository); + observer.onDidChange(changeSpy); + return observer; + } + + afterEach(async function() { + if (observer) { + await observer.destroy(); + } atomEnv.destroy(); }); it('emits a change event when the window is focused', async function() { const workdirPath = await cloneRepository('three-files'); const repository = await buildRepository(workdirPath); - - const changeSpy = sinon.spy(); - const changeObserver = new WorkspaceChangeObserver(window, workspace, repository); - changeObserver.onDidChange(changeSpy); + createObserver(repository); window.dispatchEvent(new FocusEvent('focus')); assert.isFalse(changeSpy.called); - await changeObserver.start(); + await observer.start(); window.dispatchEvent(new FocusEvent('focus')); await until(() => changeSpy.calledOnce); }); @@ -38,13 +46,14 @@ describe('WorkspaceChangeObserver', function() { it('emits a change event when a staging action takes place', async function() { const workdirPath = await cloneRepository('three-files'); const repository = await buildRepository(workdirPath); - const changeSpy = sinon.spy(); - const changeObserver = new WorkspaceChangeObserver(window, workspace, repository); - changeObserver.onDidChange(changeSpy); - await changeObserver.start(); + createObserver(repository); + await observer.start(); + console.log(`about to change file in ${workdirPath}`) await writeFile(path.join(workdirPath, 'a.txt'), 'change'); + console.log('about to stage file') await repository.stageFiles(['a.txt']); + console.log('file staged') await assert.async.isTrue(changeSpy.called); }); @@ -54,10 +63,8 @@ describe('WorkspaceChangeObserver', function() { const repository = await buildRepository(workdirPath); const editor = await workspace.open(path.join(workdirPath, 'a.txt')); - const changeSpy = sinon.spy(); - const changeObserver = new WorkspaceChangeObserver(window, workspace, repository); - changeObserver.onDidChange(changeSpy); - await changeObserver.start(); + createObserver(repository); + await observer.start(); editor.setText('change'); await editor.save(); @@ -78,10 +85,8 @@ describe('WorkspaceChangeObserver', function() { const repository = await buildRepository(workdirPath); const editor = await workspace.open(path.join(workdirPath, 'a.txt')); - const changeSpy = sinon.spy(); - const changeObserver = new WorkspaceChangeObserver(window, workspace, repository); - changeObserver.onDidChange(changeSpy); - await changeObserver.start(); + createObserver(repository); + await observer.start(); editor.getBuffer().setPath(path.join(workdirPath, 'renamed-path.txt')); @@ -100,8 +105,8 @@ describe('WorkspaceChangeObserver', function() { const repository = await buildRepository(workdirPath); const editor = await workspace.open(); - const changeObserver = new WorkspaceChangeObserver(window, workspace, repository); - await changeObserver.start(); + createObserver(repository); + await observer.start(); assert.doesNotThrow(() => editor.destroy()); }); From 96c1cf9df49837e64807636d0b67bb0c7845f13d Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 19 Feb 2018 15:09:28 -0800 Subject: [PATCH 0255/5882] :shirt: :shirt: :shirt: --- .eslintrc | 3 +- jsconfig.json | 5 + lib/github-package.js | 74 ------ lib/helpers.js | 11 +- lib/models/workdir-cache.js | 38 +-- lib/models/workspace-change-observer.js | 14 +- package-lock.json | 243 +++++++++++++++++- package.json | 1 - test/github-package.test.js | 72 +----- test/helpers.js | 18 +- .../file-system-change-observer.test.js | 2 +- test/models/repository.test.js | 6 +- test/models/workspace-change-observer.test.js | 5 +- test/runner.js | 3 - 14 files changed, 272 insertions(+), 223 deletions(-) create mode 100644 jsconfig.json diff --git a/.eslintrc b/.eslintrc index e5ba3dca1b..850db65d1e 100644 --- a/.eslintrc +++ b/.eslintrc @@ -3,7 +3,8 @@ "parser": "babel-eslint", "parserOptions": { "ecmaFeatures": { - "jsx": true + "jsx": true, + "experimentalDecorators": true, } }, "settings": { diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 0000000000..7d96cf7350 --- /dev/null +++ b/jsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "experimentalDecorators": true + } +} diff --git a/lib/github-package.js b/lib/github-package.js index e3787e3278..73ef2a558a 100644 --- a/lib/github-package.js +++ b/lib/github-package.js @@ -23,25 +23,6 @@ import AsyncQueue from './async-queue'; import WorkerManager from './worker-manager'; import getRepoPipelineManager from './get-repo-pipeline-manager'; -function logFn(target, key, descriptor) { - const orig = descriptor.value; - descriptor.value = function(...args) { - let name; - if (target && target.name) { - name = `${target.name}.${key}`; - } else if (this.constructor && this.constructor.name) { - name = `${this.constructor.name}#${key}`; - } else { - name = `#${key}`; - } - console.log(`> START: ${name}`); - const value = orig.apply(this, args); - console.log(`> END: ${name}`); - return value; - }; -} - - const defaultState = { }; @@ -105,7 +86,6 @@ export default class GithubPackage { this.filePatchItems = []; } - @logFn setupYardstick() { const stagingSeries = ['stageLine', 'stageHunk', 'unstageLine', 'unstageHunk']; @@ -159,7 +139,6 @@ export default class GithubPackage { ); } - @logFn async activate(state = {}) { this.savedState = {...defaultState, ...state}; @@ -271,7 +250,6 @@ export default class GithubPackage { this.rerender(); } - @logFn serialize() { const activeRepository = this.getActiveRepository(); const activeRepositoryPath = activeRepository ? activeRepository.getWorkingDirectoryPath() : null; @@ -283,19 +261,15 @@ export default class GithubPackage { } rerender(callback) { - console.log('> inside rerender... callback??', !!callback); if (this.workspace.isDestroyed()) { - console.log('workspace is destroyed, returning early'); return; } if (!this.activated) { - console.log('package is not activated, returning early'); return; } if (!this.element) { - console.log('no element, creating it'); this.element = document.createElement('div'); this.subscriptions.add(new Disposable(() => { ReactDom.unmountComponentAtNode(this.element); @@ -303,7 +277,6 @@ export default class GithubPackage { })); } - console.log('finally calling ReactDom.render()'); ReactDom.render( { this.controller = c; }} @@ -332,10 +305,8 @@ export default class GithubPackage { getRepositoryForWorkdir={this.getRepositoryForWorkdir} />, this.element, callback, ); - console.log('Done with rerender!'); } - @logFn async deactivate() { this.subscriptions.dispose(); this.contextPool.clear(); @@ -348,26 +319,22 @@ export default class GithubPackage { } @autobind - @logFn consumeStatusBar(statusBar) { this.statusBar = statusBar; this.rerender(); } @autobind - @logFn createGitTimingsView() { return GitTimingsView.createPaneItem(); } @autobind - @logFn createIssueishPaneItem({uri}) { return IssueishPaneItem.opener(uri); } @autobind - @logFn createDockItemStub({uri}) { let item; switch (uri) { @@ -392,14 +359,12 @@ export default class GithubPackage { return item; } - @logFn createGitTabControllerStub(uri) { return StubItem.create('git-tab-controller', { title: 'Git', }, uri); } - @logFn createGithubTabControllerStub(uri) { return StubItem.create('github-tab-controller', { title: 'GitHub (preview)', @@ -407,7 +372,6 @@ export default class GithubPackage { } @autobind - @logFn createFilePatchControllerStub({uri} = {}) { const item = StubItem.create('git-file-patch-controller', { title: 'Diff', @@ -420,7 +384,6 @@ export default class GithubPackage { } @autobind - @logFn destroyGitTabItem() { if (this.gitTabStubItem) { this.gitTabStubItem.destroy(); @@ -432,7 +395,6 @@ export default class GithubPackage { } @autobind - @logFn destroyGithubTabItem() { if (this.githubTabStubItem) { this.githubTabStubItem.destroy(); @@ -452,7 +414,6 @@ export default class GithubPackage { } @autobind - @logFn async createRepositoryForProjectPath(projectPath) { await mkdirs(projectPath); @@ -468,7 +429,6 @@ export default class GithubPackage { } @autobind - @logFn async cloneRepositoryForProjectPath(remoteUrl, projectPath) { const context = this.contextPool.getContext(projectPath); let repository; @@ -489,39 +449,32 @@ export default class GithubPackage { } @autobind - @logFn getRepositoryForWorkdir(projectPath) { const loadingGuessRepo = Repository.loadingGuess({pipelineManager: this.pipelineManager}); return this.guessedContext ? loadingGuessRepo : this.contextPool.getContext(projectPath).getRepository(); } - @logFn getActiveWorkdir() { return this.activeContext.getWorkingDirectory(); } - @logFn getActiveRepository() { return this.activeContext.getRepository(); } - @logFn getActiveResolutionProgress() { return this.activeContext.getResolutionProgress(); } - @logFn getContextPool() { return this.contextPool; } - @logFn getSwitchboard() { return this.switchboard; } @autobind - @logFn async scheduleActiveContextUpdate(savedState = {}) { this.switchboard.didScheduleActiveContextUpdate(); await this.activeContextQueue.push(this.updateActiveContext.bind(this, savedState), {parallel: false}); @@ -540,7 +493,6 @@ export default class GithubPackage { * First updates the pool of resident contexts to match all git working directories that correspond to open * projects and pane items. */ - @logFn async getNextContext(savedState) { const workdirs = new Set( await Promise.all( @@ -550,52 +502,41 @@ export default class GithubPackage { }), ), ); - console.log('got all the workdirs'); const fromPaneItem = async maybeItem => { const itemPath = pathForPaneItem(maybeItem); if (!itemPath) { - console.log('no item path, returning early'); return {}; } - console.log('getting itemWorkdir from the workdirCache'); const itemWorkdir = await this.workdirCache.find(itemPath); - console.log('maybe adding to workdirs set'); if (itemWorkdir && !this.project.contains(itemPath)) { workdirs.add(itemWorkdir); } - console.log('got an item in fromPaneItem! returning', itemPath, itemWorkdir); return {itemPath, itemWorkdir}; }; - console.log('getting active item'); const active = await fromPaneItem(this.workspace.getCenter().getActivePaneItem()); - console.log('setting contextPool'); this.contextPool.set(workdirs, savedState); if (active.itemPath) { // Prefer an active item - console.log('returning context from contextPool (active.itemPath is truthy)'); return this.contextPool.getContext(active.itemWorkdir || active.itemPath); } if (this.project.getPaths().length === 1) { - console.log('inside getPaths().length === 1'); // Single project const projectPath = this.project.getPaths()[0]; const activeWorkingDir = await this.workdirCache.find(projectPath); - console.log('returning context from contextPool (getPaths().length === 1)'); return this.contextPool.getContext(activeWorkingDir || projectPath); } if (this.project.getPaths().length === 0 && !this.activeContext.getRepository().isUndetermined()) { // No projects. Revert to the absent context unless we've guessed that more projects are on the way. - console.log('no paths and repo is undetermined; return absent'); return WorkdirContext.absent({pipelineManager: this.pipelineManager}); } @@ -603,54 +544,39 @@ export default class GithubPackage { // resident in the pool. const savedWorkingDir = savedState.activeRepositoryPath; if (savedWorkingDir) { - console.log('returning savedWorkingDir'); return this.contextPool.getContext(savedWorkingDir); } - console.log('returning this.activeContext', !!this.activeContext); return this.activeContext; } - @logFn setActiveContext(nextActiveContext) { - console.log('> START setActiveContext'); if (nextActiveContext !== this.activeContext) { if (this.activeContext === this.guessedContext) { this.guessedContext.destroy(); this.guessedContext = null; } - console.log('activeContext set, rerendering'); this.activeContext = nextActiveContext; - console.log('starting rerender'); this.rerender(() => { - console.log('finished rerender, triggering didFinishActiveContextUpdate'); this.switchboard.didFinishContextChangeRender(); this.switchboard.didFinishActiveContextUpdate(); }); } else { - console.log('got into the else case, triggering didFinishActiveContextUpdate'); this.switchboard.didFinishActiveContextUpdate(); } - console.log('> END setActiveContext'); } - @logFn async updateActiveContext(savedState = {}) { if (this.workspace.isDestroyed()) { - console.log('returning early from here'); return; } - console.log('triggering didBeginActiveContextUpdate'); this.switchboard.didBeginActiveContextUpdate(); - console.log('getting the next context'); const nextActiveContext = await this.getNextContext(savedState); - console.log('about to call setActiveContext with', !!nextActiveContext); this.setActiveContext(nextActiveContext); } - @logFn refreshAtomGitRepository(workdir) { const atomGitRepo = this.project.getRepositories().find(repo => { return repo && path.normalize(repo.getWorkingDirectory()) === workdir; diff --git a/lib/helpers.js b/lib/helpers.js index 4cae72a1e3..5bf3e91b59 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -3,7 +3,6 @@ import fs from 'fs-extra'; import os from 'os'; import temp from 'temp'; import {ncp} from 'ncp'; -import realpathNative from 'realpath-native'; import FilePatchController from './controllers/file-patch-controller'; @@ -205,18 +204,16 @@ export function getTempDir(options = {}) { if (options.symlinkOk) { resolve(folder); } else { - resolve(realpathNative(folder)); - // fs.realpath(folder, (realError, rpath) => (realError ? reject(realError) : resolve(rpath))); + fs.realpath(folder, (realError, rpath) => (realError ? reject(realError) : resolve(rpath))); } }); }); } export function realPath(folder) { - return realpathNative(folder); - // return new Promise((resolve, reject) => { - // fs.realpath(folder, (err, rpath) => (err ? reject(err) : resolve(rpath))); - // }); + return new Promise((resolve, reject) => { + fs.realpath(folder, (err, rpath) => (err ? reject(err) : resolve(rpath))); + }); } export function fsStat(absoluteFilePath) { diff --git a/lib/models/workdir-cache.js b/lib/models/workdir-cache.js index 334a5f2ced..70a4cd5bc5 100644 --- a/lib/models/workdir-cache.js +++ b/lib/models/workdir-cache.js @@ -13,34 +13,22 @@ export default class WorkdirCache { } async find(startDir) { - console.log(`> START: find ${startDir}`); try { - console.log('resolving startDir'); - const resolvedDir = await this.resolvePath(startDir); - console.log('checking cached'); + const resolvedDir = await realPath(startDir); const cached = this.known.get(resolvedDir); if (cached !== undefined) { - console.log('RETURNING cached', cached); return cached; } - console.log('walking to root'); const workDir = await this.walkToRoot(resolvedDir); - console.log('clearing known if it is too large..'); if (this.known.size >= this.maxSize) { - console.log('IT IS! clearing'); this.known.clear(); } - console.log('setting', resolvedDir, workDir); this.known.set(resolvedDir, workDir); - console.log('RETURNING', workDir); return workDir; } catch (e) { - console.log('OMG GOT AN ERROR'); - console.log(e); if (e.code === 'ENOENT') { - console.log('ENOENT, returning null'); return null; } @@ -49,7 +37,7 @@ export default class WorkdirCache { } async invalidate(baseDir) { - const resolvedBase = await this.resolvePath(baseDir); + const resolvedBase = await realPath(baseDir); for (const cachedPath of this.known.keys()) { if (cachedPath.startsWith(resolvedBase)) { this.known.delete(cachedPath); @@ -57,28 +45,6 @@ export default class WorkdirCache { } } - async resolvePath(unresolvedPath) { - console.log('START realpath'); - const resolvedPath = await realPath(unresolvedPath); - console.log('RETURNING from realpath'); - return resolvedPath; - // console.log('in resolvePath', unresolvedPath); - // return new Promise((resolve, reject) => { - // console.log('in promise callback, calling realpath'); - // realpath(unresolvedPath, (err, resolved) => { - // console.log('realpath returned'); - // if (err) { - // console.log('Rejecting with err'); - // console.log(err); - // reject(err); - // } else { - // console.log('resolving with', resolved); - // resolve(resolved); - // } - // }); - // }); - } - walkToRoot(initialDir) { return new Promise((resolve, reject) => { let currentDir = initialDir; diff --git a/lib/models/workspace-change-observer.js b/lib/models/workspace-change-observer.js index e62860e9d2..03e50c5f85 100644 --- a/lib/models/workspace-change-observer.js +++ b/lib/models/workspace-change-observer.js @@ -80,23 +80,21 @@ export default class WorkspaceChangeObserver { const ps = [event.path]; if (event.oldPath) { ps.push(event.oldPath); } return ps; - } + }; const acceptEvent = event => { return eventPaths(event).some(eventPath => { return basenamesOfInterest.includes(path.basename(eventPath)) || path.dirname(eventPath).includes(path.join('.git', 'refs')); - }) + }); }; const isWorkdirOrHeadEvent = event => { return eventPaths(event).some(eventPath => workdirOrHeadBasenames.includes(path.basename(eventPath))); }; - console.log(`about to watch ${gitDirectoryPath}`) this.currentFileWatcher = await watchPath(gitDirectoryPath, {}, events => { const filteredEvents = events.filter(acceptEvent); - console.log({events, filteredEvents}) if (filteredEvents.length) { this.logger.showEvents(filteredEvents); @@ -106,25 +104,25 @@ export default class WorkspaceChangeObserver { this.didChangeWorkdirOrHead(); } } - }) - console.log('watcher started', {currentFileWatcher: this.currentFileWatcher}) + }); this.currentFileWatcher.onDidError(error => { const workingDirectory = repository.getWorkingDirectoryPath(); // eslint-disable-next-line no-console - console.warn(`Error in WorkspaceChangeObserver in ${workingDirectory}:`, errors); + console.warn(`Error in WorkspaceChangeObserver in ${workingDirectory}:`, error); this.stopCurrentFileWatcher(); }); this.logger.showStarted(gitDirectoryPath, 'workspace emulated'); } - async stopCurrentFileWatcher() { + stopCurrentFileWatcher() { if (this.currentFileWatcher) { this.currentFileWatcher.dispose(); this.currentFileWatcher = null; this.logger.showStopped(); } + return Promise.resolve(); } activeRepositoryContainsPath(filePath) { diff --git a/package-lock.json b/package-lock.json index 277fc7f0c9..9fe63d5221 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,6 +4,12 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "7zip": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/7zip/-/7zip-0.0.6.tgz", + "integrity": "sha1-nK+xca+CMpSQNTtIFvAzR6oVCjA=", + "dev": true + }, "@binarymuse/relay-compiler": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@binarymuse/relay-compiler/-/relay-compiler-1.2.0.tgz", @@ -131,7 +137,7 @@ "globals": { "version": "9.18.0", "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", - "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==" + "integrity": "sha1-qjiWs+abSH8X4x7SFD1pqOMMLYo=" }, "graphql": { "version": "0.10.5", @@ -308,6 +314,14 @@ "resolved": "https://registry.npmjs.org/atob/-/atob-2.0.3.tgz", "integrity": "sha1-GcenYEc3dEaPILLS0DNyrX1Mv10=" }, + "atom-babel6-transpiler": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/atom-babel6-transpiler/-/atom-babel6-transpiler-1.1.3.tgz", + "integrity": "sha1-1wKxDpDrzx+R4apcSnm5zqmKm6Y=", + "requires": { + "babel-core": "6.26.0" + } + }, "atom-mocha-test-runner": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/atom-mocha-test-runner/-/atom-mocha-test-runner-1.2.0.tgz", @@ -359,6 +373,102 @@ "js-tokens": "3.0.2" } }, + "babel-core": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.26.0.tgz", + "integrity": "sha1-rzL3izGm/O8RnIew/Y2XU/A6C7g=", + "requires": { + "babel-code-frame": "6.26.0", + "babel-generator": "6.26.0", + "babel-helpers": "6.24.1", + "babel-messages": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", + "babel-register": "6.26.0", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0", + "babylon": "6.18.0", + "convert-source-map": "1.5.1", + "debug": "2.6.8", + "json5": "0.5.1", + "lodash": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", + "minimatch": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "path-is-absolute": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "private": "0.1.8", + "slash": "1.0.0", + "source-map": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz" + }, + "dependencies": { + "babel-code-frame": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", + "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", + "requires": { + "chalk": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "esutils": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "js-tokens": "3.0.2" + } + }, + "babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", + "requires": { + "core-js": "2.5.0", + "regenerator-runtime": "0.11.1" + } + }, + "babel-template": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz", + "integrity": "sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=", + "requires": { + "babel-runtime": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0", + "babylon": "6.18.0", + "lodash": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz" + } + }, + "babel-traverse": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", + "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=", + "requires": { + "babel-code-frame": "6.26.0", + "babel-messages": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", + "babel-runtime": "6.26.0", + "babel-types": "6.26.0", + "babylon": "6.18.0", + "debug": "2.6.8", + "globals": "9.18.0", + "invariant": "https://registry.npmjs.org/invariant/-/invariant-2.2.2.tgz", + "lodash": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz" + } + }, + "babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", + "requires": { + "babel-runtime": "6.26.0", + "esutils": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "lodash": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", + "to-fast-properties": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz" + } + }, + "babylon": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", + "integrity": "sha1-ry87iPpvXB5MY00aD46sT1WzleM=" + }, + "regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha1-vgWtf5v30i4Fb5cmzuUBf78Z4uk=" + } + } + }, "babel-eslint": { "version": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-7.2.3.tgz", "integrity": "sha1-sv4tgBJkcPXBlELcdXJTqJdxCCc=", @@ -475,6 +585,15 @@ "babel-types": "6.25.0" } }, + "babel-helpers": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helpers/-/babel-helpers-6.24.1.tgz", + "integrity": "sha1-NHHenK7DiOXIUOWX5Yom3fN2ArI=", + "requires": { + "babel-runtime": "6.25.0", + "babel-template": "6.25.0" + } + }, "babel-messages": { "version": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=", @@ -786,6 +905,36 @@ "babel-preset-flow": "https://registry.npmjs.org/babel-preset-flow/-/babel-preset-flow-6.23.0.tgz" } }, + "babel-register": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-register/-/babel-register-6.26.0.tgz", + "integrity": "sha1-btAhFz4vy0htestFxgCahW9kcHE=", + "requires": { + "babel-core": "6.26.0", + "babel-runtime": "6.26.0", + "core-js": "2.5.0", + "home-or-tmp": "2.0.0", + "lodash": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", + "mkdirp": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "source-map-support": "0.4.18" + }, + "dependencies": { + "babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", + "requires": { + "core-js": "2.5.0", + "regenerator-runtime": "0.11.1" + } + }, + "regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha1-vgWtf5v30i4Fb5cmzuUBf78Z4uk=" + } + } + }, "babel-runtime": { "version": "6.25.0", "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.25.0.tgz", @@ -1153,7 +1302,7 @@ "is-descriptor": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "integrity": "sha1-Nm2CQN3kh8pRgjsaufB6EKeCUco=", "requires": { "is-accessor-descriptor": "0.1.6", "is-data-descriptor": "0.1.4", @@ -1256,6 +1405,11 @@ "integrity": "sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo=", "dev": true }, + "convert-source-map": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.5.1.tgz", + "integrity": "sha1-uCeAl7m8IpNl3lxiz1/K7YtVmeU=" + }, "copy-descriptor": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", @@ -1305,6 +1459,12 @@ } } }, + "cross-unzip": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/cross-unzip/-/cross-unzip-0.0.2.tgz", + "integrity": "sha1-UYO8R6CVWb78+YzEZXlkmZNZNy8=", + "dev": true + }, "cryptiles": { "version": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=", @@ -1704,6 +1864,18 @@ "jsbn": "0.1.1" } }, + "electron-devtools-installer": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/electron-devtools-installer/-/electron-devtools-installer-2.2.3.tgz", + "integrity": "sha1-WLmk7FBzd7xG4JHNQ3FBiODDab4=", + "dev": true, + "requires": { + "7zip": "0.0.6", + "cross-unzip": "0.0.2", + "rimraf": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.1.tgz", + "semver": "5.4.1" + } + }, "encoding": { "version": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=", @@ -2672,6 +2844,15 @@ "version": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-1.2.0.tgz", "integrity": "sha1-qkSM8JhtVcxAdzsXF0t90GbLfPs=" }, + "home-or-tmp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz", + "integrity": "sha1-42w/LSyufXRqhX440Y1fMqeILbg=", + "requires": { + "os-homedir": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "os-tmpdir": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz" + } + }, "hosted-git-info": { "version": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.5.0.tgz", "integrity": "sha1-bWDjSzq7yDEwYsO3mO+NkBoHrzw=" @@ -3122,6 +3303,11 @@ "integrity": "sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE=", "dev": true }, + "json5": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=" + }, "jsonfile": { "version": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", "integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=", @@ -3163,6 +3349,14 @@ "integrity": "sha1-OGchPo3Xm/Ho8jAMDPwe+xgsDfE=", "dev": true }, + "keytar": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/keytar/-/keytar-4.1.0.tgz", + "integrity": "sha1-njkz5InWVt4aho4Sk3CTEwRJidc=", + "requires": { + "nan": "2.5.1" + } + }, "kind-of": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", @@ -4207,8 +4401,7 @@ }, "os-homedir": { "version": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", - "dev": true + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" }, "os-locale": { "version": "1.4.0", @@ -4334,6 +4527,11 @@ "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", "dev": true }, + "private": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", + "integrity": "sha1-I4Hts2ifelPWUxkAYPz4ItLzaP8=" + }, "process": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/process/-/process-0.5.2.tgz", @@ -4815,6 +5013,11 @@ } } }, + "slash": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", + "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=" + }, "slice-ansi": { "version": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz", "integrity": "sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=", @@ -4924,6 +5127,14 @@ "urix": "0.1.0" } }, + "source-map-support": { + "version": "0.4.18", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", + "integrity": "sha1-Aoam3ovkJkEzhZTpfM6nXwosWF8=", + "requires": { + "source-map": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz" + } + }, "source-map-url": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", @@ -4944,6 +5155,14 @@ "version": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz", "integrity": "sha1-yd96NCRZSt5r0RkA1ZZpbcBrrFc=" }, + "split": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", + "integrity": "sha1-YFvZvjA6pZ+zX5Ip++oN3snqB9k=", + "requires": { + "through": "https://registry.npmjs.org/through/-/through-2.3.8.tgz" + } + }, "split-string": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/split-string/-/split-string-2.1.1.tgz", @@ -5148,8 +5367,7 @@ }, "through": { "version": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", - "dev": true + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" }, "tinycolor2": { "version": "1.4.1", @@ -5415,6 +5633,19 @@ "x-is-string": "0.1.0" } }, + "what-the-diff": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/what-the-diff/-/what-the-diff-0.4.0.tgz", + "integrity": "sha1-vIXGP7yWxA1H0p+EMpM0RV18PrY=" + }, + "what-the-status": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/what-the-status/-/what-the-status-1.0.3.tgz", + "integrity": "sha1-lP3NAR/7U6Ijnnb6+NrL78mHdRA=", + "requires": { + "split": "1.0.1" + } + }, "whatwg-fetch": { "version": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz", "integrity": "sha1-nITsLc9oGH/wC8ZOEnS0QhduHIQ=" diff --git a/package.json b/package.json index a9c62329ef..04501fdef9 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,6 @@ "react": "^15.6.1", "react-dom": "^15.6.1", "react-relay": "^1.2.0-rc.1", - "realpath-native": "^1.0.0", "relay-runtime": "^1.2.0-rc.1", "temp": "^0.8.3", "tinycolor2": "^1.4.1", diff --git a/test/github-package.test.js b/test/github-package.test.js index f0ab69907a..d8ffd4a5e8 100644 --- a/test/github-package.test.js +++ b/test/github-package.test.js @@ -13,9 +13,8 @@ describe('GithubPackage', function() { let githubPackage, contextPool; beforeEach(async function() { - console.log('be: 1'); atomEnv = global.buildAtomEnvironment(); - await disableFilesystemWatchers(atomEnv) + await disableFilesystemWatchers(atomEnv); workspace = atomEnv.workspace; project = atomEnv.project; @@ -29,67 +28,30 @@ describe('GithubPackage', function() { grammars = atomEnv.grammars; getLoadSettings = atomEnv.getLoadSettings.bind(atomEnv); configDirPath = path.join(__dirname, 'fixtures', 'atomenv-config'); - console.log('be: 2'); - - config.set('core.fileSystemWatcher', 'atom') githubPackage = new GithubPackage( workspace, project, commandRegistry, notificationManager, tooltips, styles, grammars, confirm, config, deserializers, configDirPath, getLoadSettings, ); - console.log('be: 3'); sinon.stub(githubPackage, 'rerender').callsFake(callback => { - console.log('calling the FAKE rerender!'); if (callback) { - console.log('there is a callback to call... initiiating a setTimeout'); - process.nextTick(() => { - console.log('Calling that callback from the nextTick!'); - callback(); - }); - // const handle = setTimeout(() => { - // console.log('Calling that callback from the setTimeout!'); - // callback(); - // }, 0); - // console.log('the setTimeout call returned a handle:', handle); + process.nextTick(callback); } }); - console.log('be: 4'); contextPool = githubPackage.getContextPool(); - console.log('be: 5'); }); - beforeEach(async function () { - console.log('be2: 0') - await new Promise(resolve => process.nextTick(resolve)) - console.log('be2: 1') - }) - afterEach(async function() { - console.log('ae: 1'); await githubPackage.deactivate(); - console.log('ae: 2'); - - // const promises = [] - // for (let p in atomEnv.project.watcherPromisesByPath) { - // promises.push(atom.project.watcherPromisesByPath[p]) - // } - // console.log('ae: 3'); - // await Promise.all( - // promises.map(p => p.then(w => w.stop())) - // ) atomEnv.destroy(); - console.log('ae: 4'); }); async function contextUpdateAfter(chunk) { - console.log('cae: 1'); const updatePromise = githubPackage.getSwitchboard().getFinishActiveContextUpdatePromise(); - console.log('cae: 2'); await chunk(); - console.log('cae: 3'); return updatePromise; } @@ -174,81 +136,51 @@ describe('GithubPackage', function() { }); it('uses models from preexisting projects', async function() { - console.log('1a'); const [workdirPath1, workdirPath2, nonRepositoryPath] = await Promise.all([ cloneRepository('three-files'), cloneRepository('three-files'), getTempDir(), ]); - console.log('2a'); project.setPaths([workdirPath1, workdirPath2, nonRepositoryPath]); - console.log('3a'); await contextUpdateAfter(() => githubPackage.activate()); - console.log('4a'); - console.log('5a'); assert.isTrue(contextPool.getContext(workdirPath1).isPresent()); - console.log('6a'); assert.isTrue(contextPool.getContext(workdirPath2).isPresent()); - console.log('7a'); assert.isTrue(contextPool.getContext(nonRepositoryPath).isPresent()); - console.log('8a'); assert.isTrue(githubPackage.getActiveRepository().isUndetermined()); - console.log('9a'); }); it('uses an active model from a single preexisting project', async function() { - console.log('1b'); const workdirPath = await cloneRepository('three-files'); - console.log('2b'); project.setPaths([workdirPath]); - console.log('3b'); await contextUpdateAfter(() => githubPackage.activate()); - console.log('4b'); const context = contextPool.getContext(workdirPath); - console.log('5b'); assert.isTrue(context.isPresent()); - console.log('6b'); assert.strictEqual(context.getRepository(), githubPackage.getActiveRepository()); - console.log('7b'); assert.strictEqual(context.getResolutionProgress(), githubPackage.getActiveResolutionProgress()); - console.log('8b'); assert.equal(githubPackage.getActiveWorkdir(), workdirPath); - console.log('9b'); - await new Promise(resolve => setTimeout(resolve, 0)) - console.log('10b') }); it('uses an active model from a preexisting active pane item', async function() { - console.log('1c') const [workdirPath1, workdirPath2] = await Promise.all([ cloneRepository('three-files'), cloneRepository('three-files'), ]); - console.log('2c') project.setPaths([workdirPath1, workdirPath2]); - console.log('3c') await workspace.open(path.join(workdirPath2, 'a.txt')); - console.log('4c') await contextUpdateAfter(() => githubPackage.activate()); - console.log('5c') const context = contextPool.getContext(workdirPath2); - console.log('6c') assert.isTrue(context.isPresent()); - console.log('7c') assert.strictEqual(context.getRepository(), githubPackage.getActiveRepository()); - console.log('8c') assert.strictEqual(context.getResolutionProgress(), githubPackage.getActiveResolutionProgress()); - console.log('9c') assert.equal(githubPackage.getActiveWorkdir(), workdirPath2); - console.log('10c') }); it('uses an active model from serialized state', async function() { diff --git a/test/helpers.js b/test/helpers.js index 1abc6bb432..b2bb21ba99 100644 --- a/test/helpers.js +++ b/test/helpers.js @@ -12,8 +12,8 @@ import GitShellOutStrategy from '../lib/git-shell-out-strategy'; import WorkerManager from '../lib/worker-manager'; import ContextMenuInterceptor from '../lib/context-menu-interceptor'; import getRepoPipelineManager from '../lib/get-repo-pipeline-manager'; -import {realPath} from '../lib/helpers'; import {Directory} from 'atom'; +import {realPath} from '../lib/helpers'; export {toGitPathSep} from '../lib/helpers'; @@ -196,19 +196,19 @@ export function isProcessAlive(pid) { } class UnwatchedDirectory extends Directory { - onDidChangeFiles (callback) { - return {dispose: () => {}} + onDidChangeFiles(callback) { + return {dispose: () => {}}; } } -export async function disableFilesystemWatchers (atomEnv) { +export async function disableFilesystemWatchers(atomEnv) { atomEnv.packages.serviceHub.provide('atom.directory-provider', '0.1.0', { - directoryForURISync (uri) { - return new UnwatchedDirectory(uri) - } - }) + directoryForURISync(uri) { + return new UnwatchedDirectory(uri); + }, + }); - await until('directoryProvider is available', () => atomEnv.project.directoryProviders.length > 0) + await until('directoryProvider is available', () => atomEnv.project.directoryProviders.length > 0); } // eslint-disable-next-line jasmine/no-global-setup diff --git a/test/models/file-system-change-observer.test.js b/test/models/file-system-change-observer.test.js index 10c290f8eb..d04bd1bb36 100644 --- a/test/models/file-system-change-observer.test.js +++ b/test/models/file-system-change-observer.test.js @@ -27,7 +27,7 @@ describe('FileSystemChangeObserver', function() { it('emits an event when a project file is modified, created, or deleted', async function() { const workdirPath = await cloneRepository('three-files'); const repository = await buildRepository(workdirPath); - observer = createObserver(repository) + observer = createObserver(repository); await observer.start(); fs.writeFileSync(path.join(workdirPath, 'a.txt'), 'a change\n'); diff --git a/test/models/repository.test.js b/test/models/repository.test.js index 0a8aef4011..43a10b68c3 100644 --- a/test/models/repository.test.js +++ b/test/models/repository.test.js @@ -457,14 +457,14 @@ describe('Repository', function() { }); describe('commit', function() { - let realPath = ''; + let rp = ''; beforeEach(function() { - realPath = process.env.PATH; + rp = process.env.PATH; }); afterEach(function() { - process.env.PATH = realPath; + process.env.PATH = rp; }); it('creates a commit that contains the staged changes', async function() { diff --git a/test/models/workspace-change-observer.test.js b/test/models/workspace-change-observer.test.js index 50a521b167..516ace163c 100644 --- a/test/models/workspace-change-observer.test.js +++ b/test/models/workspace-change-observer.test.js @@ -12,7 +12,7 @@ describe('WorkspaceChangeObserver', function() { beforeEach(function() { atomEnv = global.buildAtomEnvironment(); - atomEnv.config.set('core.fileSystemWatcher', 'native') + atomEnv.config.set('core.fileSystemWatcher', 'native'); workspace = atomEnv.workspace; changeSpy = sinon.spy(); }); @@ -49,11 +49,8 @@ describe('WorkspaceChangeObserver', function() { createObserver(repository); await observer.start(); - console.log(`about to change file in ${workdirPath}`) await writeFile(path.join(workdirPath, 'a.txt'), 'change'); - console.log('about to stage file') await repository.stageFiles(['a.txt']); - console.log('file staged') await assert.async.isTrue(changeSpy.called); }); diff --git a/test/runner.js b/test/runner.js index 3ad337b774..40d8620a89 100644 --- a/test/runner.js +++ b/test/runner.js @@ -2,12 +2,9 @@ import {createRunner} from 'atom-mocha-test-runner'; import chai from 'chai'; import chaiAsPromised from 'chai-as-promised'; import path from 'path'; -import fs from 'fs-extra'; import until from 'test-until'; -fs.writeFileSync('mocha.pid', process.pid) - chai.use(chaiAsPromised); global.assert = chai.assert; From 8490904ea70cc4c18481ec0ae4de811b6ee506dc Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 19 Feb 2018 15:18:40 -0800 Subject: [PATCH 0256/5882] :fire: nsfw backend because watchPath is in stable :tada: --- lib/models/file-system-change-observer.js | 64 ++------------------- package-lock.json | 68 +---------------------- package.json | 1 - 3 files changed, 7 insertions(+), 126 deletions(-) diff --git a/lib/models/file-system-change-observer.js b/lib/models/file-system-change-observer.js index 130acce4d3..1538ffdd28 100644 --- a/lib/models/file-system-change-observer.js +++ b/lib/models/file-system-change-observer.js @@ -1,22 +1,12 @@ import {Emitter} from 'event-kit'; -import nsfw from 'nsfw'; import {watchPath} from 'atom'; import path from 'path'; import EventLogger from './event-logger'; -const actionText = new Map([ - [nsfw.actions.CREATED, 'created'], - [nsfw.actions.DELETED, 'deleted'], - [nsfw.actions.MODIFIED, 'modified'], - [nsfw.actions.RENAMED, 'renamed'], -]); - export default class FileSystemChangeObserver { constructor(repository) { - this.hasAtomAPI = watchPath !== undefined; - this.emitter = new Emitter(); this.repository = repository; this.logger = new EventLogger('fs watcher'); @@ -79,57 +69,15 @@ export default class FileSystemChangeObserver { } }; - if (this.hasAtomAPI) { - // Atom with watchPath API - - this.currentFileWatcher = await watchPath(this.repository.getWorkingDirectoryPath(), {}, handleEvents); - this.logger.showStarted(this.repository.getWorkingDirectoryPath(), 'Atom watchPath'); - } else { - // Atom pre-watchPath API - - this.currentFileWatcher = await nsfw( - this.repository.getWorkingDirectoryPath(), - events => { - const translated = events.map(event => { - const payload = { - action: actionText.get(event.action) || `(Unknown action: ${event.action})`, - path: path.join(event.directory, event.file || event.newFile), - }; - - if (event.oldFile) { - payload.oldPath = path.join(event.directory, event.oldFile); - } - - return payload; - }); - - handleEvents(translated); - }, - { - debounceMS: 100, - errorCallback: errors => { - const workingDirectory = this.repository.getWorkingDirectoryPath(); - // eslint-disable-next-line no-console - console.warn(`Error in FileSystemChangeObserver in ${workingDirectory}:`, errors); - this.stopCurrentFileWatcher(); - }, - }, - ); - await this.currentFileWatcher.start(); - this.logger.showStarted(this.repository.getWorkingDirectoryPath(), 'direct nsfw'); - } + this.currentFileWatcher = await watchPath(this.repository.getWorkingDirectoryPath(), {}, handleEvents); + this.logger.showStarted(this.repository.getWorkingDirectoryPath(), 'Atom watchPath'); } - async stopCurrentFileWatcher() { + stopCurrentFileWatcher() { if (this.currentFileWatcher) { - if (this.hasAtomAPI) { - this.currentFileWatcher.dispose(); - } else { - const stopPromise = this.currentFileWatcher.stop(); - this.currentFileWatcher = null; - await stopPromise; - this.logger.showStopped(); - } + this.currentFileWatcher.dispose(); + this.logger.showStopped(); } + return Promise.resolve(); } } diff --git a/package-lock.json b/package-lock.json index 9fe63d5221..5f24ff0590 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3365,14 +3365,6 @@ "is-buffer": "1.1.5" } }, - "klaw": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/klaw/-/klaw-1.3.1.tgz", - "integrity": "sha1-QIhDO0azsbolnXh4XY6W9zugJDk=", - "requires": { - "graceful-fs": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz" - } - }, "lazy-cache": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-2.0.2.tgz", @@ -3715,16 +3707,6 @@ "version": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=" }, - "lodash.isinteger": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", - "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=" - }, - "lodash.isundefined": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/lodash.isundefined/-/lodash.isundefined-3.0.1.tgz", - "integrity": "sha1-I+89lTVWUgOmbO/VuDD4SJEa+0g=" - }, "lodash.keys": { "version": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=", @@ -4135,21 +4117,6 @@ "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", "integrity": "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=" }, - "nodegit-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/nodegit-promise/-/nodegit-promise-4.0.0.tgz", - "integrity": "sha1-VyKxhPLfcycWEGSnkdLoQskWezQ=", - "requires": { - "asap": "2.0.6" - }, - "dependencies": { - "asap": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" - } - } - }, "normalize-package-data": { "version": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", "integrity": "sha1-EvlaMH1YNSB1oEkHuErIvpisAS8=", @@ -4160,32 +4127,6 @@ "validate-npm-package-license": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz" } }, - "nsfw": { - "version": "1.0.16", - "resolved": "https://registry.npmjs.org/nsfw/-/nsfw-1.0.16.tgz", - "integrity": "sha1-eLo+f1E7U9FgwiG5AY4LrxCGFMw=", - "requires": { - "fs-extra": "0.26.7", - "lodash.isinteger": "4.0.4", - "lodash.isundefined": "3.0.1", - "nan": "2.5.1", - "promisify-node": "0.3.0" - }, - "dependencies": { - "fs-extra": { - "version": "0.26.7", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.26.7.tgz", - "integrity": "sha1-muH92UiXeY7at20JGM9C0MMYT6k=", - "requires": { - "graceful-fs": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", - "jsonfile": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", - "klaw": "1.3.1", - "path-is-absolute": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "rimraf": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.1.tgz" - } - } - } - }, "nth-check": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.1.tgz", @@ -4556,14 +4497,6 @@ "asap": "2.0.6" } }, - "promisify-node": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/promisify-node/-/promisify-node-0.3.0.tgz", - "integrity": "sha1-tLVaz5D6p9K4uQyjlomQhsAwYM8=", - "requires": { - "nodegit-promise": "4.0.0" - } - }, "prop-types": { "version": "https://registry.npmjs.org/prop-types/-/prop-types-15.5.10.tgz", "integrity": "sha1-J5ffwxJhguOpXj37suiT3ddFYVQ=", @@ -4889,6 +4822,7 @@ "rimraf": { "version": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.1.tgz", "integrity": "sha1-wjOOxkPfeht/5cVPqG9XQopV8z0=", + "dev": true, "requires": { "glob": "7.1.2" } diff --git a/package.json b/package.json index 04501fdef9..1a5912af17 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,6 @@ "moment": "^2.17.1", "multi-list-selection": "^0.1.1", "ncp": "^2.0.0", - "nsfw": "1.0.16", "prop-types": "^15.5.8", "react": "^15.6.1", "react-dom": "^15.6.1", From e19eb5f488e32a55aad9243f40100fb9b8f8550a Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 19 Feb 2018 15:33:57 -0800 Subject: [PATCH 0257/5882] :fire: waitAndDump --- .travis.yml | 1 - script/cibuild | 14 -------------- 2 files changed, 15 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9c56c53610..4002cd1cb8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,7 +16,6 @@ addons: - gnome-keyring - libsecret-1-dev - python-gnomekeyring - - gdb env: global: diff --git a/script/cibuild b/script/cibuild index 03e54f8299..ea6319d795 100755 --- a/script/cibuild +++ b/script/cibuild @@ -1,18 +1,5 @@ #!/bin/bash -function waitAndDump { - # About four minutes - sleep 240 - - local NODE_PID=$(cat mocha.pid) - if [ -z "${NODE_PID}" ]; then - echo "Unable to read node pid from mocha.pid" - exit 1 - fi - - # sudo strace -p ${NODE_PID} -f -} - function updateStatus { STATUS=$1 CTX=$2 @@ -181,7 +168,6 @@ function runTests { if [ $LINT_RESULT -ne 0 ]; then echo ">>> LINTING FAILED! <<< Continuing on to tests..."; fi echo "Running specs..." - waitAndDump & "$ATOM_SCRIPT_PATH" --test test TEST_RESULT=$? From 7f8c5789136c7e9c34215807e8a41a81c3446c6c Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 19 Feb 2018 15:57:07 -0800 Subject: [PATCH 0258/5882] Always use a FileSystemChangeObserver --- lib/models/workdir-context.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/models/workdir-context.js b/lib/models/workdir-context.js index f6baa469e5..86c5f7e230 100644 --- a/lib/models/workdir-context.js +++ b/lib/models/workdir-context.js @@ -116,7 +116,8 @@ export default class WorkdirContext { } useWorkspaceChangeObserver() { - return !!process.env.ATOM_GITHUB_WORKSPACE_OBSERVER || process.platform === 'linux'; + return false; + // return !!process.env.ATOM_GITHUB_WORKSPACE_OBSERVER || process.platform === 'linux'; } // Event subscriptions From 34798e8f6cf53a66542942bd0bddda6fa1067e9f Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 19 Feb 2018 16:59:03 -0800 Subject: [PATCH 0259/5882] Handle rename events in the FileSystemChangeObserver --- lib/models/file-system-change-observer.js | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/lib/models/file-system-change-observer.js b/lib/models/file-system-change-observer.js index 1538ffdd28..dfb860303d 100644 --- a/lib/models/file-system-change-observer.js +++ b/lib/models/file-system-change-observer.js @@ -51,17 +51,27 @@ export default class FileSystemChangeObserver { } async watchRepository() { + const allPaths = event => { + const ps = [event.path]; + if (event.oldPath) { ps.push(event.oldPath); } + return ps; + }; + + const isNonGitFile = event => allPaths(event).some(eventPath => !eventPath.split(path.sep).includes('.git')); + + const isWatchedGitFile = event => allPaths(event).some(eventPath => { + return ['config', 'index', 'HEAD', 'MERGE_HEAD'].includes(path.basename(eventPath)) || + path.dirname(eventPath).includes(path.join('.git', 'refs')); + }); + const handleEvents = events => { - const isNonGitFile = event => !event.path.split(path.sep).includes('.git'); - const isWatchedGitFile = event => { - return ['config', 'index', 'HEAD', 'MERGE_HEAD'].includes(path.basename(event.path)) || - event.path.includes(path.join('.git', 'refs')); - }; const filteredEvents = events.filter(e => isNonGitFile(e) || isWatchedGitFile(e)); if (filteredEvents.length) { this.logger.showEvents(filteredEvents); this.didChange(filteredEvents); - const workdirOrHeadEvent = filteredEvents.find(e => !['config', 'index'].includes(path.basename(e.path))); + const workdirOrHeadEvent = filteredEvents.find(event => { + return allPaths(event).every(eventPath => !['config', 'index'].includes(path.basename(eventPath))); + }); if (workdirOrHeadEvent) { this.logger.showWorkdirOrHeadEvents(); this.didChangeWorkdirOrHead(); From 6fc2877d2a71b0043a57deb6a97d2180664dfbbd Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 19 Feb 2018 17:02:36 -0800 Subject: [PATCH 0260/5882] Question: if we skip just that test, is it fixed or does it recur later? --- test/github-package.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/github-package.test.js b/test/github-package.test.js index d8ffd4a5e8..fd942aa952 100644 --- a/test/github-package.test.js +++ b/test/github-package.test.js @@ -135,7 +135,7 @@ describe('GithubPackage', function() { assert.isTrue(githubPackage.getActiveRepository().isUndetermined()); }); - it('uses models from preexisting projects', async function() { + it.skip('uses models from preexisting projects', async function() { const [workdirPath1, workdirPath2, nonRepositoryPath] = await Promise.all([ cloneRepository('three-files'), cloneRepository('three-files'), From 2a1b818e6bf85b6c41ed4b31d6f0ca8bc016d0cb Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 19 Feb 2018 17:19:49 -0800 Subject: [PATCH 0261/5882] Turn the WorkspaceChangeObserver back on for now --- lib/models/workdir-context.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/models/workdir-context.js b/lib/models/workdir-context.js index 86c5f7e230..f6baa469e5 100644 --- a/lib/models/workdir-context.js +++ b/lib/models/workdir-context.js @@ -116,8 +116,7 @@ export default class WorkdirContext { } useWorkspaceChangeObserver() { - return false; - // return !!process.env.ATOM_GITHUB_WORKSPACE_OBSERVER || process.platform === 'linux'; + return !!process.env.ATOM_GITHUB_WORKSPACE_OBSERVER || process.platform === 'linux'; } // Event subscriptions From 054e5d4ecb7cd9bdf9e1f682cc96689f857a5572 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 19 Feb 2018 17:20:15 -0800 Subject: [PATCH 0262/5882] Build on dev too why not --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 4002cd1cb8..7adf2020ab 100644 --- a/.travis.yml +++ b/.travis.yml @@ -31,6 +31,7 @@ env: matrix: - ATOM_CHANNEL=stable - ATOM_CHANNEL=beta + - ATOM_CHANNEL=dev notifications: email: From 7f6bf45a9829d3cd018439b2f36163cfdb26ed14 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 19 Feb 2018 17:21:54 -0800 Subject: [PATCH 0263/5882] Conclusion: it just locks up later --- test/github-package.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/github-package.test.js b/test/github-package.test.js index fd942aa952..d8ffd4a5e8 100644 --- a/test/github-package.test.js +++ b/test/github-package.test.js @@ -135,7 +135,7 @@ describe('GithubPackage', function() { assert.isTrue(githubPackage.getActiveRepository().isUndetermined()); }); - it.skip('uses models from preexisting projects', async function() { + it('uses models from preexisting projects', async function() { const [workdirPath1, workdirPath2, nonRepositoryPath] = await Promise.all([ cloneRepository('three-files'), cloneRepository('three-files'), From 0423a0cf51093f5b6cc712c03142ca1c8e8182b6 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 19 Feb 2018 17:27:46 -0800 Subject: [PATCH 0264/5882] Dev doesn't have a suffix --- script/cibuild | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/script/cibuild b/script/cibuild index ea6319d795..673ea5cf98 100755 --- a/script/cibuild +++ b/script/cibuild @@ -139,12 +139,12 @@ function runTests { /sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -ac -screen 0 1280x1024x16 export DISPLAY=":99" dpkg-deb -x atom.deb "$HOME/atom" - if [ "$ATOM_CHANNEL" = "stable" ]; then + if [ "$ATOM_CHANNEL" = "beta" ]; then + export ATOM_SCRIPT_NAME="atom-beta" + export APM_SCRIPT_NAME="apm-beta" + else export ATOM_SCRIPT_NAME="atom" export APM_SCRIPT_NAME="apm" - else - export ATOM_SCRIPT_NAME="atom-$ATOM_CHANNEL" - export APM_SCRIPT_NAME="apm-$ATOM_CHANNEL" fi export ATOM_SCRIPT_PATH="$HOME/atom/usr/bin/$ATOM_SCRIPT_NAME" export APM_SCRIPT_PATH="$HOME/atom/usr/bin/$APM_SCRIPT_NAME" From ba04b5329c9ca6de3bd3ec240be5fd435e937372 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 19 Feb 2018 17:37:14 -0800 Subject: [PATCH 0265/5882] Let's see if yielding the event loop dies earlier --- test/github-package.test.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/github-package.test.js b/test/github-package.test.js index d8ffd4a5e8..723e753a37 100644 --- a/test/github-package.test.js +++ b/test/github-package.test.js @@ -135,6 +135,7 @@ describe('GithubPackage', function() { assert.isTrue(githubPackage.getActiveRepository().isUndetermined()); }); + /* eslint-disable no-console */ it('uses models from preexisting projects', async function() { const [workdirPath1, workdirPath2, nonRepositoryPath] = await Promise.all([ cloneRepository('three-files'), @@ -143,14 +144,27 @@ describe('GithubPackage', function() { ]); project.setPaths([workdirPath1, workdirPath2, nonRepositoryPath]); + console.log('000 >'); + await new Promise(resolve => setTimeout(resolve, 10)); + console.log('000 <'); + await contextUpdateAfter(() => githubPackage.activate()); + console.log('111 >'); + await new Promise(resolve => setTimeout(resolve, 10)); + console.log('111 <'); + assert.isTrue(contextPool.getContext(workdirPath1).isPresent()); assert.isTrue(contextPool.getContext(workdirPath2).isPresent()); assert.isTrue(contextPool.getContext(nonRepositoryPath).isPresent()); assert.isTrue(githubPackage.getActiveRepository().isUndetermined()); + + console.log('222 >'); + await new Promise(resolve => setTimeout(resolve, 10)); + console.log('222 <'); }); + /* eslint-enable no-console */ it('uses an active model from a single preexisting project', async function() { const workdirPath = await cloneRepository('three-files'); From a0bea522666093593ce1ab6c1b182caae8922836 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 19 Feb 2018 18:40:06 -0800 Subject: [PATCH 0266/5882] Is it just atom.project.setPaths() for some reason? --- test/github-package.test.js | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/test/github-package.test.js b/test/github-package.test.js index 723e753a37..49658ab603 100644 --- a/test/github-package.test.js +++ b/test/github-package.test.js @@ -144,21 +144,21 @@ describe('GithubPackage', function() { ]); project.setPaths([workdirPath1, workdirPath2, nonRepositoryPath]); - console.log('000 >'); - await new Promise(resolve => setTimeout(resolve, 10)); - console.log('000 <'); - - await contextUpdateAfter(() => githubPackage.activate()); - - console.log('111 >'); - await new Promise(resolve => setTimeout(resolve, 10)); - console.log('111 <'); - - assert.isTrue(contextPool.getContext(workdirPath1).isPresent()); - assert.isTrue(contextPool.getContext(workdirPath2).isPresent()); - assert.isTrue(contextPool.getContext(nonRepositoryPath).isPresent()); - - assert.isTrue(githubPackage.getActiveRepository().isUndetermined()); + // console.log('000 >'); + // await new Promise(resolve => setTimeout(resolve, 10)); + // console.log('000 <'); + // + // await contextUpdateAfter(() => githubPackage.activate()); + // + // console.log('111 >'); + // await new Promise(resolve => setTimeout(resolve, 10)); + // console.log('111 <'); + // + // assert.isTrue(contextPool.getContext(workdirPath1).isPresent()); + // assert.isTrue(contextPool.getContext(workdirPath2).isPresent()); + // assert.isTrue(contextPool.getContext(nonRepositoryPath).isPresent()); + // + // assert.isTrue(githubPackage.getActiveRepository().isUndetermined()); console.log('222 >'); await new Promise(resolve => setTimeout(resolve, 10)); From bae5a19c2e314e510a304675d5f515efad458aa5 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 19 Feb 2018 19:05:41 -0800 Subject: [PATCH 0267/5882] Activate the package again --- test/github-package.test.js | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/test/github-package.test.js b/test/github-package.test.js index 49658ab603..9a25a0e5e2 100644 --- a/test/github-package.test.js +++ b/test/github-package.test.js @@ -144,12 +144,8 @@ describe('GithubPackage', function() { ]); project.setPaths([workdirPath1, workdirPath2, nonRepositoryPath]); - // console.log('000 >'); - // await new Promise(resolve => setTimeout(resolve, 10)); - // console.log('000 <'); - // - // await contextUpdateAfter(() => githubPackage.activate()); - // + await contextUpdateAfter(() => githubPackage.activate()); + // console.log('111 >'); // await new Promise(resolve => setTimeout(resolve, 10)); // console.log('111 <'); @@ -160,9 +156,9 @@ describe('GithubPackage', function() { // // assert.isTrue(githubPackage.getActiveRepository().isUndetermined()); - console.log('222 >'); + console.log('000 >'); await new Promise(resolve => setTimeout(resolve, 10)); - console.log('222 <'); + console.log('000 <'); }); /* eslint-enable no-console */ From 67da5b27fc5e666d656a2d34530d031cb4095f76 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 19 Feb 2018 19:36:20 -0800 Subject: [PATCH 0268/5882] Back out a bunch of diagnostics --- test/github-package.test.js | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/test/github-package.test.js b/test/github-package.test.js index 9a25a0e5e2..05bf6a58da 100644 --- a/test/github-package.test.js +++ b/test/github-package.test.js @@ -146,19 +146,11 @@ describe('GithubPackage', function() { await contextUpdateAfter(() => githubPackage.activate()); - // console.log('111 >'); - // await new Promise(resolve => setTimeout(resolve, 10)); - // console.log('111 <'); - // - // assert.isTrue(contextPool.getContext(workdirPath1).isPresent()); - // assert.isTrue(contextPool.getContext(workdirPath2).isPresent()); - // assert.isTrue(contextPool.getContext(nonRepositoryPath).isPresent()); - // - // assert.isTrue(githubPackage.getActiveRepository().isUndetermined()); - - console.log('000 >'); - await new Promise(resolve => setTimeout(resolve, 10)); - console.log('000 <'); + assert.isTrue(contextPool.getContext(workdirPath1).isPresent()); + assert.isTrue(contextPool.getContext(workdirPath2).isPresent()); + assert.isTrue(contextPool.getContext(nonRepositoryPath).isPresent()); + + assert.isTrue(githubPackage.getActiveRepository().isUndetermined()); }); /* eslint-enable no-console */ From 610196117eb2fca0bf4386c6aa08098316e112c6 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 19 Feb 2018 19:41:07 -0800 Subject: [PATCH 0269/5882] Wait for the observers to start --- test/github-package.test.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/github-package.test.js b/test/github-package.test.js index 05bf6a58da..f621719263 100644 --- a/test/github-package.test.js +++ b/test/github-package.test.js @@ -150,6 +150,12 @@ describe('GithubPackage', function() { assert.isTrue(contextPool.getContext(workdirPath2).isPresent()); assert.isTrue(contextPool.getContext(nonRepositoryPath).isPresent()); + await Promise.all( + [workdirPath1, workdirPath2].map(eachPath => { + return contextPool.getContext(eachPath).getObserverStartedPromise(); + }), + ); + assert.isTrue(githubPackage.getActiveRepository().isUndetermined()); }); /* eslint-enable no-console */ From bb4517763b043295b4299921b517e5ea8e9f0c46 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 19 Feb 2018 19:48:51 -0800 Subject: [PATCH 0270/5882] Use the last two directory segments for better logging --- lib/models/event-logger.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/models/event-logger.js b/lib/models/event-logger.js index 1937cc4d35..289057e108 100644 --- a/lib/models/event-logger.js +++ b/lib/models/event-logger.js @@ -16,7 +16,7 @@ export default class EventLogger { showStarted(directory, implementation) { this.directory = directory; - this.shortDirectory = path.basename(directory); + this.shortDirectory = directory.split(path.sep).slice(-2).join(path.sep); if (!this.isEnabled()) { return; From b8684d02f302cfb15da04bdd6b93041e46f96d79 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 19 Feb 2018 20:00:31 -0800 Subject: [PATCH 0271/5882] Log native watcher stopping --- lib/models/workspace-change-observer.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/lib/models/workspace-change-observer.js b/lib/models/workspace-change-observer.js index 03e50c5f85..359e66170f 100644 --- a/lib/models/workspace-change-observer.js +++ b/lib/models/workspace-change-observer.js @@ -118,6 +118,21 @@ export default class WorkspaceChangeObserver { stopCurrentFileWatcher() { if (this.currentFileWatcher) { + const nw = this.currentFileWatcher.nativeWatcher; + if (nw) { + nw.onWillStop(() => { + // eslint-disable-next-line no-console + console.log(`native watcher ${nw.normalizedPath} about to stop`); + }); + nw.onDidStop(() => { + // eslint-disable-next-line no-console + console.log(`native watcher ${nw.normalizedPath} stopped`); + }); + } else { + // eslint-disable-next-line no-console + console.log('no native watcher'); + } + this.currentFileWatcher.dispose(); this.currentFileWatcher = null; this.logger.showStopped(); From cd5fd92285ecf0b154527690d437c390edcd1914 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 19 Feb 2018 20:10:14 -0800 Subject: [PATCH 0272/5882] Wait, is it actually deactivating the GitHub package now? --- test/github-package.test.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/github-package.test.js b/test/github-package.test.js index f621719263..2ef8d8f13d 100644 --- a/test/github-package.test.js +++ b/test/github-package.test.js @@ -44,7 +44,9 @@ describe('GithubPackage', function() { }); afterEach(async function() { + console.log('about to deactivate github package'); await githubPackage.deactivate(); + console.log('github package deactivated'); atomEnv.destroy(); }); From 51c412ab9b1e4a4f7b8927b87cb0e26adfbec49e Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 19 Feb 2018 20:17:23 -0800 Subject: [PATCH 0273/5882] Undo some speculative work --- test/github-package.test.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/test/github-package.test.js b/test/github-package.test.js index 2ef8d8f13d..05bf6a58da 100644 --- a/test/github-package.test.js +++ b/test/github-package.test.js @@ -44,9 +44,7 @@ describe('GithubPackage', function() { }); afterEach(async function() { - console.log('about to deactivate github package'); await githubPackage.deactivate(); - console.log('github package deactivated'); atomEnv.destroy(); }); @@ -152,12 +150,6 @@ describe('GithubPackage', function() { assert.isTrue(contextPool.getContext(workdirPath2).isPresent()); assert.isTrue(contextPool.getContext(nonRepositoryPath).isPresent()); - await Promise.all( - [workdirPath1, workdirPath2].map(eachPath => { - return contextPool.getContext(eachPath).getObserverStartedPromise(); - }), - ); - assert.isTrue(githubPackage.getActiveRepository().isUndetermined()); }); /* eslint-enable no-console */ From c9a6648ea6e4900b70fcd0dc64492c908a75c6e1 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 19 Feb 2018 20:30:50 -0800 Subject: [PATCH 0274/5882] Let's see if those .git directories are being resolved right --- lib/models/repository-states/loading.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/models/repository-states/loading.js b/lib/models/repository-states/loading.js index 8062fd47e5..9ebcae3b95 100644 --- a/lib/models/repository-states/loading.js +++ b/lib/models/repository-states/loading.js @@ -6,7 +6,9 @@ import State from './state'; */ export default class Loading extends State { async start() { + console.log('resolving .git dir'); // eslint-disable-line no-console const dotGitDir = await this.resolveDotGitDir(); + console.log(`resolved .git dir to ${dotGitDir}`); // eslint-disable-line no-console if (dotGitDir) { this.repository.setGitDirectoryPath(dotGitDir); const history = await this.loadHistoryPayload(); From 8f3f082ab3b774d97958cba481d31fc8d87c4b19 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 19 Feb 2018 20:39:39 -0800 Subject: [PATCH 0275/5882] Log .git directory resolution progress --- lib/git-shell-out-strategy.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/git-shell-out-strategy.js b/lib/git-shell-out-strategy.js index 4d88645da4..d9a6ea2bdd 100644 --- a/lib/git-shell-out-strategy.js +++ b/lib/git-shell-out-strategy.js @@ -344,20 +344,27 @@ export default class GitShellOutStrategy { } } + /* eslint-disable no-console */ async resolveDotGitDir() { try { + console.log(`resolveDotGitDir ${this.workingDir} 0`); await fsStat(this.workingDir); // fails if folder doesn't exist + console.log(`resolveDotGitDir ${this.workingDir} 1`); const output = await this.exec(['rev-parse', '--resolve-git-dir', path.join(this.workingDir, '.git')]); + console.log(`resolveDotGitDir ${this.workingDir} 2 ${output}`); const dotGitDir = output.trim(); + console.log(`resolveDotGitDir ${this.workingDir} 3 ${dotGitDir}`); if (path.isAbsolute(dotGitDir)) { return toNativePathSep(dotGitDir); } else { return toNativePathSep(path.resolve(path.join(this.workingDir, dotGitDir))); } } catch (e) { + console.log(`resolveDotGitDir ${this.workingDir} err`, e); return null; } } + /* eslint-enable no-console */ init() { return this.exec(['init', this.workingDir]); From df53d21e5aabaf9383627fd6135f26e11710237b Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 19 Feb 2018 20:53:28 -0800 Subject: [PATCH 0276/5882] Let's see if this is killing that git process at a bad time? --- lib/github-package.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/github-package.js b/lib/github-package.js index 73ef2a558a..bdd25b39d2 100644 --- a/lib/github-package.js +++ b/lib/github-package.js @@ -310,7 +310,7 @@ export default class GithubPackage { async deactivate() { this.subscriptions.dispose(); this.contextPool.clear(); - WorkerManager.reset(true); + // WorkerManager.reset(true); if (this.guessedContext) { this.guessedContext.destroy(); this.guessedContext = null; From 237763c7c357c6d75e84b5c2659c281f30ca318a Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 19 Feb 2018 20:59:32 -0800 Subject: [PATCH 0277/5882] Okay that breaks other tests so let's try .reset(false) instead --- lib/github-package.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/github-package.js b/lib/github-package.js index bdd25b39d2..fab1215446 100644 --- a/lib/github-package.js +++ b/lib/github-package.js @@ -310,7 +310,7 @@ export default class GithubPackage { async deactivate() { this.subscriptions.dispose(); this.contextPool.clear(); - // WorkerManager.reset(true); + WorkerManager.reset(false); if (this.guessedContext) { this.guessedContext.destroy(); this.guessedContext = null; From 7741afbf415b622634988bf393a30c0320086b6d Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 19 Feb 2018 21:15:34 -0800 Subject: [PATCH 0278/5882] :fire: diagnostic code --- lib/git-shell-out-strategy.js | 5 ----- lib/models/repository-states/loading.js | 2 -- test/github-package.test.js | 2 -- 3 files changed, 9 deletions(-) diff --git a/lib/git-shell-out-strategy.js b/lib/git-shell-out-strategy.js index d9a6ea2bdd..950b471136 100644 --- a/lib/git-shell-out-strategy.js +++ b/lib/git-shell-out-strategy.js @@ -347,20 +347,15 @@ export default class GitShellOutStrategy { /* eslint-disable no-console */ async resolveDotGitDir() { try { - console.log(`resolveDotGitDir ${this.workingDir} 0`); await fsStat(this.workingDir); // fails if folder doesn't exist - console.log(`resolveDotGitDir ${this.workingDir} 1`); const output = await this.exec(['rev-parse', '--resolve-git-dir', path.join(this.workingDir, '.git')]); - console.log(`resolveDotGitDir ${this.workingDir} 2 ${output}`); const dotGitDir = output.trim(); - console.log(`resolveDotGitDir ${this.workingDir} 3 ${dotGitDir}`); if (path.isAbsolute(dotGitDir)) { return toNativePathSep(dotGitDir); } else { return toNativePathSep(path.resolve(path.join(this.workingDir, dotGitDir))); } } catch (e) { - console.log(`resolveDotGitDir ${this.workingDir} err`, e); return null; } } diff --git a/lib/models/repository-states/loading.js b/lib/models/repository-states/loading.js index 9ebcae3b95..8062fd47e5 100644 --- a/lib/models/repository-states/loading.js +++ b/lib/models/repository-states/loading.js @@ -6,9 +6,7 @@ import State from './state'; */ export default class Loading extends State { async start() { - console.log('resolving .git dir'); // eslint-disable-line no-console const dotGitDir = await this.resolveDotGitDir(); - console.log(`resolved .git dir to ${dotGitDir}`); // eslint-disable-line no-console if (dotGitDir) { this.repository.setGitDirectoryPath(dotGitDir); const history = await this.loadHistoryPayload(); diff --git a/test/github-package.test.js b/test/github-package.test.js index 05bf6a58da..d8ffd4a5e8 100644 --- a/test/github-package.test.js +++ b/test/github-package.test.js @@ -135,7 +135,6 @@ describe('GithubPackage', function() { assert.isTrue(githubPackage.getActiveRepository().isUndetermined()); }); - /* eslint-disable no-console */ it('uses models from preexisting projects', async function() { const [workdirPath1, workdirPath2, nonRepositoryPath] = await Promise.all([ cloneRepository('three-files'), @@ -152,7 +151,6 @@ describe('GithubPackage', function() { assert.isTrue(githubPackage.getActiveRepository().isUndetermined()); }); - /* eslint-enable no-console */ it('uses an active model from a single preexisting project', async function() { const workdirPath = await cloneRepository('three-files'); From d5a239cc8bcaba5d3e26dd1d06592b7845b1903d Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 19 Feb 2018 21:18:35 -0800 Subject: [PATCH 0279/5882] :fire: unused no-console guards --- lib/git-shell-out-strategy.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/git-shell-out-strategy.js b/lib/git-shell-out-strategy.js index 950b471136..4d88645da4 100644 --- a/lib/git-shell-out-strategy.js +++ b/lib/git-shell-out-strategy.js @@ -344,7 +344,6 @@ export default class GitShellOutStrategy { } } - /* eslint-disable no-console */ async resolveDotGitDir() { try { await fsStat(this.workingDir); // fails if folder doesn't exist @@ -359,7 +358,6 @@ export default class GitShellOutStrategy { return null; } } - /* eslint-enable no-console */ init() { return this.exec(['init', this.workingDir]); From f908b223da2ce7cecbd19fb87ad40be90641073e Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 19 Feb 2018 21:46:24 -0800 Subject: [PATCH 0280/5882] Missed some logging --- lib/models/workspace-change-observer.js | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/lib/models/workspace-change-observer.js b/lib/models/workspace-change-observer.js index 359e66170f..03e50c5f85 100644 --- a/lib/models/workspace-change-observer.js +++ b/lib/models/workspace-change-observer.js @@ -118,21 +118,6 @@ export default class WorkspaceChangeObserver { stopCurrentFileWatcher() { if (this.currentFileWatcher) { - const nw = this.currentFileWatcher.nativeWatcher; - if (nw) { - nw.onWillStop(() => { - // eslint-disable-next-line no-console - console.log(`native watcher ${nw.normalizedPath} about to stop`); - }); - nw.onDidStop(() => { - // eslint-disable-next-line no-console - console.log(`native watcher ${nw.normalizedPath} stopped`); - }); - } else { - // eslint-disable-next-line no-console - console.log('no native watcher'); - } - this.currentFileWatcher.dispose(); this.currentFileWatcher = null; this.logger.showStopped(); From 2a8e40760755c4a3787dc4d2ab6c25bec950e4dc Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 20 Feb 2018 21:27:08 -0800 Subject: [PATCH 0281/5882] Draft of how-we-work --- docs/how-we-work.md | 131 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 docs/how-we-work.md diff --git a/docs/how-we-work.md b/docs/how-we-work.md new file mode 100644 index 0000000000..8b2b1ff100 --- /dev/null +++ b/docs/how-we-work.md @@ -0,0 +1,131 @@ +# How we work + +:sparkles: _Fabulously_ :sparkles: + +This is an attempt to make explicit the way that the core team plans, designs, and works day to day on the GitHub package. Our goal is to reduce ambiguity and uncertainty and improve communication, while avoiding artificial confinement and ceremony for ceremony's sake. + +Process should serve the developers who use it and not the other way around. This is a live document! As our needs change and as we find that something here isn't bringing us the value we want, we should send pull requests to change it. + +## Kinds of change + +One size does not fit all, and accordingly, we do not prescribe the same amount of rigor for every pull request. These options lay out a spectrum of approaches to be followed for changes of increasing complexity and scope. Not everything will fall neatly into one of these categories; we trust each other's judgement in choosing which is appropriate for any given effort. When in doubt, ask and we can decide together. + +### Minor or cosmetic + +This includes work like typos in comments or documentation, localized work, or rote mechanical changes. + +##### Examples + +* Changing or upgrading dependencies. +* Renaming components. +* Minor refactoring that only touches a file or two. + +##### Process + +1. Isolate work on a feature branch in the `atom/github` repository and open a pull request. Title-only pull requests are fine. If it's _really_ minor, like a one-line diff, committing directly to `master` is also perfectly acceptable. +2. Ensure that our CI remains green across platforms. +3. Merge your own pull request; no code review necessary. + +### Bug fixes + +Addressing unhandled exceptions, lock-ups, or correcting other unintended behavior in established functionality follows this process. For bug fixes that have UX, substantial UI, or package scope implications or tradeoffs, consider following [the new feature RFC process](#new-features) instead, to ensure we have a chance to collect design and community feedback before we proceed with a fix. + +##### Process + +1. Open an issue on `atom/github` describing the bug if there isn't one already. +2. Identify the root cause of the bug and leave a description of it as an issue comment. If necessary, modify the issue body and title to clarify the bug as you go. +3. When you're ready to begin writing the fix, assign the issue to yourself and move it to the "in progress" column on the [short-term roadmap project](https://github.com/atom/github/projects/8). :rainbow: _This signals to the team and to the community that it's actively being addressed, and keeps us from colliding._ +4. Work on a feature branch in the `atom/github` repository and open a pull request. +5. Write a failing test case that demonstrates the bug (or a rationale for why it isn't worth it -- but bias toward writing one). +6. Iteratively make whatever changes are necessary to make the test suite pass on that branch. +7. Merge your own pull request and close the issue. +8. _Depending on the severity of the bug:_ consider releasing a new version of the GitHub package and hot-fixing beta or stable with the fix. See ["How we ship"](#how-we-ship) for our criteria for each. + +### Re-architecting + +Major, cross-cutting refactoring efforts fit within this category. Our goals with work like this are to address technical debt, to improve our ability to comprehend the codebase, to ease the burdens of new contributors, and to make other planned features and bug fixes simpler to enact in the future. + +##### Examples + +* Porting the package from Etch to React. +* Introducing additional diagnostics or metrics. +* Restructuring an entire component tree. + +##### Process + +1. Propose these changes first in a conversation in Slack, a stand-up, or another synchronous channel. The decisions to be made here are: + * Does this change make sense to people other than me? + * Will this impact other work in flight? +2. Capture the context of the change in an issue, which can then be prioritized accordingly within our normal channels. + * Should we stop or delay existing work in favor of a refactoring? + * Should we leave it as-is until we complete other work that's more impactful? +3. When you're ready to begin refactoring, assign the issue to yourself and move it to "in progress" column on the [short-term roadmap project](https://github.com/atom/github/projects/8). +4. Work in a feature branch in the `atom/github` repository and open a pull request to track your progress. +5. Iteratively change code and tests until the change is complete and CI builds are green. +6. Merge your own pull request and close the issue. + +### New features + +To introduce brand-new functionality into the package, follow this guide. + +##### Process + +1. On a feature branch, write a proposal as a markdown document beneath [`docs/rfcs`]() in this repository. Copy the [template]() to begin. Open a pull request. The RFC document should include: + * A description of the feature, writted as though it already exists; + * An analysis of the risks and drawbacks; + * A specification of when the feature will be considered "done"; + * Unresolved questions or possible follow-on work; + * A sequence of discrete phases that can be used to realize the full feature; + * The acceptance criteria for the RFC itself, as chosen by your current understanding of its scope and impact. Some options you may use here include _(a)_ you're satisfied with its state; _(b)_ the pull request has collected a predetermined number of :+1: votes from core team members; or _(c)_ unanimous :+1: votes from the full core team. +2. @-mention @simurai on the open pull request for design input. Begin hashing out mock-ups, look and feel, specific user interaction details, and decide on a high-level direction for the feature. +3. The RFC's author is responsible for recognizing when its acceptance criteria have been met and merging its pull request. :rainbow: _Our intent here is to give the feature's advocate the ability to cut [bikeshedding](https://en.wiktionary.org/wiki/bikeshedding) short and accept responsibility for guiding it forward._ +4. Work on the RFC's implementation is performed in one or more pull requests. + * Consider gating your work behind a feature flag or a configuration option. + * Write tests for your new work. + * Optionally [request reviewers](#how-we-review) if you want feedback. Ping @simurai for ongoing UI/UX considerations if appropriate. + * Merge your pull request yourself when CI is green and any reviewers you have requested have approved the PR. + * As the design evolves and opinions change, modify the existing RFC to stay accurate. +5. When the feature is complete, update the RFC to a "completed" state. + +### Expansions or retractions of package scope + +As a team, we maintain a [shared understanding](/docs/vision) of what we will and will not build as part of this package, which we use to guide our decisions about accepting new features. Like everything else, this understanding is itself fluid. + +##### Process + +1. Open a pull request that modifies a `docs/vision/*.md` file in this repository. Mention @atom/github-package for discussion. +2. When the full core team have left :+1: votes on the pull request, merge it. + +## How we review + +Code review is useful to validate the decisions you've made in the course of performing some work and to disseminate knowledge about ongoing changes across the team. We do **not** require review of all changes; instead, we prefer to allow each team member to use their own discretion about when a review would feel useful, considering the time and context switching it involves. + +Review comments are stream-of-consciousness style. Not every comment needs to be addressed before merge - they can include discussions of alternatives, possible refactorings or follow-on work, and things that you like. As a reviewer, err on the side of providing more information and making your expectations for each comment explicit. Remember that while egoless code is the ideal, we are human and should be mindful of one another in our writing. + +If you believe an issue _should_ be addressed before merge, mark that comment with a :rotating_light:. + +When finalizing your review: + +* "Approve" means "click merge whenever you're ready, I'm okay with this shipping exactly as I see it here if you are." +* "Comment" means "I've noted things that you may want to consider doing differently. Feel free to merge if you disagree, but further conversation might be useful to bring me on board." +* "Request changes" means "I believe that merging this right now would break something. Dismiss my review once you've addressed the urgent issues that I've identified." + +## How we ship + +The github package ships as a bundled part of Atom, which affects the way that our progress is delivered to users. After using `apm` to publish a new version, we also need to add a commit to [Atom's `package.json` file](https://github.com/atom/atom/blob/master/package.json#L114) to make our work available. + +When the team is preparing to ship a new version of Atom, run `apm publish minor` and update `package.json` on Atom's master branch to reference the new version. This will ship our work to Atom's [beta channel](https://atom.io/beta) and allow a smaller subset of our users to discover regressions before we release it to the full Atom user population. + +When you've merged substantial new functionality, consider running `apm publish minor` and updating `package.json` on Atom's master branch outside of the Atom release cycle, to give the rest of the Atom team time to dogfood the change internally and weigh in with opinions. + +After shipping a minor version release for either of the above situations, create and push a release branch from that version's tag: + +```sh +$ apm publish minor +version 0.11.0 +$ git branch 0.11-releases && git push -u origin 0.11-releases +``` + +When you merge a fix for a bug, cherry-pick the merge commit onto to the most recent release branch, then run `apm publish patch` and update `package.json` on the most recent beta release branch on the `atom/atom` repository. This will ensure bug fixes are delivered to users on Atom's stable channel as part of the next release. + +When you merge a fix for a **security problem**, a **data loss bug**, or fix a **crash** or a **lock-up** that affect a large portion of the user population, run `apm publish patch` and update `package.json` on the most recent beta _and_ stable release branches on the `atom/atom` repository. Consider advocating for a hotfix release of Atom to deliver these fixes to the user population as soon as possible. From 9eca240f2a08747fcc90b52875669ac49b5001b0 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 21 Feb 2018 10:09:02 -0800 Subject: [PATCH 0282/5882] Ensure the GSOS.exec() promise is rejected if the dugite promise rejects --- lib/git-shell-out-strategy.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/git-shell-out-strategy.js b/lib/git-shell-out-strategy.js index 4d88645da4..bd974ecc86 100644 --- a/lib/git-shell-out-strategy.js +++ b/lib/git-shell-out-strategy.js @@ -219,7 +219,10 @@ export default class GitShellOutStrategy { })); } - const {stdout, stderr, exitCode, timing} = await promise; + const {stdout, stderr, exitCode, timing} = await promise.catch(err => { + reject(err); + return {}; + }); if (timing) { const {execTime, spawnTime, ipcTime} = timing; From c7505f5e383c9bd406317148b93ba18835b65852 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 21 Feb 2018 10:09:35 -0800 Subject: [PATCH 0283/5882] Consistently reject the Promise for errors instead of just logging them --- lib/git-shell-out-strategy.js | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/lib/git-shell-out-strategy.js b/lib/git-shell-out-strategy.js index bd974ecc86..3f1638f44b 100644 --- a/lib/git-shell-out-strategy.js +++ b/lib/git-shell-out-strategy.js @@ -316,18 +316,9 @@ export default class GitShellOutStrategy { options.processCallback = child => { childPid = child.pid; - child.on('error', err => { - /* eslint-disable no-console */ - console.error(`Error spawning: git ${args.join(' ')} in ${this.workingDir}`); - console.error(err); - /* eslint-enable no-console */ - }); - child.stdin.on('error', err => { - /* eslint-disable no-console */ - console.error(`Error writing to stdin: git ${args.join(' ')} in ${this.workingDir}\n${options.stdin}`); - console.error(err); - /* eslint-enable no-console */ + throw new Error( + `Error writing to stdin: git ${args.join(' ')} in ${this.workingDir}\n${options.stdin}\n${err}`); }); }; From 8530d58506a3b8c3d04974c5c0110f64843d3844 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 21 Feb 2018 10:10:07 -0800 Subject: [PATCH 0284/5882] Use `git rev-parse --show-toplevel` to back WorkdirCache --- lib/github-package.js | 4 +- lib/models/workdir-cache.js | 100 +++++++----------------------- test/models/workdir-cache.test.js | 31 ++++----- 3 files changed, 41 insertions(+), 94 deletions(-) diff --git a/lib/github-package.js b/lib/github-package.js index fab1215446..f60bf51618 100644 --- a/lib/github-package.js +++ b/lib/github-package.js @@ -419,7 +419,7 @@ export default class GithubPackage { const repository = this.contextPool.add(projectPath).getRepository(); await repository.init(); - this.workdirCache.invalidate(projectPath); + this.workdirCache.invalidate(); if (!this.project.contains(projectPath)) { this.project.addPath(projectPath); @@ -441,7 +441,7 @@ export default class GithubPackage { await repository.clone(remoteUrl); } - this.workdirCache.invalidate(projectPath); + this.workdirCache.invalidate(); this.project.addPath(projectPath); diff --git a/lib/models/workdir-cache.js b/lib/models/workdir-cache.js index 70a4cd5bc5..4e0167ecc9 100644 --- a/lib/models/workdir-cache.js +++ b/lib/models/workdir-cache.js @@ -1,7 +1,7 @@ import path from 'path'; -import fs from 'fs'; -import {readFile, realPath, isValidWorkdir} from '../helpers'; +import CompositeGitStrategy from '../composite-git-strategy'; +import {fsStat} from '../helpers'; /** * Locate the nearest git working directory above a given starting point, caching results. @@ -12,87 +12,33 @@ export default class WorkdirCache { this.known = new Map(); } - async find(startDir) { - try { - const resolvedDir = await realPath(startDir); - const cached = this.known.get(resolvedDir); - if (cached !== undefined) { - return cached; - } + async find(startPath) { + const cached = this.known.get(startPath); + if (cached !== undefined) { + return cached; + } - const workDir = await this.walkToRoot(resolvedDir); + const workDir = await this.revParse(startPath); - if (this.known.size >= this.maxSize) { - this.known.clear(); - } - this.known.set(resolvedDir, workDir); - return workDir; - } catch (e) { - if (e.code === 'ENOENT') { - return null; - } - - throw e; + if (this.known.size >= this.maxSize) { + this.known.clear(); } - } + this.known.set(startPath, workDir); - async invalidate(baseDir) { - const resolvedBase = await realPath(baseDir); - for (const cachedPath of this.known.keys()) { - if (cachedPath.startsWith(resolvedBase)) { - this.known.delete(cachedPath); - } - } + return workDir; } - walkToRoot(initialDir) { - return new Promise((resolve, reject) => { - let currentDir = initialDir; - - const check = () => { - if (!isValidWorkdir(currentDir)) { - return walk(); - } - - const dotGit = path.join(currentDir, '.git'); - fs.stat(dotGit, async (statError, stat) => { - if (statError) { - if (statError.code === 'ENOENT' || statError.code === 'ENOTDIR') { - // File not found. This is not the directory we're looking for. Continue walking. - return walk(); - } - - return reject(statError); - } - - if (!stat.isDirectory()) { - const contents = await readFile(dotGit, 'utf8'); - if (contents.startsWith('gitdir: ')) { - return resolve(currentDir); - } else { - return walk(); - } - } - - // .git directory found! Mission accomplished. - return resolve(currentDir); - }); - return null; - }; - - const walk = () => { - const parentDir = path.resolve(currentDir, '..'); - if (parentDir === currentDir) { - // Root directory. Traversal done, no working directory found. - resolve(null); - return; - } - - currentDir = parentDir; - check(); - }; + invalidate() { + this.known.clear(); + } - check(); - }); + async revParse(startPath) { + try { + const startDir = (await fsStat(startPath)).isDirectory() ? startPath : path.dirname(startPath); + const workDir = await CompositeGitStrategy.create(startDir).exec(['rev-parse', '--show-toplevel']); + return workDir.trim(); + } catch (e) { + return null; + } } } diff --git a/test/models/workdir-cache.test.js b/test/models/workdir-cache.test.js index 9e120a7cc9..e205124f07 100644 --- a/test/models/workdir-cache.test.js +++ b/test/models/workdir-cache.test.js @@ -64,14 +64,15 @@ describe('WorkdirCache', function() { // Prime the cache await cache.find(givenDir); + assert.isTrue(cache.known.has(givenDir)); - sinon.spy(cache, 'walkToRoot'); + sinon.spy(cache, 'revParse'); const actualDir = await cache.find(givenDir); assert.equal(actualDir, expectedDir); - assert.isFalse(cache.walkToRoot.called); + assert.isFalse(cache.revParse.called); }); - it('removes cached entries for all subdirectories of an entry', async function() { + it('removes all cached entries', async function() { const [dir0, dir1] = await Promise.all([ cloneRepository('three-files'), cloneRepository('three-files'), @@ -95,26 +96,26 @@ describe('WorkdirCache', function() { assert.deepEqual(initial, expectedWorkdirs); // Re-lookup and hit the cache - sinon.spy(cache, 'walkToRoot'); + sinon.spy(cache, 'revParse'); const relookup = await Promise.all( pathsToCheck.map(input => cache.find(input)), ); assert.deepEqual(relookup, expectedWorkdirs); - assert.equal(cache.walkToRoot.callCount, 0); + assert.equal(cache.revParse.callCount, 0); - // Clear dir0 - await cache.invalidate(dir0); + // Clear the cache + await cache.invalidate(); - // Re-lookup and hit the cache once + // Re-lookup and miss the cache const after = await Promise.all( pathsToCheck.map(input => cache.find(input)), ); assert.deepEqual(after, expectedWorkdirs); - assert.isTrue(cache.walkToRoot.calledWith(dir0)); - assert.isTrue(cache.walkToRoot.calledWith(path.join(dir0, 'a.txt'))); - assert.isTrue(cache.walkToRoot.calledWith(path.join(dir0, 'subdir-1'))); - assert.isTrue(cache.walkToRoot.calledWith(path.join(dir0, 'subdir-1', 'b.txt'))); - assert.isFalse(cache.walkToRoot.calledWith(dir1)); + assert.isTrue(cache.revParse.calledWith(dir0)); + assert.isTrue(cache.revParse.calledWith(path.join(dir0, 'a.txt'))); + assert.isTrue(cache.revParse.calledWith(path.join(dir0, 'subdir-1'))); + assert.isTrue(cache.revParse.calledWith(path.join(dir0, 'subdir-1', 'b.txt'))); + assert.isTrue(cache.revParse.calledWith(dir1)); }); it('clears the cache when the maximum size is exceeded', async function() { @@ -125,9 +126,9 @@ describe('WorkdirCache', function() { await Promise.all(dirs.map(dir => cache.find(dir))); const expectedDir = dirs[1]; - sinon.spy(cache, 'walkToRoot'); + sinon.spy(cache, 'revParse'); const actualDir = await cache.find(expectedDir); assert.equal(actualDir, expectedDir); - assert.isTrue(cache.walkToRoot.called); + assert.isTrue(cache.revParse.called); }); }); From 27dc93ccafdae5b8461bd246ef60f6fdf0c0ed62 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 21 Feb 2018 10:16:07 -0800 Subject: [PATCH 0285/5882] Force inline git exec on Travis --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 7adf2020ab..c041eb402d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -28,6 +28,7 @@ env: - UNTIL_TIMEOUT=30000 - TRAVIS_BUILD_JOB=runtests - ATOM_GITHUB_DISABLE_KEYTAR=1 + - ATOM_GITHUB_INLINE_GIT_EXEC=1 matrix: - ATOM_CHANNEL=stable - ATOM_CHANNEL=beta From 98a98416d772ca4beb238c420f95590cdb0a5b68 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 21 Feb 2018 10:51:26 -0800 Subject: [PATCH 0286/5882] Focus WorkerManager tests --- .travis.yml | 1 - test/worker-manager.test.js | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index c041eb402d..7adf2020ab 100644 --- a/.travis.yml +++ b/.travis.yml @@ -28,7 +28,6 @@ env: - UNTIL_TIMEOUT=30000 - TRAVIS_BUILD_JOB=runtests - ATOM_GITHUB_DISABLE_KEYTAR=1 - - ATOM_GITHUB_INLINE_GIT_EXEC=1 matrix: - ATOM_CHANNEL=stable - ATOM_CHANNEL=beta diff --git a/test/worker-manager.test.js b/test/worker-manager.test.js index 9491114c46..59a2597c93 100644 --- a/test/worker-manager.test.js +++ b/test/worker-manager.test.js @@ -4,7 +4,7 @@ const {BrowserWindow} = remote; import WorkerManager, {Operation, Worker} from '../lib/worker-manager'; import {isProcessAlive} from './helpers'; -describe('WorkerManager', function() { +describe.stress(10, 'WorkerManager', function() { let workerManager; beforeEach(() => { workerManager = new WorkerManager(); From 5a4910994a6a8fbf5e74d6226978e2807d5272ca Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 21 Feb 2018 11:21:00 -0800 Subject: [PATCH 0287/5882] Hypothesis: destroying a renderer process while an IPC call is in flight --- test/worker-manager.test.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/test/worker-manager.test.js b/test/worker-manager.test.js index 59a2597c93..6e4e3be76c 100644 --- a/test/worker-manager.test.js +++ b/test/worker-manager.test.js @@ -4,7 +4,7 @@ const {BrowserWindow} = remote; import WorkerManager, {Operation, Worker} from '../lib/worker-manager'; import {isProcessAlive} from './helpers'; -describe.stress(10, 'WorkerManager', function() { +describe('WorkerManager', function() { let workerManager; beforeEach(() => { workerManager = new WorkerManager(); @@ -140,6 +140,18 @@ describe.stress(10, 'WorkerManager', function() { await assert.async.isFalse(isProcessAlive(worker1.getPid())); await assert.async.isFalse(isProcessAlive(worker2.getPid())); }); + + it.stress(20, 'gives recently issued send operations a chance to complete before destroying the process', async function() { + const worker = workerManager.getActiveWorker(); + await worker.getReadyPromise(); + + workerManager.request({ + args: ['rev-parse', 'HEAD'], + workingDir: __dirname, + options: {}, + }); + workerManager.destroy(true); + }); }); describe('when the manager process is destroyed', function() { From 5ec91b9f1289e5e0aa7fbb942328ea3b561ef624 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 21 Feb 2018 11:28:06 -0800 Subject: [PATCH 0288/5882] Yield the event loop to give things a chance to die --- test/worker-manager.test.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/worker-manager.test.js b/test/worker-manager.test.js index 6e4e3be76c..aa8576919c 100644 --- a/test/worker-manager.test.js +++ b/test/worker-manager.test.js @@ -151,6 +151,8 @@ describe('WorkerManager', function() { options: {}, }); workerManager.destroy(true); + + await new Promise(resolve => setTimeout(resolve, 10)); }); }); From e241aa3d374f2a558e5f5dc4e9db197ed6e80b84 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 21 Feb 2018 13:25:59 -0800 Subject: [PATCH 0289/5882] Create a bunch of requests in a tight loop maybe --- test/worker-manager.test.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/test/worker-manager.test.js b/test/worker-manager.test.js index aa8576919c..72f24fd3df 100644 --- a/test/worker-manager.test.js +++ b/test/worker-manager.test.js @@ -145,11 +145,13 @@ describe('WorkerManager', function() { const worker = workerManager.getActiveWorker(); await worker.getReadyPromise(); - workerManager.request({ - args: ['rev-parse', 'HEAD'], - workingDir: __dirname, - options: {}, - }); + for (let i = 0; i < 5; i++) { + workerManager.request({ + args: ['rev-parse', 'HEAD'], + workingDir: __dirname, + options: {}, + }); + } workerManager.destroy(true); await new Promise(resolve => setTimeout(resolve, 10)); From 3a29ae35262aca2684d335429d4c6cebc04d7e82 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 21 Feb 2018 13:43:33 -0800 Subject: [PATCH 0290/5882] Unset ATOM_GITHUB_INLINE_GIT_EXEC during GithubPackage tests --- test/github-package.test.js | 4 ++++ test/worker-manager.test.js | 16 ---------------- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/test/github-package.test.js b/test/github-package.test.js index d8ffd4a5e8..b73188d72c 100644 --- a/test/github-package.test.js +++ b/test/github-package.test.js @@ -13,6 +13,8 @@ describe('GithubPackage', function() { let githubPackage, contextPool; beforeEach(async function() { + process.env.ATOM_GITHUB_INLINE_GIT_EXEC = '1'; + atomEnv = global.buildAtomEnvironment(); await disableFilesystemWatchers(atomEnv); @@ -47,6 +49,8 @@ describe('GithubPackage', function() { await githubPackage.deactivate(); atomEnv.destroy(); + + delete process.env.ATOM_GITHUB_INLINE_GIT_EXEC; }); async function contextUpdateAfter(chunk) { diff --git a/test/worker-manager.test.js b/test/worker-manager.test.js index 72f24fd3df..9491114c46 100644 --- a/test/worker-manager.test.js +++ b/test/worker-manager.test.js @@ -140,22 +140,6 @@ describe('WorkerManager', function() { await assert.async.isFalse(isProcessAlive(worker1.getPid())); await assert.async.isFalse(isProcessAlive(worker2.getPid())); }); - - it.stress(20, 'gives recently issued send operations a chance to complete before destroying the process', async function() { - const worker = workerManager.getActiveWorker(); - await worker.getReadyPromise(); - - for (let i = 0; i < 5; i++) { - workerManager.request({ - args: ['rev-parse', 'HEAD'], - workingDir: __dirname, - options: {}, - }); - } - workerManager.destroy(true); - - await new Promise(resolve => setTimeout(resolve, 10)); - }); }); describe('when the manager process is destroyed', function() { From 8666ce4fdad319010aa46b7d3a1141b0ccb86ffa Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 21 Feb 2018 17:58:25 -0800 Subject: [PATCH 0291/5882] Disable WorkerManager execution for tests _except_ WorkerManager tests --- .travis.yml | 1 + test/github-package.test.js | 4 ---- test/worker-manager.test.js | 7 ++++++- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 7adf2020ab..c041eb402d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -28,6 +28,7 @@ env: - UNTIL_TIMEOUT=30000 - TRAVIS_BUILD_JOB=runtests - ATOM_GITHUB_DISABLE_KEYTAR=1 + - ATOM_GITHUB_INLINE_GIT_EXEC=1 matrix: - ATOM_CHANNEL=stable - ATOM_CHANNEL=beta diff --git a/test/github-package.test.js b/test/github-package.test.js index b73188d72c..d8ffd4a5e8 100644 --- a/test/github-package.test.js +++ b/test/github-package.test.js @@ -13,8 +13,6 @@ describe('GithubPackage', function() { let githubPackage, contextPool; beforeEach(async function() { - process.env.ATOM_GITHUB_INLINE_GIT_EXEC = '1'; - atomEnv = global.buildAtomEnvironment(); await disableFilesystemWatchers(atomEnv); @@ -49,8 +47,6 @@ describe('GithubPackage', function() { await githubPackage.deactivate(); atomEnv.destroy(); - - delete process.env.ATOM_GITHUB_INLINE_GIT_EXEC; }); async function contextUpdateAfter(chunk) { diff --git a/test/worker-manager.test.js b/test/worker-manager.test.js index 9491114c46..b2025cb581 100644 --- a/test/worker-manager.test.js +++ b/test/worker-manager.test.js @@ -5,13 +5,18 @@ import WorkerManager, {Operation, Worker} from '../lib/worker-manager'; import {isProcessAlive} from './helpers'; describe('WorkerManager', function() { - let workerManager; + let workerManager, inlineOption; beforeEach(() => { + inlineOption = process.env.ATOM_GITHUB_INLINE_GIT_EXEC; + delete process.env.ATOM_GITHUB_INLINE_GIT_EXEC; workerManager = new WorkerManager(); }); afterEach(() => { workerManager.destroy(true); + if (inlineOption) { + process.env.ATOM_GITHUB_INLINE_GIT_EXEC = inlineOption; + } }); describe('isReady()', function() { From aa08ee4602ab93f04b3548c065674f340f7942d4 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 21 Feb 2018 18:21:38 -0800 Subject: [PATCH 0292/5882] Skip tests that _need_ WorkerManager exec --- test/git-strategies.test.js | 10 ++++++++++ test/worker-manager.test.js | 7 +------ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/test/git-strategies.test.js b/test/git-strategies.test.js index b6dddead71..6536747b33 100644 --- a/test/git-strategies.test.js +++ b/test/git-strategies.test.js @@ -937,6 +937,11 @@ import {fsStat, normalizeGitHelperPath, writeFile, getTempDir} from '../lib/help describe('executeGitCommand', function() { it('shells out in process until WorkerManager instance is ready', async function() { + if (process.env.ATOM_GITHUB_INLINE_GIT_EXEC) { + this.pending(); + return; + } + const workingDirPath = await cloneRepository('three-files'); const git = createTestStrategy(workingDirPath); const workerManager = WorkerManager.getInstance(); @@ -1059,6 +1064,11 @@ import {fsStat, normalizeGitHelperPath, writeFile, getTempDir} from '../lib/help }); it('fails the command on dialog cancel', async function() { + if (process.env.ATOM_GITHUB_INLINE_GIT_EXEC) { + this.pending(); + return; + } + let query = null; const git = await withHttpRemote({ prompt: q => { diff --git a/test/worker-manager.test.js b/test/worker-manager.test.js index b2025cb581..9491114c46 100644 --- a/test/worker-manager.test.js +++ b/test/worker-manager.test.js @@ -5,18 +5,13 @@ import WorkerManager, {Operation, Worker} from '../lib/worker-manager'; import {isProcessAlive} from './helpers'; describe('WorkerManager', function() { - let workerManager, inlineOption; + let workerManager; beforeEach(() => { - inlineOption = process.env.ATOM_GITHUB_INLINE_GIT_EXEC; - delete process.env.ATOM_GITHUB_INLINE_GIT_EXEC; workerManager = new WorkerManager(); }); afterEach(() => { workerManager.destroy(true); - if (inlineOption) { - process.env.ATOM_GITHUB_INLINE_GIT_EXEC = inlineOption; - } }); describe('isReady()', function() { From 59a7cad5dce82416b364da3110516a7e30f1de09 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 21 Feb 2018 18:30:41 -0800 Subject: [PATCH 0293/5882] ... Oh right it's `this.skip()` --- test/git-strategies.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/git-strategies.test.js b/test/git-strategies.test.js index 6536747b33..8154d1482c 100644 --- a/test/git-strategies.test.js +++ b/test/git-strategies.test.js @@ -938,7 +938,7 @@ import {fsStat, normalizeGitHelperPath, writeFile, getTempDir} from '../lib/help describe('executeGitCommand', function() { it('shells out in process until WorkerManager instance is ready', async function() { if (process.env.ATOM_GITHUB_INLINE_GIT_EXEC) { - this.pending(); + this.skip(); return; } @@ -1065,7 +1065,7 @@ import {fsStat, normalizeGitHelperPath, writeFile, getTempDir} from '../lib/help it('fails the command on dialog cancel', async function() { if (process.env.ATOM_GITHUB_INLINE_GIT_EXEC) { - this.pending(); + this.skip(); return; } From 0d982b937517e2da44217637bbb34df1a73907fc Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 21 Feb 2018 18:38:54 -0800 Subject: [PATCH 0294/5882] Skip WorkerManager tests when using inline git exec --- test/worker-manager.test.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/worker-manager.test.js b/test/worker-manager.test.js index 9491114c46..441de510bb 100644 --- a/test/worker-manager.test.js +++ b/test/worker-manager.test.js @@ -7,6 +7,11 @@ import {isProcessAlive} from './helpers'; describe('WorkerManager', function() { let workerManager; beforeEach(() => { + if (process.env.ATOM_GITHUB_INLINE_GIT_EXEC) { + this.skip(); + return; + } + workerManager = new WorkerManager(); }); From 5ebac04ceb35d45b5fb49a35b2bfaef70040c1ff Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 21 Feb 2018 18:48:10 -0800 Subject: [PATCH 0295/5882] this.skip() doesn't work in arrow functions --- test/worker-manager.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/worker-manager.test.js b/test/worker-manager.test.js index 441de510bb..82f5d9d251 100644 --- a/test/worker-manager.test.js +++ b/test/worker-manager.test.js @@ -6,7 +6,7 @@ import {isProcessAlive} from './helpers'; describe('WorkerManager', function() { let workerManager; - beforeEach(() => { + beforeEach(function() { if (process.env.ATOM_GITHUB_INLINE_GIT_EXEC) { this.skip(); return; @@ -15,7 +15,7 @@ describe('WorkerManager', function() { workerManager = new WorkerManager(); }); - afterEach(() => { + afterEach(function() { workerManager.destroy(true); }); From f7e05c3f89bbab734563cf698f912dadf6ce52b6 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 21 Feb 2018 19:24:37 -0800 Subject: [PATCH 0296/5882] Use native path separators on Windows --- lib/models/workdir-cache.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/models/workdir-cache.js b/lib/models/workdir-cache.js index 4e0167ecc9..1e3b86647e 100644 --- a/lib/models/workdir-cache.js +++ b/lib/models/workdir-cache.js @@ -1,7 +1,7 @@ import path from 'path'; import CompositeGitStrategy from '../composite-git-strategy'; -import {fsStat} from '../helpers'; +import {fsStat, toNativePathSep} from '../helpers'; /** * Locate the nearest git working directory above a given starting point, caching results. @@ -36,7 +36,7 @@ export default class WorkdirCache { try { const startDir = (await fsStat(startPath)).isDirectory() ? startPath : path.dirname(startPath); const workDir = await CompositeGitStrategy.create(startDir).exec(['rev-parse', '--show-toplevel']); - return workDir.trim(); + return toNativePathSep(workDir.trim()); } catch (e) { return null; } From 4954334d326f594d81b08048ce0a962f79a95f33 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 21 Feb 2018 22:10:26 -0800 Subject: [PATCH 0297/5882] Let's isolate the flake --- test/git-prompt-server.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/git-prompt-server.test.js b/test/git-prompt-server.test.js index d4d4acc77e..55e8a5924e 100644 --- a/test/git-prompt-server.test.js +++ b/test/git-prompt-server.test.js @@ -151,7 +151,7 @@ describe('GitPromptServer', function() { 'quit=true\n'); }); - it('creates a flag file if remember is set to true', async function() { + it.stress(50, 'creates a flag file if remember is set to true', async function() { this.timeout(10000); function queryHandler(query) { From 033abdc39e0e3c21fd6622e6d3d336560ca24638 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 21 Feb 2018 22:22:52 -0800 Subject: [PATCH 0298/5882] Good old-fashioned diagnostic console.logs --- test/git-prompt-server.test.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/test/git-prompt-server.test.js b/test/git-prompt-server.test.js index 55e8a5924e..5b5feb0af4 100644 --- a/test/git-prompt-server.test.js +++ b/test/git-prompt-server.test.js @@ -32,27 +32,27 @@ describe('GitPromptServer', function() { }); async function runCredentialScript(command, queryHandler, processHandler) { + console.log('0'); await server.start(queryHandler); + console.log('1'); - let err, stdout, stderr; - await new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { + console.log('2'); const child = execFile( getAtomHelperPath(), [tempDir.getCredentialHelperJs(), tempDir.getSocketPath(), command], {env: electronEnv}, - (_err, _stdout, _stderr) => { - err = _err; - stdout = _stdout; - stderr = _stderr; - resolve(); + (err, stdout, stderr) => { + console.log('3'); + resolve({err, stdout, stderr}); }, ); + console.log('3'); child.stderr.on('data', console.log); // eslint-disable-line no-console processHandler(child); + console.log('4'); }); - - return {err, stdout, stderr}; } it('prompts for user input and writes collected credentials to stdout', async function() { From bb20acac0374c7e451619aa4acb64a34b4890712 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 21 Feb 2018 22:28:12 -0800 Subject: [PATCH 0299/5882] Give the socket a chance to start listening --- test/git-prompt-server.test.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/test/git-prompt-server.test.js b/test/git-prompt-server.test.js index 5b5feb0af4..7b837cf157 100644 --- a/test/git-prompt-server.test.js +++ b/test/git-prompt-server.test.js @@ -32,12 +32,10 @@ describe('GitPromptServer', function() { }); async function runCredentialScript(command, queryHandler, processHandler) { - console.log('0'); await server.start(queryHandler); - console.log('1'); + await new Promise(resolve => setTimeout(resolve, 10)); return new Promise((resolve, reject) => { - console.log('2'); const child = execFile( getAtomHelperPath(), [tempDir.getCredentialHelperJs(), tempDir.getSocketPath(), command], {env: electronEnv}, @@ -46,12 +44,10 @@ describe('GitPromptServer', function() { resolve({err, stdout, stderr}); }, ); - console.log('3'); child.stderr.on('data', console.log); // eslint-disable-line no-console processHandler(child); - console.log('4'); }); } From 7bc00c4cbb2f36c9fb90fbe55c2c4c45f9913835 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 21 Feb 2018 22:38:53 -0800 Subject: [PATCH 0300/5882] Use socket.end() to flush the payload --- bin/git-credential-atom.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/git-credential-atom.js b/bin/git-credential-atom.js index bc99de12df..beb9fa0eb5 100755 --- a/bin/git-credential-atom.js +++ b/bin/git-credential-atom.js @@ -223,7 +223,7 @@ function dialog(query) { }); log('writing payload'); - socket.write(JSON.stringify(payload) + '\u0000', 'utf8'); + socket.end(JSON.stringify(payload) + '\u0000', 'utf8'); log('payload written'); }); socket.setEncoding('utf8'); From 0bc8849ccb188f948798045127a149616e156843 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 21 Feb 2018 22:40:01 -0800 Subject: [PATCH 0301/5882] :fire: debugging --- test/git-prompt-server.test.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/git-prompt-server.test.js b/test/git-prompt-server.test.js index 7b837cf157..c359b0c6b0 100644 --- a/test/git-prompt-server.test.js +++ b/test/git-prompt-server.test.js @@ -33,14 +33,12 @@ describe('GitPromptServer', function() { async function runCredentialScript(command, queryHandler, processHandler) { await server.start(queryHandler); - await new Promise(resolve => setTimeout(resolve, 10)); return new Promise((resolve, reject) => { const child = execFile( getAtomHelperPath(), [tempDir.getCredentialHelperJs(), tempDir.getSocketPath(), command], {env: electronEnv}, (err, stdout, stderr) => { - console.log('3'); resolve({err, stdout, stderr}); }, ); From cc3e153a3fc02aea96221304ead8fc0a2d3a6af1 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 21 Feb 2018 22:48:48 -0800 Subject: [PATCH 0302/5882] Log some prompt server stuff --- lib/git-prompt-server.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/git-prompt-server.js b/lib/git-prompt-server.js index 7139e2de10..106b789df9 100644 --- a/lib/git-prompt-server.js +++ b/lib/git-prompt-server.js @@ -22,6 +22,7 @@ export default class GitPromptServer { const parts = []; connection.on('data', data => { + console.log(`got data: ${data}`); const nullIndex = data.indexOf('\u0000'); if (nullIndex === -1) { parts.push(data); @@ -37,6 +38,7 @@ export default class GitPromptServer { } handleData(connection, data) { + console.log(`handling data:\n${data}`); let query; try { query = JSON.parse(data); @@ -45,7 +47,7 @@ export default class GitPromptServer { } Promise.resolve(this.promptForInput(query)) - .then(answer => connection.end(JSON.stringify(answer), 'utf-8')) + .then(answer => connection.end(JSON.stringify(answer), 'utf8')) .catch(() => this.emitter.emit('did-cancel', {handlerPid: query.pid})); } From 7295406cc2707dfcee60451f6a70413ba5c427f8 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 22 Feb 2018 09:23:47 -0800 Subject: [PATCH 0303/5882] Try using allowHalfOpen and .end() I feel like there's some reason we haven't done this already, but I don't remember why at all. --- bin/git-credential-atom.js | 4 ++-- lib/git-prompt-server.js | 17 ++++++----------- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/bin/git-credential-atom.js b/bin/git-credential-atom.js index beb9fa0eb5..1e5363f002 100755 --- a/bin/git-credential-atom.js +++ b/bin/git-credential-atom.js @@ -188,7 +188,7 @@ function dialog(query) { log('requesting dialog through Atom socket'); log(`prompt = "${prompt}" includeUsername = ${includeUsername}`); - const socket = net.connect(sockPath, () => { + const socket = net.connect({allowHalfOpen: true, path: sockPath}, () => { log('connection established'); const parts = []; @@ -223,7 +223,7 @@ function dialog(query) { }); log('writing payload'); - socket.end(JSON.stringify(payload) + '\u0000', 'utf8'); + socket.end(JSON.stringify(payload), 'utf8'); log('payload written'); }); socket.setEncoding('utf8'); diff --git a/lib/git-prompt-server.js b/lib/git-prompt-server.js index 106b789df9..44ce3452e6 100644 --- a/lib/git-prompt-server.js +++ b/lib/git-prompt-server.js @@ -16,20 +16,16 @@ export default class GitPromptServer { startListening(socketPath) { return new Promise(resolve => { - const server = net.createServer(connection => { + const server = net.createServer({allowHalfOpen: true}, connection => { connection.setEncoding('utf8'); const parts = []; - connection.on('data', data => { - console.log(`got data: ${data}`); - const nullIndex = data.indexOf('\u0000'); - if (nullIndex === -1) { - parts.push(data); - } else { - parts.push(data.substring(0, nullIndex)); - this.handleData(connection, parts.join('')); - } + connection.on('data', data => parts.push(data)); + connection.on('end', () => { + const payload = parts.join(''); + console.log(`got data:\n${payload}\n`); + this.handleData(connection, payload); }); }); @@ -38,7 +34,6 @@ export default class GitPromptServer { } handleData(connection, data) { - console.log(`handling data:\n${data}`); let query; try { query = JSON.parse(data); From f6c7990932cf7f668e901c7a657c7af7da40fbb3 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 22 Feb 2018 09:40:39 -0800 Subject: [PATCH 0304/5882] Log the credential helper payload as well --- bin/git-credential-atom.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/bin/git-credential-atom.js b/bin/git-credential-atom.js index 1e5363f002..9cc9d536e2 100755 --- a/bin/git-credential-atom.js +++ b/bin/git-credential-atom.js @@ -195,10 +195,11 @@ function dialog(query) { socket.on('data', data => parts.push(data)); socket.on('end', () => { - log('Atom socket stream terminated'); + const payload = parts.join(''); + log(`Atom socket stream terminated\n${payload}\n`); try { - const reply = JSON.parse(parts.join('')); + const reply = JSON.parse(payload); const writeReply = function() { const lines = []; @@ -217,7 +218,7 @@ function dialog(query) { writeReply(); } } catch (e) { - log(`Unable to parse reply from Atom:\n${e.stack}`); + log(`Unable to parse reply from Atom:\n${payload}\n${e.stack}`); reject(e); } }); From 38ecbcb48d5badb9118e506089d98e95ea91b903 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 22 Feb 2018 10:02:07 -0800 Subject: [PATCH 0305/5882] :art: Use the correct indentation for sub-bullets --- docs/how-we-work.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/how-we-work.md b/docs/how-we-work.md index 8b2b1ff100..785afa1a72 100644 --- a/docs/how-we-work.md +++ b/docs/how-we-work.md @@ -54,11 +54,11 @@ Major, cross-cutting refactoring efforts fit within this category. Our goals wit ##### Process 1. Propose these changes first in a conversation in Slack, a stand-up, or another synchronous channel. The decisions to be made here are: - * Does this change make sense to people other than me? - * Will this impact other work in flight? + * Does this change make sense to people other than me? + * Will this impact other work in flight? 2. Capture the context of the change in an issue, which can then be prioritized accordingly within our normal channels. - * Should we stop or delay existing work in favor of a refactoring? - * Should we leave it as-is until we complete other work that's more impactful? + * Should we stop or delay existing work in favor of a refactoring? + * Should we leave it as-is until we complete other work that's more impactful? 3. When you're ready to begin refactoring, assign the issue to yourself and move it to "in progress" column on the [short-term roadmap project](https://github.com/atom/github/projects/8). 4. Work in a feature branch in the `atom/github` repository and open a pull request to track your progress. 5. Iteratively change code and tests until the change is complete and CI builds are green. From 90ed85ca79d54a6b72ee7c83035358da71cbc85f Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 22 Feb 2018 10:10:17 -0800 Subject: [PATCH 0306/5882] Clarify that the merge commits need to be on both release branches --- docs/how-we-work.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/how-we-work.md b/docs/how-we-work.md index 785afa1a72..a6196a9c07 100644 --- a/docs/how-we-work.md +++ b/docs/how-we-work.md @@ -128,4 +128,4 @@ $ git branch 0.11-releases && git push -u origin 0.11-releases When you merge a fix for a bug, cherry-pick the merge commit onto to the most recent release branch, then run `apm publish patch` and update `package.json` on the most recent beta release branch on the `atom/atom` repository. This will ensure bug fixes are delivered to users on Atom's stable channel as part of the next release. -When you merge a fix for a **security problem**, a **data loss bug**, or fix a **crash** or a **lock-up** that affect a large portion of the user population, run `apm publish patch` and update `package.json` on the most recent beta _and_ stable release branches on the `atom/atom` repository. Consider advocating for a hotfix release of Atom to deliver these fixes to the user population as soon as possible. +When you merge a fix for a **security problem**, a **data loss bug**, or fix a **crash** or a **lock-up** that affect a large portion of the user population, cherry-pick the merge commit onto the most recent stable _and_ beta release branches of atom/github, then run `apm publish patch` on both and update `package.json` on the most recent beta and stable release branches on the `atom/atom` repository. Consider advocating for a hotfix release of Atom to deliver these fixes to the user population as soon as possible. From ee5d39ba6b4ab9d427cf3b869bcde9da54dc0ef9 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 22 Feb 2018 10:33:39 -0800 Subject: [PATCH 0307/5882] Clarify that hotfixes only apply to branches that contain the bug. --- docs/how-we-work.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/how-we-work.md b/docs/how-we-work.md index a6196a9c07..53ccad82f7 100644 --- a/docs/how-we-work.md +++ b/docs/how-we-work.md @@ -128,4 +128,4 @@ $ git branch 0.11-releases && git push -u origin 0.11-releases When you merge a fix for a bug, cherry-pick the merge commit onto to the most recent release branch, then run `apm publish patch` and update `package.json` on the most recent beta release branch on the `atom/atom` repository. This will ensure bug fixes are delivered to users on Atom's stable channel as part of the next release. -When you merge a fix for a **security problem**, a **data loss bug**, or fix a **crash** or a **lock-up** that affect a large portion of the user population, cherry-pick the merge commit onto the most recent stable _and_ beta release branches of atom/github, then run `apm publish patch` on both and update `package.json` on the most recent beta and stable release branches on the `atom/atom` repository. Consider advocating for a hotfix release of Atom to deliver these fixes to the user population as soon as possible. +When you merge a fix for a **security problem**, a **data loss bug**, or fix a **crash** or a **lock-up** that affect a large portion of the user population, cherry-pick the merge commit onto the most recent beta _and_ stable release branches of atom/github that contain the bug, then run `apm publish patch` on both and update `package.json` on the affected release branches on the `atom/atom` repository. Consider advocating for a hotfix release of Atom to deliver these fixes to the user population as soon as possible. From e9fd65d59b136dc5b166d53c7c68ff175f458d76 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 22 Feb 2018 11:56:27 -0800 Subject: [PATCH 0308/5882] "Borrow" the RFC template from atom/teletype with minor changes --- docs/rfcs/000-template.md | 43 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 docs/rfcs/000-template.md diff --git a/docs/rfcs/000-template.md b/docs/rfcs/000-template.md new file mode 100644 index 0000000000..bfc1ec53be --- /dev/null +++ b/docs/rfcs/000-template.md @@ -0,0 +1,43 @@ +# Feature title + +## Status + +Proposed + +## Summary + +One paragraph explanation of the feature. + +## Motivation + +Why are we doing this? What use cases does it support? What is the expected outcome? + +## Explanation + +Explain the proposal as if it was already implemented in the Teletype package and you were describing it to an Atom user. That generally means: + +- Introducing new named concepts. +- Explaining the feature largely in terms of examples. +- Explaining any changes to existing workflows. +- Design mock-ups or diagrams depicting any new UI that will be introduced. + +## Drawbacks + +Why should we *not* do this? + +## Rationale and alternatives + +- Why is this approach the best in the space of possible approaches? +- What other approaches have been considered and what is the rationale for not choosing them? +- What is the impact of not doing this? + +## Unresolved questions + +- What unresolved questions do you expect to resolve through the RFC process before this gets merged? +- What unresolved questions do you expect to resolve through the implementation of this feature before it is released in a new version of the package? +- What related issues do you consider out of scope for this RFC that could be addressed in the future independently of the solution that comes out of this RFC? + +## Implementation phases + +- Can this functionality be introduced in multiple, distinct, self-contained pull requests? +- A specification for when the feature is considered "done." From 429ac5965e73df3a0cdf4633ccd706ec31af5a18 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 22 Feb 2018 11:56:53 -0800 Subject: [PATCH 0309/5882] Work so far --- docs/rfcs/001-recent-commits.md | 72 +++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 docs/rfcs/001-recent-commits.md diff --git a/docs/rfcs/001-recent-commits.md b/docs/rfcs/001-recent-commits.md new file mode 100644 index 0000000000..f72ce128fa --- /dev/null +++ b/docs/rfcs/001-recent-commits.md @@ -0,0 +1,72 @@ +# Recent commit view + +## Status + +Proposed + +## Summary + +Display the most recent few commits in a chronologically-ordered list beneath the mini commit editor. Show commit author and committer usernames and avatars, the commit message, and relative timestamp of each. + +## Motivation + +Provide useful context about recent work and where you left off. +Allow user to easily revert and reset to recent commits. +Make it easy to undo most recent commit action, supersede amend check box. +Reinforce the visual "flow" of changes through being unstaged, staged, and now committed. +Provide a discoverable launch point for an eventual log feature to explore the full history. + +## Explanation + +If the active repository has no commits yet, display a short panel with a background message: "Make your first commit". + +Otherwise, display a **recent commits** section containing a sequence of horizontal bars for each of the top three commits reachable from the current `HEAD`, with the most recently created commit on top. The user can resize the recent commits section. As it is expanded or shrunk, the number of visible commits is changed responsively. + +Each **recent commit** within the recent commits section summarizes that commit's metadata, to include: + +* GitHub avatar for both the committer and (if applicable) author. If either do not exist, show a placeholder. +* The commit message (first line of the commit body) elided if it would be too wide. +* A relative timestamp indicating how long ago the commit was created. + +On hover, reveal a tool-tip containing: + +* Additional user information consistently with the GitHub integration's user mention item. +* The full commit message and body. +* The absolute timestamp of the commit. + +On the most recent commit, display an "undo" button. Clicking "undo" performs a `git reset` and re-populates the commit message editor with the existing message. + +Right-clicking a recent commit reveals a context menu offering interactions with the chosen commit. The context menu contains: + +* For the most recent commit only, an "Amend" option. "Amend" is enabled if changes have been staged or the commit message mini-editor contains text. Choosing this applies the staged changes and modified commit message to the most recent commit, in a direct analogue to using `git commit --amend` from the command line. +* A "Revert" option. Choosing this performs a `git revert` on the chosen commit. +* A "Hard reset" option. Choosing this performs a `git reset --hard` which moves `HEAD` and the working copy to the chosen commit. When chosen, display a modal explaining that this action will discard commits and unstaged working directory context. If there are unstaged working directory contents, artificially perform a dangling commit, disabling GPG if configured, before enacting the reset. This will record the dangling commit in the reflog for `HEAD` but not the branch itself. +* A "Soft reset" option. Choosing this performs a `git reset --soft` which moves `HEAD` to the chosen commit and populates the staged changes list with all of the cumulative changes from all commits between the chosen one and the previous `HEAD`. + +If any of the recent commits have been pushed to a remote, display a divider after the most recently pushed commit that shows an octocat icon. On hover, show the name of the remote tracking branch. + +## Drawbacks + +Consumes vertical real estate in Git panel. + +The "undo" button is not a native git concept. This can be mitigated by adding a tooltip to the "undo" button that defines its action: a `git reset` and commit message edit. + +The "soft reset" and "hard reset" context menu options are useful for expert git users, but likely to be confusing. It would be beneficial to provide additional information about the actions that both will take. + +The modal dialog on "hard reset" is disruptive considering that the lost changes are recoverable from `git reflog`. We may wish to remove it once we visit a reflog view within the package. + +## Rationale and alternatives + +- + +## Unresolved questions + +- Allow users to view the changes introduced by recent commits. For example, interacting with one of the recent commits could launch a pane item that showed the full commit body and diff, with additional controls for reverting, discarding, and commit-anchored interactions. +- Providing a bridge to navigate to an expanded log view that allows more flexible and powerful history exploration. +- Show an info icon and provide introductory information when no commits exist yet. +- Add a "view diff from this commit" option to the recent commit context menu. + +## Implementation phases + +- Can this functionality be introduced in multiple, distinct, self-contained pull requests? +- A specification for when the feature is considered "done." From 56bebda4bc715749a572b6dd4c54bc3c3f3d1b52 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 22 Feb 2018 12:04:32 -0800 Subject: [PATCH 0310/5882] HI @kuychaco!!! --- docs/rfcs/001-recent-commits.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/rfcs/001-recent-commits.md b/docs/rfcs/001-recent-commits.md index f72ce128fa..81ba49fcd0 100644 --- a/docs/rfcs/001-recent-commits.md +++ b/docs/rfcs/001-recent-commits.md @@ -53,11 +53,11 @@ The "undo" button is not a native git concept. This can be mitigated by adding a The "soft reset" and "hard reset" context menu options are useful for expert git users, but likely to be confusing. It would be beneficial to provide additional information about the actions that both will take. -The modal dialog on "hard reset" is disruptive considering that the lost changes are recoverable from `git reflog`. We may wish to remove it once we visit a reflog view within the package. +The modal dialog on "hard reset" is disruptive considering that the lost changes are recoverable from `git reflog`. We may wish to remove it once we visit a reflog view within the package. Optionally add "Don't show" checkbox to disable modal. ## Rationale and alternatives -- +- Display tracking branch in separator that indicates which commits have been pushed. This could make the purpose of the divider more clear. Drawback is that this takes up space. ## Unresolved questions From 8032a5ca8cbdd1101c163a6cab488d444063caba Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 22 Feb 2018 12:05:33 -0800 Subject: [PATCH 0311/5882] HI AGAIN @kuychaco!!!! --- docs/rfcs/001-recent-commits.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/rfcs/001-recent-commits.md b/docs/rfcs/001-recent-commits.md index 81ba49fcd0..e3fa96dd7d 100644 --- a/docs/rfcs/001-recent-commits.md +++ b/docs/rfcs/001-recent-commits.md @@ -40,7 +40,7 @@ Right-clicking a recent commit reveals a context menu offering interactions with * For the most recent commit only, an "Amend" option. "Amend" is enabled if changes have been staged or the commit message mini-editor contains text. Choosing this applies the staged changes and modified commit message to the most recent commit, in a direct analogue to using `git commit --amend` from the command line. * A "Revert" option. Choosing this performs a `git revert` on the chosen commit. -* A "Hard reset" option. Choosing this performs a `git reset --hard` which moves `HEAD` and the working copy to the chosen commit. When chosen, display a modal explaining that this action will discard commits and unstaged working directory context. If there are unstaged working directory contents, artificially perform a dangling commit, disabling GPG if configured, before enacting the reset. This will record the dangling commit in the reflog for `HEAD` but not the branch itself. +* A "Hard reset" option. Choosing this performs a `git reset --hard` which moves `HEAD` and the working copy to the chosen commit. When chosen, display a modal explaining that this action will discard commits and unstaged working directory context. Extra security: If there are unstaged working directory contents, artificially perform a dangling commit, disabling GPG if configured, before enacting the reset. This will record the dangling commit in the reflog for `HEAD` but not the branch itself. * A "Soft reset" option. Choosing this performs a `git reset --soft` which moves `HEAD` to the chosen commit and populates the staged changes list with all of the cumulative changes from all commits between the chosen one and the previous `HEAD`. If any of the recent commits have been pushed to a remote, display a divider after the most recently pushed commit that shows an octocat icon. On hover, show the name of the remote tracking branch. From f4a6dfa63ff3c91d2dc359c697d49f7d4526f6f4 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 22 Feb 2018 15:00:38 -0800 Subject: [PATCH 0312/5882] Eh let's just stick with .retries() for now --- test/git-prompt-server.test.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/git-prompt-server.test.js b/test/git-prompt-server.test.js index c359b0c6b0..98f0d0ea5a 100644 --- a/test/git-prompt-server.test.js +++ b/test/git-prompt-server.test.js @@ -145,8 +145,9 @@ describe('GitPromptServer', function() { 'quit=true\n'); }); - it.stress(50, 'creates a flag file if remember is set to true', async function() { + it('creates a flag file if remember is set to true', async function() { this.timeout(10000); + this.retries(5); function queryHandler(query) { return { From fcb1330130f62611fc8bf591509fbdb2f41ff7bb Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 22 Feb 2018 15:03:26 -0800 Subject: [PATCH 0313/5882] Revert prompt server changes for now --- bin/git-credential-atom.js | 11 +++++------ lib/git-prompt-server.js | 17 ++++++++++------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/bin/git-credential-atom.js b/bin/git-credential-atom.js index 9cc9d536e2..bc99de12df 100755 --- a/bin/git-credential-atom.js +++ b/bin/git-credential-atom.js @@ -188,18 +188,17 @@ function dialog(query) { log('requesting dialog through Atom socket'); log(`prompt = "${prompt}" includeUsername = ${includeUsername}`); - const socket = net.connect({allowHalfOpen: true, path: sockPath}, () => { + const socket = net.connect(sockPath, () => { log('connection established'); const parts = []; socket.on('data', data => parts.push(data)); socket.on('end', () => { - const payload = parts.join(''); - log(`Atom socket stream terminated\n${payload}\n`); + log('Atom socket stream terminated'); try { - const reply = JSON.parse(payload); + const reply = JSON.parse(parts.join('')); const writeReply = function() { const lines = []; @@ -218,13 +217,13 @@ function dialog(query) { writeReply(); } } catch (e) { - log(`Unable to parse reply from Atom:\n${payload}\n${e.stack}`); + log(`Unable to parse reply from Atom:\n${e.stack}`); reject(e); } }); log('writing payload'); - socket.end(JSON.stringify(payload), 'utf8'); + socket.write(JSON.stringify(payload) + '\u0000', 'utf8'); log('payload written'); }); socket.setEncoding('utf8'); diff --git a/lib/git-prompt-server.js b/lib/git-prompt-server.js index 44ce3452e6..7139e2de10 100644 --- a/lib/git-prompt-server.js +++ b/lib/git-prompt-server.js @@ -16,16 +16,19 @@ export default class GitPromptServer { startListening(socketPath) { return new Promise(resolve => { - const server = net.createServer({allowHalfOpen: true}, connection => { + const server = net.createServer(connection => { connection.setEncoding('utf8'); const parts = []; - connection.on('data', data => parts.push(data)); - connection.on('end', () => { - const payload = parts.join(''); - console.log(`got data:\n${payload}\n`); - this.handleData(connection, payload); + connection.on('data', data => { + const nullIndex = data.indexOf('\u0000'); + if (nullIndex === -1) { + parts.push(data); + } else { + parts.push(data.substring(0, nullIndex)); + this.handleData(connection, parts.join('')); + } }); }); @@ -42,7 +45,7 @@ export default class GitPromptServer { } Promise.resolve(this.promptForInput(query)) - .then(answer => connection.end(JSON.stringify(answer), 'utf8')) + .then(answer => connection.end(JSON.stringify(answer), 'utf-8')) .catch(() => this.emitter.emit('did-cancel', {handlerPid: query.pid})); } From 60b454b3f75278aee43e1bff5df6fe1893b64fd2 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 22 Feb 2018 15:10:47 -0800 Subject: [PATCH 0314/5882] Note behavior within the bottom dock --- docs/rfcs/001-recent-commits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/rfcs/001-recent-commits.md b/docs/rfcs/001-recent-commits.md index e3fa96dd7d..77887b381b 100644 --- a/docs/rfcs/001-recent-commits.md +++ b/docs/rfcs/001-recent-commits.md @@ -45,6 +45,8 @@ Right-clicking a recent commit reveals a context menu offering interactions with If any of the recent commits have been pushed to a remote, display a divider after the most recently pushed commit that shows an octocat icon. On hover, show the name of the remote tracking branch. +If the Git dock item is dragged to the bottom dock, the recent commit section will remain a vertical list but appear just to the right of the mini commit editor. + ## Drawbacks Consumes vertical real estate in Git panel. From 527e6e6fe009d42fb19b9a573ded9e6a4d2ef611 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 22 Feb 2018 15:19:38 -0800 Subject: [PATCH 0315/5882] Hover -> click --- docs/rfcs/001-recent-commits.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/rfcs/001-recent-commits.md b/docs/rfcs/001-recent-commits.md index 77887b381b..73b1014ba1 100644 --- a/docs/rfcs/001-recent-commits.md +++ b/docs/rfcs/001-recent-commits.md @@ -28,7 +28,7 @@ Each **recent commit** within the recent commits section summarizes that commit' * The commit message (first line of the commit body) elided if it would be too wide. * A relative timestamp indicating how long ago the commit was created. -On hover, reveal a tool-tip containing: +On click, reveal a tool-tip containing: * Additional user information consistently with the GitHub integration's user mention item. * The full commit message and body. From 809aa7faddd76fcb442abd3c5cdb6a72c7fc030f Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 22 Feb 2018 15:39:38 -0800 Subject: [PATCH 0316/5882] (shhhhhh) --- docs/rfcs/000-template.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/rfcs/000-template.md b/docs/rfcs/000-template.md index bfc1ec53be..3e332c5b5f 100644 --- a/docs/rfcs/000-template.md +++ b/docs/rfcs/000-template.md @@ -14,7 +14,7 @@ Why are we doing this? What use cases does it support? What is the expected outc ## Explanation -Explain the proposal as if it was already implemented in the Teletype package and you were describing it to an Atom user. That generally means: +Explain the proposal as if it was already implemented in the GitHub package and you were describing it to an Atom user. That generally means: - Introducing new named concepts. - Explaining the feature largely in terms of examples. From ba8dd34b9047bb4e6d2c85c318a1cb1a6de4a97f Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 22 Feb 2018 15:52:23 -0800 Subject: [PATCH 0317/5882] Pictures! :camera_flash: --- docs/rfcs/001-recent-commits.md | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/docs/rfcs/001-recent-commits.md b/docs/rfcs/001-recent-commits.md index 73b1014ba1..27cf4b2605 100644 --- a/docs/rfcs/001-recent-commits.md +++ b/docs/rfcs/001-recent-commits.md @@ -10,11 +10,14 @@ Display the most recent few commits in a chronologically-ordered list beneath th ## Motivation -Provide useful context about recent work and where you left off. -Allow user to easily revert and reset to recent commits. -Make it easy to undo most recent commit action, supersede amend check box. -Reinforce the visual "flow" of changes through being unstaged, staged, and now committed. -Provide a discoverable launch point for an eventual log feature to explore the full history. +* Provide useful context about recent work and where you left off. +* Allow user to easily revert and reset to recent commits. +* Make it easy to undo most recent commit action, supersede amend check box. +* Reinforce the visual "flow" of changes through being unstaged, staged, and now committed. +* Provide a discoverable launch point for an eventual log feature to explore the full history. +* Achieve greater consistency with GitHub desktop: + +![desktop](https://user-images.githubusercontent.com/7910250/36570484-1754fb3c-17e7-11e8-8da3-b658d404fd2c.png) ## Explanation @@ -28,14 +31,10 @@ Each **recent commit** within the recent commits section summarizes that commit' * The commit message (first line of the commit body) elided if it would be too wide. * A relative timestamp indicating how long ago the commit was created. -On click, reveal a tool-tip containing: - -* Additional user information consistently with the GitHub integration's user mention item. -* The full commit message and body. -* The absolute timestamp of the commit. - On the most recent commit, display an "undo" button. Clicking "undo" performs a `git reset` and re-populates the commit message editor with the existing message. +If any of the recent commits have been pushed to a remote, display a divider after the most recently pushed commit that shows an octocat icon. On hover, show the name of the remote tracking branch. + Right-clicking a recent commit reveals a context menu offering interactions with the chosen commit. The context menu contains: * For the most recent commit only, an "Amend" option. "Amend" is enabled if changes have been staged or the commit message mini-editor contains text. Choosing this applies the staged changes and modified commit message to the most recent commit, in a direct analogue to using `git commit --amend` from the command line. @@ -43,10 +42,20 @@ Right-clicking a recent commit reveals a context menu offering interactions with * A "Hard reset" option. Choosing this performs a `git reset --hard` which moves `HEAD` and the working copy to the chosen commit. When chosen, display a modal explaining that this action will discard commits and unstaged working directory context. Extra security: If there are unstaged working directory contents, artificially perform a dangling commit, disabling GPG if configured, before enacting the reset. This will record the dangling commit in the reflog for `HEAD` but not the branch itself. * A "Soft reset" option. Choosing this performs a `git reset --soft` which moves `HEAD` to the chosen commit and populates the staged changes list with all of the cumulative changes from all commits between the chosen one and the previous `HEAD`. -If any of the recent commits have been pushed to a remote, display a divider after the most recently pushed commit that shows an octocat icon. On hover, show the name of the remote tracking branch. +On click, reveal a tool-tip containing: + +* Additional user information consistently with the GitHub integration's user mention item. +* The full commit message and body. +* The absolute timestamp of the commit. +* Navigation button ("open" to a git show-ish pane item) +* Action buttons ("amend" on the most recent commit, "revert", and "reset" with "hard", "mixed", and "soft" suboptions) + +![commit-popout](https://user-images.githubusercontent.com/17565/36570682-11545cae-17e8-11e8-80a8-ffcf7328e214.JPG) If the Git dock item is dragged to the bottom dock, the recent commit section will remain a vertical list but appear just to the right of the mini commit editor. +![bottom-dock](https://user-images.githubusercontent.com/17565/36570687-14738ca2-17e8-11e8-91f7-5cf1472d871b.JPG) + ## Drawbacks Consumes vertical real estate in Git panel. From fee911461e29beceb537d5b60deadc4e6f01d4d9 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 22 Feb 2018 15:59:49 -0800 Subject: [PATCH 0318/5882] Implementation phases. --- docs/rfcs/001-recent-commits.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/rfcs/001-recent-commits.md b/docs/rfcs/001-recent-commits.md index 27cf4b2605..461b16c9f7 100644 --- a/docs/rfcs/001-recent-commits.md +++ b/docs/rfcs/001-recent-commits.md @@ -76,8 +76,11 @@ The modal dialog on "hard reset" is disruptive considering that the lost changes - Providing a bridge to navigate to an expanded log view that allows more flexible and powerful history exploration. - Show an info icon and provide introductory information when no commits exist yet. - Add a "view diff from this commit" option to the recent commit context menu. +- Integration with and navigation to "git log" or "git show" pane items when they exist. ## Implementation phases -- Can this functionality be introduced in multiple, distinct, self-contained pull requests? -- A specification for when the feature is considered "done." +1. List read-only commit information. +2. Replace the amend checkbox with the "undo" control. +3. Context menu with actions. +4. Tooltip with action buttons and additional information. From 486ff300d5df6cfac107cf023ea223f206d51dab Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 22 Feb 2018 16:05:12 -0800 Subject: [PATCH 0319/5882] Add the React conversion --- docs/rfcs/001-recent-commits.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/rfcs/001-recent-commits.md b/docs/rfcs/001-recent-commits.md index 461b16c9f7..12379cccac 100644 --- a/docs/rfcs/001-recent-commits.md +++ b/docs/rfcs/001-recent-commits.md @@ -80,7 +80,8 @@ The modal dialog on "hard reset" is disruptive considering that the lost changes ## Implementation phases -1. List read-only commit information. -2. Replace the amend checkbox with the "undo" control. -3. Context menu with actions. -4. Tooltip with action buttons and additional information. +1. Convert `GitTabController` and `GitTabView` to React. +2. List read-only commit information. +3. Replace the amend checkbox with the "undo" control. +4. Context menu with actions. +5. Tooltip with action buttons and additional information. From c4b96ed53cf5f88763dfb21683b5b78ef5c2f603 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Thu, 22 Feb 2018 22:31:21 -0800 Subject: [PATCH 0320/5882] First passing test :raised_hands: --- lib/controllers/git-tab-controller.js | 293 ++++++++++---------- lib/views/git-tab-view.js | 211 ++++++++------ test/controllers/git-tab-controller.test.js | 270 +++++++++--------- 3 files changed, 417 insertions(+), 357 deletions(-) diff --git a/lib/controllers/git-tab-controller.js b/lib/controllers/git-tab-controller.js index fe4f8da667..64ecea9f1f 100644 --- a/lib/controllers/git-tab-controller.js +++ b/lib/controllers/git-tab-controller.js @@ -1,39 +1,104 @@ -/** @jsx etch.dom */ -/* eslint react/no-unknown-property: "off" */ - import path from 'path'; -import etch from 'etch'; -import {Disposable} from 'event-kit'; + +import React from 'react'; +import PropTypes from 'prop-types'; +import {autobind} from 'core-decorators'; + +import yubikiri from 'yubikiri'; import GitTabView from '../views/git-tab-view'; -import ModelObserver from '../models/model-observer'; +import ObserveModelDecorator from '../decorators/observe-model'; import {nullBranch} from '../models/branch'; import {nullCommit} from '../models/commit'; -import {autobind} from 'core-decorators'; -import yubikiri from 'yubikiri'; -export default class GitTabController { +@ObserveModelDecorator({ + getModel: props => props.repository, + fetchData: (r, props) => { + return yubikiri({ + lastCommit: r.getLastCommit(), + isMerging: r.isMerging(), + isRebasing: r.isRebasing(), + isAmending: r.isAmending(), + hasUndoHistory: r.hasDiscardHistory(), + currentBranch: r.getCurrentBranch(), + unstagedChanges: r.getUnstagedChanges(), + stagedChanges: async query => { + const isAmending = await query.isAmending; + return isAmending ? r.getStagedChangesSinceParentCommit() : r.getStagedChanges(); + }, + mergeConflicts: r.getMergeConflicts(), + workingDirectoryPath: r.getWorkingDirectoryPath(), + mergeMessage: async query => { + const isMerging = await query.isMerging; + return isMerging ? r.getMergeMessage() : null; + }, + fetchInProgress: false, + }); + }, +}) +export default class GitTabController extends React.Component { static focus = { ...GitTabView.focus, }; - constructor(props) { - this.props = props; + static propTypes = { + repository: PropTypes.object.isRequired, + + lastCommit: PropTypes.object, + isMerging: PropTypes.bool, + isRebasing: PropTypes.bool, + isAmending: PropTypes.bool, + hasUndoHistory: PropTypes.bool, + currentBranch: PropTypes.object, + unstagedChanges: PropTypes.arrayOf(PropTypes.object), + stagedChanges: PropTypes.arrayOf(PropTypes.object), + mergeConflicts: PropTypes.arrayOf(PropTypes.object), + workingDirectoryPath: PropTypes.string, + mergeMessage: PropTypes.string, + fetchInProgress: PropTypes.bool, + + workspace: PropTypes.object.isRequired, + commandRegistry: PropTypes.object.isRequired, + grammars: PropTypes.object.isRequired, + resolutionProgress: PropTypes.object.isRequired, + notificationManager: PropTypes.object.isRequired, + config: PropTypes.object.isRequired, + project: PropTypes.object.isRequired, + tooltips: PropTypes.object.isRequired, + + confirm: PropTypes.func.isRequired, + ensureGitTab: PropTypes.func.isRequired, + refreshResolutionProgress: PropTypes.func.isRequired, + undoLastDiscard: PropTypes.func.isRequired, + discardWorkDirChangesForPaths: PropTypes.func.isRequired, + openFiles: PropTypes.func.isRequired, + didSelectFilePath: PropTypes.func.isRequired, + initializeRepo: PropTypes.func.isRequired, + }; + + static defaultProps = { + lastCommit: nullCommit, + isMerging: false, + isRebasing: false, + isAmending: false, + hasUndoHistory: false, + currentBranch: nullBranch, + unstagedChanges: [], + stagedChanges: [], + mergeConflicts: [], + workingDirectoryPath: '', + mergeMessage: null, + fetchInProgress: true, + }; + + constructor(props, context) { + super(props, context); + this.stagingOperationInProgress = false; this.lastFocus = GitTabView.focus.STAGING; - this.repositoryObserver = new ModelObserver({ - fetchData: this.fetchRepositoryData, - didUpdate: () => { - this.refreshResolutionProgress(false, false); - return this.update(); - }, - }); - this.repositoryObserver.setActiveModel(props.repository); - etch.initialize(this); - - this.element.addEventListener('focusin', this.rememberLastFocus); - this.subscriptions = new Disposable(() => this.element.removeEventListener('focusin', this.rememberLastFocus)); + // this.element.addEventListener('focusin', this.rememberLastFocus); + // this.subscriptions = new Disposable(() => this.element.removeEventListener('focusin', this.rememberLastFocus)); } serialize() { @@ -44,14 +109,23 @@ export default class GitTabController { } render() { - const modelData = this.repositoryObserver.getActiveModelData() || this.defaultRepositoryData(); - const hasUndoHistory = this.props.repository ? this.hasUndoHistory() : false; - const isAmending = this.isAmending(); return ( ); } - async update(props) { - const oldProps = this.props; - this.props = {...this.props, ...props}; - if (this.props.repository !== oldProps.repository) { - await this.repositoryObserver.setActiveModel(props.repository); - } - return etch.update(this); - } - - destroy() { - this.subscriptions.dispose(); - this.repositoryObserver.destroy(); - } - getTitle() { return 'Git'; } @@ -123,60 +183,6 @@ export default class GitTabController { return this.props.repository.getWorkingDirectoryPath(); } - getLastModelDataRefreshPromise() { - return this.repositoryObserver.getLastModelDataRefreshPromise(); - } - - getActiveRepository() { - return this.repositoryObserver.getActiveModel(); - } - - refreshModelData() { - return this.repositoryObserver.refreshModelData(); - } - - @autobind - fetchRepositoryData(repository) { - return yubikiri({ - lastCommit: repository.getLastCommit(), - isMerging: repository.isMerging(), - isRebasing: repository.isRebasing(), - currentBranch: repository.getCurrentBranch(), - unstagedChanges: repository.getUnstagedChanges(), - stagedChanges: this.fetchStagedChanges(repository), - mergeConflicts: repository.getMergeConflicts(), - workingDirectoryPath: repository.getWorkingDirectoryPath(), - mergeMessage: async query => { - const isMerging = await query.isMerging; - return isMerging ? repository.getMergeMessage() : null; - }, - fetchInProgress: Promise.resolve(false), - }); - } - - defaultRepositoryData() { - return { - lastCommit: nullCommit, - isMerging: false, - isRebasing: false, - currentBranch: nullBranch, - unstagedChanges: [], - stagedChanges: [], - mergeConflicts: [], - workingDirectoryPath: this.props.repository.getWorkingDirectoryPath(), - mergeMessage: null, - fetchInProgress: true, - }; - } - - fetchStagedChanges(repository) { - if (this.isAmending()) { - return repository.getStagedChangesSinceParentCommit(); - } else { - return repository.getStagedChanges(); - } - } - /* * Begin (but don't await) an async conflict-counting task for each merge conflict path that has no conflict * marker count yet. Omit any path that's already open in a TextEditor or that has already been counted. @@ -185,16 +191,16 @@ export default class GitTabController { * includeCounts - update marker counts for files that have been counted before */ refreshResolutionProgress(includeOpen, includeCounted) { - const data = this.repositoryObserver.getActiveModelData(); - if (!data) { + if (this.props.fetchInProgress) { return; } const openPaths = new Set( this.props.workspace.getTextEditors().map(editor => editor.getPath()), ); - for (let i = 0; i < data.mergeConflicts.length; i++) { - const conflictPath = path.join(data.workingDirectoryPath, data.mergeConflicts[i].filePath); + + for (let i = 0; i < this.props.mergeConflicts.length; i++) { + const conflictPath = path.join(this.props.workingDirectoryPath, this.props.mergeConflicts[i].filePath); if (!includeOpen && openPaths.has(conflictPath)) { continue; @@ -208,8 +214,9 @@ export default class GitTabController { } } + @autobind unstageFilePatch(filePatch) { - return this.getActiveRepository().applyPatchToIndex(filePatch.getUnstagePatch()); + return this.props.repository.applyPatchToIndex(filePatch.getUnstagePatch()); } @autobind @@ -228,6 +235,7 @@ export default class GitTabController { this.stagingOperationInProgress = true; + // FIXME move into state? const fileListUpdatePromise = this.refs.gitTab.refs.stagingView.getNextListUpdatePromise(); let stageOperationPromise; if (stageStatus === 'staged') { @@ -243,29 +251,37 @@ export default class GitTabController { } async stageFiles(filePaths) { - const pathsToIgnore = []; - const repository = this.getActiveRepository(); - for (const filePath of filePaths) { - if (await repository.pathHasMergeMarkers(filePath)) { // eslint-disable-line no-await-in-loop + const pathsToStage = new Set(filePaths); + + const mergeMarkers = await Promise.all( + filePaths.map(async filePath => { + return { + filePath, + hasMarkers: await this.props.repository.pathHasMergeMarkers(filePath), + }; + }), + ); + + for (const {filePath, hasMarkers} of mergeMarkers) { + if (hasMarkers) { const choice = this.props.confirm({ message: 'File contains merge markers: ', detailedMessage: `Do you still want to stage this file?\n${filePath}`, buttons: ['Stage', 'Cancel'], }); - if (choice !== 0) { pathsToIgnore.push(filePath); } + if (choice !== 0) { pathsToStage.delete(filePath); } } } - const pathsToStage = filePaths.filter(filePath => !pathsToIgnore.includes(filePath)); - return repository.stageFiles(pathsToStage); + + return this.props.repository.stageFiles(Array.from(pathsToStage)); } @autobind unstageFiles(filePaths) { - const repository = this.getActiveRepository(); - if (this.isAmending()) { - return repository.stageFilesFromParentCommit(filePaths); + if (this.props.isAmending) { + return this.props.repository.stageFilesFromParentCommit(filePaths); } else { - return repository.unstageFiles(filePaths); + return this.props.repository.unstageFiles(filePaths); } } @@ -276,7 +292,7 @@ export default class GitTabController { @autobind commit(message) { - return this.getActiveRepository().commit(message); + return this.props.repository.commit(message); } @autobind @@ -286,10 +302,10 @@ export default class GitTabController { detailedMessage: 'Are you sure?', buttons: ['Abort', 'Cancel'], }); - if (choice !== 0) { return null; } + if (choice !== 0) { return; } try { - await this.getActiveRepository().abortMerge(); + await this.props.repository.abortMerge(); } catch (e) { if (e.code === 'EDIRTYSTAGED') { this.props.notificationManager.addError( @@ -300,39 +316,33 @@ export default class GitTabController { throw e; } } - return etch.update(this); } @autobind async resolveAsOurs(paths) { - const data = this.repositoryObserver.getActiveModelData(); - if (!data) { + if (this.props.fetchInProgress) { return; } - const side = data.isRebasing ? 'theirs' : 'ours'; - await this.getActiveRepository().checkoutSide(side, paths); + + const side = this.props.isRebasing ? 'theirs' : 'ours'; + await this.props.repository.checkoutSide(side, paths); this.refreshResolutionProgress(false, true); } @autobind async resolveAsTheirs(paths) { - const data = this.repositoryObserver.getActiveModelData(); - if (!data) { + if (this.props.fetchInProgress) { return; } - const side = data.isRebasing ? 'ours' : 'theirs'; - await this.getActiveRepository().checkoutSide(side, paths); + + const side = this.props.isRebasing ? 'ours' : 'theirs'; + await this.props.repository.checkoutSide(side, paths); this.refreshResolutionProgress(false, true); } @autobind checkout(branchName, options) { - return this.getActiveRepository().checkout(branchName, options); - } - - @autobind - isAmending() { - return this.getActiveRepository().isAmending(); + return this.props.repository.checkout(branchName, options); } @autobind @@ -341,10 +351,12 @@ export default class GitTabController { } restoreFocus() { + // FIXME this.refs.gitTab.setFocus(this.lastFocus); } hasFocus() { + // FIXME return this.element.contains(document.activeElement); } @@ -362,9 +374,4 @@ export default class GitTabController { quietlySelectItem(filePath, stagingStatus) { return this.refs.gitTab.quietlySelectItem(filePath, stagingStatus); } - - @autobind - hasUndoHistory() { - return this.props.repository.hasDiscardHistory(); - } } diff --git a/lib/views/git-tab-view.js b/lib/views/git-tab-view.js index e6b3bb084b..f85440142a 100644 --- a/lib/views/git-tab-view.js +++ b/lib/views/git-tab-view.js @@ -1,40 +1,83 @@ -/** @jsx etch.dom */ -/* eslint react/no-unknown-property: "off" */ - -import etch from 'etch'; +import React from 'react'; +import PropTypes from 'prop-types'; import {autobind} from 'core-decorators'; import cx from 'classnames'; +import {CompositeDisposable} from 'atom'; import StagingView from './staging-view'; import GitLogo from './git-logo'; import CommitViewController from '../controllers/commit-view-controller'; +import EtchWrapper from './etch-wrapper'; import {isValidWorkdir} from '../helpers'; -export default class GitTabView { +export default class GitTabView extends React.Component { static focus = { ...StagingView.focus, ...CommitViewController.focus, - } + }; + + static propTypes = { + repository: PropTypes.object.isRequired, + isLoading: PropTypes.bool.isRequired, + + lastCommit: PropTypes.object.isRequired, + isMerging: PropTypes.bool, + isRebasing: PropTypes.bool, + isAmending: PropTypes.bool, + hasUndoHistory: PropTypes.bool, + currentBranch: PropTypes.object, + unstagedChanges: PropTypes.arrayOf(PropTypes.object), + stagedChanges: PropTypes.arrayOf(PropTypes.object), + mergeConflicts: PropTypes.arrayOf(PropTypes.object), + workingDirectoryPath: PropTypes.string, + mergeMessage: PropTypes.string, + fetchInProgress: PropTypes.bool, - constructor(props) { - this.props = props; - etch.initialize(this); + workspace: PropTypes.object.isRequired, + commandRegistry: PropTypes.object.isRequired, + grammars: PropTypes.object.isRequired, + resolutionProgress: PropTypes.object.isRequired, + notificationManager: PropTypes.object.isRequired, + config: PropTypes.object.isRequired, + project: PropTypes.object.isRequired, + tooltips: PropTypes.object.isRequired, - this.subscriptions = this.props.commandRegistry.add(this.element, { - 'tool-panel:unfocus': this.blur, - 'core:focus-next': this.advanceFocus, - 'core:focus-previous': this.retreatFocus, - }); + initializeRepo: PropTypes.func.isRequired, + abortMerge: PropTypes.func.isRequired, + commit: PropTypes.func.isRequired, + prepareToCommit: PropTypes.func.isRequired, + resolveAsOurs: PropTypes.func.isRequired, + resolveAsTheirs: PropTypes.func.isRequired, + undoLastDiscard: PropTypes.func.isRequired, + attemptStageAllOperation: PropTypes.func.isRequired, + attemptFileStageOperation: PropTypes.func.isRequired, + discardWorkDirChangesForPaths: PropTypes.func.isRequired, + openFiles: PropTypes.func.isRequired, + didSelectFilePath: PropTypes.func.isRequired, + }; + + constructor(props, context) { + super(props, context); + + this.subscriptions = new CompositeDisposable(); + + this.refRoot = null; + this.refStagingView = null; + this.refCommitViewComponent = null; } - update(props) { - this.props = props; - return etch.update(this); + componentDidMount() { + this.subscriptions.add( + this.props.commandRegistry.add(this.refRoot, { + 'tool-panel:unfocus': this.blur, + 'core:focus-next': this.advanceFocus, + 'core:focus-previous': this.retreatFocus, + }), + ); } render() { if (this.props.repository.isTooLarge()) { - const workingDir = this.props.repository.getWorkingDirectoryPath(); return (
@@ -43,7 +86,7 @@ export default class GitTabView {

Too many changes

- The repository at {workingDir} has too many changed files + The repository at {this.props.workingDirectoryPath} has too many changed files to display in Atom. Ensure that you have set up an appropriate .gitignore file.
@@ -68,7 +111,7 @@ export default class GitTabView { const inProgress = this.props.repository.showGitTabInitInProgress(); const message = this.props.repository.hasDirectory() ? ( - Initialize {this.props.repository.getWorkingDirectoryPath()} with a + Initialize {this.props.workingDirectoryPath} with a Git repository ) : Initialize a new project directory with a Git repository; @@ -80,7 +123,7 @@ export default class GitTabView {
{message}
-
@@ -90,61 +133,65 @@ export default class GitTabView { const isLoading = this.props.fetchInProgress || this.props.repository.showGitTabLoading(); return ( -
- - 0} - mergeConflictsExist={this.props.mergeConflicts.length > 0} - prepareToCommit={this.props.prepareToCommit} - commit={this.props.commit} - abortMerge={this.props.abortMerge} - branchName={this.props.branchName} - workspace={this.props.workspace} - commandRegistry={this.props.commandRegistry} - notificationManager={this.props.notificationManager} - grammars={this.props.grammars} - mergeMessage={this.props.mergeMessage} - isMerging={this.props.isMerging} - isAmending={this.props.isAmending} - isLoading={this.props.isLoading} - lastCommit={this.props.lastCommit} - repository={this.props.repository} - /> +
{ this.refRoot = c; }}> + { this.refStagingView = c; }}> + + + { this.refCommitViewController = c; }}> + 0} + mergeConflictsExist={this.props.mergeConflicts.length > 0} + prepareToCommit={this.props.prepareToCommit} + commit={this.props.commit} + abortMerge={this.props.abortMerge} + branchName={this.props.branchName} + workspace={this.props.workspace} + commandRegistry={this.props.commandRegistry} + notificationManager={this.props.notificationManager} + grammars={this.props.grammars} + mergeMessage={this.props.mergeMessage} + isMerging={this.props.isMerging} + isAmending={this.props.isAmending} + isLoading={this.props.isLoading} + lastCommit={this.props.lastCommit} + repository={this.props.repository} + /> +
); } } - destroy() { + componentWillUnmount() { this.subscriptions.dispose(); - return etch.destroy(this); } @autobind @@ -165,25 +212,25 @@ export default class GitTabView { let currentFocus = null; if (this.refs.stagingView) { - currentFocus = this.refs.stagingView.rememberFocus(event); + currentFocus = this.refStagingView.rememberFocus(event); } - if (!currentFocus && this.refs.commitViewController) { - currentFocus = this.refs.commitViewController.rememberFocus(event); + if (!currentFocus && this.refCommitViewController) { + currentFocus = this.refCommitViewController.rememberFocus(event); } return currentFocus; } setFocus(focus) { - if (this.refs.stagingView) { - if (this.refs.stagingView.setFocus(focus)) { + if (this.refStagingView) { + if (this.refStagingView.setFocus(focus)) { return true; } } - if (this.refs.commitViewController) { - if (this.refs.commitViewController.setFocus(focus)) { + if (this.refCommitViewController) { + if (this.refCommitViewController.setFocus(focus)) { return true; } } @@ -198,8 +245,8 @@ export default class GitTabView { @autobind advanceFocus(evt) { - if (!this.refs.stagingView.activateNextList()) { - if (this.refs.commitViewController.setFocus(GitTabView.focus.EDITOR)) { + if (!this.refStagingView.activateNextList()) { + if (this.refCommitViewController.setFocus(GitTabView.focus.EDITOR)) { evt.stopPropagation(); } } else { @@ -223,16 +270,16 @@ export default class GitTabView { } async focusAndSelectStagingItem(filePath, stagingStatus) { - await this.refs.stagingView.quietlySelectItem(filePath, stagingStatus); + await this.refStagingView.quietlySelectItem(filePath, stagingStatus); this.setFocus(GitTabView.focus.STAGING); } hasFocus() { - return this.element.contains(document.activeElement); + return this.refRoot.contains(document.activeElement); } @autobind quietlySelectItem(filePath, stagingStatus) { - return this.refs.stagingView.quietlySelectItem(filePath, stagingStatus); + return this.refStagingView.quietlySelectItem(filePath, stagingStatus); } } diff --git a/test/controllers/git-tab-controller.test.js b/test/controllers/git-tab-controller.test.js index a21e736fc9..2449a16fa1 100644 --- a/test/controllers/git-tab-controller.test.js +++ b/test/controllers/git-tab-controller.test.js @@ -1,6 +1,8 @@ import fs from 'fs'; import path from 'path'; -import etch from 'etch'; + +import React from 'react'; +import {mount} from 'enzyme'; import dedent from 'dedent-js'; import until from 'test-until'; @@ -16,6 +18,7 @@ import ResolutionProgress from '../../lib/models/conflicts/resolution-progress'; describe('GitTabController', function() { let atomEnvironment, workspace, workspaceElement, commandRegistry, notificationManager, config, tooltips; let resolutionProgress, refreshResolutionProgress; + let app; beforeEach(function() { atomEnvironment = global.buildAtomEnvironment(); @@ -29,46 +32,61 @@ describe('GitTabController', function() { resolutionProgress = new ResolutionProgress(); refreshResolutionProgress = sinon.spy(); + + const noop = () => {}; + + app = ( + + ); }); afterEach(function() { atomEnvironment.destroy(); }); - it('displays a loading message in GitTabView while data is being fetched', async function() { + it.only('displays a loading message in GitTabView while data is being fetched', async function() { const workdirPath = await cloneRepository('three-files'); fs.writeFileSync(path.join(workdirPath, 'a.txt'), 'a change\n'); fs.unlinkSync(path.join(workdirPath, 'b.txt')); const repository = new Repository(workdirPath); assert.isTrue(repository.isLoading()); - const controller = new GitTabController({ - workspace, commandRegistry, tooltips, config, repository, - resolutionProgress, refreshResolutionProgress, - }); - - assert.strictEqual(controller.getActiveRepository(), repository); - assert.isTrue(controller.element.classList.contains('is-loading')); - assert.lengthOf(controller.element.querySelectorAll('.github-StagingView'), 1); - assert.lengthOf(controller.element.querySelectorAll('.github-CommitView'), 1); + app = React.cloneElement(app, {repository}); + const wrapper = mount(app); - await repository.getLoadPromise(); + assert.isTrue(wrapper.hasClass('is-loading')); + assert.lengthOf(wrapper.find('EtchWrapper'), 2); - await assert.async.isFalse(controller.element.classList.contains('is-loading')); - assert.lengthOf(controller.element.querySelectorAll('.github-StagingView'), 1); - assert.lengthOf(controller.element.querySelectorAll('.github-CommitView'), 1); + await assert.async.isFalse(wrapper.hasClass('is-loading')); + assert.lengthOf(wrapper.find('EtchWrapper'), 2); }); it('displays an initialization prompt for an absent repository', function() { const repository = Repository.absent(); - const controller = new GitTabController({ - workspace, commandRegistry, tooltips, config, repository, - resolutionProgress, refreshResolutionProgress, - }); + app = React.cloneElement(app, {repository}); + const wrapper = mount(app); - assert.isTrue(controller.element.classList.contains('is-empty')); - assert.isNotNull(controller.refs.noRepoMessage); + assert.isTrue(wrapper.hasClass('is-empty')); + assert.lengthOf(wrapper.find('.no-repository'), 1); }); it('keeps the state of the GitTabView in sync with the assigned repository', async function() { @@ -79,53 +97,52 @@ describe('GitTabController', function() { fs.writeFileSync(path.join(workdirPath1, 'a.txt'), 'a change\n'); fs.unlinkSync(path.join(workdirPath1, 'b.txt')); - const controller = new GitTabController({ - workspace, commandRegistry, tooltips, config, resolutionProgress, refreshResolutionProgress, - repository: Repository.absent(), - }); + + app = React.cloneElement(app, {repository: Repository.absent()}); + const wrapper = mount(app); // Renders empty GitTabView when there is no active repository - assert.isDefined(controller.refs.gitTab); - assert.isTrue(controller.getActiveRepository().isAbsent()); - assert.isDefined(controller.refs.gitTab.refs.noRepoMessage); + + assert.isTrue(wrapper.instance().getActiveRepository().isAbsent()); + assert.lengthOf(wrapper.find('.no-repository'), 1); // Fetches data when a new repository is assigned // Does not update repository instance variable until that data is fetched - await controller.update({repository: repository1}); - assert.equal(controller.getActiveRepository(), repository1); - assert.deepEqual(controller.refs.gitTab.props.unstagedChanges, await repository1.getUnstagedChanges()); + wrapper.setProps({repository: repository1}); + assert.strictEqual(wrapper.instance().getActiveRepository(), repository1); + assert.deepEqual(wrapper.find('GitTabView').prop('unstagedChanges'), await repository1.getUnstagedChanges()); - await controller.update({repository: repository2}); - assert.equal(controller.getActiveRepository(), repository2); - assert.deepEqual(controller.refs.gitTab.props.unstagedChanges, await repository2.getUnstagedChanges()); + wrapper.setProps({repository: repository2}); + assert.strictEqual(wrapper.instance().getActiveRepository(), repository2); + assert.deepEqual(wrapper.find('GitTabView').prop('unstagedChanges'), await repository2.getUnstagedChanges()); // Fetches data and updates child view when the repository is mutated fs.writeFileSync(path.join(workdirPath2, 'a.txt'), 'a change\n'); fs.unlinkSync(path.join(workdirPath2, 'b.txt')); repository2.refresh(); - await controller.getLastModelDataRefreshPromise(); - await assert.async.deepEqual(controller.refs.gitTab.props.unstagedChanges, await repository2.getUnstagedChanges()); + + await assert.async.deepEqual(wrapper.find('GitTabView').prop('unstagedChanges'), await repository2.getUnstagedChanges()); }); it('displays the staged changes since the parent commit when amending', async function() { const workdirPath = await cloneRepository('multiple-commits'); const repository = await buildRepository(workdirPath); const ensureGitTab = () => Promise.resolve(false); - const controller = new GitTabController({ - workspace, commandRegistry, tooltips, config, repository, ensureGitTab, - resolutionProgress, refreshResolutionProgress, + + app = React.cloneElement(app, { + repository, + ensureGitTab, isAmending: false, }); - await controller.getLastModelDataRefreshPromise(); - await assert.async.deepEqual(controller.refs.gitTab.props.stagedChanges, []); + const wrapper = mount(app); + + await assert.async.deepEqual(wrapper.find('GitTabView').prop('unstagedChanges'), []); await repository.setAmending(true); await assert.async.deepEqual( - controller.refs.gitTab.props.stagedChanges, - await controller.getActiveRepository().getStagedChangesSinceParentCommit(), + wrapper.find('GitTabView').prop('stagedChanges'), + await wrapper.instance().getActiveRepository().getStagedChangesSinceParentCommit(), ); - - await controller.commit('Delete most of the code', {amend: true}); }); it('fetches conflict marker counts for conflicting files', async function() { @@ -133,13 +150,14 @@ describe('GitTabController', function() { const repository = await buildRepository(workdirPath); await assert.isRejected(repository.git.merge('origin/branch')); - const rp = new ResolutionProgress(); - rp.reportMarkerCount(path.join(workdirPath, 'added-to-both.txt'), 5); + const resolutionProgress = new ResolutionProgress(); + resolutionProgress.reportMarkerCount(path.join(workdirPath, 'added-to-both.txt'), 5); - const controller = new GitTabController({ - workspace, commandRegistry, tooltips, config, repository, resolutionProgress: rp, refreshResolutionProgress, + app = React.cloneElement(app, { + repository, + resolutionProgress, }); - await controller.getLastModelDataRefreshPromise(); + const wrapper = mount(app); await assert.async.isTrue(refreshResolutionProgress.calledWith(path.join(workdirPath, 'modified-on-both-ours.txt'))); assert.isTrue(refreshResolutionProgress.calledWith(path.join(workdirPath, 'modified-on-both-theirs.txt'))); @@ -154,10 +172,9 @@ describe('GitTabController', function() { await assert.isRejected(repository.git.merge('origin/branch')); const confirm = sinon.stub(); - const controller = new GitTabController({ - workspace, commandRegistry, tooltips, config, confirm, repository, - resolutionProgress, refreshResolutionProgress, - }); + app = React.cloneElement(app, {repository}); + const wrapper = mount(app); + await controller.getLastModelDataRefreshPromise(); let modelData = controller.repositoryObserver.getActiveModelData(); @@ -166,9 +183,9 @@ describe('GitTabController', function() { assert.isOk(modelData.mergeMessage); confirm.returns(0); - await controller.abortMerge(); + await wrapper.instance().abortMerge(); await until(() => { - modelData = controller.repositoryObserver.getActiveModelData(); + modelData = wrapper.instance().repositoryObserver.getActiveModelData(); return modelData.mergeConflicts.length === 0; }); assert.isFalse(modelData.isMerging); @@ -182,12 +199,13 @@ describe('GitTabController', function() { const repository = await buildRepository(workdirPath); const ensureGitTab = () => Promise.resolve(true); - const controller = new GitTabController({ - workspace, commandRegistry, tooltips, config, repository, ensureGitTab, - resolutionProgress, refreshResolutionProgress, + app = React.cloneElement(app, { + repository, + ensureGitTab, }); + const wrapper = mount(app); - assert.isFalse(await controller.prepareToCommit()); + assert.isFalse(await wrapper.instance().prepareToCommit()); }); it('returns true if the git panel was already visible', async function() { @@ -195,12 +213,13 @@ describe('GitTabController', function() { const repository = await buildRepository(workdirPath); const ensureGitTab = () => Promise.resolve(false); - const controller = new GitTabController({ - workspace, commandRegistry, tooltips, config, repository, ensureGitTab, - resolutionProgress, refreshResolutionProgress, + app = React.cloneElement(app, { + repository, + ensureGitTab, }); + const wrapper = mount(app); - assert.isTrue(await controller.prepareToCommit()); + assert.isTrue(await wrapper.instance().prepareToCommit()); }); }); @@ -213,13 +232,14 @@ describe('GitTabController', function() { throw new GitError('message'); }); - const controller = new GitTabController({ - workspace, commandRegistry, tooltips, config, notificationManager, repository, - resolutionProgress, refreshResolutionProgress, + app = React.cloneElement(app, { + repository, }); + const wrapper = mount(app); + notificationManager.clear(); // clear out any notifications try { - await controller.commit(); + await wrapper.instance().commit(); } catch (e) { assert(e, 'is error'); } @@ -232,13 +252,11 @@ describe('GitTabController', function() { repository.setAmending(true); sinon.stub(repository.git, 'commit').callsFake(() => Promise.resolve()); const didChangeAmending = sinon.stub(); - const controller = new GitTabController({ - workspace, commandRegistry, tooltips, config, repository, didChangeAmending, - resolutionProgress, refreshResolutionProgress, - }); + app = React.cloneElement(app, {repository}); + const wrapper = mount(app); assert.isTrue(repository.isAmending()); - await controller.commit('message'); + await wrapper.instance().commit('message'); assert.isFalse(repository.isAmending()); }); }); @@ -252,22 +270,21 @@ describe('GitTabController', function() { fs.writeFileSync(path.join(workdirPath, 'unstaged-3.txt'), 'This is an unstaged file.'); repository.refresh(); - const controller = new GitTabController({ - workspace, commandRegistry, tooltips, config, repository, - resolutionProgress, refreshResolutionProgress, - }); - await controller.getLastModelDataRefreshPromise(); - await etch.getScheduler().getNextUpdatePromise(); + app = React.cloneElement(app, {repository}); + const wrapper = mount(app); - const gitTab = controller.refs.gitTab; + await wrapper.instance().getLastModelDataRefreshPromise(); + wrapper.update(); + + const gitTab = wrapper.instance().refs.gitTab; const stagingView = gitTab.refs.stagingView; sinon.spy(stagingView, 'setFocus'); - await controller.focusAndSelectStagingItem('unstaged-2.txt', 'unstaged'); + await wrapper.instance().focusAndSelectStagingItem('unstaged-2.txt', 'unstaged'); const selections = Array.from(stagingView.selection.getSelectedItems()); - assert.equal(selections.length, 1); + assert.lengthOf(selections, 1); assert.equal(selections[0].filePath, 'unstaged-2.txt'); assert.equal(stagingView.setFocus.callCount, 1); @@ -277,27 +294,23 @@ describe('GitTabController', function() { it('does nothing on an absent repository', function() { const repository = Repository.absent(); - const controller = new GitTabController({ - workspace, commandRegistry, tooltips, config, repository, - resolutionProgress, refreshResolutionProgress, - }); - - assert.isTrue(controller.element.classList.contains('is-empty')); - assert.isNotNull(controller.refs.noRepoMessage); + app = React.cloneElement(app, {repository}); + const wrapper = mount(app); - controller.rememberLastFocus({target: controller.element}); - assert.strictEqual(controller.lastFocus, GitTabController.focus.STAGING); + assert.isTrue(wrapper.hasClass('is-empty')); + assert.lenghtOf(wrapper.find('.no-repository'), 1); - controller.restoreFocus(); + wrapper.instance().rememberLastFocus({target: null}); + assert.strictEqual(wrapper.instance().lastFocus, GitTabController.focus.STAGING); }); }); describe('keyboard navigation commands', function() { - let controller, gitTab, stagingView, commitView, commitViewController, focusElement; + let wrapper, gitTab, stagingView, commitView, commitViewController, focusElement; const focuses = GitTabController.focus; const extractReferences = () => { - gitTab = controller.refs.gitTab; + gitTab = wrapper.instance().refs.gitTab; stagingView = gitTab.refs.stagingView; commitViewController = gitTab.refs.commitViewController; commitView = commitViewController.refs.commitView; @@ -353,13 +366,9 @@ describe('GitTabController', function() { const didChangeAmending = () => {}; - controller = new GitTabController({ - workspace, commandRegistry, tooltips, config, repository, - resolutionProgress, refreshResolutionProgress, - didChangeAmending, - }); - await controller.getLastModelDataRefreshPromise(); - await etch.getScheduler().getNextUpdatePromise(); + app = React.cloneElement(app, {repository}); + wrapper = mount(app); + await wrapper.instance().getLastModelDataRefreshPromise(); extractReferences(); }); @@ -367,7 +376,7 @@ describe('GitTabController', function() { it('blurs on tool-panel:unfocus', function() { sinon.spy(workspace.getActivePane(), 'activate'); - commandRegistry.dispatch(controller.element, 'tool-panel:unfocus'); + commandRegistry.dispatch(wrapper.getNode(), 'tool-panel:unfocus'); assert.isTrue(workspace.getActivePane().activate.called); }); @@ -375,18 +384,18 @@ describe('GitTabController', function() { it('advances focus through StagingView groups and CommitView, but does not cycle', function() { assertSelected(['unstaged-1.txt']); - commandRegistry.dispatch(controller.element, 'core:focus-next'); + commandRegistry.dispatch(wrapper.getNode(), 'core:focus-next'); assertSelected(['conflict-1.txt']); - commandRegistry.dispatch(controller.element, 'core:focus-next'); + commandRegistry.dispatch(wrapper.getNode(), 'core:focus-next'); assertSelected(['staged-1.txt']); - commandRegistry.dispatch(controller.element, 'core:focus-next'); + commandRegistry.dispatch(wrapper.getNode(), 'core:focus-next'); assertSelected(['staged-1.txt']); assert.strictEqual(focusElement, commitView.editorElement); // This should be a no-op. (Actually, it'll insert a tab in the CommitView editor.) - commandRegistry.dispatch(controller.element, 'core:focus-next'); + commandRegistry.dispatch(wrapper.getNode(), 'core:focus-next'); assertSelected(['staged-1.txt']); assert.strictEqual(focusElement, commitView.editorElement); }); @@ -394,17 +403,17 @@ describe('GitTabController', function() { it('retreats focus from the CommitView through StagingView groups, but does not cycle', function() { gitTab.setFocus(focuses.EDITOR); - commandRegistry.dispatch(controller.element, 'core:focus-previous'); + commandRegistry.dispatch(wrapper.getNode(), 'core:focus-previous'); assertSelected(['staged-1.txt']); - commandRegistry.dispatch(controller.element, 'core:focus-previous'); + commandRegistry.dispatch(wrapper.getNode(), 'core:focus-previous'); assertSelected(['conflict-1.txt']); - commandRegistry.dispatch(controller.element, 'core:focus-previous'); + commandRegistry.dispatch(wrapper.getNode(), 'core:focus-previous'); assertSelected(['unstaged-1.txt']); // This should be a no-op. - commandRegistry.dispatch(controller.element, 'core:focus-previous'); + commandRegistry.dispatch(wrapper.getNode(), 'core:focus-previous'); assertSelected(['unstaged-1.txt']); }); }); @@ -423,35 +432,32 @@ describe('GitTabController', function() { const prepareToCommit = () => Promise.resolve(true); const ensureGitTab = () => Promise.resolve(false); - controller = new GitTabController({ - workspace, commandRegistry, tooltips, config, repository, didChangeAmending, prepareToCommit, ensureGitTab, - resolutionProgress, refreshResolutionProgress, - }); - await controller.getLastModelDataRefreshPromise(); - await etch.getScheduler().getNextUpdatePromise(); + app = React.cloneElement(app, {repository, ensureGitTab, prepareToCommit, didChangeAmending}); + wrapper = mount(app); + await wrapper.instance().getLastModelDataRefreshPromise(); extractReferences(); }); it('focuses the CommitView on github:commit with an empty commit message', async function() { commitView.editor.setText(''); - sinon.spy(controller, 'commit'); - await etch.update(controller); // Ensure that the spy is passed to child components in props + sinon.spy(wrapper.instance(), 'commit'); + wrapper.update(); commandRegistry.dispatch(workspaceElement, 'github:commit'); await assert.async.strictEqual(focusElement, commitView.editorElement); - assert.isFalse(controller.commit.called); + assert.isFalse(wrapper.instance().commit.called); }); it('creates a commit on github:commit with a nonempty commit message', async function() { commitView.editor.setText('I fixed the things'); - sinon.spy(controller, 'commit'); - await etch.update(controller); // Ensure that the spy is passed to child components in props + sinon.spy(wrapper.instance(), 'commit'); + wrapper.update(); commandRegistry.dispatch(workspaceElement, 'github:commit'); - await until('Commit method called', () => controller.commit.calledWith('I fixed the things')); + await until('Commit method called', () => wrapper.instance().commit.calledWith('I fixed the things')); }); }); }); @@ -463,17 +469,17 @@ describe('GitTabController', function() { fs.writeFileSync(path.join(workdirPath, 'a.txt'), 'a change\n'); fs.unlinkSync(path.join(workdirPath, 'b.txt')); const ensureGitTab = () => Promise.resolve(false); - const controller = new GitTabController({ - workspace, commandRegistry, tooltips, config, repository, ensureGitTab, didChangeAmending: sinon.stub(), - resolutionProgress, refreshResolutionProgress, - }); - await controller.getLastModelDataRefreshPromise(); - await etch.getScheduler().getNextUpdatePromise(); - const stagingView = controller.refs.gitTab.refs.stagingView; - const commitView = controller.refs.gitTab.refs.commitViewController.refs.commitView; - assert.lengthOf(stagingView.props.unstagedChanges, 2); - assert.lengthOf(stagingView.props.stagedChanges, 0); + app = React.cloneElement(app, {repository, ensureGitTab}); + const wrapper = mount(app); + + await wrapper.instance().getLastModelDataRefreshPromise(); + + const stagingView = wrapper.ref('gitTab').refs.stagingView; + const commitView = controller.ref('gitTab').refs.commitViewController.refs.commitView; + + assert.lengthOf(stagingView.prop('unstagedChanges'), 2); + assert.lengthOf(stagingView.prop('stagedChanges'), 0); stagingView.dblclickOnItem({}, stagingView.props.unstagedChanges[0]); From 5205cc211106fdecef0e6167654f92a5c2b78277 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 23 Feb 2018 10:56:45 -0800 Subject: [PATCH 0321/5882] Port GitTabController and GitTabView to React --- lib/controllers/git-tab-controller.js | 32 ++-- lib/views/git-logo.js | 19 +-- lib/views/git-tab-view.js | 21 +-- test/controllers/git-tab-controller.test.js | 177 +++++++++----------- 4 files changed, 119 insertions(+), 130 deletions(-) diff --git a/lib/controllers/git-tab-controller.js b/lib/controllers/git-tab-controller.js index 64ecea9f1f..d731b3a9e0 100644 --- a/lib/controllers/git-tab-controller.js +++ b/lib/controllers/git-tab-controller.js @@ -97,8 +97,7 @@ export default class GitTabController extends React.Component { this.stagingOperationInProgress = false; this.lastFocus = GitTabView.focus.STAGING; - // this.element.addEventListener('focusin', this.rememberLastFocus); - // this.subscriptions = new Disposable(() => this.element.removeEventListener('focusin', this.rememberLastFocus)); + this.refView = null; } serialize() { @@ -111,6 +110,8 @@ export default class GitTabController extends React.Component { render() { return ( { this.refView = c; }} + isLoading={this.props.fetchInProgress} repository={this.props.repository} @@ -159,6 +160,18 @@ export default class GitTabController extends React.Component { ); } + componentDidMount() { + this.refView.refRoot.addEventListener('focusin', this.rememberLastFocus); + } + + componentWillReceiveProps(newProps) { + this.refreshResolutionProgress(false, false); + } + + componentWillUnmount() { + this.refView.refRoot.removeEventListener('focusin', this.rememberLastFocus); + } + getTitle() { return 'Git'; } @@ -188,7 +201,7 @@ export default class GitTabController extends React.Component { * marker count yet. Omit any path that's already open in a TextEditor or that has already been counted. * * includeOpen - update marker counts for files that are currently open in TextEditors - * includeCounts - update marker counts for files that have been counted before + * includeCounted - update marker counts for files that have been counted before */ refreshResolutionProgress(includeOpen, includeCounted) { if (this.props.fetchInProgress) { @@ -235,8 +248,7 @@ export default class GitTabController extends React.Component { this.stagingOperationInProgress = true; - // FIXME move into state? - const fileListUpdatePromise = this.refs.gitTab.refs.stagingView.getNextListUpdatePromise(); + const fileListUpdatePromise = this.refView.refStagingView.getWrappedComponent().getNextListUpdatePromise(); let stageOperationPromise; if (stageStatus === 'staged') { stageOperationPromise = this.unstageFiles(filePaths); @@ -347,17 +359,15 @@ export default class GitTabController extends React.Component { @autobind rememberLastFocus(event) { - this.lastFocus = this.refs.gitTab.rememberFocus(event) || GitTabView.focus.STAGING; + this.lastFocus = this.refView.rememberFocus(event) || GitTabView.focus.STAGING; } restoreFocus() { - // FIXME - this.refs.gitTab.setFocus(this.lastFocus); + this.refView.setFocus(this.lastFocus); } hasFocus() { - // FIXME - return this.element.contains(document.activeElement); + return this.refView.refRoot.contains(document.activeElement); } wasActivated(isStillActive) { @@ -367,7 +377,7 @@ export default class GitTabController extends React.Component { } focusAndSelectStagingItem(filePath, stagingStatus) { - return this.refs.gitTab.focusAndSelectStagingItem(filePath, stagingStatus); + return this.refView.focusAndSelectStagingItem(filePath, stagingStatus); } @autobind diff --git a/lib/views/git-logo.js b/lib/views/git-logo.js index 3af325c6f3..51012c5618 100644 --- a/lib/views/git-logo.js +++ b/lib/views/git-logo.js @@ -1,24 +1,17 @@ -/** @jsx etch.dom */ -import etch from 'etch'; - -export default class GitLogo { - constructor() { - etch.initialize(this); - } - - update() {} +import React from 'react'; +export default class GitLogo extends React.Component { render() { - /* eslint-disable */ + /* eslint-disable max-len */ return ( - + - ) - /* eslint-enable */ + ); + /* eslint-enable max-len */ } } diff --git a/lib/views/git-tab-view.js b/lib/views/git-tab-view.js index f85440142a..99549dbd11 100644 --- a/lib/views/git-tab-view.js +++ b/lib/views/git-tab-view.js @@ -79,7 +79,7 @@ export default class GitTabView extends React.Component { render() { if (this.props.repository.isTooLarge()) { return ( -
+
{ this.refRoot = c; }}>
@@ -95,7 +95,7 @@ export default class GitTabView extends React.Component { } else if (this.props.repository.hasDirectory() && !isValidWorkdir(this.props.repository.getWorkingDirectoryPath())) { return ( -
+
{ this.refRoot = c; }}>
@@ -117,7 +117,7 @@ export default class GitTabView extends React.Component { Initialize a new project directory with a Git repository; return ( -
+
{ this.refRoot = c; }}>
@@ -224,13 +224,13 @@ export default class GitTabView extends React.Component { setFocus(focus) { if (this.refStagingView) { - if (this.refStagingView.setFocus(focus)) { + if (this.refStagingView.getWrappedComponent().setFocus(focus)) { return true; } } if (this.refCommitViewController) { - if (this.refCommitViewController.setFocus(focus)) { + if (this.refCommitViewController.getWrappedComponent().setFocus(focus)) { return true; } } @@ -245,8 +245,8 @@ export default class GitTabView extends React.Component { @autobind advanceFocus(evt) { - if (!this.refStagingView.activateNextList()) { - if (this.refCommitViewController.setFocus(GitTabView.focus.EDITOR)) { + if (!this.refStagingView.getWrappedComponent().activateNextList()) { + if (this.refCommitViewController.getWrappedComponent().setFocus(GitTabView.focus.EDITOR)) { evt.stopPropagation(); } } else { @@ -256,7 +256,8 @@ export default class GitTabView extends React.Component { @autobind retreatFocus(evt) { - const {stagingView, commitViewController} = this.refs; + const stagingView = this.refStagingView.getWrappedComponent(); + const commitViewController = this.refCommitViewController.getWrappedComponent(); if (commitViewController.hasFocus()) { if (stagingView.activateLastList()) { @@ -270,7 +271,7 @@ export default class GitTabView extends React.Component { } async focusAndSelectStagingItem(filePath, stagingStatus) { - await this.refStagingView.quietlySelectItem(filePath, stagingStatus); + await this.refStagingView.getWrappedComponent().quietlySelectItem(filePath, stagingStatus); this.setFocus(GitTabView.focus.STAGING); } @@ -280,6 +281,6 @@ export default class GitTabView extends React.Component { @autobind quietlySelectItem(filePath, stagingStatus) { - return this.refStagingView.quietlySelectItem(filePath, stagingStatus); + return this.refStagingView.getWrappedComponent().quietlySelectItem(filePath, stagingStatus); } } diff --git a/test/controllers/git-tab-controller.test.js b/test/controllers/git-tab-controller.test.js index 2449a16fa1..7d157afcee 100644 --- a/test/controllers/git-tab-controller.test.js +++ b/test/controllers/git-tab-controller.test.js @@ -62,7 +62,7 @@ describe('GitTabController', function() { atomEnvironment.destroy(); }); - it.only('displays a loading message in GitTabView while data is being fetched', async function() { + it('displays a loading message in GitTabView while data is being fetched', async function() { const workdirPath = await cloneRepository('three-files'); fs.writeFileSync(path.join(workdirPath, 'a.txt'), 'a change\n'); fs.unlinkSync(path.join(workdirPath, 'b.txt')); @@ -103,18 +103,16 @@ describe('GitTabController', function() { // Renders empty GitTabView when there is no active repository - assert.isTrue(wrapper.instance().getActiveRepository().isAbsent()); + assert.isTrue(wrapper.prop('repository').isAbsent()); assert.lengthOf(wrapper.find('.no-repository'), 1); // Fetches data when a new repository is assigned // Does not update repository instance variable until that data is fetched wrapper.setProps({repository: repository1}); - assert.strictEqual(wrapper.instance().getActiveRepository(), repository1); - assert.deepEqual(wrapper.find('GitTabView').prop('unstagedChanges'), await repository1.getUnstagedChanges()); + await assert.async.deepEqual(wrapper.find('GitTabView').prop('unstagedChanges'), await repository1.getUnstagedChanges()); wrapper.setProps({repository: repository2}); - assert.strictEqual(wrapper.instance().getActiveRepository(), repository2); - assert.deepEqual(wrapper.find('GitTabView').prop('unstagedChanges'), await repository2.getUnstagedChanges()); + await assert.async.deepEqual(wrapper.find('GitTabView').prop('unstagedChanges'), await repository2.getUnstagedChanges()); // Fetches data and updates child view when the repository is mutated fs.writeFileSync(path.join(workdirPath2, 'a.txt'), 'a change\n'); @@ -141,7 +139,7 @@ describe('GitTabController', function() { await repository.setAmending(true); await assert.async.deepEqual( wrapper.find('GitTabView').prop('stagedChanges'), - await wrapper.instance().getActiveRepository().getStagedChangesSinceParentCommit(), + await repository.getStagedChangesSinceParentCommit(), ); }); @@ -150,14 +148,14 @@ describe('GitTabController', function() { const repository = await buildRepository(workdirPath); await assert.isRejected(repository.git.merge('origin/branch')); - const resolutionProgress = new ResolutionProgress(); - resolutionProgress.reportMarkerCount(path.join(workdirPath, 'added-to-both.txt'), 5); + const rp = new ResolutionProgress(); + rp.reportMarkerCount(path.join(workdirPath, 'added-to-both.txt'), 5); app = React.cloneElement(app, { repository, - resolutionProgress, + resolutionProgress: rp, }); - const wrapper = mount(app); + mount(app); await assert.async.isTrue(refreshResolutionProgress.calledWith(path.join(workdirPath, 'modified-on-both-ours.txt'))); assert.isTrue(refreshResolutionProgress.calledWith(path.join(workdirPath, 'modified-on-both-theirs.txt'))); @@ -172,24 +170,20 @@ describe('GitTabController', function() { await assert.isRejected(repository.git.merge('origin/branch')); const confirm = sinon.stub(); - app = React.cloneElement(app, {repository}); + app = React.cloneElement(app, {repository, confirm}); const wrapper = mount(app); + const view = wrapper.find('GitTabView'); - await controller.getLastModelDataRefreshPromise(); - let modelData = controller.repositoryObserver.getActiveModelData(); - - assert.notEqual(modelData.mergeConflicts.length, 0); - assert.isTrue(modelData.isMerging); - assert.isOk(modelData.mergeMessage); + await assert.async.isTrue(view.prop('isMerging')); + assert.notEqual(view.prop('mergeConflicts').length, 0); + assert.isOk(view.prop('mergeMessage')); confirm.returns(0); - await wrapper.instance().abortMerge(); - await until(() => { - modelData = wrapper.instance().repositoryObserver.getActiveModelData(); - return modelData.mergeConflicts.length === 0; - }); - assert.isFalse(modelData.isMerging); - assert.isNull(modelData.mergeMessage); + await wrapper.instance().getWrappedComponentInstance().abortMerge(); + + await assert.async.lengthOf(view.prop('mergeConflicts'), 0); + assert.isFalse(view.prop('isMerging')); + assert.isNull(view.prop('mergeMessage')); }); }); @@ -199,13 +193,10 @@ describe('GitTabController', function() { const repository = await buildRepository(workdirPath); const ensureGitTab = () => Promise.resolve(true); - app = React.cloneElement(app, { - repository, - ensureGitTab, - }); + app = React.cloneElement(app, {repository, ensureGitTab}); const wrapper = mount(app); - assert.isFalse(await wrapper.instance().prepareToCommit()); + assert.isFalse(await wrapper.instance().getWrappedComponentInstance().prepareToCommit()); }); it('returns true if the git panel was already visible', async function() { @@ -213,13 +204,10 @@ describe('GitTabController', function() { const repository = await buildRepository(workdirPath); const ensureGitTab = () => Promise.resolve(false); - app = React.cloneElement(app, { - repository, - ensureGitTab, - }); + app = React.cloneElement(app, {repository, ensureGitTab}); const wrapper = mount(app); - assert.isTrue(await wrapper.instance().prepareToCommit()); + assert.isTrue(await wrapper.instance().getWrappedComponentInstance().prepareToCommit()); }); }); @@ -232,14 +220,12 @@ describe('GitTabController', function() { throw new GitError('message'); }); - app = React.cloneElement(app, { - repository, - }); + app = React.cloneElement(app, {repository}); const wrapper = mount(app); notificationManager.clear(); // clear out any notifications try { - await wrapper.instance().commit(); + await wrapper.instance().getWrappedComponentInstance().commit(); } catch (e) { assert(e, 'is error'); } @@ -252,11 +238,12 @@ describe('GitTabController', function() { repository.setAmending(true); sinon.stub(repository.git, 'commit').callsFake(() => Promise.resolve()); const didChangeAmending = sinon.stub(); - app = React.cloneElement(app, {repository}); + + app = React.cloneElement(app, {repository, didChangeAmending}); const wrapper = mount(app); assert.isTrue(repository.isAmending()); - await wrapper.instance().commit('message'); + await wrapper.instance().getWrappedComponentInstance().commit('message'); assert.isFalse(repository.isAmending()); }); }); @@ -273,15 +260,15 @@ describe('GitTabController', function() { app = React.cloneElement(app, {repository}); const wrapper = mount(app); - await wrapper.instance().getLastModelDataRefreshPromise(); - wrapper.update(); + await assert.async.lengthOf(wrapper.find('GitTabView').prop('unstagedChanges'), 3); - const gitTab = wrapper.instance().refs.gitTab; - const stagingView = gitTab.refs.stagingView; + const controller = wrapper.instance().getWrappedComponentInstance(); + const gitTab = controller.refView; + const stagingView = gitTab.refStagingView.getWrappedComponent(); sinon.spy(stagingView, 'setFocus'); - await wrapper.instance().focusAndSelectStagingItem('unstaged-2.txt', 'unstaged'); + await controller.focusAndSelectStagingItem('unstaged-2.txt', 'unstaged'); const selections = Array.from(stagingView.selection.getSelectedItems()); assert.lengthOf(selections, 1); @@ -296,12 +283,13 @@ describe('GitTabController', function() { app = React.cloneElement(app, {repository}); const wrapper = mount(app); + const controller = wrapper.instance().getWrappedComponentInstance(); assert.isTrue(wrapper.hasClass('is-empty')); - assert.lenghtOf(wrapper.find('.no-repository'), 1); + assert.lengthOf(wrapper.find('.no-repository'), 1); - wrapper.instance().rememberLastFocus({target: null}); - assert.strictEqual(wrapper.instance().lastFocus, GitTabController.focus.STAGING); + controller.rememberLastFocus({target: null}); + assert.strictEqual(controller.lastFocus, GitTabController.focus.STAGING); }); }); @@ -310,9 +298,9 @@ describe('GitTabController', function() { const focuses = GitTabController.focus; const extractReferences = () => { - gitTab = wrapper.instance().refs.gitTab; - stagingView = gitTab.refs.stagingView; - commitViewController = gitTab.refs.commitViewController; + gitTab = wrapper.instance().getWrappedComponentInstance().refView; + stagingView = gitTab.refStagingView.getWrappedComponent(); + commitViewController = gitTab.refCommitViewController.getWrappedComponent(); commitView = commitViewController.refs.commitView; focusElement = stagingView.element; @@ -366,9 +354,9 @@ describe('GitTabController', function() { const didChangeAmending = () => {}; - app = React.cloneElement(app, {repository}); + app = React.cloneElement(app, {repository, didChangeAmending}); wrapper = mount(app); - await wrapper.instance().getLastModelDataRefreshPromise(); + await assert.async.lengthOf(wrapper.find('GitTabView').prop('unstagedChanges'), 3); extractReferences(); }); @@ -376,7 +364,7 @@ describe('GitTabController', function() { it('blurs on tool-panel:unfocus', function() { sinon.spy(workspace.getActivePane(), 'activate'); - commandRegistry.dispatch(wrapper.getNode(), 'tool-panel:unfocus'); + commandRegistry.dispatch(wrapper.find('.github-Panel').getNode(), 'tool-panel:unfocus'); assert.isTrue(workspace.getActivePane().activate.called); }); @@ -384,18 +372,18 @@ describe('GitTabController', function() { it('advances focus through StagingView groups and CommitView, but does not cycle', function() { assertSelected(['unstaged-1.txt']); - commandRegistry.dispatch(wrapper.getNode(), 'core:focus-next'); + commandRegistry.dispatch(gitTab.refRoot, 'core:focus-next'); assertSelected(['conflict-1.txt']); - commandRegistry.dispatch(wrapper.getNode(), 'core:focus-next'); + commandRegistry.dispatch(gitTab.refRoot, 'core:focus-next'); assertSelected(['staged-1.txt']); - commandRegistry.dispatch(wrapper.getNode(), 'core:focus-next'); + commandRegistry.dispatch(gitTab.refRoot, 'core:focus-next'); assertSelected(['staged-1.txt']); assert.strictEqual(focusElement, commitView.editorElement); // This should be a no-op. (Actually, it'll insert a tab in the CommitView editor.) - commandRegistry.dispatch(wrapper.getNode(), 'core:focus-next'); + commandRegistry.dispatch(gitTab.refRoot, 'core:focus-next'); assertSelected(['staged-1.txt']); assert.strictEqual(focusElement, commitView.editorElement); }); @@ -403,17 +391,17 @@ describe('GitTabController', function() { it('retreats focus from the CommitView through StagingView groups, but does not cycle', function() { gitTab.setFocus(focuses.EDITOR); - commandRegistry.dispatch(wrapper.getNode(), 'core:focus-previous'); + commandRegistry.dispatch(gitTab.refRoot, 'core:focus-previous'); assertSelected(['staged-1.txt']); - commandRegistry.dispatch(wrapper.getNode(), 'core:focus-previous'); + commandRegistry.dispatch(gitTab.refRoot, 'core:focus-previous'); assertSelected(['conflict-1.txt']); - commandRegistry.dispatch(wrapper.getNode(), 'core:focus-previous'); + commandRegistry.dispatch(gitTab.refRoot, 'core:focus-previous'); assertSelected(['unstaged-1.txt']); // This should be a no-op. - commandRegistry.dispatch(wrapper.getNode(), 'core:focus-previous'); + commandRegistry.dispatch(gitTab.refRoot, 'core:focus-previous'); assertSelected(['unstaged-1.txt']); }); }); @@ -435,29 +423,29 @@ describe('GitTabController', function() { app = React.cloneElement(app, {repository, ensureGitTab, prepareToCommit, didChangeAmending}); wrapper = mount(app); - await wrapper.instance().getLastModelDataRefreshPromise(); + await assert.async.lengthOf(wrapper.find('GitTabView').prop('stagedChanges'), 1); extractReferences(); }); it('focuses the CommitView on github:commit with an empty commit message', async function() { commitView.editor.setText(''); - sinon.spy(wrapper.instance(), 'commit'); + sinon.spy(wrapper.instance().getWrappedComponentInstance(), 'commit'); wrapper.update(); commandRegistry.dispatch(workspaceElement, 'github:commit'); await assert.async.strictEqual(focusElement, commitView.editorElement); - assert.isFalse(wrapper.instance().commit.called); + assert.isFalse(wrapper.instance().getWrappedComponentInstance().commit.called); }); it('creates a commit on github:commit with a nonempty commit message', async function() { commitView.editor.setText('I fixed the things'); - sinon.spy(wrapper.instance(), 'commit'); + sinon.spy(wrapper.instance().getWrappedComponentInstance(), 'commit'); wrapper.update(); commandRegistry.dispatch(workspaceElement, 'github:commit'); - await until('Commit method called', () => wrapper.instance().commit.calledWith('I fixed the things')); + await until('Commit method called', () => wrapper.instance().getWrappedComponentInstance().commit.calledWith('I fixed the things')); }); }); }); @@ -473,13 +461,15 @@ describe('GitTabController', function() { app = React.cloneElement(app, {repository, ensureGitTab}); const wrapper = mount(app); - await wrapper.instance().getLastModelDataRefreshPromise(); + await assert.async.lengthOf(wrapper.find('GitTabView').prop('unstagedChanges'), 2); - const stagingView = wrapper.ref('gitTab').refs.stagingView; - const commitView = controller.ref('gitTab').refs.commitViewController.refs.commitView; + const gitTab = wrapper.instance().getWrappedComponentInstance().refView; + const stagingView = gitTab.refStagingView.getWrappedComponent(); + const commitViewController = gitTab.refCommitViewController.getWrappedComponent(); + const commitView = commitViewController.refs.commitView; - assert.lengthOf(stagingView.prop('unstagedChanges'), 2); - assert.lengthOf(stagingView.prop('stagedChanges'), 0); + assert.lengthOf(stagingView.props.unstagedChanges, 2); + assert.lengthOf(stagingView.props.stagedChanges, 0); stagingView.dblclickOnItem({}, stagingView.props.unstagedChanges[0]); @@ -509,14 +499,13 @@ describe('GitTabController', function() { await assert.isRejected(repository.git.merge('origin/branch')); const confirm = sinon.stub(); - const controller = new GitTabController({ - workspace, commandRegistry, tooltips, config, confirm, repository, - resolutionProgress, refreshResolutionProgress, - }); - await assert.async.isDefined(controller.refs.gitTab.refs.stagingView); - const stagingView = controller.refs.gitTab.refs.stagingView; + app = React.cloneElement(app, {repository, confirm}); + const wrapper = mount(app); - await assert.async.equal(stagingView.props.mergeConflicts.length, 5); + await assert.async.lengthOf(wrapper.find('GitTabView').prop('mergeConflicts'), 5); + const stagingView = wrapper.instance().getWrappedComponentInstance().refView.refStagingView.getWrappedComponent(); + + assert.equal(stagingView.props.mergeConflicts.length, 5); assert.equal(stagingView.props.stagedChanges.length, 0); const conflict1 = stagingView.props.mergeConflicts.filter(c => c.filePath === 'modified-on-both-ours.txt')[0]; @@ -557,13 +546,11 @@ describe('GitTabController', function() { const repository = await buildRepository(workdirPath); fs.unlinkSync(path.join(workdirPath, 'a.txt')); fs.unlinkSync(path.join(workdirPath, 'b.txt')); - const controller = new GitTabController({ - workspace, commandRegistry, tooltips, config, repository, resolutionProgress, refreshResolutionProgress, - }); - await assert.async.isDefined(controller.refs.gitTab.refs.stagingView); - const stagingView = controller.refs.gitTab.refs.stagingView; + app = React.cloneElement(app, {repository}); + const wrapper = mount(app); + const stagingView = wrapper.instance().getWrappedComponentInstance().refView.refStagingView.getWrappedComponent(); await assert.async.lengthOf(stagingView.props.unstagedChanges, 2); // ensure staging the same file twice does not cause issues @@ -588,12 +575,11 @@ describe('GitTabController', function() { const repository = await buildRepository(workdirPath); fs.writeFileSync(path.join(workdirPath, 'new-file.txt'), 'foo\nbar\nbaz\n'); - const controller = new GitTabController({ - workspace, commandRegistry, tooltips, config, repository, resolutionProgress, refreshResolutionProgress, - }); - await controller.getLastModelDataRefreshPromise(); - await etch.getScheduler().getNextUpdatePromise(); - const stagingView = controller.refs.gitTab.refs.stagingView; + app = React.cloneElement(app, {repository}); + const wrapper = mount(app); + + const stagingView = wrapper.instance().getWrappedComponentInstance().refView.refStagingView.getWrappedComponent(); + await assert.async.include(stagingView.props.unstagedChanges.map(c => c.filePath), 'new-file.txt'); const [addedFilePatch] = stagingView.props.unstagedChanges; assert.equal(addedFilePatch.filePath, 'new-file.txt'); @@ -610,15 +596,14 @@ describe('GitTabController', function() { // partially stage contents in the newly added file await repository.git.applyPatch(patchString, {index: true}); repository.refresh(); - await controller.getLastModelDataRefreshPromise(); - await etch.getScheduler().getNextUpdatePromise(); // since unstaged changes are calculated relative to the index, // which now has new-file.txt on it, the working directory version of // new-file.txt has a modified status - const [modifiedFilePatch] = stagingView.props.unstagedChanges; - assert.equal(modifiedFilePatch.status, 'modified'); - assert.equal(modifiedFilePatch.filePath, 'new-file.txt'); + await until('modification arrives', () => { + const [modifiedFilePatch] = stagingView.props.unstagedChanges; + return modifiedFilePatch.status === 'modified' && modifiedFilePatch.filePath === 'new-file.txt'; + }); }); }); }); From 887e4e6d5e366cc91127bfff455598854e38ce71 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 23 Feb 2018 11:00:12 -0800 Subject: [PATCH 0322/5882] No more EtchWrapper around GitTabController --- lib/controllers/root-controller.js | 41 ++++++++++++++---------------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/lib/controllers/root-controller.js b/lib/controllers/root-controller.js index a645a86954..222f138105 100644 --- a/lib/controllers/root-controller.js +++ b/lib/controllers/root-controller.js @@ -176,29 +176,26 @@ export default class RootController extends React.Component { onDidCloseItem={this.props.destroyGitTabItem} stubItem={this.props.gitTabStubItem} activate={this.props.startOpen}> - { this.gitTabController = c; }} - className="github-PanelEtchWrapper" - reattachDomNode={false}> - - + + workspace={this.props.workspace} + commandRegistry={this.props.commandRegistry} + notificationManager={this.props.notificationManager} + tooltips={this.props.tooltips} + grammars={this.props.grammars} + project={this.props.project} + confirm={this.props.confirm} + config={this.props.config} + repository={this.props.repository} + initializeRepo={this.initializeRepo} + resolutionProgress={this.props.resolutionProgress} + ensureGitTab={this.gitTabTracker.ensureVisible} + openFiles={this.openFiles} + discardWorkDirChangesForPaths={this.discardWorkDirChangesForPaths} + undoLastDiscard={this.undoLastDiscard} + refreshResolutionProgress={this.refreshResolutionProgress} + /> ); From e8ad3cad65059204d33d97dd6f21310f797d0497 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 23 Feb 2018 11:52:19 -0800 Subject: [PATCH 0323/5882] Wait for props to propagate into Etch land --- test/controllers/git-tab-controller.test.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/test/controllers/git-tab-controller.test.js b/test/controllers/git-tab-controller.test.js index 7d157afcee..a5a0ddf16f 100644 --- a/test/controllers/git-tab-controller.test.js +++ b/test/controllers/git-tab-controller.test.js @@ -407,9 +407,11 @@ describe('GitTabController', function() { }); describe('with staged changes', function() { + let repository; + beforeEach(async function() { const workdirPath = await cloneRepository('each-staging-group'); - const repository = await buildRepository(workdirPath); + 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.'); @@ -423,8 +425,8 @@ describe('GitTabController', function() { app = React.cloneElement(app, {repository, ensureGitTab, prepareToCommit, didChangeAmending}); wrapper = mount(app); - await assert.async.lengthOf(wrapper.find('GitTabView').prop('stagedChanges'), 1); extractReferences(); + await assert.async.isTrue(commitView.props.stagedChangesExist); }); it('focuses the CommitView on github:commit with an empty commit message', async function() { @@ -440,12 +442,11 @@ describe('GitTabController', function() { it('creates a commit on github:commit with a nonempty commit message', async function() { commitView.editor.setText('I fixed the things'); - sinon.spy(wrapper.instance().getWrappedComponentInstance(), 'commit'); - wrapper.update(); + sinon.spy(repository, 'commit'); commandRegistry.dispatch(workspaceElement, 'github:commit'); - await until('Commit method called', () => wrapper.instance().getWrappedComponentInstance().commit.calledWith('I fixed the things')); + await until('Commit method called', () => repository.commit.calledWith('I fixed the things')); }); }); }); From bd950c7f40fe316a8055858e57802980d99095c8 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 23 Feb 2018 12:01:34 -0800 Subject: [PATCH 0324/5882] :shirt: :shirt: :shirt: --- lib/controllers/root-controller.js | 1 - lib/views/git-tab-view.js | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/controllers/root-controller.js b/lib/controllers/root-controller.js index 222f138105..047111ef13 100644 --- a/lib/controllers/root-controller.js +++ b/lib/controllers/root-controller.js @@ -6,7 +6,6 @@ import React from 'react'; import PropTypes from 'prop-types'; import {autobind} from 'core-decorators'; -import EtchWrapper from '../views/etch-wrapper'; import StatusBar from '../views/status-bar'; import Panel from '../views/panel'; import PaneItem from '../views/pane-item'; diff --git a/lib/views/git-tab-view.js b/lib/views/git-tab-view.js index 99549dbd11..a43f379eeb 100644 --- a/lib/views/git-tab-view.js +++ b/lib/views/git-tab-view.js @@ -172,7 +172,7 @@ export default class GitTabView extends React.Component { prepareToCommit={this.props.prepareToCommit} commit={this.props.commit} abortMerge={this.props.abortMerge} - branchName={this.props.branchName} + branchName={''} workspace={this.props.workspace} commandRegistry={this.props.commandRegistry} notificationManager={this.props.notificationManager} From 0dc7caf15eacf143419755a69ae5cf661e05cdb5 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 23 Feb 2018 15:06:08 -0800 Subject: [PATCH 0325/5882] I know I've seen this one fail recently --- test/models/workdir-cache.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/models/workdir-cache.test.js b/test/models/workdir-cache.test.js index e205124f07..d9a6bc7622 100644 --- a/test/models/workdir-cache.test.js +++ b/test/models/workdir-cache.test.js @@ -118,7 +118,7 @@ describe('WorkdirCache', function() { assert.isTrue(cache.revParse.calledWith(dir1)); }); - it('clears the cache when the maximum size is exceeded', async function() { + it.stress(50, 'clears the cache when the maximum size is exceeded', async function() { const dirs = await Promise.all( Array(6).fill(null, 0, 6).map(() => cloneRepository('three-files')), ); From f95b585acfcca15337fd6b0dc192ee9df565e4dc Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 23 Feb 2018 16:19:57 -0800 Subject: [PATCH 0326/5882] focus --- test/models/workdir-cache.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/models/workdir-cache.test.js b/test/models/workdir-cache.test.js index d9a6bc7622..72f684faf4 100644 --- a/test/models/workdir-cache.test.js +++ b/test/models/workdir-cache.test.js @@ -128,7 +128,7 @@ describe('WorkdirCache', function() { sinon.spy(cache, 'revParse'); const actualDir = await cache.find(expectedDir); - assert.equal(actualDir, expectedDir); + assert.strictEqual(actualDir, expectedDir); assert.isTrue(cache.revParse.called); }); }); From 07282ce86a3c0cbb08d47efd3c86ca4622fe83ab Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 23 Feb 2018 17:14:51 -0800 Subject: [PATCH 0327/5882] Don't serialize the git dock item as the github dock item --- lib/controllers/git-tab-controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/controllers/git-tab-controller.js b/lib/controllers/git-tab-controller.js index d731b3a9e0..6c4978f8e0 100644 --- a/lib/controllers/git-tab-controller.js +++ b/lib/controllers/git-tab-controller.js @@ -102,7 +102,7 @@ export default class GitTabController extends React.Component { serialize() { return { - deserializer: 'GithubDockItem', + deserializer: 'GitDockItem', uri: this.getURI(), }; } From 0016a0c4556f41f71d87457f9a63f7166aef6bca Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 23 Feb 2018 17:15:19 -0800 Subject: [PATCH 0328/5882] Turns out nobody was actually using didSelectFilePath --- lib/controllers/git-tab-controller.js | 2 -- lib/views/git-tab-view.js | 2 -- 2 files changed, 4 deletions(-) diff --git a/lib/controllers/git-tab-controller.js b/lib/controllers/git-tab-controller.js index 6c4978f8e0..4608c6e72a 100644 --- a/lib/controllers/git-tab-controller.js +++ b/lib/controllers/git-tab-controller.js @@ -72,7 +72,6 @@ export default class GitTabController extends React.Component { undoLastDiscard: PropTypes.func.isRequired, discardWorkDirChangesForPaths: PropTypes.func.isRequired, openFiles: PropTypes.func.isRequired, - didSelectFilePath: PropTypes.func.isRequired, initializeRepo: PropTypes.func.isRequired, }; @@ -138,7 +137,6 @@ export default class GitTabController extends React.Component { config={this.props.config} initializeRepo={this.props.initializeRepo} - didSelectFilePath={this.props.didSelectFilePath} openFiles={this.props.openFiles} discardWorkDirChangesForPaths={this.props.discardWorkDirChangesForPaths} undoLastDiscard={this.props.undoLastDiscard} diff --git a/lib/views/git-tab-view.js b/lib/views/git-tab-view.js index a43f379eeb..1599c0b048 100644 --- a/lib/views/git-tab-view.js +++ b/lib/views/git-tab-view.js @@ -53,7 +53,6 @@ export default class GitTabView extends React.Component { attemptFileStageOperation: PropTypes.func.isRequired, discardWorkDirChangesForPaths: PropTypes.func.isRequired, openFiles: PropTypes.func.isRequired, - didSelectFilePath: PropTypes.func.isRequired, }; constructor(props, context) { @@ -147,7 +146,6 @@ export default class GitTabView extends React.Component { mergeConflicts={this.props.mergeConflicts} workingDirectoryPath={this.props.workingDirectoryPath} resolutionProgress={this.props.resolutionProgress} - didSelectFilePath={this.props.didSelectFilePath} openFiles={this.props.openFiles} discardWorkDirChangesForPaths={this.props.discardWorkDirChangesForPaths} attemptFileStageOperation={this.props.attemptFileStageOperation} From 7ff94d78d6943eec5e8474abe2cbe44b8e78844f Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 23 Feb 2018 17:15:30 -0800 Subject: [PATCH 0329/5882] That DockItem isn't wrapping an EtchWrapper any more --- lib/controllers/root-controller.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/controllers/root-controller.js b/lib/controllers/root-controller.js index 047111ef13..2525e283bc 100644 --- a/lib/controllers/root-controller.js +++ b/lib/controllers/root-controller.js @@ -171,7 +171,6 @@ export default class RootController extends React.Component { { this.gitDockItem = c; }} workspace={this.props.workspace} - getItem={({subtree}) => subtree.getWrappedComponent()} onDidCloseItem={this.props.destroyGitTabItem} stubItem={this.props.gitTabStubItem} activate={this.props.startOpen}> From fc4a237a8032ccd69f3308d2665c104f13e5e8f6 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 23 Feb 2018 17:15:48 -0800 Subject: [PATCH 0330/5882] Unwrap ObserveModel-decorated components in TabTracker --- lib/controllers/root-controller.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/controllers/root-controller.js b/lib/controllers/root-controller.js index 2525e283bc..f8fea2612f 100644 --- a/lib/controllers/root-controller.js +++ b/lib/controllers/root-controller.js @@ -727,11 +727,15 @@ class TabTracker { getControllerComponent() { const controller = this.getController(); - if (!controller.getWrappedComponent) { - return controller; + if (controller.getWrappedComponentInstance) { + return controller.getWrappedComponentInstance(); } - return controller.getWrappedComponent(); + if (controller.getWrappedComponent) { + return controller.getWrappedComponent(); + } + + return controller; } @autobind From a0a20e75cb67374a8a717839738a66a3c8b665c1 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 23 Feb 2018 17:16:04 -0800 Subject: [PATCH 0331/5882] Restore missing .getWrappedComponent() calls --- lib/views/git-tab-view.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/views/git-tab-view.js b/lib/views/git-tab-view.js index 1599c0b048..73eae6a34c 100644 --- a/lib/views/git-tab-view.js +++ b/lib/views/git-tab-view.js @@ -210,11 +210,11 @@ export default class GitTabView extends React.Component { let currentFocus = null; if (this.refs.stagingView) { - currentFocus = this.refStagingView.rememberFocus(event); + currentFocus = this.refStagingView.getWrappedComponent().rememberFocus(event); } if (!currentFocus && this.refCommitViewController) { - currentFocus = this.refCommitViewController.rememberFocus(event); + currentFocus = this.refCommitViewController.getWrappedComponent().rememberFocus(event); } return currentFocus; From 186a895f419216978259c937100b40d187e2f159 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 23 Feb 2018 17:16:39 -0800 Subject: [PATCH 0332/5882] Apply github-PanelEtchWrapper at random until it looks right --- lib/views/git-tab-view.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/views/git-tab-view.js b/lib/views/git-tab-view.js index 73eae6a34c..eddb92e2ee 100644 --- a/lib/views/git-tab-view.js +++ b/lib/views/git-tab-view.js @@ -136,7 +136,7 @@ export default class GitTabView extends React.Component { className={cx('github-Panel', {'is-loading': isLoading})} tabIndex="-1" ref={c => { this.refRoot = c; }}> - { this.refStagingView = c; }}> + { this.refStagingView = c; }}> Date: Fri, 23 Feb 2018 20:29:04 -0800 Subject: [PATCH 0333/5882] console.logs for the console.log throne --- lib/models/workdir-cache.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/models/workdir-cache.js b/lib/models/workdir-cache.js index 1e3b86647e..9c36322f0c 100644 --- a/lib/models/workdir-cache.js +++ b/lib/models/workdir-cache.js @@ -12,21 +12,30 @@ export default class WorkdirCache { this.known = new Map(); } + /* eslint-disable no-console */ async find(startPath) { + console.log(`WorkdirCache.find('${startPath}')`); const cached = this.known.get(startPath); if (cached !== undefined) { + console.log(`... cache hit: '${cached}'`); return cached; } + console.log('... cache miss, about to revparse'); const workDir = await this.revParse(startPath); + console.log(`... workdir = '${workDir}'`); + console.log(`... cache size: ${this.known.size}`); if (this.known.size >= this.maxSize) { + console.log('clearing cache'); this.known.clear(); } this.known.set(startPath, workDir); + console.log(`... returning workdir = ${workDir}`); return workDir; } + /* eslint-enable no-console */ invalidate() { this.known.clear(); From f7130a4399c57a395db4966f15f187bd34292496 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 23 Feb 2018 21:02:26 -0800 Subject: [PATCH 0334/5882] Was that cache entry just not being cleared yet? --- lib/models/workdir-cache.js | 4 +++- test/models/workdir-cache.test.js | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/models/workdir-cache.js b/lib/models/workdir-cache.js index 9c36322f0c..b2128cd482 100644 --- a/lib/models/workdir-cache.js +++ b/lib/models/workdir-cache.js @@ -15,6 +15,8 @@ export default class WorkdirCache { /* eslint-disable no-console */ async find(startPath) { console.log(`WorkdirCache.find('${startPath}')`); + console.log(`... cache =\n ${Array.from(this.known, each => each[0] + ' => ' + each[1]).join('\n ')}`); + const cached = this.known.get(startPath); if (cached !== undefined) { console.log(`... cache hit: '${cached}'`); @@ -27,7 +29,7 @@ export default class WorkdirCache { console.log(`... cache size: ${this.known.size}`); if (this.known.size >= this.maxSize) { - console.log('clearing cache'); + console.log('... clearing cache'); this.known.clear(); } this.known.set(startPath, workDir); diff --git a/test/models/workdir-cache.test.js b/test/models/workdir-cache.test.js index 72f684faf4..5a384d94dc 100644 --- a/test/models/workdir-cache.test.js +++ b/test/models/workdir-cache.test.js @@ -124,7 +124,7 @@ describe('WorkdirCache', function() { ); await Promise.all(dirs.map(dir => cache.find(dir))); - const expectedDir = dirs[1]; + const expectedDir = dirs[5]; sinon.spy(cache, 'revParse'); const actualDir = await cache.find(expectedDir); From 7796da26539974c95f9a3dbb6711c78c80ca0da1 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 23 Feb 2018 21:10:57 -0800 Subject: [PATCH 0335/5882] The Array index shuffle --- test/models/workdir-cache.test.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/models/workdir-cache.test.js b/test/models/workdir-cache.test.js index 5a384d94dc..4336417b3d 100644 --- a/test/models/workdir-cache.test.js +++ b/test/models/workdir-cache.test.js @@ -123,9 +123,11 @@ describe('WorkdirCache', function() { Array(6).fill(null, 0, 6).map(() => cloneRepository('three-files')), ); - await Promise.all(dirs.map(dir => cache.find(dir))); - const expectedDir = dirs[5]; + await Promise.all(dirs.slice(0, 5).map(dir => cache.find(dir))); + console.log('----'); + await cache.find(dirs[5]); + const expectedDir = dirs[2]; sinon.spy(cache, 'revParse'); const actualDir = await cache.find(expectedDir); assert.strictEqual(actualDir, expectedDir); From 589309058ae1dd229c5a706e9257d1e12e98ad1f Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 23 Feb 2018 21:15:59 -0800 Subject: [PATCH 0336/5882] :fire: `console.log()` :fire: --- lib/models/workdir-cache.js | 11 ----------- test/models/workdir-cache.test.js | 1 - 2 files changed, 12 deletions(-) diff --git a/lib/models/workdir-cache.js b/lib/models/workdir-cache.js index b2128cd482..1e3b86647e 100644 --- a/lib/models/workdir-cache.js +++ b/lib/models/workdir-cache.js @@ -12,32 +12,21 @@ export default class WorkdirCache { this.known = new Map(); } - /* eslint-disable no-console */ async find(startPath) { - console.log(`WorkdirCache.find('${startPath}')`); - console.log(`... cache =\n ${Array.from(this.known, each => each[0] + ' => ' + each[1]).join('\n ')}`); - const cached = this.known.get(startPath); if (cached !== undefined) { - console.log(`... cache hit: '${cached}'`); return cached; } - console.log('... cache miss, about to revparse'); const workDir = await this.revParse(startPath); - console.log(`... workdir = '${workDir}'`); - console.log(`... cache size: ${this.known.size}`); if (this.known.size >= this.maxSize) { - console.log('... clearing cache'); this.known.clear(); } this.known.set(startPath, workDir); - console.log(`... returning workdir = ${workDir}`); return workDir; } - /* eslint-enable no-console */ invalidate() { this.known.clear(); diff --git a/test/models/workdir-cache.test.js b/test/models/workdir-cache.test.js index 4336417b3d..3b7953b38e 100644 --- a/test/models/workdir-cache.test.js +++ b/test/models/workdir-cache.test.js @@ -124,7 +124,6 @@ describe('WorkdirCache', function() { ); await Promise.all(dirs.slice(0, 5).map(dir => cache.find(dir))); - console.log('----'); await cache.find(dirs[5]); const expectedDir = dirs[2]; From c0b566b07db587c44dc395cd45d86005b9c848e1 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 23 Feb 2018 21:19:23 -0800 Subject: [PATCH 0337/5882] :fire: `it.stress()` :fire: --- test/models/workdir-cache.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/models/workdir-cache.test.js b/test/models/workdir-cache.test.js index 3b7953b38e..61746eaaa5 100644 --- a/test/models/workdir-cache.test.js +++ b/test/models/workdir-cache.test.js @@ -118,7 +118,7 @@ describe('WorkdirCache', function() { assert.isTrue(cache.revParse.calledWith(dir1)); }); - it.stress(50, 'clears the cache when the maximum size is exceeded', async function() { + it('clears the cache when the maximum size is exceeded', async function() { const dirs = await Promise.all( Array(6).fill(null, 0, 6).map(() => cloneRepository('three-files')), ); From a798ca48f89c2ef5278cc33fe81d5f89c495cd26 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 26 Feb 2018 09:41:57 -0500 Subject: [PATCH 0338/5882] Reactify CommitViewController --- lib/controllers/commit-view-controller.js | 95 +++--- .../commit-view-controller.test.js | 288 ++++++++++-------- 2 files changed, 212 insertions(+), 171 deletions(-) diff --git a/lib/controllers/commit-view-controller.js b/lib/controllers/commit-view-controller.js index e433ae5d75..a67bb66213 100644 --- a/lib/controllers/commit-view-controller.js +++ b/lib/controllers/commit-view-controller.js @@ -1,9 +1,7 @@ -/** @jsx etch.dom */ -/* eslint react/no-unknown-property: "off" */ - import path from 'path'; -import etch from 'etch'; +import React from 'react'; +import PropTypes from 'prop-types'; import {autobind} from 'core-decorators'; import {CompositeDisposable} from 'event-kit'; @@ -12,20 +10,39 @@ import {writeFile, readFile} from '../helpers'; export const COMMIT_GRAMMAR_SCOPE = 'text.git-commit'; -export default class CommitViewController { +export default class CommitViewController extends React.Component { static focus = { ...CommitView.focus, } - constructor(props) { - this.props = props; + static propTypes = { + workspace: PropTypes.object.isRequired, + grammars: PropTypes.object.isRequired, + commandRegistry: PropTypes.object.isRequired, + config: PropTypes.object.isRequired, + tooltips: PropTypes.object.isRequired, + + repository: PropTypes.object.isRequired, + isMerging: PropTypes.bool.isRequired, + isAmending: PropTypes.bool.isRequired, + mergeMessage: PropTypes.string.isRequired, + mergeConflictsExist: PropTypes.bool.isRequired, + stagedChangesExist: PropTypes.bool.isRequired, + lastCommit: PropTypes.object.isRequired, + + prepareToCommit: PropTypes.func.isRequired, + commit: PropTypes.func.isRequired, + abortMerge: PropTypes.func.isRequired, + didMoveUpOnFirstLine: PropTypes.func.isRequired, + } - if (this.props.isMerging && this.props.mergeMessage) { - this.props.repository.setRegularCommitMessage(this.props.mergeMessage); - } + constructor(props, context) { + super(props, context); this.subscriptions = new CompositeDisposable(); - etch.initialize(this); + } + + componentWillMount() { this.subscriptions.add( this.props.workspace.onDidAddTextEditor(({textEditor}) => { if (this.props.repository.isPresent() && textEditor.getPath() === this.getCommitMessagePath()) { @@ -34,7 +51,6 @@ export default class CommitViewController { textEditor.setGrammar(grammar); } } - etch.update(this); }), this.props.workspace.onDidDestroyPaneItem(async ({item}) => { if (this.props.repository.isPresent() && item.getPath && item.getPath() === this.getCommitMessagePath() && @@ -50,28 +66,16 @@ export default class CommitViewController { if (e.code !== 'ENOENT') { throw e; } - } finally { - // update even if the file was deleted - etch.update(this); } } }), ); - } - update(props) { - const wasAmending = this.props.isAmending; - const wasMerging = this.props.isMerging; - this.props = {...this.props, ...props}; - // If we just checked the "amend" box and we don't yet have a saved amending message, - // initialize it to be the message from the last commit. - const switchToAmending = !wasAmending && this.props.isAmending; - if (switchToAmending && !this.getAmendingCommitMessage() && this.props.lastCommit.isPresent()) { - this.setAmendingCommitMessage(props.lastCommit.getMessage()); - } else if (!wasMerging && this.props.isMerging && !this.getRegularCommitMessage()) { + if (this.props.isAmending && !this.getAmendingCommitMessage() && this.props.lastCommit.isPresent()) { + this.setAmendingCommitMessage(this.props.lastCommit.getMessage()); + } else if (this.props.isMerging && !this.getRegularCommitMessage()) { this.setRegularCommitMessage(this.props.mergeMessage || ''); } - return etch.update(this); } render() { @@ -89,7 +93,7 @@ export default class CommitViewController { commit={this.commit} setAmending={this.setAmending} abortMerge={this.props.abortMerge} - branchName={this.props.branchName} + branchName={''} commandRegistry={this.props.commandRegistry} maximumCharacterLimit={72} message={message} @@ -105,9 +109,26 @@ export default class CommitViewController { ); } + componentWillReceiveProps(nextProps) { + const switchToAmending = !this.props.isAmending && nextProps.isAmending; + if (switchToAmending && !this.getAmendingCommitMessage() && nextProps.lastCommit.isPresent()) { + this.setAmendingCommitMessage(nextProps.lastCommit.getMessage()); + } else if (!this.props.isMerging && nextProps.isMerging && !this.getRegularCommitMessage()) { + this.setRegularCommitMessage(nextProps.mergeMessage || ''); + } + } + + componentWillUnmount() { + this.subscriptions.dispose(); + } + @autobind setAmending(amending) { + const changed = this.props.repository.isAmending() !== amending; this.props.repository.setAmending(amending); + if (changed) { + this.forceUpdate(); + } } @autobind @@ -129,7 +150,10 @@ export default class CommitViewController { } setAmendingCommitMessage(message) { + if (!this.props.repository.isPresent()) { return; } + const changed = this.props.repository.getAmendingCommitMessage() !== message; this.props.repository.setAmendingCommitMessage(message); + if (changed) { this.forceUpdate(); } } getAmendingCommitMessage() { @@ -137,7 +161,10 @@ export default class CommitViewController { } setRegularCommitMessage(message) { + if (!this.props.repository.isPresent()) { return; } + const changed = this.props.repository.getRegularCommitMessage() !== message; this.props.repository.setRegularCommitMessage(message); + if (changed) { this.forceUpdate(); } } getRegularCommitMessage() { @@ -158,7 +185,6 @@ export default class CommitViewController { } else { this.setRegularCommitMessage(newMessage); } - etch.update(this); } getCommitMessageEditors() { @@ -169,7 +195,7 @@ export default class CommitViewController { } @autobind - toggleExpandedCommitMessageEditor(messageFromBox) { + async toggleExpandedCommitMessageEditor(messageFromBox) { if (this.getCommitMessageEditors().length > 0) { if (this.commitMessageEditorIsInForeground()) { this.closeAllOpenCommitMessageEditors(); @@ -177,7 +203,8 @@ export default class CommitViewController { this.activateCommitMessageEditor(); } } else { - this.openCommitMessageEditor(messageFromBox); + await this.openCommitMessageEditor(messageFromBox); + this.forceUpdate(); } } @@ -223,7 +250,6 @@ export default class CommitViewController { this.grammarSubscription = this.props.grammars.onDidAddGrammar(this.grammarAdded); this.subscriptions.add(this.grammarSubscription); } - etch.update(this); } @autobind @@ -245,11 +271,6 @@ export default class CommitViewController { hasFocus() { return this.element.contains(document.activeElement); } - - destroy() { - this.subscriptions.dispose(); - return etch.destroy(this); - } } function wrapCommitMessage(message) { diff --git a/test/controllers/commit-view-controller.test.js b/test/controllers/commit-view-controller.test.js index 2ade376c5f..f67ba7e710 100644 --- a/test/controllers/commit-view-controller.test.js +++ b/test/controllers/commit-view-controller.test.js @@ -1,8 +1,7 @@ import path from 'path'; import fs from 'fs'; - -import etch from 'etch'; -import until from 'test-until'; +import React from 'react'; +import {shallow} from 'enzyme'; import Commit from '../../lib/models/commit'; import {writeFile} from '../../lib/helpers'; @@ -11,19 +10,42 @@ import CommitViewController, {COMMIT_GRAMMAR_SCOPE} from '../../lib/controllers/ import {cloneRepository, buildRepository, buildRepositoryWithPipeline} from '../helpers'; describe('CommitViewController', function() { - let atomEnvironment, workspace, commandRegistry, notificationManager, grammars, lastCommit, config, confirm, tooltips; + let atomEnvironment, workspace, commandRegistry, notificationManager, lastCommit, config, confirm, tooltips; + let app; beforeEach(function() { atomEnvironment = global.buildAtomEnvironment(); workspace = atomEnvironment.workspace; commandRegistry = atomEnvironment.commands; notificationManager = atomEnvironment.notifications; - grammars = atomEnvironment.grammars; config = atomEnvironment.config; tooltips = atomEnvironment.tooltips; confirm = sinon.stub(atomEnvironment, 'confirm'); lastCommit = new Commit('a1e23fd45', 'last commit message'); + const noop = () => {}; + + app = ( + + ); }); afterEach(function() { @@ -35,135 +57,132 @@ describe('CommitViewController', function() { const repository1 = await buildRepository(workdirPath1); const workdirPath2 = await cloneRepository('three-files'); const repository2 = await buildRepository(workdirPath2); - const controller = new CommitViewController({ - workspace, commandRegistry, tooltips, config, notificationManager, lastCommit, repository: repository1, - }); - assert.equal(controller.getRegularCommitMessage(), ''); - assert.equal(controller.getAmendingCommitMessage(), ''); + app = React.cloneElement(app, {repository: repository1}); + const wrapper = shallow(app); + + assert.strictEqual(wrapper.instance().getRegularCommitMessage(), ''); + assert.strictEqual(wrapper.instance().getAmendingCommitMessage(), ''); - controller.setRegularCommitMessage('regular message 1'); - controller.setAmendingCommitMessage('amending message 1'); + wrapper.instance().setRegularCommitMessage('regular message 1'); + wrapper.instance().setAmendingCommitMessage('amending message 1'); - await controller.update({repository: repository2}); - assert.equal(controller.getRegularCommitMessage(), ''); - assert.equal(controller.getAmendingCommitMessage(), ''); + wrapper.setProps({repository: repository2}); - await controller.update({repository: repository1}); - assert.equal(controller.getRegularCommitMessage(), 'regular message 1'); - assert.equal(controller.getAmendingCommitMessage(), 'amending message 1'); + assert.strictEqual(wrapper.instance().getRegularCommitMessage(), ''); + assert.strictEqual(wrapper.instance().getAmendingCommitMessage(), ''); + + wrapper.setProps({repository: repository1}); + assert.equal(wrapper.instance().getRegularCommitMessage(), 'regular message 1'); + assert.equal(wrapper.instance().getAmendingCommitMessage(), 'amending message 1'); }); describe('the passed commit message', function() { - let controller, commitView; + let repository; + beforeEach(async function() { const workdirPath = await cloneRepository('three-files'); - const repository = await buildRepository(workdirPath); - controller = new CommitViewController({workspace, commandRegistry, tooltips, config, notificationManager, lastCommit, repository}); - commitView = controller.refs.commitView; + repository = await buildRepository(workdirPath); + app = React.cloneElement(app, {repository}); }); - it('is set to the getRegularCommitMessage() in the default case', async function() { - controller.setRegularCommitMessage('regular message'); - await controller.update(); - assert.equal(commitView.props.message, 'regular message'); + it('is set to the getRegularCommitMessage() in the default case', function() { + repository.setRegularCommitMessage('regular message'); + const wrapper = shallow(app); + assert.strictEqual(wrapper.find('CommitView').prop('message'), 'regular message'); }); describe('when isAmending is true', function() { - it('is set to the last commits message if getAmendingCommitMessage() is blank', async function() { - controller.setAmendingCommitMessage('amending commit message'); - await controller.update({isAmending: true, lastCommit}); - assert.equal(commitView.props.message, 'amending commit message'); + it("is set to the last commit's message if getAmendingCommitMessage() is blank", function() { + repository.setAmendingCommitMessage(''); + app = React.cloneElement(app, {isAmending: true, lastCommit}); + const wrapper = shallow(app); + assert.strictEqual(wrapper.find('CommitView').prop('message'), 'last commit message'); }); - it('is set to getAmendingCommitMessage() if it is set', async function() { - controller.setAmendingCommitMessage('amending commit message'); - await controller.update({isAmending: true, lastCommit}); - assert.equal(commitView.props.message, 'amending commit message'); + it('is set to getAmendingCommitMessage() if it is set', function() { + repository.setAmendingCommitMessage('amending commit message'); + app = React.cloneElement(app, {isAmending: true, lastCommit}); + const wrapper = shallow(app); + assert.strictEqual(wrapper.find('CommitView').prop('message'), 'amending commit message'); }); }); describe('when a merge message is defined', function() { - it('is set to the merge message when merging', async function() { - await controller.update({isMerging: true, mergeMessage: 'merge conflict!'}); - assert.equal(commitView.props.message, 'merge conflict!'); + it('is set to the merge message when merging', function() { + app = React.cloneElement(app, {isMerging: true, mergeMessage: 'merge conflict!'}); + const wrapper = shallow(app); + assert.strictEqual(wrapper.find('CommitView').prop('message'), 'merge conflict!'); }); - it('is set to getRegularCommitMessage() if it is set', async function() { - controller.setRegularCommitMessage('regular commit message'); - await controller.update({isMerging: true, mergeMessage: 'merge conflict!'}); - assert.equal(commitView.props.message, 'regular commit message'); + it('is set to getRegularCommitMessage() if it is set', function() { + repository.setRegularCommitMessage('regular commit message'); + app = React.cloneElement(app, {isMerging: true, mergeMessage: 'merge conflict!'}); + const wrapper = shallow(app); + assert.strictEqual(wrapper.find('CommitView').prop('message'), 'regular commit message'); }); }); }); describe('committing', function() { - let controller, workdirPath, repository; + let workdirPath, repository; beforeEach(async function() { workdirPath = await cloneRepository('three-files'); repository = await buildRepositoryWithPipeline(workdirPath, {confirm, notificationManager, workspace}); const commit = message => repository.commit(message); - controller = new CommitViewController({ - workspace, - commandRegistry, - notificationManager, - grammars, - config, - tooltips, - lastCommit, - repository, - commit, - }); - }); - - afterEach(() => { - controller.destroy(); + app = React.cloneElement(app, {repository, commit}); }); it('clears the regular and amending commit messages', async function() { - controller.setRegularCommitMessage('regular'); - controller.setAmendingCommitMessage('amending'); + repository.setRegularCommitMessage('regular'); + repository.setAmendingCommitMessage('amending'); await writeFile(path.join(workdirPath, 'a.txt'), 'some changes'); await repository.git.exec(['add', '.']); - await controller.commit('message'); - assert.equal(controller.getRegularCommitMessage(), ''); - assert.equal(controller.getAmendingCommitMessage(), ''); + const wrapper = shallow(app); + await wrapper.instance().commit('message'); + + assert.strictEqual(repository.getRegularCommitMessage(), ''); + assert.strictEqual(repository.getAmendingCommitMessage(), ''); }); it('issues a notification on failure', async function() { - controller.setRegularCommitMessage('regular'); - controller.setAmendingCommitMessage('amending'); + repository.setRegularCommitMessage('regular'); + repository.setAmendingCommitMessage('amending'); sinon.spy(notificationManager, 'addError'); + const wrapper = shallow(app); + // Committing with no staged changes should cause commit error try { - await controller.commit('message'); + await wrapper.instance().commit('message'); } catch (e) { assert(e, 'is error'); } assert.isTrue(notificationManager.addError.called); - assert.equal(controller.getRegularCommitMessage(), 'regular'); - assert.equal(controller.getAmendingCommitMessage(), 'amending'); + assert.strictEqual(repository.getRegularCommitMessage(), 'regular'); + assert.strictEqual(repository.getAmendingCommitMessage(), 'amending'); }); describe('message formatting', function() { let commitSpy; beforeEach(function() { commitSpy = sinon.stub().returns(Promise.resolve()); - controller.update({commit: commitSpy}); + app = React.cloneElement(app, {commit: commitSpy}); }); it('wraps the commit message body at 72 characters if github.automaticCommitMessageWrapping is true', async function() { config.set('github.automaticCommitMessageWrapping', false); - await controller.commit([ + const wrapper = shallow(app); + + await wrapper.instance().commit([ 'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor', '', 'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.', @@ -178,7 +197,7 @@ describe('CommitViewController', function() { commitSpy.reset(); config.set('github.automaticCommitMessageWrapping', true); - await controller.commit([ + await wrapper.instance().commit([ 'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor', '', 'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.', @@ -195,82 +214,86 @@ describe('CommitViewController', function() { describe('toggling between commit box and commit editor', function() { it('transfers the commit message contents of the last editor', async function() { - controller.refs.commitView.editor.setText('message in box'); + const wrapper = shallow(app); - commandRegistry.dispatch(atomEnvironment.views.getView(workspace), 'github:toggle-expanded-commit-message-editor'); - await assert.async.equal(workspace.getActiveTextEditor().getPath(), controller.getCommitMessagePath()); - await assert.async.isTrue(controller.refs.commitView.props.deactivateCommitBox); - const editor = workspace.getActiveTextEditor(); - assert.equal(editor.getText(), 'message in box'); + wrapper.find('CommitView').prop('toggleExpandedCommitMessageEditor')('message in box'); + await assert.async.equal(workspace.getActiveTextEditor().getPath(), wrapper.instance().getCommitMessagePath()); + assert.isTrue(wrapper.find('CommitView').prop('deactivateCommitBox')); + const editor = workspace.getActiveTextEditor(); + assert.strictEqual(editor.getText(), 'message in box'); editor.setText('message in editor'); await editor.save(); - commandRegistry.dispatch(atomEnvironment.views.getView(editor), 'pane:split-right-and-copy-active-item'); + workspace.paneForItem(editor).splitRight({copyActiveItem: true}); await assert.async.notEqual(workspace.getActiveTextEditor(), editor); - sinon.spy(controller.refs.commitView, 'update'); editor.destroy(); - await until(() => controller.refs.commitView.update.called); - assert.equal(controller.refs.commitView.editor.getText(), 'message in box'); - assert.isTrue(controller.refs.commitView.props.deactivateCommitBox); + assert.isTrue(wrapper.find('CommitView').prop('deactivateCommitBox')); workspace.getActiveTextEditor().destroy(); - await assert.async.isFalse(controller.refs.commitView.props.deactivateCommitBox); - await assert.async.equal(controller.refs.commitView.editor.getText(), 'message in editor'); + assert.isTrue(wrapper.find('CommitView').prop('deactivateCommitBox')); + await assert.async.strictEqual(wrapper.find('CommitView').prop('message'), 'message in editor'); }); it('transfers the commit message contents when in amending state', async function() { const originalMessage = 'message in box before amending'; - controller.refs.commitView.editor.setText(originalMessage); + repository.setRegularCommitMessage(originalMessage); + const wrapper = shallow(app); + + assert.strictEqual(wrapper.find('CommitView').prop('message'), originalMessage); - await controller.update({isAmending: true, lastCommit}); - assert.equal(controller.refs.commitView.editor.getText(), lastCommit.getMessage()); + wrapper.setProps({isAmending: true, lastCommit}); + assert.strictEqual(wrapper.find('CommitView').prop('message'), lastCommit.getMessage()); - commandRegistry.dispatch(atomEnvironment.views.getView(workspace), 'github:toggle-expanded-commit-message-editor'); - await assert.async.equal(workspace.getActiveTextEditor().getPath(), controller.getCommitMessagePath()); + wrapper.find('CommitView').prop('toggleExpandedCommitMessageEditor')(lastCommit.getMessage()); + await assert.async.strictEqual(workspace.getActiveTextEditor().getPath(), wrapper.instance().getCommitMessagePath()); const editor = workspace.getActiveTextEditor(); - assert.equal(editor.getText(), lastCommit.getMessage()); + assert.strictEqual(editor.getText(), lastCommit.getMessage()); const amendedMessage = lastCommit.getMessage() + 'plus some changes'; editor.setText(amendedMessage); await editor.save(); - editor.destroy(); - await assert.async.equal(controller.refs.commitView.editor.getText(), amendedMessage); - await controller.update({isAmending: false}); - await assert.async.equal(controller.refs.commitView.editor.getText(), originalMessage); + await assert.async.strictEqual(wrapper.find('CommitView').prop('message'), amendedMessage); - await controller.update({isAmending: true, lastCommit}); - assert.equal(controller.refs.commitView.editor.getText(), amendedMessage); + wrapper.setProps({isAmending: false}); + assert.strictEqual(wrapper.find('CommitView').prop('message'), originalMessage); + + wrapper.setProps({isAmending: true}); + assert.strictEqual(wrapper.find('CommitView').prop('message'), amendedMessage); }); it('activates editor if already opened but in background', async function() { - commandRegistry.dispatch(atomEnvironment.views.getView(workspace), 'github:toggle-expanded-commit-message-editor'); - await assert.async.equal(workspace.getActiveTextEditor().getPath(), controller.getCommitMessagePath()); + const wrapper = shallow(app); + + wrapper.find('CommitView').prop('toggleExpandedCommitMessageEditor')('sup'); + await assert.async.strictEqual(workspace.getActiveTextEditor().getPath(), wrapper.instance().getCommitMessagePath()); const editor = workspace.getActiveTextEditor(); await workspace.open(path.join(workdirPath, 'a.txt')); workspace.getActivePane().splitRight(); await workspace.open(path.join(workdirPath, 'b.txt')); - assert.notEqual(workspace.getActiveTextEditor(), editor); + assert.notStrictEqual(workspace.getActiveTextEditor(), editor); - commandRegistry.dispatch(atomEnvironment.views.getView(workspace), 'github:toggle-expanded-commit-message-editor'); - await assert.async.equal(workspace.getActiveTextEditor(), editor); + wrapper.find('CommitView').prop('toggleExpandedCommitMessageEditor')(); + await assert.async.strictEqual(workspace.getActiveTextEditor(), editor); }); it('closes all open commit message editors if one is in the foreground of a pane, prompting for unsaved changes', async function() { - commandRegistry.dispatch(atomEnvironment.views.getView(workspace), 'github:toggle-expanded-commit-message-editor'); - await assert.async.equal(workspace.getActiveTextEditor().getPath(), controller.getCommitMessagePath()); + const wrapper = shallow(app); + + wrapper.find('CommitView').prop('toggleExpandedCommitMessageEditor')('sup'); + await assert.async.strictEqual(workspace.getActiveTextEditor().getPath(), wrapper.instance().getCommitMessagePath()); const editor = workspace.getActiveTextEditor(); - commandRegistry.dispatch(atomEnvironment.views.getView(editor), 'pane:split-right-and-copy-active-item'); - assert.equal(controller.getCommitMessageEditors().length, 2); + workspace.paneForItem(editor).splitRight({copyActiveItem: true}); + assert.lengthOf(wrapper.instance().getCommitMessageEditors(), 2); // Activate another editor but keep commit message editor in foreground of inactive pane await workspace.open(path.join(workdirPath, 'a.txt')); - assert.notEqual(workspace.getActiveTextEditor(), editor); + assert.notStrictEqual(workspace.getActiveTextEditor(), editor); editor.setText('make some new changes'); @@ -281,65 +304,62 @@ describe('CommitViewController', function() { } return 0; // TODO: Remove this return and typeof check once https://github.com/atom/atom/pull/16229 is on stable }); - commandRegistry.dispatch(atomEnvironment.views.getView(workspace), 'github:toggle-expanded-commit-message-editor'); - await assert.async.equal(controller.getCommitMessageEditors().length, 0); + wrapper.find('CommitView').prop('toggleExpandedCommitMessageEditor')(); + await assert.async.lengthOf(wrapper.instance().getCommitMessageEditors(), 0); assert.isTrue(atomEnvironment.applicationDelegate.confirm.called); - await assert.async.equal(controller.refs.commitView.editor.getText(), 'make some new changes'); + await assert.async.strictEqual(wrapper.find('CommitView').prop('message'), 'make some new changes'); }); }); describe('committing from commit editor', function() { it('uses git commit grammar in the editor', async function() { + const wrapper = shallow(app); await atomEnvironment.packages.activatePackage('language-git'); - commandRegistry.dispatch(atomEnvironment.views.getView(workspace), 'github:toggle-expanded-commit-message-editor'); - await assert.async.equal(workspace.getActiveTextEditor().getGrammar().scopeName, COMMIT_GRAMMAR_SCOPE); + wrapper.find('CommitView').prop('toggleExpandedCommitMessageEditor')('sup'); + await assert.async.strictEqual(workspace.getActiveTextEditor().getGrammar().scopeName, COMMIT_GRAMMAR_SCOPE); }); it('takes the commit message from the editor and deletes the `ATOM_COMMIT_EDITMSG` file', async function() { fs.writeFileSync(path.join(workdirPath, 'a.txt'), 'some changes'); await repository.stageFiles(['a.txt']); - await controller.update({ - prepareToCommit: () => true, - stagedChangesExist: true, - }); + app = React.cloneElement(app, {prepareToCommit: () => true, stagedChangesExist: true}); + const wrapper = shallow(app); - commandRegistry.dispatch(atomEnvironment.views.getView(workspace), 'github:toggle-expanded-commit-message-editor'); + wrapper.find('CommitView').prop('toggleExpandedCommitMessageEditor')(); + await assert.async.strictEqual(workspace.getActiveTextEditor().getPath(), wrapper.instance().getCommitMessagePath()); - await assert.async.isTrue(controller.refs.commitView.isCommitButtonEnabled()); const editor = workspace.getActiveTextEditor(); - assert.equal(editor.getPath(), controller.getCommitMessagePath()); - editor.setText('message in editor'); await editor.save(); - commandRegistry.dispatch(atomEnvironment.views.getView(workspace), 'github:commit'); + wrapper.find('CommitView').prop('commit')('message in box'); - await assert.async.equal((await repository.getLastCommit()).getMessage(), 'message in editor'); - await assert.async.isFalse(fs.existsSync(controller.getCommitMessagePath())); + await assert.async.strictEqual((await repository.getLastCommit()).getMessage(), 'message in editor'); + await assert.async.isFalse(fs.existsSync(wrapper.instance().getCommitMessagePath())); }); it('asks user to confirm if commit editor has unsaved changes', async function() { + app = React.cloneElement(app, {confirm, prepareToCommit: () => true, stagedChangesExist: true}); + const wrapper = shallow(app); + sinon.stub(repository.git, 'commit'); - await controller.update({confirm, prepareToCommit: () => true, stagedChangesExist: true}); - commandRegistry.dispatch(atomEnvironment.views.getView(workspace), 'github:toggle-expanded-commit-message-editor'); - await assert.async.equal(workspace.getActiveTextEditor().getPath(), controller.getCommitMessagePath()); - const editor = workspace.getActiveTextEditor(); + wrapper.find('CommitView').prop('toggleExpandedCommitMessageEditor')(); + await assert.async.strictEqual(workspace.getActiveTextEditor().getPath(), wrapper.instance().getCommitMessagePath()); + const editor = workspace.getActiveTextEditor(); editor.setText('unsaved changes'); - commandRegistry.dispatch(atomEnvironment.views.getView(editor), 'pane:split-right-and-copy-active-item'); - await assert.async.notEqual(workspace.getActiveTextEditor(), editor); - assert.equal(workspace.getTextEditors().length, 2); + workspace.paneForItem(editor).splitRight({copyActiveItem: true}); + await assert.async.notStrictEqual(workspace.getActiveTextEditor(), editor); + assert.lengthOf(workspace.getTextEditors(), 2); confirm.returns(1); // Cancel - commandRegistry.dispatch(atomEnvironment.views.getView(workspace), 'github:commit'); - await etch.getScheduler().getNextUpdatePromise(); - assert.equal(repository.git.commit.callCount, 0); + wrapper.find('CommitView').prop('commit')('message in box'); + assert.strictEqual(repository.git.commit.callCount, 0); confirm.returns(0); // Commit - commandRegistry.dispatch(atomEnvironment.views.getView(workspace), 'github:commit'); - await etch.getScheduler().getNextUpdatePromise(); + wrapper.find('CommitView').prop('commit')('message in box'); await assert.async.equal(repository.git.commit.callCount, 1); - assert.equal(workspace.getTextEditors().length, 0); + await assert.async.lengthOf(workspace.getTextEditors(), 0); }); }); }); From 6a87f52efafc603c213d2c2ccd687447302ccba1 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 26 Feb 2018 15:04:12 -0500 Subject: [PATCH 0339/5882] Port CommitView and its tests to React --- lib/views/commit-view.js | 292 ++++++++++++++++++--------------- test/views/commit-view.test.js | 279 ++++++++++++++++--------------- 2 files changed, 302 insertions(+), 269 deletions(-) diff --git a/lib/views/commit-view.js b/lib/views/commit-view.js index 4dcf378c73..43d582395d 100644 --- a/lib/views/commit-view.js +++ b/lib/views/commit-view.js @@ -1,18 +1,15 @@ -/** @jsx etch.dom */ -/* eslint react/no-unknown-property: "off" */ - -import {TextEditor} from 'atom'; +import React from 'react'; +import PropTypes from 'prop-types'; import {CompositeDisposable} from 'event-kit'; - -import etch from 'etch'; import {autobind} from 'core-decorators'; import cx from 'classnames'; +import Tooltip from './tooltip'; import {shortenSha} from '../helpers'; const LINE_ENDING_REGEX = /\r?\n/; -export default class CommitView { +export default class CommitView extends React.Component { static focus = { EDITOR: Symbol('commit-editor'), ABORT_MERGE_BUTTON: Symbol('commit-abort-merge-button'), @@ -20,69 +17,61 @@ export default class CommitView { COMMIT_BUTTON: Symbol('commit-button'), }; - constructor(props) { - this.props = props; - - // We don't want the user to see the UI flicker in the case - // the commit takes a very small time to complete. Instead we - // will only show the working message if we are working for longer - // than 1 second as per https://www.nngroup.com/articles/response-times-3-important-limits/ - // - // The closure is created to restrict variable access - this.shouldShowWorking = (() => { - let showWorking = false; - let timeoutHandle = null; - - return () => { - if (this.props.isCommitting) { - if (!showWorking && timeoutHandle === null) { - timeoutHandle = setTimeout(() => { - showWorking = true; - etch.update(this); - timeoutHandle = null; - }, 1000); - } - } else { - clearTimeout(timeoutHandle); - timeoutHandle = null; - showWorking = false; - } - - return showWorking; - }; - })(); - - etch.initialize(this); - - this.editor = this.refs.editor; - // FIXME Use props-injected view registry instead of the Atom global - this.editorElement = atom.views.getView(this.editor); - this.editor.setText(this.props.message || ''); - this.subscriptions = new CompositeDisposable( - this.editor.onDidChange(() => this.props.onChangeMessage && this.props.onChangeMessage(this.editor.getText())), - this.editor.onDidChangeCursorPosition(() => { etch.update(this); }), - props.commandRegistry.add('atom-workspace', { - 'github:commit': this.commit, - 'github:toggle-expanded-commit-message-editor': this.toggleExpandedCommitMessageEditor, - }), - props.config.onDidChange('github.automaticCommitMessageWrapping', () => etch.update(this)), - ); - this.registerTooltips(); + static propTypes = { + config: PropTypes.object.isRequired, + tooltips: PropTypes.object.isRequired, + commandRegistry: PropTypes.object.isRequired, + + lastCommit: PropTypes.object.isRequired, + isAmending: PropTypes.bool.isRequired, + isMerging: PropTypes.bool.isRequired, + mergeConflictsExist: PropTypes.bool.isRequired, + stagedChangesExist: PropTypes.bool.isRequired, + isCommitting: PropTypes.bool.isRequired, + deactivateCommitBox: PropTypes.bool.isRequired, + maximumCharacterLimit: PropTypes.number.isRequired, + message: PropTypes.string.isRequired, + + commit: PropTypes.func.isRequired, + abortMerge: PropTypes.func.isRequired, + setAmending: PropTypes.func.isRequired, + onChangeMessage: PropTypes.func.isRequired, + toggleExpandedCommitMessageEditor: PropTypes.func.isRequired, + }; + + constructor(props, context) { + super(props, context); + + this.state = {showWorking: false}; + this.timeoutHandle = null; + this.subscriptions = new CompositeDisposable(); + + this.refExpandButton = null; + this.refCommitButton = null; + this.refAmendCheckbox = null; + this.refHardWrapButton = null; + this.refAbortMergeButton = null; } - destroy() { - this.subscriptions.dispose(); - etch.destroy(this); + componentWillMount() { + this.triggerShowWorking(this.props); } - update(props) { - const previousMessage = this.props.message; - this.props = {...this.props, ...props}; - const newMessage = this.props.message; - if (this.editor && previousMessage !== newMessage && this.editor.getText() !== newMessage) { - this.editor.setText(newMessage); + componentDidMount() { + if (this.editor.getText() !== this.props.message) { + this.editor.setText(this.props.message); + this.forceUpdate(); } - return etch.update(this); + + this.subscriptions = new CompositeDisposable( + this.editor.onDidChange(() => this.props.onChangeMessage(this.editor.getText())), + this.editor.onDidChangeCursorPosition(() => this.forceUpdate()), + this.props.commandRegistry.add('atom-workspace', { + 'github:commit': this.commit, + 'github:toggle-expanded-commit-message-editor': this.toggleExpandedCommitMessageEditor, + }), + this.props.config.onDidChange('github.automaticCommitMessageWrapping', () => this.forceUpdate()), + ); } render() { @@ -94,17 +83,17 @@ export default class CommitView { } const showAbortMergeButton = this.props.isMerging || null; - const showAmendBox = ( - !this.props.isMerging && - this.props.lastCommit.isPresent() && - !this.props.lastCommit.isUnbornRef() - ) || null; + const showAmendBox = !this.props.isMerging && this.props.lastCommit.isPresent() + && !this.props.lastCommit.isUnbornRef(); return ( -
+
- { + this.editorElement = c; + this.editor = c && c.getModel(); + }} softWrapped={true} placeholderText="Commit message" lineNumberGutterVisible={false} @@ -112,32 +101,55 @@ export default class CommitView { autoHeight={false} scrollPastEnd={false} /> - {this.renderHardWrapIcons()} - + this.refHardWrapButton} + className="github-CommitView-hardwrap-tooltip" + title="Toggle hard wrap on commit" + /> +
{showAbortMergeButton && - + } {showAmendBox && } - -
+
{this.getRemainingCharacters()}
@@ -145,12 +157,12 @@ export default class CommitView { ); } - @autobind - renderHardWrapIcons() { + renderHardWrapIcon() { const singleLineMessage = this.editor && this.editor.getText().split(LINE_ENDING_REGEX).length === 1; const hardWrap = this.props.config.get('github.automaticCommitMessageWrapping'); const notApplicable = this.props.deactivateCommitBox || singleLineMessage; + /* eslint-disable max-len */ const svgPaths = { hardWrapEnabled: { path1: 'M7.058 10.2h-.975v2.4L2 9l4.083-3.6v2.4h.97l1.202 1.203L7.058 10.2zm2.525-4.865V4.2h2.334v1.14l-1.164 1.165-1.17-1.17z', // eslint-disable-line max-len @@ -160,55 +172,44 @@ export default class CommitView { path1: 'M11.917 8.4c0 .99-.788 1.8-1.75 1.8H6.083v2.4L2 9l4.083-3.6v2.4h3.5V4.2h2.334v4.2z', }, }; + /* eslint-enable max-line */ - return ( -
-
+ if (notApplicable) { + return null; + } + + if (hardWrap) { + return ( +
- - - - +
-
+ ); + } else { + return ( +
- + + + +
-
- ); - } - - writeAfterUpdate() { - if (this.props.deactivateCommitBox && this.tooltipsExist()) { - this.disposeTooltips(); - } else if (!this.props.deactivateCommitBox && !this.tooltipsExist()) { - this.registerTooltips(); + ); } } - registerTooltips() { - this.expandTooltip = this.props.tooltips.add(this.refs.expandButton, { - title: 'Expand commit message editor', - class: 'github-CommitView-expandButton-tooltip', - }); - this.hardWrapTooltip = this.props.tooltips.add(this.refs.hardWrapButton, { - title: 'Toggle hard wrap on commit', - class: 'github-CommitView-hardwrap-tooltip', - }); - this.subscriptions.add(this.expandTooltip, this.hardWrapTooltip); - } + componentWillReceiveProps(nextProps) { + if (this.editor && this.props.message !== nextProps.message && this.editor.getText() !== nextProps.message) { + this.editor.setText(nextProps.message); + } - tooltipsExist() { - return this.expandTooltip || this.hardWrapTooltip; + this.triggerShowWorking(nextProps); } - disposeTooltips() { - this.expandTooltip && this.expandTooltip.dispose(); - this.hardWrapTooltip && this.hardWrapTooltip.dispose(); - this.expandTooltip = null; - this.hardWrapTooltip = null; + componentWillUnmount() { + this.subscriptions.dispose(); } @autobind @@ -223,8 +224,8 @@ export default class CommitView { } @autobind - handleAmendBoxClick() { - this.props.setAmending(this.refs.amend.checked); + handleAmendBoxClick(event) { + this.props.setAmending(this.refAmendCheckbox.checked); } @autobind @@ -252,6 +253,27 @@ export default class CommitView { } } + // We don't want the user to see the UI flicker in the case + // the commit takes a very small time to complete. Instead we + // will only show the working message if we are working for longer + // than 1 second as per https://www.nngroup.com/articles/response-times-3-important-limits/ + // + // The closure is created to restrict variable access + triggerShowWorking(props) { + if (props.isCommitting) { + if (!this.state.showWorking && this.timeoutHandle === null) { + this.timeoutHandle = setTimeout(() => { + this.timeoutHandle = null; + this.setState({showWorking: true}); + }, 1000); + } + } else { + clearTimeout(this.timeoutHandle); + this.timeoutHandle = null; + this.setState({showWorking: false}); + } + } + isCommitButtonEnabled() { return !this.props.isCommitting && this.props.stagedChangesExist && @@ -263,7 +285,7 @@ export default class CommitView { commitButtonText() { if (this.props.isAmending) { return `Amend commit (${shortenSha(this.props.lastCommit.getSha())})`; - } else if (this.shouldShowWorking()) { + } else if (this.state.showWorking) { return 'Working...'; } else { if (this.props.branchName) { @@ -284,15 +306,15 @@ export default class CommitView { return CommitView.focus.EDITOR; } - if (this.refs.abortMergeButton && this.refs.abortMergeButton.contains(event.target)) { + if (this.refAbortMergeButton && this.refAbortMergeButton.contains(event.target)) { return CommitView.focus.ABORT_MERGE_BUTTON; } - if (this.refs.amend && this.refs.amend.contains(event.target)) { + if (this.refAmendCheckbox && this.refAmendCheckbox.contains(event.target)) { return CommitView.focus.AMEND_BOX; } - if (this.refs.commitButton && this.refs.commitButton.contains(event.target)) { + if (this.refCommitButton && this.refCommitButton.contains(event.target)) { return CommitView.focus.COMMIT_BUTTON; } @@ -308,8 +330,8 @@ export default class CommitView { } if (focus === CommitView.focus.ABORT_MERGE_BUTTON) { - if (this.refs.abortMergeButton) { - this.refs.abortMergeButton.focus(); + if (this.refAbortMergeButton) { + this.refAbortMergeButton.focus(); return true; } else { fallback = true; @@ -317,8 +339,8 @@ export default class CommitView { } if (focus === CommitView.focus.AMEND_BOX) { - if (this.refs.amend) { - this.refs.amend.focus(); + if (this.refAmendCheckbox) { + this.refAmendCheckbox.focus(); return true; } else { fallback = true; @@ -326,8 +348,8 @@ export default class CommitView { } if (focus === CommitView.focus.COMMIT_BUTTON) { - if (this.refs.commitButton) { - this.refs.commitButton.focus(); + if (this.refCommitButton) { + this.refCommitButton.focus(); return true; } else { fallback = true; diff --git a/test/views/commit-view.test.js b/test/views/commit-view.test.js index ae2b3fcf17..5d45b6343e 100644 --- a/test/views/commit-view.test.js +++ b/test/views/commit-view.test.js @@ -1,12 +1,13 @@ -import {cloneRepository, buildRepository} from '../helpers'; -import etch from 'etch'; -import until from 'test-until'; +import React from 'react'; +import {shallow, mount} from 'enzyme'; +import {cloneRepository, buildRepository} from '../helpers'; import Commit, {nullCommit} from '../../lib/models/commit'; import CommitView from '../../lib/views/commit-view'; -describe('CommitView', function() { +describe.only('CommitView', function() { let atomEnv, commandRegistry, tooltips, config, lastCommit; + let app; beforeEach(function() { atomEnv = global.buildAtomEnvironment(); @@ -15,6 +16,29 @@ describe('CommitView', function() { config = atomEnv.config; lastCommit = new Commit('1234abcd', 'commit message'); + const noop = () => {}; + + app = ( + + ); }); afterEach(function() { @@ -22,224 +46,211 @@ describe('CommitView', function() { }); describe('when the repo is loading', function() { - let view; - beforeEach(function() { - view = new CommitView({ - commandRegistry, tooltips, config, lastCommit: nullCommit, - stagedChangesExist: false, maximumCharacterLimit: 72, message: '', - }); + app = React.cloneElement(app, {lastCommit: nullCommit}); }); it("doesn't show the amend checkbox", function() { - assert.isUndefined(view.refs.amend); + const wrapper = shallow(app); + assert.isFalse(wrapper.find('.github-CommitView-amend').exists()); }); - it('disables the commit button', async function() { - view.refs.editor.setText('even with text'); - await view.update({}); + it('disables the commit button', function() { + app = React.cloneElement(app, {message: 'even with text'}); + const wrapper = shallow(app); - assert.isTrue(view.refs.commitButton.disabled); + assert.isTrue(wrapper.find('.github-CommitView-commit').prop('disabled')); }); }); - it('displays the remaining characters limit based on which line is being edited', async function() { - const view = new CommitView({ - commandRegistry, tooltips, config, lastCommit, - stagedChangesExist: true, maximumCharacterLimit: 72, message: '', - }); - assert.equal(view.refs.remainingCharacters.textContent, '72'); - - await view.update({message: 'abcde fghij'}); - assert.equal(view.refs.remainingCharacters.textContent, '61'); - assert(!view.refs.remainingCharacters.classList.contains('is-error')); - assert(!view.refs.remainingCharacters.classList.contains('is-warning')); - - await view.update({message: '\nklmno'}); - assert.equal(view.refs.remainingCharacters.textContent, '∞'); - assert(!view.refs.remainingCharacters.classList.contains('is-error')); - assert(!view.refs.remainingCharacters.classList.contains('is-warning')); - - await view.update({message: 'abcde\npqrst'}); - assert.equal(view.refs.remainingCharacters.textContent, '∞'); - assert(!view.refs.remainingCharacters.classList.contains('is-error')); - assert(!view.refs.remainingCharacters.classList.contains('is-warning')); - - view.editor.setCursorBufferPosition([0, 3]); - await etch.getScheduler().getNextUpdatePromise(); - assert.equal(view.refs.remainingCharacters.textContent, '67'); - assert(!view.refs.remainingCharacters.classList.contains('is-error')); - assert(!view.refs.remainingCharacters.classList.contains('is-warning')); - - await view.update({stagedChangesExist: true, maximumCharacterLimit: 50}); - assert.equal(view.refs.remainingCharacters.textContent, '45'); - assert(!view.refs.remainingCharacters.classList.contains('is-error')); - assert(!view.refs.remainingCharacters.classList.contains('is-warning')); - - await view.update({message: 'a'.repeat(41)}); - assert.equal(view.refs.remainingCharacters.textContent, '9'); - assert(!view.refs.remainingCharacters.classList.contains('is-error')); - assert(view.refs.remainingCharacters.classList.contains('is-warning')); - - await view.update({message: 'a'.repeat(58)}); - assert.equal(view.refs.remainingCharacters.textContent, '-8'); - assert(view.refs.remainingCharacters.classList.contains('is-error')); - assert(!view.refs.remainingCharacters.classList.contains('is-warning')); + it('displays the remaining characters limit based on which line is being edited', function() { + const wrapper = mount(app); + const remaining = wrapper.find('.github-CommitView-remaining-characters'); + assert.strictEqual(remaining.text(), '72'); + + wrapper.setProps({message: 'abcde fghij'}); + assert.strictEqual(remaining.text(), '61'); + assert.isFalse(remaining.hasClass('is-error')); + assert.isFalse(remaining.hasClass('is-warning')); + + wrapper.setProps({message: '\nklmno'}); + assert.strictEqual(remaining.text(), '∞'); + assert.isFalse(remaining.hasClass('is-error')); + assert.isFalse(remaining.hasClass('is-warning')); + + wrapper.setProps({message: 'abcde\npqrst'}); + assert.strictEqual(remaining.text(), '∞'); + assert.isFalse(remaining.hasClass('is-error')); + assert.isFalse(remaining.hasClass('is-warning')); + + wrapper.find('atom-text-editor').getNode().getModel().setCursorBufferPosition([0, 3]); + wrapper.update(); + assert.strictEqual(remaining.text(), '67'); + assert.isFalse(remaining.hasClass('is-error')); + assert.isFalse(remaining.hasClass('is-warning')); + + wrapper.setProps({stagedChangesExist: true, maximumCharacterLimit: 50}); + assert.strictEqual(remaining.text(), '45'); + assert.isFalse(remaining.hasClass('is-error')); + assert.isFalse(remaining.hasClass('is-warning')); + + wrapper.setProps({message: 'a'.repeat(41)}); + assert.strictEqual(remaining.text(), '9'); + assert.isFalse(remaining.hasClass('is-error')); + assert.isTrue(remaining.hasClass('is-warning')); + + wrapper.setProps({message: 'a'.repeat(58)}); + assert.strictEqual(wrapper.find('.github-CommitView-remaining-characters').text(), '-8'); + assert.isTrue(wrapper.find('.github-CommitView-remaining-characters').hasClass('is-error')); + assert.isFalse(wrapper.find('.github-CommitView-remaining-characters').hasClass('is-warning')); }); describe('the commit button', function() { - let view, editor, commitButton; + let wrapper, commitButton; beforeEach(async function() { const workdirPath = await cloneRepository('three-files'); const repository = await buildRepository(workdirPath); - const viewState = {}; - view = new CommitView({ - repository, commandRegistry, tooltips, config, lastCommit, - stagedChangesExist: true, mergeConflictsExist: false, viewState, + + app = React.cloneElement(app, { + repository, + stagedChangesExist: true, + mergeConflictsExist: false, + message: 'something', }); - editor = view.refs.editor; - commitButton = view.refs.commitButton; + wrapper = mount(app); - editor.setText('something'); - await etch.getScheduler().getNextUpdatePromise(); + commitButton = wrapper.find('.github-CommitView-commit'); }); - it('is disabled when no changes are staged', async function() { - await view.update({stagedChangesExist: false}); - assert.isTrue(commitButton.disabled); + it('is disabled when no changes are staged', function() { + wrapper.setProps({stagedChangesExist: false}); + assert.isTrue(commitButton.prop('disabled')); - await view.update({stagedChangesExist: true}); - assert.isFalse(commitButton.disabled); + wrapper.setProps({stagedChangesExist: true}); + assert.isFalse(commitButton.prop('disabled')); }); - it('is disabled when there are merge conflicts', async function() { - await view.update({mergeConflictsExist: false}); - assert.isFalse(commitButton.disabled); + it('is disabled when there are merge conflicts', function() { + wrapper.setProps({mergeConflictsExist: true}); + assert.isTrue(commitButton.prop('disabled')); - await view.update({mergeConflictsExist: true}); - assert.isTrue(commitButton.disabled); + wrapper.setProps({mergeConflictsExist: false}); + assert.isFalse(commitButton.prop('disabled')); }); - it('is disabled when the commit message is empty', async function() { - editor.setText(''); - await etch.getScheduler().getNextUpdatePromise(); - assert.isTrue(commitButton.disabled); + it('is disabled when the commit message is empty', function() { + wrapper.setProps({message: ''}); + assert.isTrue(commitButton.prop('disabled')); - editor.setText('Not empty'); - await etch.getScheduler().getNextUpdatePromise(); - assert.isFalse(commitButton.disabled); + wrapper.setProps({message: 'Not empty'}); + assert.isFalse(commitButton.prop('disabled')); }); }); describe('committing', function() { - let view, commit, prepareToCommitResolution; - let editor, commitButton, workspaceElement; + let commit, prepareToCommitResolution; + let wrapper, editorElement, editor, commitButton, workspaceElement; - beforeEach(async function() { + beforeEach(function() { const prepareToCommit = () => Promise.resolve(prepareToCommitResolution); commit = sinon.spy(); - view = new CommitView({ - commandRegistry, tooltips, config, lastCommit, - stagedChangesExist: true, prepareToCommit, commit, message: 'Something', - }); - sinon.spy(view.editorElement, 'focus'); + app = React.cloneElement(app, {stagedChangesExist: true, prepareToCommit, commit, message: 'Something'}); + wrapper = mount(app); - editor = view.refs.editor; - commitButton = view.refs.commitButton; + editorElement = wrapper.find('atom-text-editor').getNode(); + sinon.spy(editorElement, 'focus'); + editor = editorElement.getModel(); - workspaceElement = atomEnv.views.getView(atomEnv.workspace); + commitButton = wrapper.find('.github-CommitView-commit'); - await view.update(); + workspaceElement = atomEnv.views.getView(atomEnv.workspace); }); describe('when props.prepareToCommit() resolves true', function() { - beforeEach(function() { prepareToCommitResolution = true; }); + beforeEach(function() { + prepareToCommitResolution = true; + }); it('calls props.commit(message) when the commit button is clicked', async function() { - commitButton.dispatchEvent(new MouseEvent('click')); + commitButton.simulate('click'); - await until('props.commit() is called', () => commit.calledWith('Something')); + await assert.async.isTrue(commit.calledWith('Something')); // undo history is cleared - commandRegistry.dispatch(editor.element, 'core:undo'); + commandRegistry.dispatch(editorElement, 'core:undo'); assert.equal(editor.getText(), ''); }); it('calls props.commit(message) when github:commit is dispatched', async function() { commandRegistry.dispatch(workspaceElement, 'github:commit'); - await until('props.commit() is called', () => commit.calledWith('Something')); + await assert.async.isTrue(commit.calledWith('Something')); }); }); describe('when props.prepareToCommit() resolves false', function() { - beforeEach(function() { prepareToCommitResolution = false; }); + beforeEach(function() { + prepareToCommitResolution = false; + }); it('takes no further action when the commit button is clicked', async function() { - commitButton.dispatchEvent(new MouseEvent('click')); + commitButton.simulate('click'); - await assert.async.isTrue(view.editorElement.focus.called); + await assert.async.isTrue(editorElement.focus.called); assert.isFalse(commit.called); }); it('takes no further action when github:commit is dispatched', async function() { commandRegistry.dispatch(workspaceElement, 'github:commit'); - await assert.async.isTrue(view.editorElement.focus.called); + await assert.async.isTrue(editorElement.focus.called); assert.isFalse(commit.called); }); }); }); - it('shows the "Abort Merge" button when props.isMerging is true', async function() { - const view = new CommitView({commandRegistry, tooltips, config, lastCommit, stagedChangesExist: true, isMerging: false}); - assert.isUndefined(view.refs.abortMergeButton); - - await view.update({isMerging: true}); - assert.isDefined(view.refs.abortMergeButton); + it('shows the "Abort Merge" button when props.isMerging is true', function() { + app = React.cloneElement(app, {isMerging: true}); + const wrapper = shallow(app); + assert.isTrue(wrapper.find('.github-CommitView-abortMerge').exists()); - await view.update({isMerging: false}); - assert.isUndefined(view.refs.abortMergeButton); + wrapper.setProps({isMerging: false}); + assert.isFalse(wrapper.find('.github-CommitView-abortMerge').exists()); }); it('calls props.abortMerge() when the "Abort Merge" button is clicked', function() { - const abortMerge = sinon.spy(() => Promise.resolve()); - const view = new CommitView({ - commandRegistry, tooltips, config, lastCommit, - stagedChangesExist: true, isMerging: true, abortMerge, - }); - const {abortMergeButton} = view.refs; - abortMergeButton.dispatchEvent(new MouseEvent('click')); - assert(abortMerge.calledOnce); + const abortMerge = sinon.stub().resolves(); + app = React.cloneElement(app, {abortMerge, stagedChangesExist: true, isMerging: true}); + const wrapper = shallow(app); + + wrapper.find('.github-CommitView-abortMerge').simulate('click'); + assert.isTrue(abortMerge.calledOnce); }); describe('amending', function() { it('calls props.setAmending() when the box is checked or unchecked', function() { const previousCommit = new Commit('111', "previous commit's message"); - const setAmending = sinon.spy(); - const view = new CommitView({ - commandRegistry, tooltips, config, - stagedChangesExist: false, lastCommit: previousCommit, setAmending, - }); - const {amend} = view.refs; - amend.click(); + app = React.cloneElement(app, {stagedChangesExist: false, lastCommit: previousCommit, setAmending}); + const wrapper = mount(app); + + wrapper.setProps({isAmending: true}); + wrapper.find('.github-CommitView-amend').simulate('click'); assert.deepEqual(setAmending.args, [[true]]); - amend.click(); + wrapper.setProps({isAmending: false}); + wrapper.find('.github-CommitView-amend').simulate('click'); assert.deepEqual(setAmending.args, [[true], [false]]); }); it('is hidden when HEAD is an unborn ref', function() { - const view = new CommitView({ - commandRegistry, tooltips, config, - stagedChangesExist: false, - lastCommit: Commit.createUnborn(), - }); - assert.isUndefined(view.refs.amend); + app = React.cloneElement(app, {stagedChangesExist: false, lastCommit: Commit.createUnborn()}); + const wrapper = shallow(app); + + assert.isFalse(wrapper.find('.github-CommitView-amend').exists()); }); }); }); From f6f6cb37f730032f0aafefd3c4a2737d6b8563c4 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 26 Feb 2018 15:06:24 -0500 Subject: [PATCH 0340/5882] Un-Etch-Wrap CommitViewController --- lib/views/git-tab-view.js | 51 +++++++++++++++++++-------------------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/lib/views/git-tab-view.js b/lib/views/git-tab-view.js index eddb92e2ee..eb96d7c27a 100644 --- a/lib/views/git-tab-view.js +++ b/lib/views/git-tab-view.js @@ -161,28 +161,27 @@ export default class GitTabView extends React.Component { isMerging={this.props.isMerging} /> - { this.refCommitViewController = c; }}> - 0} - mergeConflictsExist={this.props.mergeConflicts.length > 0} - prepareToCommit={this.props.prepareToCommit} - commit={this.props.commit} - abortMerge={this.props.abortMerge} - branchName={''} - workspace={this.props.workspace} - commandRegistry={this.props.commandRegistry} - notificationManager={this.props.notificationManager} - grammars={this.props.grammars} - mergeMessage={this.props.mergeMessage} - isMerging={this.props.isMerging} - isAmending={this.props.isAmending} - isLoading={this.props.isLoading} - lastCommit={this.props.lastCommit} - repository={this.props.repository} - /> - + { this.refCommitViewController = c; }} + tooltips={this.props.tooltips} + config={this.props.config} + stagedChangesExist={this.props.stagedChanges.length > 0} + mergeConflictsExist={this.props.mergeConflicts.length > 0} + prepareToCommit={this.props.prepareToCommit} + commit={this.props.commit} + abortMerge={this.props.abortMerge} + branchName={''} + workspace={this.props.workspace} + commandRegistry={this.props.commandRegistry} + notificationManager={this.props.notificationManager} + grammars={this.props.grammars} + mergeMessage={this.props.mergeMessage} + isMerging={this.props.isMerging} + isAmending={this.props.isAmending} + isLoading={this.props.isLoading} + lastCommit={this.props.lastCommit} + repository={this.props.repository} + />
); } @@ -214,7 +213,7 @@ export default class GitTabView extends React.Component { } if (!currentFocus && this.refCommitViewController) { - currentFocus = this.refCommitViewController.getWrappedComponent().rememberFocus(event); + currentFocus = this.refCommitViewController.rememberFocus(event); } return currentFocus; @@ -228,7 +227,7 @@ export default class GitTabView extends React.Component { } if (this.refCommitViewController) { - if (this.refCommitViewController.getWrappedComponent().setFocus(focus)) { + if (this.refCommitViewController.setFocus(focus)) { return true; } } @@ -244,7 +243,7 @@ export default class GitTabView extends React.Component { @autobind advanceFocus(evt) { if (!this.refStagingView.getWrappedComponent().activateNextList()) { - if (this.refCommitViewController.getWrappedComponent().setFocus(GitTabView.focus.EDITOR)) { + if (this.refCommitViewController.setFocus(GitTabView.focus.EDITOR)) { evt.stopPropagation(); } } else { @@ -255,7 +254,7 @@ export default class GitTabView extends React.Component { @autobind retreatFocus(evt) { const stagingView = this.refStagingView.getWrappedComponent(); - const commitViewController = this.refCommitViewController.getWrappedComponent(); + const commitViewController = this.refCommitViewController; if (commitViewController.hasFocus()) { if (stagingView.activateLastList()) { From 8cf5fe6da8191ca2ad601316535609e128bbac5a Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 27 Feb 2018 11:26:28 -0500 Subject: [PATCH 0341/5882] Remove unused props --- lib/controllers/commit-view-controller.js | 4 +--- test/controllers/commit-view-controller.test.js | 1 - 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/controllers/commit-view-controller.js b/lib/controllers/commit-view-controller.js index a67bb66213..c7be36e9bc 100644 --- a/lib/controllers/commit-view-controller.js +++ b/lib/controllers/commit-view-controller.js @@ -25,7 +25,7 @@ export default class CommitViewController extends React.Component { repository: PropTypes.object.isRequired, isMerging: PropTypes.bool.isRequired, isAmending: PropTypes.bool.isRequired, - mergeMessage: PropTypes.string.isRequired, + mergeMessage: PropTypes.string, mergeConflictsExist: PropTypes.bool.isRequired, stagedChangesExist: PropTypes.bool.isRequired, lastCommit: PropTypes.object.isRequired, @@ -33,7 +33,6 @@ export default class CommitViewController extends React.Component { prepareToCommit: PropTypes.func.isRequired, commit: PropTypes.func.isRequired, abortMerge: PropTypes.func.isRequired, - didMoveUpOnFirstLine: PropTypes.func.isRequired, } constructor(props, context) { @@ -102,7 +101,6 @@ export default class CommitViewController extends React.Component { isCommitting={operationStates.isCommitInProgress()} lastCommit={this.props.lastCommit} onChangeMessage={this.handleMessageChange} - didMoveUpOnFirstLine={this.props.didMoveUpOnFirstLine} toggleExpandedCommitMessageEditor={this.toggleExpandedCommitMessageEditor} deactivateCommitBox={!!this.getCommitMessageEditors().length > 0} /> diff --git a/test/controllers/commit-view-controller.test.js b/test/controllers/commit-view-controller.test.js index f67ba7e710..fde1289f67 100644 --- a/test/controllers/commit-view-controller.test.js +++ b/test/controllers/commit-view-controller.test.js @@ -42,7 +42,6 @@ describe('CommitViewController', function() { lastCommit={lastCommit} prepareToCommit={noop} commit={noop} - didMoveUpOnFirstLine={noop} abortMerge={noop} /> ); From cbe874096fff761143f15fc08bb9b5aa547663ea Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 27 Feb 2018 11:26:49 -0500 Subject: [PATCH 0342/5882] Extract real component from ModelObserver wrapper --- lib/controllers/root-controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/controllers/root-controller.js b/lib/controllers/root-controller.js index f8fea2612f..c742c64eb7 100644 --- a/lib/controllers/root-controller.js +++ b/lib/controllers/root-controller.js @@ -173,10 +173,10 @@ export default class RootController extends React.Component { workspace={this.props.workspace} onDidCloseItem={this.props.destroyGitTabItem} stubItem={this.props.gitTabStubItem} + getItem={({subtree}) => subtree.getWrappedComponentInstance()} activate={this.props.startOpen}> { this.gitTabController = c; }} - workspace={this.props.workspace} commandRegistry={this.props.commandRegistry} notificationManager={this.props.notificationManager} From 05d01fefea1a8181ad39b9ac52fb2a067e69d8d3 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 27 Feb 2018 14:59:46 -0500 Subject: [PATCH 0343/5882] wip --- lib/views/atom-text-editor.js | 125 ++++++++++++++++++++++++++++++++++ lib/views/commit-view.js | 35 +++++----- 2 files changed, 143 insertions(+), 17 deletions(-) create mode 100644 lib/views/atom-text-editor.js diff --git a/lib/views/atom-text-editor.js b/lib/views/atom-text-editor.js new file mode 100644 index 0000000000..430ebd0844 --- /dev/null +++ b/lib/views/atom-text-editor.js @@ -0,0 +1,125 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import {CompositeDisposable} from 'event-kit'; + +const editorProps = { + autoIndent: PropTypes.bool, + autoIndentOnPaste: PropTypes.bool, + undoGroupingInterval: PropTypes.number, + scrollSensitivity: PropTypes.number, + encoding: PropTypes.string, + softTabs: PropTypes.bool, + atomicSoftTabs: PropTypes.bool, + tabLength: PropTypes.number, + softWrapped: PropTypes.bool, + softWrapHangingIndentLenth: PropTypes.number, + softWrapAtPreferredLineLength: PropTypes.bool, + preferredLineLength: PropTypes.number, + maxScreenLineLength: PropTypes.number, + mini: PropTypes.bool, + readOnly: PropTypes.bool, + placeholderText: PropTypes.string, + lineNumberGutterVisible: PropTypes.bool, + showIndentGuide: PropTypes.bool, + showLineNumbers: PropTypes.bool, + showInvisibles: PropTypes.bool, + invisibles: PropTypes.string, + editorWidthInChars: PropTypes.number, + width: PropTypes.number, + scrollPastEnd: PropTypes.bool, + autoHeight: PropTypes.bool, + autoWidth: PropTypes.bool, + showCursorOnSelection: PropTypes.bool, +}; + +const nullDisposable = { + dispose() {}, +}; + +export default class AtomTextEditor extends React.PureComponent { + static propTypes = { + ...editorProps, + text: PropTypes.string, + didChange: PropTypes.func, + didChangeCursorPosition: PropTypes.func, + } + + constructor(props, context) { + super(props, context); + + this.subs = new CompositeDisposable(); + this.didChangeSub = nullDisposable; + this.didChangeCursorPositionSub = nullDisposable; + } + + render() { + return ( + { this.refElement = c; }} /> + ); + } + + componentDidMount() { + this.setAttributesOnElement(this.props); + } + + componentDidUpdate(nextProps) { + this.setAttributesOnElement(nextProps); + } + + componentWillUnmount() { + this.subs.dispose(); + } + + setAttributesOnElement(theProps) { + const modelProps = Object.keys(editorProps).reduce((ps, key) => { + if (theProps[key] !== undefined) { + ps[key] = theProps[key]; + } + return ps; + }, {}); + + const editor = this.getModel(); + editor.update(modelProps); + + if (theProps.text && editor.getText() !== theProps.text) { + editor.setText(theProps.text); + } + + if (theProps.didChange && + (theProps === this.props || theProps.didChange !== this.props.didChange)) { + this.didChangeSub.dispose(); + this.subs.remove(this.didChangeSub); + this.didChangeSub = editor.onDidChange(() => { + console.log('onDidChange handler...'); + if (!this.props.text || this.getModel().getText() !== this.props.text) { + console.log(`${this.getModel().getText()} vs. ${this.props.text}`); + theProps.didChange(this.getModel()); + } + }); + this.subs.add(this.didChangeSub); + } + + if (theProps.didChangeCursorPosition && + (theProps === this.props || theProps.didChangeCursorPosition !== this.props.didChangeCursorPosition)) { + this.didChangeCursorPositionSub.dispose(); + this.subs.remove(this.didChangeCursorPositionSub); + this.didChangeCursorPositionSub = editor.onDidChangeCursorPosition(() => { + theProps.didChangeCursorPosition(this.getModel()); + }); + this.subs.add(this.didChangeCursorPositionSub); + } + } + + contains(element) { + return this.refElement.contains(element); + } + + focus() { + this.refElement.focus(); + } + + getModel() { + return this.refElement.getModel(); + } +} diff --git a/lib/views/commit-view.js b/lib/views/commit-view.js index 43d582395d..47d9e30e03 100644 --- a/lib/views/commit-view.js +++ b/lib/views/commit-view.js @@ -5,6 +5,7 @@ import {autobind} from 'core-decorators'; import cx from 'classnames'; import Tooltip from './tooltip'; +import AtomTextEditor from './atom-text-editor'; import {shortenSha} from '../helpers'; const LINE_ENDING_REGEX = /\r?\n/; @@ -54,18 +55,9 @@ export default class CommitView extends React.Component { } componentWillMount() { - this.triggerShowWorking(this.props); - } - - componentDidMount() { - if (this.editor.getText() !== this.props.message) { - this.editor.setText(this.props.message); - this.forceUpdate(); - } + this.scheduleShowWorking(this.props); this.subscriptions = new CompositeDisposable( - this.editor.onDidChange(() => this.props.onChangeMessage(this.editor.getText())), - this.editor.onDidChangeCursorPosition(() => this.forceUpdate()), this.props.commandRegistry.add('atom-workspace', { 'github:commit': this.commit, 'github:toggle-expanded-commit-message-editor': this.toggleExpandedCommitMessageEditor, @@ -89,7 +81,7 @@ export default class CommitView extends React.Component { return (
- { this.editorElement = c; this.editor = c && c.getModel(); @@ -100,6 +92,9 @@ export default class CommitView extends React.Component { showInvisibles={false} autoHeight={false} scrollPastEnd={false} + text={this.props.message} + didChange={this.didChangeCommitMessage} + didChangeCursorPosition={this.didMoveCursor} />
); } diff --git a/lib/views/recent-commits-view.js b/lib/views/recent-commits-view.js new file mode 100644 index 0000000000..480c224519 --- /dev/null +++ b/lib/views/recent-commits-view.js @@ -0,0 +1,59 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import {autobind} from 'core-decorators'; + +class RecentCommitView extends React.Component { + static propTypes = { + commit: PropTypes.object.isRequired, + }; + + render() { + return ( +
Herp derp derp
+ ); + } +} + +export default class RecentCommitsView extends React.Component { + static propTypes = { + commits: PropTypes.arrayOf(PropTypes.object).isRequired, + isLoading: PropTypes.bool.isRequired, + }; + + render() { + return ( +
+ {this.renderCommits()} +
+ ); + } + + renderCommits() { + if (this.props.commits.length === 0) { + if (this.props.isLoading) { + return ( +
+ Recent commits +
+ ); + } else { + return ( +
+ Make your first commit +
+ ); + } + } else { + return ( +
    + {this.props.commits.map(commit => { + return ( + + ); + })} +
+ ); + } + + } +} diff --git a/test/controllers/recent-commits-controller.test.js b/test/controllers/recent-commits-controller.test.js new file mode 100644 index 0000000000..10951cc1a7 --- /dev/null +++ b/test/controllers/recent-commits-controller.test.js @@ -0,0 +1,25 @@ +import React from 'react'; +import {shallow} from 'enzyme'; + +import RecentCommitsController from '../../lib/controllers/recent-commits-controller'; + +describe('RecentCommitsController', function() { + let app; + + beforeEach(function() { + app = ; + }); + + it('passes recent commits to the RecentCommitsView', function() { + const commits = [Symbol('1'), Symbol('2'), Symbol('3')]; + app = React.cloneElement(app, {commits}); + const wrapper = shallow(app); + assert.deepEqual(wrapper.find('RecentCommitsView').prop('commits'), commits); + }); + + it('passes fetch progress to the RecentCommitsView', function() { + app = React.cloneElement(app, {isLoading: true}); + const wrapper = shallow(app); + assert.isTrue(wrapper.find('RecentCommitsView').prop('isLoading')); + }); +}); diff --git a/test/views/recent-commits-view.test.js b/test/views/recent-commits-view.test.js new file mode 100644 index 0000000000..21142e6a93 --- /dev/null +++ b/test/views/recent-commits-view.test.js @@ -0,0 +1,43 @@ +import React from 'react'; +import {shallow} from 'enzyme'; + +import RecentCommitsView from '../../lib/views/recent-commits-view'; + +describe('RecentCommitsView', function() { + let app; + + beforeEach(function() { + app = ; + }); + + it('shows a placeholder while commits are empty and loading', function() { + app = React.cloneElement(app, {commits: [], isLoading: true}); + const wrapper = shallow(app); + + assert.isFalse(wrapper.find('RecentCommitView').exists()); + assert.strictEqual(wrapper.find('.github-RecentCommit-message').text(), 'Recent commits'); + }); + + it('shows a prompting message while commits are empty and not loading', function() { + app = React.cloneElement(app, {commits: [], isLoading: false}); + const wrapper = shallow(app); + + assert.isFalse(wrapper.find('RecentCommitView').exists()); + assert.strictEqual(wrapper.find('.github-RecentCommit-message').text(), 'Make your first commit'); + }); + + it('renders a RecentCommitView for each commit', function() { + const commits = ['1', '2', '3'].map(sha => { + return { + getSha() { return sha; }, + }; + }); + + app = React.cloneElement(app, {commits}); + const wrapper = shallow(app); + + assert.deepEqual(wrapper.find('RecentCommitView').map(w => w.prop('commit')), commits); + }); + + +}); From 725e77b5ce71bfae6a09e333a42d4ff5c6a2c1cb Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 23 Feb 2018 18:32:07 -0800 Subject: [PATCH 0360/5882] Render basic commit information --- lib/controllers/git-tab-controller.js | 2 +- lib/git-shell-out-strategy.js | 16 ++++++++++++---- lib/models/commit.js | 2 +- lib/models/repository-states/present.js | 4 ++-- lib/views/recent-commits-view.js | 6 +++++- 5 files changed, 21 insertions(+), 9 deletions(-) diff --git a/lib/controllers/git-tab-controller.js b/lib/controllers/git-tab-controller.js index f0875cf6c9..3bcef3bab3 100644 --- a/lib/controllers/git-tab-controller.js +++ b/lib/controllers/git-tab-controller.js @@ -16,7 +16,7 @@ import {nullCommit} from '../models/commit'; fetchData: (r, props) => { return yubikiri({ lastCommit: r.getLastCommit(), - recentCommits: r.getRecentCommits(), + recentCommits: r.getRecentCommits({max: 10}), isMerging: r.isMerging(), isRebasing: r.isRebasing(), isAmending: r.isAmending(), diff --git a/lib/git-shell-out-strategy.js b/lib/git-shell-out-strategy.js index 55d464386d..16f9fc1b65 100644 --- a/lib/git-shell-out-strategy.js +++ b/lib/git-shell-out-strategy.js @@ -569,10 +569,18 @@ export default class GitShellOutStrategy { } }); - return output.trim().split('\n').filter(Boolean).map(line => { - const [sha, authorEmail, authorDate, message, body] = line.split('\0'); - return {sha, authorEmail, authorDate: parseInt(authorDate), message, body}; - }); + const fields = output.trim().split('\0'); + const commits = []; + for (let i = 0; i < fields.length; i += 5) { + commits.push({ + sha: fields[i] && fields[i].trim(), + authorEmail: fields[i + 1], + authorDate: parseInt(fields[i + 2]), + message: fields[i + 3], + body: fields[i + 4], + }); + } + return commits; } readFileFromIndex(filePath) { diff --git a/lib/models/commit.js b/lib/models/commit.js index 2aaa7d5afe..554ad8fccd 100644 --- a/lib/models/commit.js +++ b/lib/models/commit.js @@ -9,7 +9,7 @@ export default class Commit { this.sha = sha; this.authorEmail = authorEmail; this.authorDate = authorDate; - this.message = message.trim(); + this.message = message; this.body = body; this.unbornRef = unbornRef === UNBORN; } diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js index d5c6f3f445..2d3782b68c 100644 --- a/lib/models/repository-states/present.js +++ b/lib/models/repository-states/present.js @@ -604,9 +604,9 @@ export default class Present extends State { }); } - getRecentCommits() { + getRecentCommits(options) { return this.cache.getOrSet(Keys.recentCommits, async () => { - const commits = await this.git().getRecentCommits(); + const commits = await this.git().getRecentCommits(options); return commits.map(commit => new Commit(commit)); }); } diff --git a/lib/views/recent-commits-view.js b/lib/views/recent-commits-view.js index 480c224519..4bc8cd025c 100644 --- a/lib/views/recent-commits-view.js +++ b/lib/views/recent-commits-view.js @@ -9,7 +9,11 @@ class RecentCommitView extends React.Component { render() { return ( -
Herp derp derp
+
  • +

    {this.props.commit.getAuthorEmail()}

    +

    {this.props.commit.getAuthorDate()}

    +

    {this.props.commit.getMessage()}

    +
  • ); } } From c2e5bd0594c0f4996ce964916844ff9dd0304159 Mon Sep 17 00:00:00 2001 From: simurai Date: Tue, 27 Feb 2018 09:34:45 +0900 Subject: [PATCH 0361/5882] Style RecentCommits --- lib/views/recent-commits-view.js | 4 ++-- styles/recent-commits.less | 34 ++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 styles/recent-commits.less diff --git a/lib/views/recent-commits-view.js b/lib/views/recent-commits-view.js index 4bc8cd025c..e0cfa4a3b9 100644 --- a/lib/views/recent-commits-view.js +++ b/lib/views/recent-commits-view.js @@ -10,9 +10,9 @@ class RecentCommitView extends React.Component { render() { return (
  • +

    {this.props.commit.getMessage()}

    {this.props.commit.getAuthorEmail()}

    {this.props.commit.getAuthorDate()}

    -

    {this.props.commit.getMessage()}

  • ); } @@ -49,7 +49,7 @@ export default class RecentCommitsView extends React.Component { } } else { return ( -
      +
        {this.props.commits.map(commit => { return ( diff --git a/styles/recent-commits.less b/styles/recent-commits.less new file mode 100644 index 0000000000..bab7109100 --- /dev/null +++ b/styles/recent-commits.less @@ -0,0 +1,34 @@ +@import "variables"; + +// Recents commits +// Shown under the commit message input + +.github-RecentCommits { + flex: 1; + max-height: 20vh; + overflow-x: hidden; + overflow-y: auto; + border-top: 1px solid @base-border-color; + + &-list { + margin: 0; + padding: 0; + list-style: none; + } +} + +.github-RecentCommit { + padding: @component-padding/2 @component-padding; + + p { + margin: 0; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + } + + & + & { + border-top: 1px solid @base-border-color; + } + +} From 5623b30d7a04a332fe6066e51146b36439131df9 Mon Sep 17 00:00:00 2001 From: simurai Date: Tue, 27 Feb 2018 11:38:36 +0900 Subject: [PATCH 0362/5882] Make commits compact So they fit on a single line --- lib/views/recent-commits-view.js | 12 +++++++++--- styles/recent-commits.less | 29 ++++++++++++++++++++++------- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/lib/views/recent-commits-view.js b/lib/views/recent-commits-view.js index e0cfa4a3b9..5c557c6d3c 100644 --- a/lib/views/recent-commits-view.js +++ b/lib/views/recent-commits-view.js @@ -10,9 +10,15 @@ class RecentCommitView extends React.Component { render() { return (
      • -

        {this.props.commit.getMessage()}

        -

        {this.props.commit.getAuthorEmail()}

        -

        {this.props.commit.getAuthorDate()}

        + + {this.props.commit.getMessage()} +
      • ); } diff --git a/styles/recent-commits.less b/styles/recent-commits.less index bab7109100..f95302352c 100644 --- a/styles/recent-commits.less +++ b/styles/recent-commits.less @@ -3,32 +3,47 @@ // Recents commits // Shown under the commit message input +@size: 16px; // make sure to double the size of the avatar in recent-commits-view.js to make it look crisp +@show-max: 3; // how many commits should be shown + .github-RecentCommits { - flex: 1; - max-height: 20vh; + // Fit @show-max + 1px for the top border + max-height: @show-max * @size + @component-padding * @show-max + @component-padding + 1px; overflow-x: hidden; overflow-y: auto; border-top: 1px solid @base-border-color; &-list { margin: 0; - padding: 0; + padding: @component-padding/2 0; list-style: none; } } .github-RecentCommit { + + + display: flex; padding: @component-padding/2 @component-padding; + line-height: @size; - p { - margin: 0; + &-avatar { + width: @size; + height: @size; + border-radius: @component-border-radius; + } + + &-message { + flex: 1; + margin: 0 .5em; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; + } - & + & { - border-top: 1px solid @base-border-color; + &-time { + color: @text-color-subtle; } } From 7b3bdf315effe54fc24883d9861140cedfc58b4d Mon Sep 17 00:00:00 2001 From: simurai Date: Tue, 27 Feb 2018 12:21:01 +0900 Subject: [PATCH 0363/5882] Style RecentCommits-message --- lib/views/recent-commits-view.js | 4 ++-- styles/recent-commits.less | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/views/recent-commits-view.js b/lib/views/recent-commits-view.js index 5c557c6d3c..489afd4ce5 100644 --- a/lib/views/recent-commits-view.js +++ b/lib/views/recent-commits-view.js @@ -42,13 +42,13 @@ export default class RecentCommitsView extends React.Component { if (this.props.commits.length === 0) { if (this.props.isLoading) { return ( -
        +
        Recent commits
        ); } else { return ( -
        +
        Make your first commit
        ); diff --git a/styles/recent-commits.less b/styles/recent-commits.less index f95302352c..dcbcbdd5e0 100644 --- a/styles/recent-commits.less +++ b/styles/recent-commits.less @@ -18,6 +18,12 @@ padding: @component-padding/2 0; list-style: none; } + + &-message { + padding: @component-padding/2 @component-padding; + text-align: center; + color: @text-color-subtle; + } } .github-RecentCommit { From 241242540b166611ebb9c51f358ea819240b2b99 Mon Sep 17 00:00:00 2001 From: simurai Date: Tue, 27 Feb 2018 15:10:42 +0900 Subject: [PATCH 0364/5882] Add default avatar --- img/avatar.svg | 3 +++ styles/recent-commits.less | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 img/avatar.svg diff --git a/img/avatar.svg b/img/avatar.svg new file mode 100644 index 0000000000..93c6a9647a --- /dev/null +++ b/img/avatar.svg @@ -0,0 +1,3 @@ + + + diff --git a/styles/recent-commits.less b/styles/recent-commits.less index dcbcbdd5e0..3624a45a07 100644 --- a/styles/recent-commits.less +++ b/styles/recent-commits.less @@ -37,6 +37,7 @@ width: @size; height: @size; border-radius: @component-border-radius; + background-color: mix(@text-color, @base-background-color, 15%); } &-message { @@ -45,7 +46,6 @@ text-overflow: ellipsis; white-space: nowrap; overflow: hidden; - } &-time { From 603969ad9e9e7ce36fd87eae0d7af4e4cf1ccefc Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 27 Feb 2018 15:32:36 -0500 Subject: [PATCH 0365/5882] Use -z and --pretty=format to \0-separate log --- lib/git-shell-out-strategy.js | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/lib/git-shell-out-strategy.js b/lib/git-shell-out-strategy.js index 16f9fc1b65..091c8a12a7 100644 --- a/lib/git-shell-out-strategy.js +++ b/lib/git-shell-out-strategy.js @@ -560,22 +560,23 @@ export default class GitShellOutStrategy { // %at - timestamp, UNIX timestamp // %s - subject // %b - body - const output = await this.exec(['log', '--pretty=%H%x00%ae%x00%at%x00%s%x00%b%x00', '--no-abbrev-commit', '-n', max, ref, '--']) - .catch(err => { - if (/unknown revision/.test(err.stdErr) || /bad revision 'HEAD'/.test(err.stdErr)) { - return ''; - } else { - throw err; - } - }); + const output = await this.exec([ + 'log', '--pretty=format:%H%x00%ae%x00%at%x00%s%x00%b', '--no-abbrev-commit', '-z', '-n', max, ref, '--', + ]).catch(err => { + if (/unknown revision/.test(err.stdErr) || /bad revision 'HEAD'/.test(err.stdErr)) { + return ''; + } else { + throw err; + } + }); const fields = output.trim().split('\0'); const commits = []; for (let i = 0; i < fields.length; i += 5) { commits.push({ sha: fields[i] && fields[i].trim(), - authorEmail: fields[i + 1], - authorDate: parseInt(fields[i + 2]), + authorEmail: fields[i + 1] && fields[i + 1].trim(), + authorDate: parseInt(fields[i + 2], 10), message: fields[i + 3], body: fields[i + 4], }); From 14672771248a890a90cd92096e8f406e2e77aa43 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 27 Feb 2018 15:32:41 -0500 Subject: [PATCH 0366/5882] :shirt: --- lib/views/recent-commits-view.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/views/recent-commits-view.js b/lib/views/recent-commits-view.js index 489afd4ce5..8b977ff6a7 100644 --- a/lib/views/recent-commits-view.js +++ b/lib/views/recent-commits-view.js @@ -9,13 +9,13 @@ class RecentCommitView extends React.Component { render() { return ( -
      • - + - {this.props.commit.getMessage()} -
      • {this.props.commit.getMessage()}
      • {this.props.commit.getMessage()} - +
      • ); } diff --git a/test/views/recent-commits-view.test.js b/test/views/recent-commits-view.test.js index 24b38bb616..87e64188fd 100644 --- a/test/views/recent-commits-view.test.js +++ b/test/views/recent-commits-view.test.js @@ -1,5 +1,6 @@ import React from 'react'; import {shallow, mount} from 'enzyme'; +import moment from 'moment'; import RecentCommitsView from '../../lib/views/recent-commits-view'; import Commit from '../../lib/models/commit'; @@ -56,4 +57,17 @@ describe('RecentCommitsView', function() { ], ); }); + + it("renders the commit's relative age", function() { + const commit = new Commit({ + sha: '1111111111', + authorEmail: 'me@hooray.party', + authorDate: 1519848555, + message: 'x', + }); + + app = React.cloneElement(app, {commits: [commit]}); + const wrapper = mount(app); + assert.isTrue(wrapper.find('Timeago').prop('time').isSame(1519848555000)); + }); }); From 97cec8dae60e29b92ed61a7a3ea3bdd024d4f4bc Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Wed, 28 Feb 2018 15:56:21 -0500 Subject: [PATCH 0378/5882] Render the full commit body in the title hover --- lib/views/recent-commits-view.js | 6 +++++- test/views/recent-commits-view.test.js | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/lib/views/recent-commits-view.js b/lib/views/recent-commits-view.js index 61886e6c14..fdaf289849 100644 --- a/lib/views/recent-commits-view.js +++ b/lib/views/recent-commits-view.js @@ -18,7 +18,11 @@ class RecentCommitView extends React.Component { src={'https://avatars.githubusercontent.com/u/e?email=' + encodeURIComponent(this.props.commit.getAuthorEmail())} title={`${this.props.commit.getAuthorEmail()}`} /> - {this.props.commit.getMessage()} + + {this.props.commit.getMessage()} + Date: Wed, 28 Feb 2018 15:57:14 -0500 Subject: [PATCH 0379/5882] :fire: unused import --- test/views/recent-commits-view.test.js | 1 - 1 file changed, 1 deletion(-) diff --git a/test/views/recent-commits-view.test.js b/test/views/recent-commits-view.test.js index ba875d7821..ad202cb181 100644 --- a/test/views/recent-commits-view.test.js +++ b/test/views/recent-commits-view.test.js @@ -1,6 +1,5 @@ import React from 'react'; import {shallow, mount} from 'enzyme'; -import moment from 'moment'; import RecentCommitsView from '../../lib/views/recent-commits-view'; import Commit from '../../lib/models/commit'; From aed0f9bcac741558f13ba01dd38d3806ad41956f Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Wed, 28 Feb 2018 16:01:59 -0500 Subject: [PATCH 0380/5882] s/fetchInProgress/isLoading/ --- lib/views/git-tab-view.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/views/git-tab-view.js b/lib/views/git-tab-view.js index dac5961fce..92e9c4ab32 100644 --- a/lib/views/git-tab-view.js +++ b/lib/views/git-tab-view.js @@ -131,7 +131,7 @@ export default class GitTabView extends React.Component {
        ); } else { - const isLoading = this.props.fetchInProgress || this.props.repository.showGitTabLoading(); + const isLoading = this.props.isLoading || this.props.repository.showGitTabLoading(); return (
        Date: Wed, 28 Feb 2018 16:02:05 -0500 Subject: [PATCH 0381/5882] :fire: unused import --- lib/controllers/recent-commits-controller.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/controllers/recent-commits-controller.js b/lib/controllers/recent-commits-controller.js index a4ff497f9b..1a375ad711 100644 --- a/lib/controllers/recent-commits-controller.js +++ b/lib/controllers/recent-commits-controller.js @@ -1,6 +1,5 @@ import React from 'react'; import PropTypes from 'prop-types'; -import {autobind} from 'core-decorators'; import RecentCommitsView from '../views/recent-commits-view'; From f71b5acab995d257efa63bcb02f91541ceaccb6f Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Wed, 28 Feb 2018 14:36:02 -0800 Subject: [PATCH 0382/5882] Fix getRecentCommits test for case when no commits exist yet --- lib/git-shell-out-strategy.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/git-shell-out-strategy.js b/lib/git-shell-out-strategy.js index 091c8a12a7..94f43d7dcc 100644 --- a/lib/git-shell-out-strategy.js +++ b/lib/git-shell-out-strategy.js @@ -570,6 +570,8 @@ export default class GitShellOutStrategy { } }); + if (output === '') { return []; } + const fields = output.trim().split('\0'); const commits = []; for (let i = 0; i < fields.length; i += 5) { From 0f74e588bbb41f72560b9e135528ffe5a9287f25 Mon Sep 17 00:00:00 2001 From: simurai Date: Thu, 1 Mar 2018 12:47:59 +0900 Subject: [PATCH 0383/5882] Add avatar size Makes it look sharper and probably uses less memory. --- lib/views/recent-commits-view.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/views/recent-commits-view.js b/lib/views/recent-commits-view.js index fdaf289849..7b375c0887 100644 --- a/lib/views/recent-commits-view.js +++ b/lib/views/recent-commits-view.js @@ -15,7 +15,7 @@ class RecentCommitView extends React.Component { return (
      • Date: Thu, 1 Mar 2018 14:29:08 +0900 Subject: [PATCH 0384/5882] Shorten time ago --- lib/views/recent-commits-view.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/lib/views/recent-commits-view.js b/lib/views/recent-commits-view.js index 7b375c0887..ec663765bd 100644 --- a/lib/views/recent-commits-view.js +++ b/lib/views/recent-commits-view.js @@ -4,6 +4,25 @@ import moment from 'moment'; import Timeago from './timeago'; +moment.updateLocale('en', { + relativeTime : { + future: "%s", + past: "%s", + s : 'Now', + ss : '<1m', + m: "1m", + mm: "%dm", + h: "1h", + hh: "%dh", + d: "1d", + dd: "%dd", + M: "1M", + MM: "%dM", + y: "1y", + yy: "%d y" + } +}); + class RecentCommitView extends React.Component { static propTypes = { commit: PropTypes.object.isRequired, From 921ade322278886b177f48bf52505997e8c95afa Mon Sep 17 00:00:00 2001 From: simurai Date: Thu, 1 Mar 2018 16:40:30 +0900 Subject: [PATCH 0385/5882] Make commits list responsive --- styles/recent-commits.less | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/styles/recent-commits.less b/styles/recent-commits.less index 3624a45a07..2a279dbfcd 100644 --- a/styles/recent-commits.less +++ b/styles/recent-commits.less @@ -53,3 +53,21 @@ } } + + +// Responsive +// Show more commits if window height gets larger + +@media (min-height: 900px) { + .github-RecentCommits { + @show-max: 5; + max-height: @show-max * @size + @component-padding * @show-max + @component-padding + 1px; + } +} + +@media (min-height: 1200px) { + .github-RecentCommits { + @show-max: 10; + max-height: @show-max * @size + @component-padding * @show-max + @component-padding + 1px; + } +} From ff51446730f13608f6511791f67d5fae63fcf91e Mon Sep 17 00:00:00 2001 From: simurai Date: Thu, 1 Mar 2018 17:25:59 +0900 Subject: [PATCH 0386/5882] Don't switch to full date when older than a month --- lib/views/timeago.js | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/lib/views/timeago.js b/lib/views/timeago.js index 6fa2587a1d..752f3a66ad 100644 --- a/lib/views/timeago.js +++ b/lib/views/timeago.js @@ -18,13 +18,7 @@ export default class Timeago extends React.Component { static getTimeDisplay(time, now = moment()) { const m = moment(time); - const diff = m.diff(now, 'months', true); - if (Math.abs(diff) <= 1) { - return m.from(now); - } else { - const format = m.format('MMM Do, YYYY'); - return `on ${format}`; - } + return m.from(now, true); } componentDidMount() { From 147a8ff4085fe7ea0483e486fe74eb33303aeac7 Mon Sep 17 00:00:00 2001 From: simurai Date: Thu, 1 Mar 2018 17:26:21 +0900 Subject: [PATCH 0387/5882] :art: --- lib/views/recent-commits-view.js | 19 ------------------- lib/views/timeago.js | 19 +++++++++++++++++++ 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/lib/views/recent-commits-view.js b/lib/views/recent-commits-view.js index ec663765bd..7b375c0887 100644 --- a/lib/views/recent-commits-view.js +++ b/lib/views/recent-commits-view.js @@ -4,25 +4,6 @@ import moment from 'moment'; import Timeago from './timeago'; -moment.updateLocale('en', { - relativeTime : { - future: "%s", - past: "%s", - s : 'Now', - ss : '<1m', - m: "1m", - mm: "%dm", - h: "1h", - hh: "%dh", - d: "1d", - dd: "%dd", - M: "1M", - MM: "%dM", - y: "1y", - yy: "%d y" - } -}); - class RecentCommitView extends React.Component { static propTypes = { commit: PropTypes.object.isRequired, diff --git a/lib/views/timeago.js b/lib/views/timeago.js index 752f3a66ad..6d98b969ae 100644 --- a/lib/views/timeago.js +++ b/lib/views/timeago.js @@ -3,6 +3,25 @@ import PropTypes from 'prop-types'; import moment from 'moment'; import cx from 'classnames'; +moment.updateLocale('en', { + relativeTime : { + future: "in %s", + past: "%s ago", + s : 'Now', + ss : '<1m', + m: "1m", + mm: "%dm", + h: "1h", + hh: "%dh", + d: "1d", + dd: "%dd", + M: "1M", + MM: "%dM", + y: "1y", + yy: "%d y" + } +}); + export default class Timeago extends React.Component { static propTypes = { time: PropTypes.any.isRequired, From f696da2959a24cc3b1e168f0be5d4bd3818c9d24 Mon Sep 17 00:00:00 2001 From: simurai Date: Thu, 1 Mar 2018 21:50:12 +0900 Subject: [PATCH 0388/5882] Add titles to Explanation --- docs/rfcs/001-recent-commits.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/rfcs/001-recent-commits.md b/docs/rfcs/001-recent-commits.md index af97b93096..4b58a4328e 100644 --- a/docs/rfcs/001-recent-commits.md +++ b/docs/rfcs/001-recent-commits.md @@ -21,8 +21,12 @@ Display the most recent few commits in a chronologically-ordered list beneath th ## Explanation +#### Blank slate + If the active repository has no commits yet, display a short panel with a background message: "Make your first commit". +#### Recent commits + Otherwise, display a **recent commits** section containing a sequence of horizontal bars for ten **relevant** commits with the most recently created commit on top. The commits that are considered **relevant** include: * Commits reachable by the remote tracking branch that is the current upstream of `HEAD`. If more than three of these commits are not reachable by `HEAD`, they will be hidden behind an expandable accordion divider. @@ -31,6 +35,8 @@ Otherwise, display a **recent commits** section containing a sequence of horizon The most recent three commits are visible by default and the user can scroll to see up to the most recent ten commits. The user can also drag a handle to resize the recent commits section and show more of the available ten. +#### Commit metadata + Each **recent commit** within the recent commits section summarizes that commit's metadata, to include: * GitHub avatar for both the committer and (if applicable) author. If either do not exist, show a placeholder. @@ -38,10 +44,16 @@ Each **recent commit** within the recent commits section summarizes that commit' * A relative timestamp indicating how long ago the commit was created. * A greyed-out state if the commit is reachable from the remote tracking branch but _not_ from HEAD (meaning, if it has been fetched but not pulled). +#### Undo + On the most recent commit, display an "undo" button. Clicking "undo" performs a `git reset` and re-populates the commit message editor with the existing message. +#### Refs + Annotate visible commits that correspond to refs in the git repository (branches and tags). If the commit list has been truncated down to ten commits from the full set of relevant commits, display a message below the last commit indicating that additional commits are present but hidden. +#### Context menu + Right-clicking a recent commit reveals a context menu offering interactions with the chosen commit. The context menu contains: * For the most recent commit only, an "Amend" option. "Amend" is enabled if changes have been staged or the commit message mini-editor contains text. Choosing this applies the staged changes and modified commit message to the most recent commit, in a direct analogue to using `git commit --amend` from the command line. @@ -50,6 +62,8 @@ Right-clicking a recent commit reveals a context menu offering interactions with * A "Mixed reset" option. Choosing this performs a `git reset` on the chosen commit. * A "Soft reset" option. Choosing this performs a `git reset --soft` which moves `HEAD` to the chosen commit and populates the staged changes list with all of the cumulative changes from all commits between the chosen one and the previous `HEAD`. +#### Balloon + On click, select the commit and reveal a balloon containing: * Additional user information consistently with the GitHub integration's user mention item. @@ -60,6 +74,8 @@ On click, select the commit and reveal a balloon containing: ![commit-popout](https://user-images.githubusercontent.com/17565/36570682-11545cae-17e8-11e8-80a8-ffcf7328e214.JPG) +#### Bottom Dock + If the Git dock item is dragged to the bottom dock, the recent commit section will remain a vertical list but appear just to the right of the mini commit editor. ![bottom-dock](https://user-images.githubusercontent.com/17565/36570687-14738ca2-17e8-11e8-91f7-5cf1472d871b.JPG) From 8f83aa4e2418d71112579ce6d797b907ebefc158 Mon Sep 17 00:00:00 2001 From: simurai Date: Thu, 1 Mar 2018 21:51:37 +0900 Subject: [PATCH 0389/5882] Make Explanation titles stronger --- docs/rfcs/001-recent-commits.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/rfcs/001-recent-commits.md b/docs/rfcs/001-recent-commits.md index 4b58a4328e..3380e92906 100644 --- a/docs/rfcs/001-recent-commits.md +++ b/docs/rfcs/001-recent-commits.md @@ -21,11 +21,11 @@ Display the most recent few commits in a chronologically-ordered list beneath th ## Explanation -#### Blank slate +### Blank slate If the active repository has no commits yet, display a short panel with a background message: "Make your first commit". -#### Recent commits +### Recent commits Otherwise, display a **recent commits** section containing a sequence of horizontal bars for ten **relevant** commits with the most recently created commit on top. The commits that are considered **relevant** include: @@ -35,7 +35,7 @@ Otherwise, display a **recent commits** section containing a sequence of horizon The most recent three commits are visible by default and the user can scroll to see up to the most recent ten commits. The user can also drag a handle to resize the recent commits section and show more of the available ten. -#### Commit metadata +### Commit metadata Each **recent commit** within the recent commits section summarizes that commit's metadata, to include: @@ -44,15 +44,15 @@ Each **recent commit** within the recent commits section summarizes that commit' * A relative timestamp indicating how long ago the commit was created. * A greyed-out state if the commit is reachable from the remote tracking branch but _not_ from HEAD (meaning, if it has been fetched but not pulled). -#### Undo +### Undo On the most recent commit, display an "undo" button. Clicking "undo" performs a `git reset` and re-populates the commit message editor with the existing message. -#### Refs +### Refs Annotate visible commits that correspond to refs in the git repository (branches and tags). If the commit list has been truncated down to ten commits from the full set of relevant commits, display a message below the last commit indicating that additional commits are present but hidden. -#### Context menu +### Context menu Right-clicking a recent commit reveals a context menu offering interactions with the chosen commit. The context menu contains: @@ -62,7 +62,7 @@ Right-clicking a recent commit reveals a context menu offering interactions with * A "Mixed reset" option. Choosing this performs a `git reset` on the chosen commit. * A "Soft reset" option. Choosing this performs a `git reset --soft` which moves `HEAD` to the chosen commit and populates the staged changes list with all of the cumulative changes from all commits between the chosen one and the previous `HEAD`. -#### Balloon +### Balloon On click, select the commit and reveal a balloon containing: @@ -74,7 +74,7 @@ On click, select the commit and reveal a balloon containing: ![commit-popout](https://user-images.githubusercontent.com/17565/36570682-11545cae-17e8-11e8-80a8-ffcf7328e214.JPG) -#### Bottom Dock +### Bottom Dock If the Git dock item is dragged to the bottom dock, the recent commit section will remain a vertical list but appear just to the right of the mini commit editor. From 8f8f32635fe819cfc9901b3f8b7acb2cb032529c Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 1 Mar 2018 08:40:10 -0500 Subject: [PATCH 0390/5882] :fire: unused props --- lib/controllers/commit-controller.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/controllers/commit-controller.js b/lib/controllers/commit-controller.js index bd482ba5b0..075047725e 100644 --- a/lib/controllers/commit-controller.js +++ b/lib/controllers/commit-controller.js @@ -84,7 +84,6 @@ export default class CommitController extends React.Component { return ( Date: Thu, 1 Mar 2018 08:41:20 -0500 Subject: [PATCH 0391/5882] Oh right we changed the Commit constructor arguments --- test/controllers/commit-controller.test.js | 2 +- test/views/commit-view.test.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test/controllers/commit-controller.test.js b/test/controllers/commit-controller.test.js index 6dc3466abf..79b5fe20de 100644 --- a/test/controllers/commit-controller.test.js +++ b/test/controllers/commit-controller.test.js @@ -23,7 +23,7 @@ describe('CommitController', function() { tooltips = atomEnvironment.tooltips; confirm = sinon.stub(atomEnvironment, 'confirm'); - lastCommit = new Commit('a1e23fd45', 'last commit message'); + lastCommit = new Commit({sha: 'a1e23fd45', message: 'last commit message'}); const noop = () => {}; app = ( diff --git a/test/views/commit-view.test.js b/test/views/commit-view.test.js index 5a1e1efbec..a319ae4a61 100644 --- a/test/views/commit-view.test.js +++ b/test/views/commit-view.test.js @@ -16,7 +16,7 @@ describe('CommitView', function() { tooltips = atomEnv.tooltips; config = atomEnv.config; - lastCommit = new Commit('1234abcd', 'commit message'); + lastCommit = new Commit({sha: '1234abcd', message: 'commit message'}); const noop = () => {}; app = ( @@ -264,7 +264,7 @@ describe('CommitView', function() { describe('amending', function() { it('calls props.setAmending() when the box is checked or unchecked', function() { - const previousCommit = new Commit('111', "previous commit's message"); + const previousCommit = new Commit({sha: '111', message: "previous commit's message"}); const setAmending = sinon.spy(); app = React.cloneElement(app, {stagedChangesExist: false, lastCommit: previousCommit, setAmending}); From 663acf9d3fd5e4e1cb16fcb5f40415f1fc18f6a7 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 1 Mar 2018 08:54:58 -0500 Subject: [PATCH 0392/5882] Use new Commit constructor in Commit.createUnborn() --- lib/models/commit.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/models/commit.js b/lib/models/commit.js index 554ad8fccd..68d5624368 100644 --- a/lib/models/commit.js +++ b/lib/models/commit.js @@ -2,7 +2,7 @@ const UNBORN = Symbol('unborn'); export default class Commit { static createUnborn() { - return new Commit('', '', UNBORN); + return new Commit({unbornRef: UNBORN}); } constructor({sha, authorEmail, authorDate, message, body, unbornRef}) { From 0d54d4c4399f01b5e9c1887327039d3abb94934a Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 1 Mar 2018 08:55:11 -0500 Subject: [PATCH 0393/5882] Extract refs the Reacty way --- lib/controllers/commit-controller.js | 2 ++ test/controllers/git-tab-controller.test.js | 14 +++++++------- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/lib/controllers/commit-controller.js b/lib/controllers/commit-controller.js index 075047725e..4436110603 100644 --- a/lib/controllers/commit-controller.js +++ b/lib/controllers/commit-controller.js @@ -40,6 +40,7 @@ export default class CommitController extends React.Component { super(props, context); this.subscriptions = new CompositeDisposable(); + this.refCommitView = null; } componentWillMount() { @@ -84,6 +85,7 @@ export default class CommitController extends React.Component { return ( { this.refCommitView = c; }} tooltips={this.props.tooltips} config={this.props.config} stagedChangesExist={this.props.stagedChangesExist} diff --git a/test/controllers/git-tab-controller.test.js b/test/controllers/git-tab-controller.test.js index 1cd7258928..7e3f5fcaf1 100644 --- a/test/controllers/git-tab-controller.test.js +++ b/test/controllers/git-tab-controller.test.js @@ -303,7 +303,7 @@ describe('GitTabController', function() { gitTab = wrapper.instance().getWrappedComponentInstance().refView; stagingView = gitTab.refStagingView.getWrappedComponent(); commitController = gitTab.refCommitController; - commitView = commitController.refs.commitView; + commitView = commitController.refCommitView; focusElement = stagingView.element; const stubFocus = element => { @@ -316,16 +316,16 @@ describe('GitTabController', function() { }; stubFocus(stagingView.element); stubFocus(commitView.editorElement); - stubFocus(commitView.refs.abortMergeButton); - stubFocus(commitView.refs.amend); - stubFocus(commitView.refs.commitButton); + stubFocus(commitView.refAbortMergeButton); + stubFocus(commitView.refAmendCheckbox); + stubFocus(commitView.refCommitButton); sinon.stub(commitController, 'hasFocus').callsFake(() => { return [ commitView.editorElement, - commitView.refs.abortMergeButton, - commitView.refs.amend, - commitView.refs.commitButton, + commitView.refAbortMergeButton, + commitView.refAmendCheckbox, + commitView.refCommitButton, ].includes(focusElement); }); }; From 913b5321ce4e8f71522e954fbff6eaa4054b1cfe Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 1 Mar 2018 08:55:22 -0500 Subject: [PATCH 0394/5882] Pass missing prop to CommitView --- test/views/commit-view.test.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/views/commit-view.test.js b/test/views/commit-view.test.js index a319ae4a61..9a148a34ec 100644 --- a/test/views/commit-view.test.js +++ b/test/views/commit-view.test.js @@ -18,6 +18,7 @@ describe('CommitView', function() { lastCommit = new Commit({sha: '1234abcd', message: 'commit message'}); const noop = () => {}; + const returnTruthyPromise = () => Promise.resolve(true); app = ( Date: Thu, 1 Mar 2018 08:56:16 -0500 Subject: [PATCH 0395/5882] Update asserted URLs to include size parameter --- test/views/recent-commits-view.test.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/views/recent-commits-view.test.js b/test/views/recent-commits-view.test.js index ad202cb181..cb4dee9536 100644 --- a/test/views/recent-commits-view.test.js +++ b/test/views/recent-commits-view.test.js @@ -50,9 +50,9 @@ describe('RecentCommitsView', function() { assert.deepEqual( wrapper.find('img.github-RecentCommit-avatar').map(w => w.prop('src')), [ - 'https://avatars.githubusercontent.com/u/e?email=thr%26ee%40z.com', - 'https://avatars.githubusercontent.com/u/e?email=two%40y.com', - 'https://avatars.githubusercontent.com/u/e?email=one%40x.com', + 'https://avatars.githubusercontent.com/u/e?email=thr%26ee%40z.com&s=32', + 'https://avatars.githubusercontent.com/u/e?email=two%40y.com&s=32', + 'https://avatars.githubusercontent.com/u/e?email=one%40x.com&s=32', ], ); }); From bd9662c3c430918271ec89c063603976e7ad6d06 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 1 Mar 2018 08:57:36 -0500 Subject: [PATCH 0396/5882] Missed an old-style ref access --- lib/controllers/commit-controller.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/controllers/commit-controller.js b/lib/controllers/commit-controller.js index 4436110603..93e191d1ac 100644 --- a/lib/controllers/commit-controller.js +++ b/lib/controllers/commit-controller.js @@ -261,11 +261,11 @@ export default class CommitController extends React.Component { } rememberFocus(event) { - return this.refs.commitView.rememberFocus(event); + return this.refCommitView.rememberFocus(event); } setFocus(focus) { - return this.refs.commitView.setFocus(focus); + return this.refCommitView.setFocus(focus); } hasFocus() { From 04674ed9a3d357a39d554b0172e35eed19970633 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 1 Mar 2018 09:00:30 -0500 Subject: [PATCH 0397/5882] :shirt: --- lib/views/timeago.js | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/lib/views/timeago.js b/lib/views/timeago.js index 6d98b969ae..88e2bcd96c 100644 --- a/lib/views/timeago.js +++ b/lib/views/timeago.js @@ -4,22 +4,22 @@ import moment from 'moment'; import cx from 'classnames'; moment.updateLocale('en', { - relativeTime : { - future: "in %s", - past: "%s ago", - s : 'Now', - ss : '<1m', - m: "1m", - mm: "%dm", - h: "1h", - hh: "%dh", - d: "1d", - dd: "%dd", - M: "1M", - MM: "%dM", - y: "1y", - yy: "%d y" - } + relativeTime: { + future: 'in %s', + past: '%s ago', + s: 'Now', + ss: '<1m', + m: '1m', + mm: '%dm', + h: '1h', + hh: '%dh', + d: '1d', + dd: '%dd', + M: '1M', + MM: '%dM', + y: '1y', + yy: '%d y', + }, }); export default class Timeago extends React.Component { From a13ecd78b4eb9b7b19d63e3334033d8f70df6367 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 1 Mar 2018 09:31:02 -0500 Subject: [PATCH 0398/5882] Support and test both Timeago styles --- lib/views/timeago.js | 21 +++++++++++++---- test/views/timeago.test.js | 48 +++++++++++++++++++++++++------------- 2 files changed, 49 insertions(+), 20 deletions(-) diff --git a/lib/views/timeago.js b/lib/views/timeago.js index 88e2bcd96c..52588aecac 100644 --- a/lib/views/timeago.js +++ b/lib/views/timeago.js @@ -3,7 +3,8 @@ import PropTypes from 'prop-types'; import moment from 'moment'; import cx from 'classnames'; -moment.updateLocale('en', { +moment.defineLocale('en-shortdiff', { + parentLocale: 'en', relativeTime: { future: 'in %s', past: '%s ago', @@ -18,7 +19,7 @@ moment.updateLocale('en', { M: '1M', MM: '%dM', y: '1y', - yy: '%d y', + yy: '%dy', }, }); @@ -35,9 +36,21 @@ export default class Timeago extends React.Component { type: 'span', } - static getTimeDisplay(time, now = moment()) { + static getTimeDisplay(time, now = moment(), style = 'long') { const m = moment(time); - return m.from(now, true); + if (style === 'short') { + m.locale('en-shortdiff'); + return m.from(now, true); + } else { + const diff = m.diff(now, 'months', true); + if (Math.abs(diff) <= 1) { + m.locale('en'); + return m.from(now); + } else { + const format = m.format('MMM Do, YYYY'); + return `on ${format}`; + } + } } componentDidMount() { diff --git a/test/views/timeago.test.js b/test/views/timeago.test.js index 8dda2b3ff8..637af44482 100644 --- a/test/views/timeago.test.js +++ b/test/views/timeago.test.js @@ -2,28 +2,44 @@ import moment from 'moment'; import Timeago from '../../lib/views/timeago'; -function test(kindOfDisplay, modifier, expectation) { +function test(kindOfDisplay, style, modifier, expectation) { it(`displays correctly for ${kindOfDisplay}`, function() { const base = moment('May 6 1987', 'MMMM D YYYY'); const m = modifier(moment(base)); // copy `base` since `modifier` mutates - assert.equal(Timeago.getTimeDisplay(m, base), expectation); + assert.equal(Timeago.getTimeDisplay(m, base, style), expectation); }); } describe('Timeago component', function() { - describe('time display calcuation', function() { - test('recent items', m => m, 'a few seconds ago'); - test('items within a minute', m => m.subtract(45, 'seconds'), 'a minute ago'); - test('items within two minutes', m => m.subtract(2, 'minutes'), '2 minutes ago'); - test('items within five minutes', m => m.subtract(5, 'minutes'), '5 minutes ago'); - test('items within thirty minutes', m => m.subtract(30, 'minutes'), '30 minutes ago'); - test('items within an hour', m => m.subtract(1, 'hours'), 'an hour ago'); - test('items within the same day', m => m.subtract(20, 'hours'), '20 hours ago'); - test('items within a day', m => m.subtract(1, 'day'), 'a day ago'); - test('items within the same week', m => m.subtract(4, 'days'), '4 days ago'); - test('items within the same month', m => m.subtract(20, 'days'), '20 days ago'); - test('items within a month', m => m.subtract(1, 'month'), 'a month ago'); - test('items beyond a month', m => m.subtract(31, 'days'), 'on Apr 5th, 1987'); - test('items way beyond a month', m => m.subtract(2, 'years'), 'on May 6th, 1985'); + describe('long time display calcuation', function() { + test('recent items', 'long', m => m, 'a few seconds ago'); + test('items within a minute', 'long', m => m.subtract(45, 'seconds'), 'a minute ago'); + test('items within two minutes', 'long', m => m.subtract(2, 'minutes'), '2 minutes ago'); + test('items within five minutes', 'long', m => m.subtract(5, 'minutes'), '5 minutes ago'); + test('items within thirty minutes', 'long', m => m.subtract(30, 'minutes'), '30 minutes ago'); + test('items within an hour', 'long', m => m.subtract(1, 'hours'), 'an hour ago'); + test('items within the same day', 'long', m => m.subtract(20, 'hours'), '20 hours ago'); + test('items within a day', 'long', m => m.subtract(1, 'day'), 'a day ago'); + test('items within the same week', 'long', m => m.subtract(4, 'days'), '4 days ago'); + test('items within the same month', 'long', m => m.subtract(20, 'days'), '20 days ago'); + test('items within a month', 'long', m => m.subtract(1, 'month'), 'a month ago'); + test('items beyond a month', 'long', m => m.subtract(31, 'days'), 'on Apr 5th, 1987'); + test('items way beyond a month', 'long', m => m.subtract(2, 'years'), 'on May 6th, 1985'); + }); + + describe('short time display calcuation', function() { + test('recent items', 'short', m => m, 'Now'); + test('items within a minute', 'short', m => m.subtract(45, 'seconds'), '1m'); + test('items within two minutes', 'short', m => m.subtract(2, 'minutes'), '2m'); + test('items within five minutes', 'short', m => m.subtract(5, 'minutes'), '5m'); + test('items within thirty minutes', 'short', m => m.subtract(30, 'minutes'), '30m'); + test('items within an hour', 'short', m => m.subtract(1, 'hours'), '1h'); + test('items within the same day', 'short', m => m.subtract(20, 'hours'), '20h'); + test('items within a day', 'short', m => m.subtract(1, 'day'), '1d'); + test('items within the same week', 'short', m => m.subtract(4, 'days'), '4d'); + test('items within the same month', 'short', m => m.subtract(20, 'days'), '20d'); + test('items within a month', 'short', m => m.subtract(1, 'month'), '1M'); + test('items beyond a month', 'short', m => m.subtract(31, 'days'), '1M'); + test('items way beyond a month', 'short', m => m.subtract(2, 'years'), '2y'); }); }); From d8cdc8a2c974d397f8755b79f45f26e4ebeaa358 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 1 Mar 2018 09:32:23 -0500 Subject: [PATCH 0399/5882] Use a prop to configure the Timeago style --- lib/views/timeago.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/views/timeago.js b/lib/views/timeago.js index 52588aecac..514bbc7660 100644 --- a/lib/views/timeago.js +++ b/lib/views/timeago.js @@ -30,13 +30,15 @@ export default class Timeago extends React.Component { PropTypes.string, PropTypes.func, ]), + style: PropTypes.oneOf(['short', 'long']), } static defaultProps = { type: 'span', + style: 'long', } - static getTimeDisplay(time, now = moment(), style = 'long') { + static getTimeDisplay(time, now, style) { const m = moment(time); if (style === 'short') { m.locale('en-shortdiff'); @@ -63,7 +65,7 @@ export default class Timeago extends React.Component { render() { const {type, time, ...others} = this.props; - const display = Timeago.getTimeDisplay(time); + const display = Timeago.getTimeDisplay(time, moment(), this.props.style); const Type = type; const className = cx('timeago', others.className); return ( From 52aed1b4b9cbacef8706219af6b402102bd9f341 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 1 Mar 2018 09:35:30 -0500 Subject: [PATCH 0400/5882] Use "display" instead of "style" to keep React happy --- lib/views/timeago.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/views/timeago.js b/lib/views/timeago.js index 514bbc7660..fe2ec43687 100644 --- a/lib/views/timeago.js +++ b/lib/views/timeago.js @@ -22,6 +22,7 @@ moment.defineLocale('en-shortdiff', { yy: '%dy', }, }); +moment.locale('en'); export default class Timeago extends React.Component { static propTypes = { @@ -30,12 +31,12 @@ export default class Timeago extends React.Component { PropTypes.string, PropTypes.func, ]), - style: PropTypes.oneOf(['short', 'long']), + displayStyle: PropTypes.oneOf(['short', 'long']), } static defaultProps = { type: 'span', - style: 'long', + displayStyle: 'long', } static getTimeDisplay(time, now, style) { @@ -65,7 +66,7 @@ export default class Timeago extends React.Component { render() { const {type, time, ...others} = this.props; - const display = Timeago.getTimeDisplay(time, moment(), this.props.style); + const display = Timeago.getTimeDisplay(time, moment(), this.props.displayStyle); const Type = type; const className = cx('timeago', others.className); return ( From c952157be284c5debcf77a2d8a28713e530b20d9 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 1 Mar 2018 09:37:23 -0500 Subject: [PATCH 0401/5882] Use the short display style in RecentCommitView --- lib/views/recent-commits-view.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/views/recent-commits-view.js b/lib/views/recent-commits-view.js index 7b375c0887..36f877c019 100644 --- a/lib/views/recent-commits-view.js +++ b/lib/views/recent-commits-view.js @@ -26,6 +26,7 @@ class RecentCommitView extends React.Component { From 53ebcc3c453288e73231eb73dd6699432c4749ec Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 1 Mar 2018 09:40:03 -0500 Subject: [PATCH 0402/5882] Extract displayStyle from props --- lib/views/timeago.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/views/timeago.js b/lib/views/timeago.js index fe2ec43687..aaab1139e7 100644 --- a/lib/views/timeago.js +++ b/lib/views/timeago.js @@ -65,8 +65,8 @@ export default class Timeago extends React.Component { } render() { - const {type, time, ...others} = this.props; - const display = Timeago.getTimeDisplay(time, moment(), this.props.displayStyle); + const {type, time, displayStyle, ...others} = this.props; + const display = Timeago.getTimeDisplay(time, moment(), displayStyle); const Type = type; const className = cx('timeago', others.className); return ( From 0810dfbb9d239aefd2cd514eda01ee99408e8148 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Sun, 4 Mar 2018 22:38:53 -0800 Subject: [PATCH 0403/5882] Test automatically updating selection due to repo changes --- test/views/staging-view.test.js | 84 ++++++++++++++++++++++++++++++++- 1 file changed, 83 insertions(+), 1 deletion(-) diff --git a/test/views/staging-view.test.js b/test/views/staging-view.test.js index 76e3e728ec..54bb8e1171 100644 --- a/test/views/staging-view.test.js +++ b/test/views/staging-view.test.js @@ -331,7 +331,7 @@ describe('StagingView', function() { }); }); - describe('when the selection changes', function() { + describe('when the selection changes due to keyboard navigation', function() { let showFilePatchItem, showMergeConflictFileForPath; beforeEach(function() { showFilePatchItem = sinon.stub(StagingView.prototype, 'showFilePatchItem'); @@ -478,6 +478,88 @@ describe('StagingView', function() { }); }); + describe('when the selection changes due to a repo update', function() { + let showFilePatchItem; + beforeEach(function() { + atom.config.set('github.keyboardNavigationDelay', 0); + showFilePatchItem = sinon.stub(StagingView.prototype, 'showFilePatchItem'); + }); + + // such as files being staged/unstaged, discarded or stashed + it('calls showFilePatchItem if there is a pending file patch item open', async function() { + const filePatches = [ + {filePath: 'a.txt', status: 'modified'}, + {filePath: 'b.txt', status: 'deleted'}, + ]; + + const view = new StagingView({ + workspace, workingDirectoryPath, commandRegistry, + unstagedChanges: filePatches, mergeConflicts: [], stagedChanges: [], + }); + document.body.appendChild(view.element); + + let selectedItems = view.getSelectedItems(); + assert.lengthOf(selectedItems, 1); + assert.strictEqual(selectedItems[0].filePath, 'a.txt'); + sinon.stub(view, 'getPanesWithStalePendingFilePatchItem').returns(['item1']); + const newFilePatches = filePatches.slice(1); // remove first item, as though it was staged or discarded + await view.update({unstagedChanges: newFilePatches}); + selectedItems = view.getSelectedItems(); + assert.lengthOf(selectedItems, 1); + assert.strictEqual(selectedItems[0].filePath, 'b.txt'); + assert.isTrue(showFilePatchItem.called); + assert.isTrue(showFilePatchItem.calledWith('b.txt')); + + view.element.remove(); + }); + + it('does not call showFilePatchItem if a new set of file patches are being fetched', async function() { + const view = new StagingView({ + workspace, workingDirectoryPath, commandRegistry, + unstagedChanges: [{filePath: 'a.txt', status: 'modified'}], mergeConflicts: [], stagedChanges: [], + }); + document.body.appendChild(view.element); + + sinon.stub(view, 'getPanesWithStalePendingFilePatchItem').returns(['item1']); + await view.update({unstagedChanges: []}); // when repo is changed, lists are cleared out and data is fetched for new repo + assert.isFalse(showFilePatchItem.called); + + await view.update({unstagedChanges: [{filePath: 'b.txt', status: 'deleted'}]}); // data for new repo is loaded + assert.isFalse(showFilePatchItem.called); + + await view.update({unstagedChanges: [{filePath: 'c.txt', status: 'added'}]}); + assert.isTrue(showFilePatchItem.called); + + view.element.remove(); + }); + }); + + it('updates the selection when there is an `activeFilePatch`', async function() { + const view = new StagingView({ + workspace, workingDirectoryPath, commandRegistry, + unstagedChanges: [{filePath: 'file.txt', status: 'modified'}], mergeConflicts: [], stagedChanges: [], + }); + document.body.appendChild(view.element); + let selectedItems = view.getSelectedItems(); + assert.lengthOf(selectedItems, 1); + assert.strictEqual(selectedItems[0].filePath, 'file.txt'); + + view.activeFilePatch = { + getFilePath() { return 'b.txt'; }, + getStagingStatus() { return 'unstaged'; }, + }; + await view.update({unstagedChanges: [ + {filePath: 'a.txt', status: 'modified'}, + {filePath: 'b.txt', status: 'deleted'}, + ]}); + selectedItems = view.getSelectedItems(); + assert.lengthOf(selectedItems, 1); + assert.strictEqual(selectedItems[0].filePath, view.activeFilePatch.getFilePath()); + + view.element.remove(); + + }); + describe('when dragging a mouse across multiple items', function() { let showFilePatchItem; beforeEach(function() { From 8010d8a1d838a21f0a6077829070d28fd63c752f Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Sun, 4 Mar 2018 22:40:04 -0800 Subject: [PATCH 0404/5882] Fix bug in check to see if selections were present before and after update --- lib/views/staging-view.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/views/staging-view.js b/lib/views/staging-view.js index d40b1d95ab..fd2cb3d1bb 100644 --- a/lib/views/staging-view.js +++ b/lib/views/staging-view.js @@ -149,7 +149,7 @@ export default class StagingView { this.quietlySelectItem(this.activeFilePatch.getFilePath(), this.activeFilePatch.getStagingStatus()); } else { const isRepoSame = oldProps.workingDirectoryPath === this.props.workingDirectoryPath; - const selectionsPresent = previouslySelectedItems.size > 0 && currentlySelectedItems > 0; + const selectionsPresent = previouslySelectedItems.size > 0 && currentlySelectedItems.size > 0; // ignore when data has not yet been fetched and no selection is present const selectionChanged = selectionsPresent && !isEqual(previouslySelectedItems, currentlySelectedItems); if (isRepoSame && selectionChanged) { From 4ecc5f3407e0e6ce8e7801fb348d6d0525773521 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Sun, 4 Mar 2018 22:52:20 -0800 Subject: [PATCH 0405/5882] More thoroughly check for arguments to spy --- test/views/staging-view.test.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/views/staging-view.test.js b/test/views/staging-view.test.js index 54bb8e1171..d9ed5ac999 100644 --- a/test/views/staging-view.test.js +++ b/test/views/staging-view.test.js @@ -362,11 +362,13 @@ describe('StagingView', function() { getPanesWithStalePendingFilePatchItem.returns(['item1', 'item2']); await view.selectPrevious(); assert.isTrue(showFilePatchItem.calledTwice); - assert.isTrue(showFilePatchItem.calledWith(filePatches[0].filePath)); + assert.strictEqual(showFilePatchItem.args[0][0], filePatches[0].filePath); + assert.strictEqual(showFilePatchItem.args[1][0], filePatches[0].filePath); showFilePatchItem.reset(); await view.selectNext(); assert.isTrue(showFilePatchItem.calledTwice); - assert.isTrue(showFilePatchItem.calledWith(filePatches[1].filePath)); + assert.strictEqual(showFilePatchItem.args[0][0], filePatches[1].filePath); + assert.strictEqual(showFilePatchItem.args[1][0], filePatches[1].filePath); view.element.remove(); }); From 09b782208cd3db63200bc3e1edc7a9852ea61f3b Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Sun, 4 Mar 2018 22:53:21 -0800 Subject: [PATCH 0406/5882] Add comment about items being stale only if they belong to active repo --- lib/views/staging-view.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/views/staging-view.js b/lib/views/staging-view.js index fd2cb3d1bb..8495277e83 100644 --- a/lib/views/staging-view.js +++ b/lib/views/staging-view.js @@ -437,6 +437,7 @@ export default class StagingView { const pendingItem = pane.getPendingItem(); if (!pendingItem) { return false; } const isDiffViewItem = pendingItem.getRealItem && pendingItem.getRealItem() instanceof FilePatchController; + // We only want to update pending diff views for currently active repo const isInActiveRepo = pendingItem.getWorkingDirectory() === this.props.workingDirectoryPath; const isStale = !this.changedFileExists(pendingItem.getFilePath(), pendingItem.getStagingStatus()); return isDiffViewItem && isInActiveRepo && isStale; From 42786b04f34da6709d9c8f95f02af9892e172687 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Sun, 4 Mar 2018 23:26:18 -0800 Subject: [PATCH 0407/5882] Include co-author information in GSOS#getRecentCommits() output --- lib/git-shell-out-strategy.js | 10 ++++++++++ test/git-strategies.test.js | 21 +++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/lib/git-shell-out-strategy.js b/lib/git-shell-out-strategy.js index 94f43d7dcc..d201cb4fec 100644 --- a/lib/git-shell-out-strategy.js +++ b/lib/git-shell-out-strategy.js @@ -575,12 +575,22 @@ export default class GitShellOutStrategy { const fields = output.trim().split('\0'); const commits = []; for (let i = 0; i < fields.length; i += 5) { + const body = fields[i + 4]; + + // There's probably a better way. I tried finding a regex to do it in one fell swoop but had no luck + const coAuthors = body.split(LINE_ENDING_REGEX).reduce((emails, line) => { + const match = line.match(/\s*Co-authored-by: .*<(.*)>\s*/); + if (match && match[1]) { emails.push(match[1]); } + return emails; + }, []); + commits.push({ sha: fields[i] && fields[i].trim(), authorEmail: fields[i + 1] && fields[i + 1].trim(), authorDate: parseInt(fields[i + 2], 10), message: fields[i + 3], body: fields[i + 4], + coAuthors, }); } return commits; diff --git a/test/git-strategies.test.js b/test/git-strategies.test.js index 0d22efc640..aa44a99e86 100644 --- a/test/git-strategies.test.js +++ b/test/git-strategies.test.js @@ -141,6 +141,7 @@ import {fsStat, normalizeGitHelperPath, writeFile, getTempDir} from '../lib/help authorDate: 1471113656, message: 'third commit', body: '', + coAuthors: [], }); assert.deepEqual(commits[1], { sha: '18920c900bfa6e4844853e7e246607a31c3e2e8c', @@ -148,6 +149,7 @@ import {fsStat, normalizeGitHelperPath, writeFile, getTempDir} from '../lib/help authorDate: 1471113642, message: 'second commit', body: '', + coAuthors: [], }); assert.deepEqual(commits[2], { sha: '46c0d7179fc4e348c3340ff5e7957b9c7d89c07f', @@ -155,6 +157,7 @@ import {fsStat, normalizeGitHelperPath, writeFile, getTempDir} from '../lib/help authorDate: 1471113625, message: 'first commit', body: '', + coAuthors: [], }); }); @@ -173,6 +176,24 @@ import {fsStat, normalizeGitHelperPath, writeFile, getTempDir} from '../lib/help assert.strictEqual(commits[0].message, 'Commit 10'); assert.strictEqual(commits[9].message, 'Commit 1'); }); + + it('includes co-authors based on commit body trailers', async function() { + const workingDirPath = await cloneRepository('multiple-commits'); + const git = createTestStrategy(workingDirPath); + + await git.commit(dedent` + Implemented feature collaboratively + + Co-authored-by: name + Co-authored-by: another-name " + Co-authored-by: yet-another " + `, {allowEmpty: true}); + + const commits = await git.getRecentCommits({max: 1}); + assert.lengthOf(commits, 1); + assert.deepEqual(commits[0].coAuthors, ['name@example.com', 'another-name@example.com', 'yet-another@example.com']); + }); + }); describe('diffFileStatus', function() { From c8363257823d6f8344dfe22a04abab4c5bb591cb Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Sun, 4 Mar 2018 23:35:16 -0800 Subject: [PATCH 0408/5882] Display multiple GitHub avatars for co-authored commits --- lib/models/commit.js | 7 ++++++- lib/views/recent-commits-view.js | 23 +++++++++++++++++++---- test/views/recent-commits-view.test.js | 21 +++++++++++++++++++++ 3 files changed, 46 insertions(+), 5 deletions(-) diff --git a/lib/models/commit.js b/lib/models/commit.js index 68d5624368..8493b72fca 100644 --- a/lib/models/commit.js +++ b/lib/models/commit.js @@ -5,9 +5,10 @@ export default class Commit { return new Commit({unbornRef: UNBORN}); } - constructor({sha, authorEmail, authorDate, message, body, unbornRef}) { + constructor({sha, authorEmail, coAuthors, authorDate, message, body, unbornRef}) { this.sha = sha; this.authorEmail = authorEmail; + this.coAuthors = coAuthors; this.authorDate = authorDate; this.message = message; this.body = body; @@ -26,6 +27,10 @@ export default class Commit { return this.authorDate; } + getCoAuthorEmails() { + return this.coAuthors; + } + getMessage() { return this.message; } diff --git a/lib/views/recent-commits-view.js b/lib/views/recent-commits-view.js index 36f877c019..42624125d6 100644 --- a/lib/views/recent-commits-view.js +++ b/lib/views/recent-commits-view.js @@ -14,10 +14,7 @@ class RecentCommitView extends React.Component { return (
      • - + {this.renderAuthors()} @@ -33,6 +30,24 @@ class RecentCommitView extends React.Component {
      • ); } + + renderAuthors() { + const authorEmails = [this.props.commit.getAuthorEmail(), ...this.props.commit.getCoAuthorEmails()]; + + return ( +
        + {authorEmails.map(authorEmail => { + return ( + + ); + })} +
        + ); + } } export default class RecentCommitsView extends React.Component { diff --git a/test/views/recent-commits-view.test.js b/test/views/recent-commits-view.test.js index cb4dee9536..63607d9b25 100644 --- a/test/views/recent-commits-view.test.js +++ b/test/views/recent-commits-view.test.js @@ -57,6 +57,27 @@ describe('RecentCommitsView', function() { ); }); + it('renders multiple avatars for co-authored commits', function() { + const commits = [new Commit({ + sha: '1111111111', + authorEmail: 'thr&ee@z.com', + authorDate: 0, + message: 'x', + coAuthors: ['two@y.com', 'one@x.com'], + })]; + + app = React.cloneElement(app, {commits}); + const wrapper = mount(app); + assert.deepEqual( + wrapper.find('img.github-RecentCommit-avatar').map(w => w.prop('src')), + [ + 'https://avatars.githubusercontent.com/u/e?email=thr%26ee%40z.com&s=32', + 'https://avatars.githubusercontent.com/u/e?email=two%40y.com&s=32', + 'https://avatars.githubusercontent.com/u/e?email=one%40x.com&s=32', + ], + ); + }); + it("renders the commit's relative age", function() { const commit = new Commit({ sha: '1111111111', From 3cbfcad3db7dc65744f1de876ca6be12efaecf1d Mon Sep 17 00:00:00 2001 From: simurai Date: Fri, 2 Mar 2018 12:28:51 +0900 Subject: [PATCH 0409/5882] Style co-authors --- lib/views/recent-commits-view.js | 4 ++-- styles/recent-commits.less | 27 +++++++++++++++++++++++++-- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/lib/views/recent-commits-view.js b/lib/views/recent-commits-view.js index 42624125d6..6c5681496d 100644 --- a/lib/views/recent-commits-view.js +++ b/lib/views/recent-commits-view.js @@ -35,7 +35,7 @@ class RecentCommitView extends React.Component { const authorEmails = [this.props.commit.getAuthorEmail(), ...this.props.commit.getCoAuthorEmails()]; return ( -
        + {authorEmails.map(authorEmail => { return ( ); })} -
        + ); } } diff --git a/styles/recent-commits.less b/styles/recent-commits.less index 2a279dbfcd..d907a8a33b 100644 --- a/styles/recent-commits.less +++ b/styles/recent-commits.less @@ -27,22 +27,45 @@ } .github-RecentCommit { - - + position: relative; display: flex; padding: @component-padding/2 @component-padding; line-height: @size; + &-authors { + position: absolute; + } + &-avatar { + position: relative; width: @size; height: @size; border-radius: @component-border-radius; background-color: mix(@text-color, @base-background-color, 15%); + box-shadow: 1px 0 @base-background-color; + + margin-right: -@size + 3px; + transition: margin .1s ease-in-out; + + &:nth-child(1) { z-index: 3; } + &:nth-child(2) { z-index: 2; } + &:nth-child(3) { z-index: 1; } + + &:nth-child(n+4) { + display: none; + } + + } + + &-authors:hover .github-RecentCommit-avatar { + display: inline-block; + margin-right: 1px; } &-message { flex: 1; margin: 0 .5em; + margin-left: @size + @component-padding; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; From 43087c7ad10c0487e9c9fb26ef5f1a492e4f67b4 Mon Sep 17 00:00:00 2001 From: simurai Date: Fri, 2 Mar 2018 16:11:55 +0900 Subject: [PATCH 0410/5882] Tweak co-author styling --- styles/recent-commits.less | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/styles/recent-commits.less b/styles/recent-commits.less index d907a8a33b..74bb7535f1 100644 --- a/styles/recent-commits.less +++ b/styles/recent-commits.less @@ -34,32 +34,35 @@ &-authors { position: absolute; + display: flex; } &-avatar { position: relative; + margin-right: -@size + 3px; width: @size; height: @size; border-radius: @component-border-radius; background-color: mix(@text-color, @base-background-color, 15%); box-shadow: 1px 0 @base-background-color; - - margin-right: -@size + 3px; - transition: margin .1s ease-in-out; + transition: margin .12s cubic-bezier(.5,.1,0,1); &:nth-child(1) { z-index: 3; } - &:nth-child(2) { z-index: 2; } - &:nth-child(3) { z-index: 1; } - + &:nth-child(2) { z-index: 2; opacity: .7; } + &:nth-child(3) { z-index: 1; opacity: .4; } &:nth-child(n+4) { display: none; } - + &:only-child, + &:last-child { + box-shadow: none; + } } &-authors:hover .github-RecentCommit-avatar { display: inline-block; margin-right: 1px; + opacity: 1; } &-message { From 87256d9a0aef1176133f56ff005e1f43cae1af13 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Thu, 1 Mar 2018 20:19:43 -0800 Subject: [PATCH 0411/5882] Fix broken dom node reference from Etch -> React conversion --- lib/controllers/commit-controller.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/controllers/commit-controller.js b/lib/controllers/commit-controller.js index 93e191d1ac..63fa6109ee 100644 --- a/lib/controllers/commit-controller.js +++ b/lib/controllers/commit-controller.js @@ -1,6 +1,7 @@ import path from 'path'; import React from 'react'; +import ReactDom from 'react-dom'; import PropTypes from 'prop-types'; import {autobind} from 'core-decorators'; import {CompositeDisposable} from 'event-kit'; @@ -79,6 +80,10 @@ export default class CommitController extends React.Component { } } + componentDidMount() { + this.domNode = ReactDom.findDOMNode(this); + } + render() { const message = this.getCommitMessage(); const operationStates = this.props.repository.getOperationStates(); @@ -269,7 +274,7 @@ export default class CommitController extends React.Component { } hasFocus() { - return this.element.contains(document.activeElement); + return this.domNode && this.domNode.contains(document.activeElement); } } From d1e7614217d356ff81dd7f39bf30f1e92e2e6c6d Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 2 Mar 2018 11:38:43 -0500 Subject: [PATCH 0412/5882] Find worktrees for files within .git --- lib/models/workdir-cache.js | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/lib/models/workdir-cache.js b/lib/models/workdir-cache.js index 1e3b86647e..7686d02104 100644 --- a/lib/models/workdir-cache.js +++ b/lib/models/workdir-cache.js @@ -35,8 +35,19 @@ export default class WorkdirCache { async revParse(startPath) { try { const startDir = (await fsStat(startPath)).isDirectory() ? startPath : path.dirname(startPath); - const workDir = await CompositeGitStrategy.create(startDir).exec(['rev-parse', '--show-toplevel']); - return toNativePathSep(workDir.trim()); + + // Within a git worktree, return a non-empty string containing the path to the worktree root. + // Within a gitdir or outside of a worktree, return an empty string. + // Throw if startDir does not exist. + const topLevel = await CompositeGitStrategy.create(startDir).exec(['rev-parse', '--show-toplevel']); + if (/\S/.test(topLevel)) { + return toNativePathSep(topLevel.trim()); + } + + // Within a gitdir, return the absolute path to the gitdir. + // Outside of a gitdir or worktree, throw. + const gitDir = await CompositeGitStrategy.create(startDir).exec(['rev-parse', '--absolute-git-dir']); + return this.revParse(path.resolve(gitDir, '..')); } catch (e) { return null; } From f94767f51bdd1e6f7b12dd5930e01685eddf1579 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Sun, 4 Mar 2018 23:46:46 -0800 Subject: [PATCH 0413/5882] Default coAuthors property on Commit instances to be an empty array --- lib/models/commit.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/models/commit.js b/lib/models/commit.js index 8493b72fca..74ad376eaa 100644 --- a/lib/models/commit.js +++ b/lib/models/commit.js @@ -8,7 +8,7 @@ export default class Commit { constructor({sha, authorEmail, coAuthors, authorDate, message, body, unbornRef}) { this.sha = sha; this.authorEmail = authorEmail; - this.coAuthors = coAuthors; + this.coAuthors = coAuthors || []; this.authorDate = authorDate; this.message = message; this.body = body; From 54caefa898750ca1011e38dc903871965eb09f9e Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 28 Feb 2018 13:52:07 -0500 Subject: [PATCH 0414/5882] wip --- lib/models/github-login-model.js | 127 +------------------------------ lib/models/keytar-strategy.js | 127 +++++++++++++++++++++++++++++++ test/git-prompt-server.test.js | 76 +++++++++++++++++- 3 files changed, 202 insertions(+), 128 deletions(-) create mode 100644 lib/models/keytar-strategy.js diff --git a/lib/models/github-login-model.js b/lib/models/github-login-model.js index 21c4362b62..c3b6a03d51 100644 --- a/lib/models/github-login-model.js +++ b/lib/models/github-login-model.js @@ -1,117 +1,9 @@ -import {execFile} from 'child_process'; - import {Emitter} from 'event-kit'; -export const UNAUTHENTICATED = Symbol('UNAUTHENTICATED'); - -export class KeytarStrategy { - static get keytar() { - return require('keytar'); - } - - static async isValid() { - // Allow for disabling Keytar on problematic CI environments - if (process.env.ATOM_GITHUB_DISABLE_KEYTAR) { - return false; - } - - const keytar = this.keytar; - - try { - const rand = Math.floor(Math.random() * 10e20).toString(16); - await keytar.setPassword('atom-test-service', rand, rand); - const pass = await keytar.getPassword('atom-test-service', rand); - const success = pass === rand; - keytar.deletePassword('atom-test-service', rand); - return success; - } catch (err) { - return false; - } - } - - getPassword(service, account) { - return this.constructor.keytar.getPassword(service, account); - } - - replacePassword(service, account, password) { - return this.constructor.keytar.setPassword(service, account, password); - } - - deletePassword(service, account) { - return this.constructor.keytar.deletePassword(service, account); - } -} - -export class SecurityBinaryStrategy { - static isValid() { - return process.platform === 'darwin'; - } - - async getPassword(service, account) { - try { - const password = await this.exec(['find-generic-password', '-s', service, '-a', account, '-w']); - return password.trim() || UNAUTHENTICATED; - } catch (err) { - return UNAUTHENTICATED; - } - } - - replacePassword(service, account, newPassword) { - return this.exec(['add-generic-password', '-s', service, '-a', account, '-w', newPassword, '-U']); - } - - deletePassword(service, account) { - return this.exec(['delete-generic-password', '-s', service, '-a', account]); - } - - exec(securityArgs, {binary} = {binary: 'security'}) { - return new Promise((resolve, reject) => { - execFile(binary, securityArgs, (error, stdout) => { - if (error) { return reject(error); } - return resolve(stdout); - }); - }); - } -} - -export class InMemoryStrategy { - static isValid() { - return true; - } - - constructor() { - if (!atom.inSpecMode()) { - // eslint-disable-next-line no-console - console.warn( - 'Using an InMemoryStrategy strategy for storing tokens. ' + - 'The tokens will only be stored for the current window.', - ); - } - this.passwordsByService = new Map(); - } - - getPassword(service, account) { - const passwords = this.passwordsByService.get(service) || new Map(); - const password = passwords.get(account); - return password || UNAUTHENTICATED; - } - - replacePassword(service, account, newPassword) { - const passwords = this.passwordsByService.get(service) || new Map(); - passwords.set(account, newPassword); - this.passwordsByService.set(service, passwords); - } - - deletePassword(service, account) { - const passwords = this.passwordsByService.get(service); - if (passwords) { - passwords.delete(account); - } - } -} +import {UNAUTHENTICATED, createStrategy} from './keytar-strategy'; let instance = null; -const strategies = [KeytarStrategy, SecurityBinaryStrategy, InMemoryStrategy]; + export default class GithubLoginModel { static get() { if (!instance) { @@ -136,20 +28,7 @@ export default class GithubLoginModel { return this._strategy; } - let Strategy; - for (let i = 0; i < strategies.length; i++) { - const strat = strategies[i]; - const isValid = await strat.isValid(); - if (isValid) { - Strategy = strat; - break; - } - } - // const Strategy = this._Strategy || strategies.find(strat => strat.isValid()); - if (!Strategy) { - throw new Error('None of the listed GithubLoginModel strategies returned true for `isValid`'); - } - this._strategy = new Strategy(); + this._strategy = await createStrategy(); return this._strategy; } diff --git a/lib/models/keytar-strategy.js b/lib/models/keytar-strategy.js new file mode 100644 index 0000000000..37e6336745 --- /dev/null +++ b/lib/models/keytar-strategy.js @@ -0,0 +1,127 @@ +import {execFile} from 'child_process'; + +export const UNAUTHENTICATED = Symbol('UNAUTHENTICATED'); + +export class KeytarStrategy { + static get keytar() { + return require('keytar'); + } + + static async isValid() { + // Allow for disabling Keytar on problematic CI environments + if (process.env.ATOM_GITHUB_DISABLE_KEYTAR) { + return false; + } + + const keytar = this.keytar; + + try { + const rand = Math.floor(Math.random() * 10e20).toString(16); + await keytar.setPassword('atom-test-service', rand, rand); + const pass = await keytar.getPassword('atom-test-service', rand); + const success = pass === rand; + keytar.deletePassword('atom-test-service', rand); + return success; + } catch (err) { + return false; + } + } + + getPassword(service, account) { + return this.constructor.keytar.getPassword(service, account); + } + + replacePassword(service, account, password) { + return this.constructor.keytar.setPassword(service, account, password); + } + + deletePassword(service, account) { + return this.constructor.keytar.deletePassword(service, account); + } +} + +export class SecurityBinaryStrategy { + static isValid() { + return process.platform === 'darwin'; + } + + async getPassword(service, account) { + try { + const password = await this.exec(['find-generic-password', '-s', service, '-a', account, '-w']); + return password.trim() || UNAUTHENTICATED; + } catch (err) { + return UNAUTHENTICATED; + } + } + + replacePassword(service, account, newPassword) { + return this.exec(['add-generic-password', '-s', service, '-a', account, '-w', newPassword, '-U']); + } + + deletePassword(service, account) { + return this.exec(['delete-generic-password', '-s', service, '-a', account]); + } + + exec(securityArgs, {binary} = {binary: 'security'}) { + return new Promise((resolve, reject) => { + execFile(binary, securityArgs, (error, stdout) => { + if (error) { return reject(error); } + return resolve(stdout); + }); + }); + } +} + +export class InMemoryStrategy { + static isValid() { + return true; + } + + constructor() { + if (!atom.inSpecMode()) { + // eslint-disable-next-line no-console + console.warn( + 'Using an InMemoryStrategy strategy for storing tokens. ' + + 'The tokens will only be stored for the current window.', + ); + } + this.passwordsByService = new Map(); + } + + getPassword(service, account) { + const passwords = this.passwordsByService.get(service) || new Map(); + const password = passwords.get(account); + return password || UNAUTHENTICATED; + } + + replacePassword(service, account, newPassword) { + const passwords = this.passwordsByService.get(service) || new Map(); + passwords.set(account, newPassword); + this.passwordsByService.set(service, passwords); + } + + deletePassword(service, account) { + const passwords = this.passwordsByService.get(service); + if (passwords) { + passwords.delete(account); + } + } +} + +const strategies = [KeytarStrategy, SecurityBinaryStrategy, InMemoryStrategy]; + +export async function createStrategy() { + let Strategy; + for (let i = 0; i < strategies.length; i++) { + const strat = strategies[i]; + const isValid = await strat.isValid(); + if (isValid) { + Strategy = strat; + break; + } + } + if (!Strategy) { + throw new Error('None of the listed keytar strategies returned true for `isValid`'); + } + return Strategy; +} diff --git a/test/git-prompt-server.test.js b/test/git-prompt-server.test.js index 98f0d0ea5a..3ba901bafc 100644 --- a/test/git-prompt-server.test.js +++ b/test/git-prompt-server.test.js @@ -3,13 +3,14 @@ import path from 'path'; import GitPromptServer from '../lib/git-prompt-server'; import GitTempDir from '../lib/git-temp-dir'; -import {fileExists, getAtomHelperPath} from '../lib/helpers'; +import {fileExists, writeFile, readFile, getAtomHelperPath} from '../lib/helpers'; -describe('GitPromptServer', function() { +describe.only('GitPromptServer', function() { const electronEnv = { ELECTRON_RUN_AS_NODE: '1', ELECTRON_NO_ATTACH_CONSOLE: '1', ATOM_GITHUB_DUGITE_PATH: require.resolve('dugite'), + ATOM_GITHUB_KEYTAR_PATH: require.resolve('keytar'), ATOM_GITHUB_ORIGINAL_PATH: process.env.PATH, ATOM_GITHUB_WORKDIR_PATH: path.join(__dirname, '..'), ATOM_GITHUB_SPEC_MODE: 'true', @@ -25,9 +26,11 @@ describe('GitPromptServer', function() { }); describe('credential helper', function() { - let server; + let server, stderrData, stdoutData; beforeEach(function() { + stderrData = []; + stdoutData = []; server = new GitPromptServer(tempDir); }); @@ -43,12 +46,24 @@ describe('GitPromptServer', function() { }, ); - child.stderr.on('data', console.log); // eslint-disable-line no-console + child.stdout.on('data', data => stdoutData.push(data)); + child.stderr.on('data', data => stderrData.push(data)); processHandler(child); }); } + afterEach(function() { + if (this.currentTest.state === 'failed') { + if (stderrData.length > 0) { + /* eslint-disable no-console */ + console.log(`STDERR:\n${stderrData.join('')}\n`); + console.log(`STDOUT:\n${stdoutData.join('')}\n`); + /* eslint-enable no-console */ + } + } + }); + it('prompts for user input and writes collected credentials to stdout', async function() { this.retries(5); // Known Flake this.timeout(10000); @@ -168,6 +183,59 @@ describe('GitPromptServer', function() { assert.isTrue(await fileExists(tempDir.getScriptPath('remember'))); }); + it('uses credentials from keytar if available without prompting', async function() { + this.timeout(10000); + this.retries(5); + + let called = false; + function queryHandler() { + called = true; + return {}; + } + + function processHandler(child) { + child.stdin.write('protocol=https\n'); + child.stdin.write('host=what-is-your-favorite-color.com\n'); + child.stdin.end('\n'); + } + + await writeFile(tempDir.getScriptPath('fake-keytar'), 'swordfish'); + const {err, stdout} = await runCredentialScript('get', queryHandler, processHandler); + assert.ifError(err); + assert.isFalse(called); + + assert.equal(stdout, + 'protocol=https\nhost=what-is-your-favorite-color.com\n' + + 'username=old-man-from-scene-24\npassword=swordfish\n' + + 'quit=true\n'); + }); + + it('stores credentials in keytar if a flag file is present', async function() { + this.timeout(10000); + this.retries(5); + + let called = false; + function queryHandler() { + called = true; + return {}; + } + + function processHandler(child) { + child.stdin.write('protocol=https\n'); + child.stdin.write('host=what-is-your-favorite-color.com\n'); + child.stdin.write('password=shhhh'); + child.stdin.end('\n'); + } + + await writeFile(tempDir.getScriptPath('remember'), ''); + const {err} = await runCredentialScript('store', queryHandler, processHandler); + assert.ifError(err); + assert.isFalse(called); + + const stored = await readFile(tempDir.getScriptPath('fake-keytar')); + assert.strictEqual(stored, 'shhhh'); + }); + afterEach(async function() { await server.terminate(); }); From bc2dbaf95da03038e0052e147f1104c76a4211da Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 1 Mar 2018 10:12:03 -0500 Subject: [PATCH 0415/5882] Log the test title before stderr and stdout --- test/git-prompt-server.test.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/git-prompt-server.test.js b/test/git-prompt-server.test.js index 3ba901bafc..8ee8c0835c 100644 --- a/test/git-prompt-server.test.js +++ b/test/git-prompt-server.test.js @@ -57,6 +57,7 @@ describe.only('GitPromptServer', function() { if (this.currentTest.state === 'failed') { if (stderrData.length > 0) { /* eslint-disable no-console */ + console.log(this.currentTest.fullTitle()); console.log(`STDERR:\n${stderrData.join('')}\n`); console.log(`STDOUT:\n${stdoutData.join('')}\n`); /* eslint-enable no-console */ From eb115501c1e019dbe9d0546ecfde72db89c2af7c Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 1 Mar 2018 10:12:40 -0500 Subject: [PATCH 0416/5882] Refactor keytar strategy creation out of GithubLoginModel --- lib/models/keytar-strategy.js | 2 +- test/models/github-login-model.test.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/models/keytar-strategy.js b/lib/models/keytar-strategy.js index 37e6336745..5559cb401f 100644 --- a/lib/models/keytar-strategy.js +++ b/lib/models/keytar-strategy.js @@ -123,5 +123,5 @@ export async function createStrategy() { if (!Strategy) { throw new Error('None of the listed keytar strategies returned true for `isValid`'); } - return Strategy; + return new Strategy(); } diff --git a/test/models/github-login-model.test.js b/test/models/github-login-model.test.js index eb15abc715..abf934eac1 100644 --- a/test/models/github-login-model.test.js +++ b/test/models/github-login-model.test.js @@ -1,4 +1,5 @@ -import GithubLoginModel, {KeytarStrategy, SecurityBinaryStrategy, InMemoryStrategy, UNAUTHENTICATED} from '../../lib/models/github-login-model'; +import GithubLoginModel from '../../lib/models/github-login-model'; +import {KeytarStrategy, SecurityBinaryStrategy, InMemoryStrategy, UNAUTHENTICATED} from '../../lib/models/keytar-strategy'; describe('GithubLoginModel', function() { [null, KeytarStrategy, SecurityBinaryStrategy, InMemoryStrategy].forEach(function(Strategy) { From 9acff0cfa8e9b48a1d6c6a3ac435e3b85b370722 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 1 Mar 2018 10:13:33 -0500 Subject: [PATCH 0417/5882] :fire: .only() --- test/git-prompt-server.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/git-prompt-server.test.js b/test/git-prompt-server.test.js index 8ee8c0835c..29b8bc4401 100644 --- a/test/git-prompt-server.test.js +++ b/test/git-prompt-server.test.js @@ -5,7 +5,7 @@ import GitPromptServer from '../lib/git-prompt-server'; import GitTempDir from '../lib/git-temp-dir'; import {fileExists, writeFile, readFile, getAtomHelperPath} from '../lib/helpers'; -describe.only('GitPromptServer', function() { +describe('GitPromptServer', function() { const electronEnv = { ELECTRON_RUN_AS_NODE: '1', ELECTRON_NO_ATTACH_CONSOLE: '1', From b10f97a03259718ed456862a8a12de406370e012 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 1 Mar 2018 16:19:09 -0500 Subject: [PATCH 0418/5882] Manually transpile selected files to require them from bin/ scripts --- test/helpers.js | 24 +++++++++++++++++++++--- test/transpiled/.gitignore | 2 ++ 2 files changed, 23 insertions(+), 3 deletions(-) create mode 100644 test/transpiled/.gitignore diff --git a/test/helpers.js b/test/helpers.js index 56c612fb0f..da8845c4f2 100644 --- a/test/helpers.js +++ b/test/helpers.js @@ -2,6 +2,7 @@ import fs from 'fs-extra'; import path from 'path'; import temp from 'temp'; import until from 'test-until'; +import transpiler from 'atom-babel6-transpiler'; import React from 'react'; import ReactDom from 'react-dom'; @@ -13,9 +14,7 @@ import WorkerManager from '../lib/worker-manager'; import ContextMenuInterceptor from '../lib/context-menu-interceptor'; import getRepoPipelineManager from '../lib/get-repo-pipeline-manager'; import {Directory} from 'atom'; -import {realPath} from '../lib/helpers'; - -export {toGitPathSep} from '../lib/helpers'; +import {realPath, readFile, writeFile, mkdirs} from '../lib/helpers'; assert.autocrlfEqual = (actual, expected, ...args) => { const newActual = actual.replace(/\r\n/g, '\n'); @@ -213,6 +212,25 @@ export async function disableFilesystemWatchers(atomEnv) { await until('directoryProvider is available', () => atomEnv.project.directoryProviders.length > 0); } +const packageRoot = path.resolve(__dirname, '..'); +const transpiledRoot = path.resolve(__dirname, 'transpiled'); + +export function transpile(...relPaths) { + return Promise.all( + relPaths.map(async relPath => { + const untranspiledPath = require.resolve(relPath); + const transpiledPath = path.join(transpiledRoot, path.relative(packageRoot, untranspiledPath)); + + const untranspiledSource = await readFile(untranspiledPath); + const transpiledSource = transpiler.transpile(untranspiledSource, untranspiledPath, {}, {}).code; + + await mkdirs(path.dirname(transpiledPath)); + await writeFile(transpiledPath, transpiledSource); + return transpiledPath; + }), + ); +} + // eslint-disable-next-line jasmine/no-global-setup beforeEach(function() { global.sinon = sinon.sandbox.create(); diff --git a/test/transpiled/.gitignore b/test/transpiled/.gitignore new file mode 100644 index 0000000000..d6b7ef32c8 --- /dev/null +++ b/test/transpiled/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore From fc85b9ef00b74a6f68dc58d0bf4058bc96f74817 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 1 Mar 2018 16:19:49 -0500 Subject: [PATCH 0419/5882] Shim the atom global when run from the credential helper process --- lib/models/keytar-strategy.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/models/keytar-strategy.js b/lib/models/keytar-strategy.js index 5559cb401f..3b249fdee2 100644 --- a/lib/models/keytar-strategy.js +++ b/lib/models/keytar-strategy.js @@ -1,4 +1,13 @@ import {execFile} from 'child_process'; +import path from 'path'; +import fs from 'fs'; + +if (typeof atom === 'undefined') { + global.atom = { + inSpecMode() { return !!process.env.ATOM_GITHUB_SPEC_MODE; }, + inDevMode() { return false; }, + }; +} export const UNAUTHENTICATED = Symbol('UNAUTHENTICATED'); From b10aa39d0fe19659b385ad6071d1a4c0051401c3 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 1 Mar 2018 16:20:27 -0500 Subject: [PATCH 0420/5882] Include a FileStrategy that backs keytar with a file --- lib/models/keytar-strategy.js | 112 ++++++++++++++++++++++++++++++++-- 1 file changed, 107 insertions(+), 5 deletions(-) diff --git a/lib/models/keytar-strategy.js b/lib/models/keytar-strategy.js index 3b249fdee2..85e9f57e32 100644 --- a/lib/models/keytar-strategy.js +++ b/lib/models/keytar-strategy.js @@ -117,20 +117,122 @@ export class InMemoryStrategy { } } -const strategies = [KeytarStrategy, SecurityBinaryStrategy, InMemoryStrategy]; +export class FileStrategy { + static isValid() { + if (!atom.inSpecMode() && !atom.inDevMode()) { + return false; + } + + return Boolean(process.env.ATOM_GITHUB_KEYTAR_FILE); + } + + constructor() { + this.filePath = process.env.ATOM_GITHUB_KEYTAR_FILE; + + if (!atom.inSpecMode()) { + // eslint-disable-next-line no-console + console.warn( + 'Using a FileStrategy strategy for storing tokens. ' + + 'The tokens will be stored %cin the clear%c in a file at %s. ' + + "You probably shouldn't use real credentials while this strategy is in use. " + + 'Unset ATOM_GITHUB_KEYTAR_FILE_STRATEGY to disable it.', + 'color: red; font-weight: bold; font-style: italic', + 'color: black; font-weight: normal; font-style: normal', + this.filePath, + ); + } + } + + async getPassword(service, account) { + process.stderr.write('getPassword()'); + const payload = await this.load(); + process.stderr.write(`payload = ${require('util').inspect(payload)}\n`); + const forService = payload[service]; + if (forService === undefined) { + return UNAUTHENTICATED; + } + const passwd = forService[account]; + if (passwd === undefined) { + return UNAUTHENTICATED; + } + return passwd; + } + + replacePassword(service, account, password) { + return this.modify(payload => { + let forService = payload[service]; + if (forService === undefined) { + forService = {}; + payload[service] = forService; + } + forService[account] = password; + }); + } + + deletePassword(service, account) { + return this.modify(payload => { + const forService = payload[service]; + if (forService === undefined) { + return; + } + delete forService[account]; + if (Object.keys(forService).length === 0) { + delete payload[service]; + } + }); + } + + load() { + return new Promise((resolve, reject) => { + fs.readFile(this.filePath, 'utf8', (err, content) => { + if (err && err.code === 'ENOENT') { + return resolve({}); + } + if (err) { + return reject(err); + } + return resolve(JSON.parse(content)); + }); + }); + } + + save(payload) { + return new Promise((resolve, reject) => { + fs.writeFile(this.filePath, JSON.stringify(payload), 'utf8', err => { + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); + } + + async modify(callback) { + const payload = await this.load(); + callback(payload); + await this.save(payload); + } +} + +const strategies = [FileStrategy, KeytarStrategy, SecurityBinaryStrategy, InMemoryStrategy]; +let ValidStrategy = null; export async function createStrategy() { - let Strategy; + if (ValidStrategy) { + return new ValidStrategy(); + } + for (let i = 0; i < strategies.length; i++) { const strat = strategies[i]; const isValid = await strat.isValid(); if (isValid) { - Strategy = strat; + ValidStrategy = strat; break; } } - if (!Strategy) { + if (!ValidStrategy) { throw new Error('None of the listed keytar strategies returned true for `isValid`'); } - return new Strategy(); + return new ValidStrategy(); } From 61058439eacf85342aa4cba9103654b03b6616db Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 1 Mar 2018 16:20:42 -0500 Subject: [PATCH 0421/5882] Import shuffle --- lib/controllers/issueish-pane-item-controller.js | 3 ++- lib/controllers/pr-info-controller.js | 2 +- lib/controllers/remote-pr-controller.js | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/controllers/issueish-pane-item-controller.js b/lib/controllers/issueish-pane-item-controller.js index bba1ce75f0..eff4ea861b 100644 --- a/lib/controllers/issueish-pane-item-controller.js +++ b/lib/controllers/issueish-pane-item-controller.js @@ -5,7 +5,8 @@ import {autobind} from 'core-decorators'; import {QueryRenderer, graphql} from 'react-relay'; import RelayNetworkLayerManager from '../relay-network-layer-manager'; -import GithubLoginModel, {UNAUTHENTICATED} from '../models/github-login-model'; +import GithubLoginModel from '../models/github-login-model'; +import {UNAUTHENTICATED} from '../models/keytar-strategy'; import GithubLoginView from '../views/github-login-view'; import ObserveModel from '../views/observe-model'; import IssueishLookupByNumberContainer from '../containers/issueish-lookup-by-number-container'; diff --git a/lib/controllers/pr-info-controller.js b/lib/controllers/pr-info-controller.js index fde7859c73..5a4081ce09 100644 --- a/lib/controllers/pr-info-controller.js +++ b/lib/controllers/pr-info-controller.js @@ -8,7 +8,7 @@ import PrSelectionByUrlContainer from '../containers/pr-selection-by-url-contain import PrSelectionByBranchContainer from '../containers/pr-selection-by-branch-container'; import GithubLoginView from '../views/github-login-view'; import RelayNetworkLayerManager from '../relay-network-layer-manager'; -import {UNAUTHENTICATED} from '../models/github-login-model'; +import {UNAUTHENTICATED} from '../models/keytar-strategy'; export default class PrInfoController extends React.Component { static propTypes = { diff --git a/lib/controllers/remote-pr-controller.js b/lib/controllers/remote-pr-controller.js index 7e5d6527d6..311ac7b4a9 100644 --- a/lib/controllers/remote-pr-controller.js +++ b/lib/controllers/remote-pr-controller.js @@ -6,7 +6,7 @@ import yubikiri from 'yubikiri'; import {RemotePropType} from '../prop-types'; import ObserveModelDecorator from '../decorators/observe-model'; import GithubLoginView from '../views/github-login-view'; -import {UNAUTHENTICATED} from '../models/github-login-model'; +import {UNAUTHENTICATED} from '../models/keytar-strategy'; import {nullRemote} from '../models/remote'; import PrInfoController from './pr-info-controller'; From 9757437c2bb9238bad620585b4d31111cbb16710 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 1 Mar 2018 16:21:30 -0500 Subject: [PATCH 0422/5882] Pre-transpile keytar-strategy.js for GitPromptServer tests --- test/git-prompt-server.test.js | 66 +++++++++++++++++++++++++++++++--- 1 file changed, 62 insertions(+), 4 deletions(-) diff --git a/test/git-prompt-server.test.js b/test/git-prompt-server.test.js index 29b8bc4401..c36570fa70 100644 --- a/test/git-prompt-server.test.js +++ b/test/git-prompt-server.test.js @@ -4,13 +4,15 @@ import path from 'path'; import GitPromptServer from '../lib/git-prompt-server'; import GitTempDir from '../lib/git-temp-dir'; import {fileExists, writeFile, readFile, getAtomHelperPath} from '../lib/helpers'; +import {transpile} from './helpers'; describe('GitPromptServer', function() { const electronEnv = { ELECTRON_RUN_AS_NODE: '1', ELECTRON_NO_ATTACH_CONSOLE: '1', + ATOM_GITHUB_KEYTAR_FILE: null, ATOM_GITHUB_DUGITE_PATH: require.resolve('dugite'), - ATOM_GITHUB_KEYTAR_PATH: require.resolve('keytar'), + ATOM_GITHUB_KEYTAR_STRATEGY_PATH: null, // Computed lazily in beforeEach() ATOM_GITHUB_ORIGINAL_PATH: process.env.PATH, ATOM_GITHUB_WORKDIR_PATH: path.join(__dirname, '..'), ATOM_GITHUB_SPEC_MODE: 'true', @@ -23,6 +25,13 @@ describe('GitPromptServer', function() { beforeEach(async function() { tempDir = new GitTempDir(); await tempDir.ensure(); + + electronEnv.ATOM_GITHUB_KEYTAR_FILE = tempDir.getScriptPath('fake-keytar'); + + if (!electronEnv.ATOM_GITHUB_KEYTAR_STRATEGY_PATH) { + const [keytarStrategyPath] = await transpile(require.resolve('../lib/models/keytar-strategy')); + electronEnv.ATOM_GITHUB_KEYTAR_STRATEGY_PATH = keytarStrategyPath; + } }); describe('credential helper', function() { @@ -184,7 +193,7 @@ describe('GitPromptServer', function() { assert.isTrue(await fileExists(tempDir.getScriptPath('remember'))); }); - it('uses credentials from keytar if available without prompting', async function() { + it('uses matching credentials from keytar if available without prompting', async function() { this.timeout(10000); this.retries(5); @@ -197,10 +206,54 @@ describe('GitPromptServer', function() { function processHandler(child) { child.stdin.write('protocol=https\n'); child.stdin.write('host=what-is-your-favorite-color.com\n'); + child.stdin.write('username=old-man-from-scene-24'); child.stdin.end('\n'); } - await writeFile(tempDir.getScriptPath('fake-keytar'), 'swordfish'); + await writeFile(tempDir.getScriptPath('fake-keytar'), ` + { + "atom-github-git @ what-is-your-favorite-color.com": { + "old-man-from-scene-24": "swordfish", + "github.com": "nope" + }, + "atom-github-git @ github.com": { + "old-man-from-scene-24": "nope" + } + } + `); + const {err, stdout} = await runCredentialScript('get', queryHandler, processHandler); + assert.ifError(err); + assert.isFalse(called); + + assert.equal(stdout, + 'protocol=https\nhost=what-is-your-favorite-color.com\n' + + 'username=old-man-from-scene-24\npassword=swordfish\n' + + 'quit=true\n'); + }); + + it('uses credentials from the GitHub tab if available', async function() { + this.timeout(10000); + this.retries(5); + + let called = false; + function queryHandler() { + called = true; + return {}; + } + + function processHandler(child) { + child.stdin.write('protocol=https\n'); + child.stdin.write('host=what-is-your-favorite-color.com\n'); + child.stdin.end('\n'); + } + + await writeFile(tempDir.getScriptPath('fake-keytar'), ` + { + "atom-github": { + "github.com": "swordfish" + } + } + `); const {err, stdout} = await runCredentialScript('get', queryHandler, processHandler); assert.ifError(err); assert.isFalse(called); @@ -224,6 +277,7 @@ describe('GitPromptServer', function() { function processHandler(child) { child.stdin.write('protocol=https\n'); child.stdin.write('host=what-is-your-favorite-color.com\n'); + child.stdin.write('username=old-man-from-scene-24'); child.stdin.write('password=shhhh'); child.stdin.end('\n'); } @@ -234,7 +288,11 @@ describe('GitPromptServer', function() { assert.isFalse(called); const stored = await readFile(tempDir.getScriptPath('fake-keytar')); - assert.strictEqual(stored, 'shhhh'); + assert.deepEqual(JSON.parse(stored), { + 'atom-github-git @ what-is-your-favorite-color.com': { + 'old-man-from-scene-24': 'shhhh', + }, + }); }); afterEach(async function() { From 6e58df11d6af233abce30d973711bbaf0b466c34 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 1 Mar 2018 16:22:06 -0500 Subject: [PATCH 0423/5882] Read credentials from keytar if present --- bin/git-credential-atom.js | 53 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 51 insertions(+), 2 deletions(-) diff --git a/bin/git-credential-atom.js b/bin/git-credential-atom.js index bc99de12df..479257d859 100755 --- a/bin/git-credential-atom.js +++ b/bin/git-credential-atom.js @@ -5,6 +5,7 @@ const fs = require('fs'); const path = require('path'); const {execFile} = require('child_process'); const {GitProcess} = require(process.env.ATOM_GITHUB_DUGITE_PATH); +const {createStrategy, UNAUTHENTICATED} = require(process.env.ATOM_GITHUB_KEYTAR_STRATEGY_PATH); const diagnosticsEnabled = process.env.GIT_TRACE && process.env.GIT_TRACE.length !== 0; const workdirPath = process.env.ATOM_GITHUB_WORKDIR_PATH; @@ -166,10 +167,58 @@ function fromOtherHelpers(query) { } /* - * This is a placeholder for eventual support of storage of credentials in an OS keychain. + * Attempt to read credentials previously stored in keytar. */ function fromKeytar(query) { - return Promise.reject(new Error('Not implemented')); + let strategy = null; + let password = UNAUTHENTICATED; + + log('reading credentials stored in your OS keychain'); + return createStrategy() + .then(s => { strategy = s; }) + .then(() => { + const service = `atom-github-git @ ${query.host}`; + log(`reading service "${service}" and account "${query.username}"`); + return strategy.getPassword(service, query.username); + }) + .then(p => { + if (p !== UNAUTHENTICATED) { + password = p; + log('password found in keychain'); + return UNAUTHENTICATED; + } + + let host = query.host; + if (query.host === 'github.com') { + host = 'https://api.github.com'; + } + + log(`reading service "atom-github" and account "${host}"`); + return strategy.getPassword('atom-github', host); + }) + .then(p => { + if (p !== UNAUTHENTICATED) { + // TODO fetch username from API + if (!query.username) { + log('token found in keychain, but no username'); + return; + } + log('token found in keychain'); + password = p; + } + }) + .then(() => { + if (password !== UNAUTHENTICATED) { + const lines = ['protocol', 'host', 'username'] + .filter(k => query[k] !== undefined) + .map(k => `${k}=${query[k]}\n`); + lines.push(`password=${password}\n`); + return lines.join('') + 'quit=true\n'; + } else { + log('no password found in keychain'); + return Promise.reject(new Error('Unable to read password from keychain')); + } + }); } /* From 38a7cf1e01bb9fd484d45e005d8778cd8166419a Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 1 Mar 2018 22:25:32 -0500 Subject: [PATCH 0424/5882] Hooray async await :tada: --- bin/git-credential-atom.js | 220 ++++++++++++++++++------------------- 1 file changed, 109 insertions(+), 111 deletions(-) diff --git a/bin/git-credential-atom.js b/bin/git-credential-atom.js index 479257d859..16d0b429fb 100755 --- a/bin/git-credential-atom.js +++ b/bin/git-credential-atom.js @@ -66,30 +66,28 @@ function systemCredentialHelpers() { * Dispatch a `git credential` subcommand to all configured credential helpers. Return a Promise that * resolves with the exit status, stdout, and stderr of the subcommand. */ -function withAllHelpers(query, subAction) { - return systemCredentialHelpers() - .then(systemHelpers => { - const env = { - GIT_ASKPASS: process.env.ATOM_GITHUB_ORIGINAL_GIT_ASKPASS || '', - SSH_ASKPASS: process.env.ATOM_GITHUB_ORIGINAL_SSH_ASKPASS || '', - GIT_CONFIG_PARAMETERS: '', // Only you can prevent forkbombs - }; - - const stdin = Object.keys(query).map(k => `${k}=${query[k]}\n`).join('') + '\n'; - const stdinEncoding = 'utf8'; - - const args = []; - systemHelpers.forEach(helper => args.push('-c', `credential.helper=${helper}`)); - args.push('credential', subAction); - - log(`attempting to run ${subAction} with user-configured credential helpers`); - log(`GIT_ASKPASS = ${env.GIT_ASKPASS}`); - log(`SSH_ASKPASS = ${env.SSH_ASKPASS}`); - log(`arguments = ${args.join(' ')}`); - log(`stdin =\n${stdin.replace(/password=[^\n]+/, 'password=*******')}`); - - return GitProcess.exec(args, workdirPath, {env, stdin, stdinEncoding}); - }); +async function withAllHelpers(query, subAction) { + const systemHelpers = await systemCredentialHelpers(); + const env = { + GIT_ASKPASS: process.env.ATOM_GITHUB_ORIGINAL_GIT_ASKPASS || '', + SSH_ASKPASS: process.env.ATOM_GITHUB_ORIGINAL_SSH_ASKPASS || '', + GIT_CONFIG_PARAMETERS: '', // Only you can prevent forkbombs + }; + + const stdin = Object.keys(query).map(k => `${k}=${query[k]}\n`).join('') + '\n'; + const stdinEncoding = 'utf8'; + + const args = []; + systemHelpers.forEach(helper => args.push('-c', `credential.helper=${helper}`)); + args.push('credential', subAction); + + log(`attempting to run ${subAction} with user-configured credential helpers`); + log(`GIT_ASKPASS = ${env.GIT_ASKPASS}`); + log(`SSH_ASKPASS = ${env.SSH_ASKPASS}`); + log(`arguments = ${args.join(' ')}`); + log(`stdin =\n${stdin.replace(/password=[^\n]+/, 'password=*******')}`); + + return GitProcess.exec(args, workdirPath, {env, stdin, stdinEncoding}); } /* @@ -143,82 +141,66 @@ function parse() { * Attempt to use user-configured credential handlers through the normal git channels. If they actually work, * hooray! Report the results to stdout. Otherwise, reject the promise and collect credentials through Atom. */ -function fromOtherHelpers(query) { - return withAllHelpers(query, 'fill') - .then(({stdout, stderr, exitCode}) => { - if (exitCode !== 0) { - log(`stdout:\n${stdout}`); - log(`stderr:\n${stderr}`); - log(`user-configured credential helpers failed with exit code ${exitCode}. this is ok`); - - throw new Error('git-credential fill failed'); - } +async function fromOtherHelpers(query) { + const {stdout, stderr, exitCode} = await withAllHelpers(query, 'fill'); + if (exitCode !== 0) { + log(`stdout:\n${stdout}`); + log(`stderr:\n${stderr}`); + log(`user-configured credential helpers failed with exit code ${exitCode}. this is ok`); + + throw new Error('git-credential fill failed'); + } - if (/password=/.test(stdout)) { - log('password received from user-configured credential helper'); + if (/password=/.test(stdout)) { + log('password received from user-configured credential helper'); - return stdout; - } else { - log(`no password received from user-configured credential helper:\n${stdout}`); + return stdout; + } else { + log(`no password received from user-configured credential helper:\n${stdout}`); - throw new Error('No password reported from upstream git-credential fill'); - } - }); + throw new Error('No password reported from upstream git-credential fill'); + } } /* * Attempt to read credentials previously stored in keytar. */ -function fromKeytar(query) { - let strategy = null; - let password = UNAUTHENTICATED; - +async function fromKeytar(query) { log('reading credentials stored in your OS keychain'); - return createStrategy() - .then(s => { strategy = s; }) - .then(() => { - const service = `atom-github-git @ ${query.host}`; - log(`reading service "${service}" and account "${query.username}"`); - return strategy.getPassword(service, query.username); - }) - .then(p => { - if (p !== UNAUTHENTICATED) { - password = p; - log('password found in keychain'); - return UNAUTHENTICATED; - } - - let host = query.host; - if (query.host === 'github.com') { - host = 'https://api.github.com'; - } + let password = UNAUTHENTICATED; + const strategy = await createStrategy(); + + // TODO find username if absent + const gitService = `atom-github-git @ ${query.host}`; + log(`reading service "${gitService}" and account "${query.username}"`); + const gitPassword = await strategy.getPassword(gitService, query.username); + if (gitPassword !== UNAUTHENTICATED) { + log('password found in keychain'); + password = gitPassword; + } - log(`reading service "atom-github" and account "${host}"`); - return strategy.getPassword('atom-github', host); - }) - .then(p => { - if (p !== UNAUTHENTICATED) { - // TODO fetch username from API - if (!query.username) { - log('token found in keychain, but no username'); - return; - } - log('token found in keychain'); - password = p; - } - }) - .then(() => { - if (password !== UNAUTHENTICATED) { - const lines = ['protocol', 'host', 'username'] - .filter(k => query[k] !== undefined) - .map(k => `${k}=${query[k]}\n`); - lines.push(`password=${password}\n`); - return lines.join('') + 'quit=true\n'; + const githubHost = `${query.protocol}://api.${query.host}`; + log(`reading service "atom-github" and account "${githubHost}"`); + const githubPassword = await strategy.getPassword('atom-github', githubHost); + if (githubPassword !== UNAUTHENTICATED) { + if (!query.username) { + // TODO find username from GitHub API + throw new Error('token found in keychain, but no username'); } else { - log('no password found in keychain'); - return Promise.reject(new Error('Unable to read password from keychain')); + password = githubPassword; } - }); + } + + if (password !== UNAUTHENTICATED) { + const lines = ['protocol', 'host', 'username'] + .filter(k => query[k] !== undefined) + .map(k => `${k}=${query[k]}\n`); + lines.push(`password=${password}\n`); + return lines.join('') + 'quit=true\n'; + } else { + log('no password found in keychain'); + throw new Error('Unable to read password from keychain'); + } } /* @@ -249,7 +231,11 @@ function dialog(query) { try { const reply = JSON.parse(parts.join('')); - const writeReply = function() { + const writeReply = function(err) { + if (err) { + log(`Unable to write remember file: ${err.stack}`); + } + const lines = []; ['protocol', 'host', 'username', 'password'].forEach(k => { const value = reply[k] !== undefined ? reply[k] : query[k]; @@ -279,35 +265,47 @@ function dialog(query) { }); } -function get() { - parse() - .then(query => { - return fromOtherHelpers(query) - .catch(() => fromKeytar(query)) - .catch(() => dialog(query)); - }) - .then(reply => { - process.stdout.write(reply); - log('success'); - process.exit(0); - }, err => { +async function get() { + const query = await parse(); + const reply = await fromOtherHelpers(query) + .catch(() => fromKeytar(query)) + .catch(() => dialog(query)) + .catch(err => { process.stderr.write(`Unable to prompt through Atom:\n${err.stack}`); log('failure'); - process.stdout.write('quit=true\n\n'); - process.exit(0); + return 'quit=true\n\n'; + }); + + await new Promise((resolve, reject) => { + process.stdout.write(reply, err => { + if (err) { reject(err); } else { resolve(); } }); + }); + + log('success'); + process.exit(0); } -function store() { - parse() - .then(query => withAllHelpers(query, 'approve')) - .then(() => process.exit(0), () => process.exit(1)); +async function store() { + try { + const query = await parse(); + await withAllHelpers(query, 'approve'); + process.exit(0); + } catch (e) { + log(`Unable to execute store: ${e.stack}`); + process.exit(1); + } } -function erase() { - parse() - .then(query => withAllHelpers(query, 'reject')) - .then(() => process.exit(0), () => process.exit(1)); +async function erase() { + try { + const query = await parse(); + await withAllHelpers(query, 'reject'); + process.exit(0); + } catch (e) { + log(`Unable to execute erase: ${e.stack}`); + process.exit(1); + } } log(`working directory = ${workdirPath}`); From 8d5e8dec5b43c308a8d43faf227019a2e8a41538 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 1 Mar 2018 22:36:27 -0500 Subject: [PATCH 0425/5882] Write accepted passwords to keytar --- bin/git-credential-atom.js | 29 +++++++++++++++++++++++++++++ test/git-prompt-server.test.js | 11 +++++++---- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/bin/git-credential-atom.js b/bin/git-credential-atom.js index 16d0b429fb..be0dcd9ef9 100755 --- a/bin/git-credential-atom.js +++ b/bin/git-credential-atom.js @@ -265,6 +265,32 @@ function dialog(query) { }); } +/* + * Write a successfully used username and password pair to the OS keychain, so that fromKeytar will find it. + */ +async function toKeytar(query) { + const rememberFlag = await new Promise(resolve => { + fs.access(rememberFile, err => resolve(!err)); + }); + if (!rememberFlag) { + return; + } + + const strategy = await createStrategy(); + + const gitService = `atom-github-git @ ${query.host}`; + log(`writing service "${gitService}" and account "${query.username}"`); + await strategy.replacePassword(gitService, query.username, query.password); + log('success'); +} + +/* + * + */ +// async function deleteFromKeytar(query) { +// // +// } + async function get() { const query = await parse(); const reply = await fromOtherHelpers(query) @@ -289,7 +315,9 @@ async function get() { async function store() { try { const query = await parse(); + await toKeytar(query); await withAllHelpers(query, 'approve'); + log('success'); process.exit(0); } catch (e) { log(`Unable to execute store: ${e.stack}`); @@ -301,6 +329,7 @@ async function erase() { try { const query = await parse(); await withAllHelpers(query, 'reject'); + log('success'); process.exit(0); } catch (e) { log(`Unable to execute erase: ${e.stack}`); diff --git a/test/git-prompt-server.test.js b/test/git-prompt-server.test.js index c36570fa70..0f4a14950a 100644 --- a/test/git-prompt-server.test.js +++ b/test/git-prompt-server.test.js @@ -3,7 +3,7 @@ import path from 'path'; import GitPromptServer from '../lib/git-prompt-server'; import GitTempDir from '../lib/git-temp-dir'; -import {fileExists, writeFile, readFile, getAtomHelperPath} from '../lib/helpers'; +import {fileExists, writeFile, readFile, deleteFileOrFolder, getAtomHelperPath} from '../lib/helpers'; import {transpile} from './helpers'; describe('GitPromptServer', function() { @@ -62,7 +62,7 @@ describe('GitPromptServer', function() { }); } - afterEach(function() { + afterEach(async function() { if (this.currentTest.state === 'failed') { if (stderrData.length > 0) { /* eslint-disable no-console */ @@ -72,6 +72,8 @@ describe('GitPromptServer', function() { /* eslint-enable no-console */ } } + + await tempDir.dispose(); }); it('prompts for user input and writes collected credentials to stdout', async function() { @@ -244,13 +246,14 @@ describe('GitPromptServer', function() { function processHandler(child) { child.stdin.write('protocol=https\n'); child.stdin.write('host=what-is-your-favorite-color.com\n'); + child.stdin.write('username=old-man-from-scene-24\n'); child.stdin.end('\n'); } await writeFile(tempDir.getScriptPath('fake-keytar'), ` { "atom-github": { - "github.com": "swordfish" + "https://api.what-is-your-favorite-color.com": "swordfish" } } `); @@ -277,7 +280,7 @@ describe('GitPromptServer', function() { function processHandler(child) { child.stdin.write('protocol=https\n'); child.stdin.write('host=what-is-your-favorite-color.com\n'); - child.stdin.write('username=old-man-from-scene-24'); + child.stdin.write('username=old-man-from-scene-24\n'); child.stdin.write('password=shhhh'); child.stdin.end('\n'); } From 9774be83ea8f406e057eb9d31435675f878ba9df Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 1 Mar 2018 22:36:36 -0500 Subject: [PATCH 0426/5882] :fire: some diagnostics --- lib/models/keytar-strategy.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/models/keytar-strategy.js b/lib/models/keytar-strategy.js index 85e9f57e32..3eb8c0d3ad 100644 --- a/lib/models/keytar-strategy.js +++ b/lib/models/keytar-strategy.js @@ -144,9 +144,7 @@ export class FileStrategy { } async getPassword(service, account) { - process.stderr.write('getPassword()'); const payload = await this.load(); - process.stderr.write(`payload = ${require('util').inspect(payload)}\n`); const forService = payload[service]; if (forService === undefined) { return UNAUTHENTICATED; From 91f91dbab4ba7d8dac831f552fadf20131db9e1f Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 1 Mar 2018 22:43:46 -0500 Subject: [PATCH 0427/5882] Forget credentials that fail --- bin/git-credential-atom.js | 14 ++++++++---- test/git-prompt-server.test.js | 40 ++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 4 deletions(-) diff --git a/bin/git-credential-atom.js b/bin/git-credential-atom.js index be0dcd9ef9..1f7614030b 100755 --- a/bin/git-credential-atom.js +++ b/bin/git-credential-atom.js @@ -285,11 +285,16 @@ async function toKeytar(query) { } /* - * + * Remove credentials that failed authentication. */ -// async function deleteFromKeytar(query) { -// // -// } +async function deleteFromKeytar(query) { + const strategy = await createStrategy(); + + const gitService = `atom-github-git @ ${query.host}`; + log(`removing account "${query.username}" from service "${gitService}"`); + await strategy.deletePassword(gitService, query.username, query.password); + log('success'); +} async function get() { const query = await parse(); @@ -329,6 +334,7 @@ async function erase() { try { const query = await parse(); await withAllHelpers(query, 'reject'); + await deleteFromKeytar(query); log('success'); process.exit(0); } catch (e) { diff --git a/test/git-prompt-server.test.js b/test/git-prompt-server.test.js index 0f4a14950a..d022e78e7d 100644 --- a/test/git-prompt-server.test.js +++ b/test/git-prompt-server.test.js @@ -298,6 +298,46 @@ describe('GitPromptServer', function() { }); }); + it('forgets stored credentials from keytar if authentication fails', async function() { + this.timeout(10000); + this.retries(5); + + function queryHandler() { + return {}; + } + + function processHandler(child) { + child.stdin.write('protocol=https\n'); + child.stdin.write('host=what-is-your-favorite-color.com\n'); + child.stdin.write('username=old-man-from-scene-24\n'); + child.stdin.write('password=shhhh'); + child.stdin.end('\n'); + } + + await writeFile(tempDir.getScriptPath('fake-keytar'), JSON.stringify({ + 'atom-github-git @ what-is-your-favorite-color.com': { + 'old-man-from-scene-24': 'shhhh', + 'someone-else': 'untouched', + }, + 'atom-github-git @ github.com': { + 'old-man-from-scene-24': 'untouched', + }, + })); + + const {err} = await runCredentialScript('erase', queryHandler, processHandler); + assert.ifError(err); + + const stored = await readFile(tempDir.getScriptPath('fake-keytar')); + assert.deepEqual(JSON.parse(stored), { + 'atom-github-git @ what-is-your-favorite-color.com': { + 'someone-else': 'untouched', + }, + 'atom-github-git @ github.com': { + 'old-man-from-scene-24': 'untouched', + }, + }); + }); + afterEach(async function() { await server.terminate(); }); From 56656da5ed8a36011e4e417eef93b33d2f77a2e6 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 1 Mar 2018 23:06:23 -0500 Subject: [PATCH 0428/5882] Use a default username for each host --- bin/git-credential-atom.js | 29 +++++++++++++++++------ test/git-prompt-server.test.js | 43 ++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 7 deletions(-) diff --git a/bin/git-credential-atom.js b/bin/git-credential-atom.js index 1f7614030b..fdb8b9a6fd 100755 --- a/bin/git-credential-atom.js +++ b/bin/git-credential-atom.js @@ -168,15 +168,30 @@ async function fromOtherHelpers(query) { async function fromKeytar(query) { log('reading credentials stored in your OS keychain'); let password = UNAUTHENTICATED; + + if (!query.host) { + throw new Error('Host unavailable'); + } + const strategy = await createStrategy(); - // TODO find username if absent - const gitService = `atom-github-git @ ${query.host}`; - log(`reading service "${gitService}" and account "${query.username}"`); - const gitPassword = await strategy.getPassword(gitService, query.username); - if (gitPassword !== UNAUTHENTICATED) { - log('password found in keychain'); - password = gitPassword; + if (!query.username) { + const metaService = `atom-github-git-meta @ ${query.host}`; + log(`reading username from service "${metaService}" and account "username"`); + const u = await strategy.getPassword(metaService, 'username'); + if (u !== UNAUTHENTICATED) { + query.username = u; + } + } + + if (query.username) { + const gitService = `atom-github-git @ ${query.host}`; + log(`reading service "${gitService}" and account "${query.username}"`); + const gitPassword = await strategy.getPassword(gitService, query.username); + if (gitPassword !== UNAUTHENTICATED) { + log('password found in keychain'); + password = gitPassword; + } } const githubHost = `${query.protocol}://api.${query.host}`; diff --git a/test/git-prompt-server.test.js b/test/git-prompt-server.test.js index d022e78e7d..3f165279e5 100644 --- a/test/git-prompt-server.test.js +++ b/test/git-prompt-server.test.js @@ -233,6 +233,49 @@ describe('GitPromptServer', function() { 'quit=true\n'); }); + it('uses a default username for the appropriate host if one is available', async function() { + this.timeout(10000); + this.retries(5); + + let called = false; + function queryHandler() { + called = true; + return {}; + } + + function processHandler(child) { + child.stdin.write('protocol=https\n'); + child.stdin.write('host=what-is-your-favorite-color.com\n'); + child.stdin.end('\n'); + } + + await writeFile(tempDir.getScriptPath('fake-keytar'), ` + { + "atom-github-git-meta @ what-is-your-favorite-color.com": { + "username": "old-man-from-scene-24" + }, + "atom-github-git @ what-is-your-favorite-color.com": { + "old-man-from-scene-24": "swordfish", + "github.com": "nope" + }, + "atom-github-git-meta @ github.com": { + "username": "nah" + }, + "atom-github-git @ github.com": { + "old-man-from-scene-24": "nope" + } + } + `); + const {err, stdout} = await runCredentialScript('get', queryHandler, processHandler); + assert.ifError(err); + assert.isFalse(called); + + assert.equal(stdout, + 'protocol=https\nhost=what-is-your-favorite-color.com\n' + + 'username=old-man-from-scene-24\npassword=swordfish\n' + + 'quit=true\n'); + }); + it('uses credentials from the GitHub tab if available', async function() { this.timeout(10000); this.retries(5); From cab8f38d4f9680246abba4d629353110eb06d6c6 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 1 Mar 2018 23:11:26 -0500 Subject: [PATCH 0429/5882] :fire: unused import --- test/git-prompt-server.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/git-prompt-server.test.js b/test/git-prompt-server.test.js index 3f165279e5..cc1795aebd 100644 --- a/test/git-prompt-server.test.js +++ b/test/git-prompt-server.test.js @@ -3,7 +3,7 @@ import path from 'path'; import GitPromptServer from '../lib/git-prompt-server'; import GitTempDir from '../lib/git-temp-dir'; -import {fileExists, writeFile, readFile, deleteFileOrFolder, getAtomHelperPath} from '../lib/helpers'; +import {fileExists, writeFile, readFile, getAtomHelperPath} from '../lib/helpers'; import {transpile} from './helpers'; describe('GitPromptServer', function() { From d3c4f6fa240e981e9880250083a82ddef3e0a926 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 1 Mar 2018 23:11:51 -0500 Subject: [PATCH 0430/5882] Store the default username for each host in a meta service --- bin/git-credential-atom.js | 5 +++++ test/git-prompt-server.test.js | 3 +++ 2 files changed, 8 insertions(+) diff --git a/bin/git-credential-atom.js b/bin/git-credential-atom.js index fdb8b9a6fd..da0c71b866 100755 --- a/bin/git-credential-atom.js +++ b/bin/git-credential-atom.js @@ -296,6 +296,11 @@ async function toKeytar(query) { const gitService = `atom-github-git @ ${query.host}`; log(`writing service "${gitService}" and account "${query.username}"`); await strategy.replacePassword(gitService, query.username, query.password); + + const metaService = `atom-github-git-meta @ ${query.host}`; + log(`writing service "${metaService}" and account "username"`); + await strategy.replacePassword(metaService, 'username', query.username); + log('success'); } diff --git a/test/git-prompt-server.test.js b/test/git-prompt-server.test.js index cc1795aebd..2fd61524e5 100644 --- a/test/git-prompt-server.test.js +++ b/test/git-prompt-server.test.js @@ -335,6 +335,9 @@ describe('GitPromptServer', function() { const stored = await readFile(tempDir.getScriptPath('fake-keytar')); assert.deepEqual(JSON.parse(stored), { + 'atom-github-git-meta @ what-is-your-favorite-color.com': { + username: 'old-man-from-scene-24', + }, 'atom-github-git @ what-is-your-favorite-color.com': { 'old-man-from-scene-24': 'shhhh', }, From db93196e0b451a29d91daa2fa5346df0f1d33e16 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 1 Mar 2018 23:12:10 -0500 Subject: [PATCH 0431/5882] Completely untested code to pull the username that issued an access token --- bin/git-credential-atom.js | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/bin/git-credential-atom.js b/bin/git-credential-atom.js index da0c71b866..42b00b2ba9 100755 --- a/bin/git-credential-atom.js +++ b/bin/git-credential-atom.js @@ -198,12 +198,29 @@ async function fromKeytar(query) { log(`reading service "atom-github" and account "${githubHost}"`); const githubPassword = await strategy.getPassword('atom-github', githubHost); if (githubPassword !== UNAUTHENTICATED) { - if (!query.username) { - // TODO find username from GitHub API + try { + if (!query.username) { + const apiUrl = githubHost === 'https://api.github.com' ? `${githubHost}/graphql` : `${githubHost}/api/v3/graphql`; + const response = await fetch(apiUrl, { + method: 'POST', + headers: { + 'content-type': 'application/json', + 'Authorization': `bearer ${githubPassword}`, + 'Accept': 'application/vnd.github.graphql-profiling+json', + }, + body: JSON.stringify({ + query: 'query { viewer { login } }', + }), + }); + + query.username = response.json().data.viewer.login; + } + } catch (e) { + log(`unable to acquire username from token: ${e.stack}`); throw new Error('token found in keychain, but no username'); - } else { - password = githubPassword; } + + password = githubPassword; } if (password !== UNAUTHENTICATED) { From b7d146fbfb6515e1714d0bc731ef30909df6cd6c Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 1 Mar 2018 23:16:09 -0500 Subject: [PATCH 0432/5882] Key on host and protocol --- bin/git-credential-atom.js | 14 +++++++------- test/git-prompt-server.test.js | 24 ++++++++++++------------ 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/bin/git-credential-atom.js b/bin/git-credential-atom.js index 42b00b2ba9..dc806a5775 100755 --- a/bin/git-credential-atom.js +++ b/bin/git-credential-atom.js @@ -169,14 +169,14 @@ async function fromKeytar(query) { log('reading credentials stored in your OS keychain'); let password = UNAUTHENTICATED; - if (!query.host) { - throw new Error('Host unavailable'); + if (!query.host && !query.protocol) { + throw new Error('Host or protocol unavailable'); } const strategy = await createStrategy(); if (!query.username) { - const metaService = `atom-github-git-meta @ ${query.host}`; + const metaService = `atom-github-git-meta @ ${query.protocol}://${query.host}`; log(`reading username from service "${metaService}" and account "username"`); const u = await strategy.getPassword(metaService, 'username'); if (u !== UNAUTHENTICATED) { @@ -185,7 +185,7 @@ async function fromKeytar(query) { } if (query.username) { - const gitService = `atom-github-git @ ${query.host}`; + const gitService = `atom-github-git @ ${query.protocol}://${query.host}`; log(`reading service "${gitService}" and account "${query.username}"`); const gitPassword = await strategy.getPassword(gitService, query.username); if (gitPassword !== UNAUTHENTICATED) { @@ -310,11 +310,11 @@ async function toKeytar(query) { const strategy = await createStrategy(); - const gitService = `atom-github-git @ ${query.host}`; + const gitService = `atom-github-git @ ${query.protocol}://${query.host}`; log(`writing service "${gitService}" and account "${query.username}"`); await strategy.replacePassword(gitService, query.username, query.password); - const metaService = `atom-github-git-meta @ ${query.host}`; + const metaService = `atom-github-git-meta @ ${query.protocol}://${query.host}`; log(`writing service "${metaService}" and account "username"`); await strategy.replacePassword(metaService, 'username', query.username); @@ -327,7 +327,7 @@ async function toKeytar(query) { async function deleteFromKeytar(query) { const strategy = await createStrategy(); - const gitService = `atom-github-git @ ${query.host}`; + const gitService = `atom-github-git @ ${query.protocol}://${query.host}`; log(`removing account "${query.username}" from service "${gitService}"`); await strategy.deletePassword(gitService, query.username, query.password); log('success'); diff --git a/test/git-prompt-server.test.js b/test/git-prompt-server.test.js index 2fd61524e5..9349b22a11 100644 --- a/test/git-prompt-server.test.js +++ b/test/git-prompt-server.test.js @@ -214,11 +214,11 @@ describe('GitPromptServer', function() { await writeFile(tempDir.getScriptPath('fake-keytar'), ` { - "atom-github-git @ what-is-your-favorite-color.com": { + "atom-github-git @ https://what-is-your-favorite-color.com": { "old-man-from-scene-24": "swordfish", "github.com": "nope" }, - "atom-github-git @ github.com": { + "atom-github-git @ https://github.com": { "old-man-from-scene-24": "nope" } } @@ -251,17 +251,17 @@ describe('GitPromptServer', function() { await writeFile(tempDir.getScriptPath('fake-keytar'), ` { - "atom-github-git-meta @ what-is-your-favorite-color.com": { + "atom-github-git-meta @ https://what-is-your-favorite-color.com": { "username": "old-man-from-scene-24" }, - "atom-github-git @ what-is-your-favorite-color.com": { + "atom-github-git @ https://what-is-your-favorite-color.com": { "old-man-from-scene-24": "swordfish", "github.com": "nope" }, - "atom-github-git-meta @ github.com": { + "atom-github-git-meta @ https://github.com": { "username": "nah" }, - "atom-github-git @ github.com": { + "atom-github-git @ https://github.com": { "old-man-from-scene-24": "nope" } } @@ -335,10 +335,10 @@ describe('GitPromptServer', function() { const stored = await readFile(tempDir.getScriptPath('fake-keytar')); assert.deepEqual(JSON.parse(stored), { - 'atom-github-git-meta @ what-is-your-favorite-color.com': { + 'atom-github-git-meta @ https://what-is-your-favorite-color.com': { username: 'old-man-from-scene-24', }, - 'atom-github-git @ what-is-your-favorite-color.com': { + 'atom-github-git @ https://what-is-your-favorite-color.com': { 'old-man-from-scene-24': 'shhhh', }, }); @@ -361,11 +361,11 @@ describe('GitPromptServer', function() { } await writeFile(tempDir.getScriptPath('fake-keytar'), JSON.stringify({ - 'atom-github-git @ what-is-your-favorite-color.com': { + 'atom-github-git @ https://what-is-your-favorite-color.com': { 'old-man-from-scene-24': 'shhhh', 'someone-else': 'untouched', }, - 'atom-github-git @ github.com': { + 'atom-github-git @ https://github.com': { 'old-man-from-scene-24': 'untouched', }, })); @@ -375,10 +375,10 @@ describe('GitPromptServer', function() { const stored = await readFile(tempDir.getScriptPath('fake-keytar')); assert.deepEqual(JSON.parse(stored), { - 'atom-github-git @ what-is-your-favorite-color.com': { + 'atom-github-git @ https://what-is-your-favorite-color.com': { 'someone-else': 'untouched', }, - 'atom-github-git @ github.com': { + 'atom-github-git @ https://github.com': { 'old-man-from-scene-24': 'untouched', }, }); From fb3da919d09b9890a76c46346d22eb0eb4138823 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 2 Mar 2018 14:46:20 -0500 Subject: [PATCH 0433/5882] Move keytar-strategy to lib/shared/ --- .../issueish-pane-item-controller.js | 2 +- lib/controllers/pr-info-controller.js | 2 +- lib/controllers/remote-pr-controller.js | 2 +- lib/models/github-login-model.js | 2 +- lib/shared/README.md | 3 +++ lib/{models => shared}/keytar-strategy.js | 26 ++++++++++++------- test/git-prompt-server.test.js | 8 +----- test/models/github-login-model.test.js | 2 +- 8 files changed, 26 insertions(+), 21 deletions(-) create mode 100644 lib/shared/README.md rename lib/{models => shared}/keytar-strategy.js (93%) diff --git a/lib/controllers/issueish-pane-item-controller.js b/lib/controllers/issueish-pane-item-controller.js index eff4ea861b..c65b721b6c 100644 --- a/lib/controllers/issueish-pane-item-controller.js +++ b/lib/controllers/issueish-pane-item-controller.js @@ -6,7 +6,7 @@ import {QueryRenderer, graphql} from 'react-relay'; import RelayNetworkLayerManager from '../relay-network-layer-manager'; import GithubLoginModel from '../models/github-login-model'; -import {UNAUTHENTICATED} from '../models/keytar-strategy'; +import {UNAUTHENTICATED} from '../shared/keytar-strategy'; import GithubLoginView from '../views/github-login-view'; import ObserveModel from '../views/observe-model'; import IssueishLookupByNumberContainer from '../containers/issueish-lookup-by-number-container'; diff --git a/lib/controllers/pr-info-controller.js b/lib/controllers/pr-info-controller.js index 5a4081ce09..f4cfdcc244 100644 --- a/lib/controllers/pr-info-controller.js +++ b/lib/controllers/pr-info-controller.js @@ -8,7 +8,7 @@ import PrSelectionByUrlContainer from '../containers/pr-selection-by-url-contain import PrSelectionByBranchContainer from '../containers/pr-selection-by-branch-container'; import GithubLoginView from '../views/github-login-view'; import RelayNetworkLayerManager from '../relay-network-layer-manager'; -import {UNAUTHENTICATED} from '../models/keytar-strategy'; +import {UNAUTHENTICATED} from '../shared/keytar-strategy'; export default class PrInfoController extends React.Component { static propTypes = { diff --git a/lib/controllers/remote-pr-controller.js b/lib/controllers/remote-pr-controller.js index 311ac7b4a9..4bdd3de17c 100644 --- a/lib/controllers/remote-pr-controller.js +++ b/lib/controllers/remote-pr-controller.js @@ -6,7 +6,7 @@ import yubikiri from 'yubikiri'; import {RemotePropType} from '../prop-types'; import ObserveModelDecorator from '../decorators/observe-model'; import GithubLoginView from '../views/github-login-view'; -import {UNAUTHENTICATED} from '../models/keytar-strategy'; +import {UNAUTHENTICATED} from '../shared/keytar-strategy'; import {nullRemote} from '../models/remote'; import PrInfoController from './pr-info-controller'; diff --git a/lib/models/github-login-model.js b/lib/models/github-login-model.js index c3b6a03d51..7da9bc6e72 100644 --- a/lib/models/github-login-model.js +++ b/lib/models/github-login-model.js @@ -1,6 +1,6 @@ import {Emitter} from 'event-kit'; -import {UNAUTHENTICATED, createStrategy} from './keytar-strategy'; +import {UNAUTHENTICATED, createStrategy} from '../shared/keytar-strategy'; let instance = null; diff --git a/lib/shared/README.md b/lib/shared/README.md new file mode 100644 index 0000000000..bc38499e7b --- /dev/null +++ b/lib/shared/README.md @@ -0,0 +1,3 @@ +# Shared code + +Modules in this folder are imported by both `bin/` and elsewhere in `lib/`. As a consequence, they can't use features provided by Babel transpilation or import files outside of those that do. diff --git a/lib/models/keytar-strategy.js b/lib/shared/keytar-strategy.js similarity index 93% rename from lib/models/keytar-strategy.js rename to lib/shared/keytar-strategy.js index 3eb8c0d3ad..9aad8e0ac5 100644 --- a/lib/models/keytar-strategy.js +++ b/lib/shared/keytar-strategy.js @@ -1,6 +1,5 @@ -import {execFile} from 'child_process'; -import path from 'path'; -import fs from 'fs'; +const {execFile} = require('child_process'); +const fs = require('fs'); if (typeof atom === 'undefined') { global.atom = { @@ -9,9 +8,9 @@ if (typeof atom === 'undefined') { }; } -export const UNAUTHENTICATED = Symbol('UNAUTHENTICATED'); +const UNAUTHENTICATED = Symbol('UNAUTHENTICATED'); -export class KeytarStrategy { +class KeytarStrategy { static get keytar() { return require('keytar'); } @@ -49,7 +48,7 @@ export class KeytarStrategy { } } -export class SecurityBinaryStrategy { +class SecurityBinaryStrategy { static isValid() { return process.platform === 'darwin'; } @@ -81,7 +80,7 @@ export class SecurityBinaryStrategy { } } -export class InMemoryStrategy { +class InMemoryStrategy { static isValid() { return true; } @@ -117,7 +116,7 @@ export class InMemoryStrategy { } } -export class FileStrategy { +class FileStrategy { static isValid() { if (!atom.inSpecMode() && !atom.inDevMode()) { return false; @@ -216,7 +215,7 @@ export class FileStrategy { const strategies = [FileStrategy, KeytarStrategy, SecurityBinaryStrategy, InMemoryStrategy]; let ValidStrategy = null; -export async function createStrategy() { +async function createStrategy() { if (ValidStrategy) { return new ValidStrategy(); } @@ -234,3 +233,12 @@ export async function createStrategy() { } return new ValidStrategy(); } + +module.exports = { + UNAUTHENTICATED, + KeytarStrategy, + SecurityBinaryStrategy, + InMemoryStrategy, + FileStrategy, + createStrategy, +}; diff --git a/test/git-prompt-server.test.js b/test/git-prompt-server.test.js index 9349b22a11..91eb5d16b1 100644 --- a/test/git-prompt-server.test.js +++ b/test/git-prompt-server.test.js @@ -4,7 +4,6 @@ import path from 'path'; import GitPromptServer from '../lib/git-prompt-server'; import GitTempDir from '../lib/git-temp-dir'; import {fileExists, writeFile, readFile, getAtomHelperPath} from '../lib/helpers'; -import {transpile} from './helpers'; describe('GitPromptServer', function() { const electronEnv = { @@ -12,7 +11,7 @@ describe('GitPromptServer', function() { ELECTRON_NO_ATTACH_CONSOLE: '1', ATOM_GITHUB_KEYTAR_FILE: null, ATOM_GITHUB_DUGITE_PATH: require.resolve('dugite'), - ATOM_GITHUB_KEYTAR_STRATEGY_PATH: null, // Computed lazily in beforeEach() + ATOM_GITHUB_KEYTAR_STRATEGY_PATH: require.resolve('../lib/shared/keytar-strategy'), ATOM_GITHUB_ORIGINAL_PATH: process.env.PATH, ATOM_GITHUB_WORKDIR_PATH: path.join(__dirname, '..'), ATOM_GITHUB_SPEC_MODE: 'true', @@ -27,11 +26,6 @@ describe('GitPromptServer', function() { await tempDir.ensure(); electronEnv.ATOM_GITHUB_KEYTAR_FILE = tempDir.getScriptPath('fake-keytar'); - - if (!electronEnv.ATOM_GITHUB_KEYTAR_STRATEGY_PATH) { - const [keytarStrategyPath] = await transpile(require.resolve('../lib/models/keytar-strategy')); - electronEnv.ATOM_GITHUB_KEYTAR_STRATEGY_PATH = keytarStrategyPath; - } }); describe('credential helper', function() { diff --git a/test/models/github-login-model.test.js b/test/models/github-login-model.test.js index abf934eac1..0a3e03da85 100644 --- a/test/models/github-login-model.test.js +++ b/test/models/github-login-model.test.js @@ -1,5 +1,5 @@ import GithubLoginModel from '../../lib/models/github-login-model'; -import {KeytarStrategy, SecurityBinaryStrategy, InMemoryStrategy, UNAUTHENTICATED} from '../../lib/models/keytar-strategy'; +import {KeytarStrategy, SecurityBinaryStrategy, InMemoryStrategy, UNAUTHENTICATED} from '../../lib/shared/keytar-strategy'; describe('GithubLoginModel', function() { [null, KeytarStrategy, SecurityBinaryStrategy, InMemoryStrategy].forEach(function(Strategy) { From 0928257a82076c09afa60cdf1111e24f47766887 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 2 Mar 2018 15:14:24 -0500 Subject: [PATCH 0434/5882] Include a "remember" checkbox in the credential dialog --- lib/views/credential-dialog.js | 23 +++++++++++++++++++++++ test/views/credential-dialog.test.js | 25 +++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/lib/views/credential-dialog.js b/lib/views/credential-dialog.js index 2102d9018b..8e08fe21a8 100644 --- a/lib/views/credential-dialog.js +++ b/lib/views/credential-dialog.js @@ -9,12 +9,14 @@ export default class CredentialDialog extends React.Component { commandRegistry: PropTypes.object.isRequired, prompt: PropTypes.string.isRequired, includeUsername: PropTypes.bool, + includeRemember: PropTypes.bool, onSubmit: PropTypes.func, onCancel: PropTypes.func, } static defaultProps = { includeUsername: false, + includeRemember: false, onSubmit: () => {}, onCancel: () => {}, } @@ -25,6 +27,7 @@ export default class CredentialDialog extends React.Component { this.state = { username: '', password: '', + remember: false, }; } @@ -67,6 +70,17 @@ export default class CredentialDialog extends React.Component {
        + {this.props.includeRemember ? ( + + ) : null}
        @@ -82,6 +96,10 @@ export default class CredentialDialog extends React.Component { payload.username = this.state.username; } + if (this.props.includeRemember) { + payload.remember = this.state.remember; + } + this.props.onSubmit(payload); } @@ -100,6 +118,11 @@ export default class CredentialDialog extends React.Component { this.setState({password: e.target.value}); } + @autobind + onRememberChange(e) { + this.setState({remember: e.target.checked}); + } + @autobind focusFirstInput() { (this.usernameInput || this.passwordInput).focus(); diff --git a/test/views/credential-dialog.test.js b/test/views/credential-dialog.test.js index 6631bbc214..a98e71613b 100644 --- a/test/views/credential-dialog.test.js +++ b/test/views/credential-dialog.test.js @@ -18,6 +18,7 @@ describe('CredentialDialog', function() { commandRegistry={atomEnv.commands} prompt="speak friend and enter" includeUsername={true} + includeRemember={false} onSubmit={didSubmit} onCancel={didCancel} /> @@ -59,5 +60,29 @@ describe('CredentialDialog', function() { password: 'twowordsuppercase', }); }); + + it('includes a "remember me" checkbox', function() { + wrapper = mount(React.cloneElement(app, {includeRemember: true})); + + const rememberBox = wrapper.find('.github-CredentialDialog-remember'); + assert.isTrue(rememberBox.exists()); + rememberBox.simulate('change', {target: {checked: true}}); + + setTextIn('.github-CredentialDialog-Username', 'someone'); + setTextIn('.github-CredentialDialog-Password', 'letmein'); + + wrapper.find('.btn-primary').simulate('click'); + + assert.deepEqual(didSubmit.firstCall.args[0], { + username: 'someone', + password: 'letmein', + remember: true, + }); + }); + + it('omits the "remember me" checkbox', function() { + wrapper = mount(app); + assert.isFalse(wrapper.find('.github-CredentialDialog-remember').exists()); + }); }); }); From bca3192206f2c70bb1f321a458be7bda5e2d6d02 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 2 Mar 2018 16:11:33 -0500 Subject: [PATCH 0435/5882] Pass keytar-strategy module path to git subprocess --- lib/git-shell-out-strategy.js | 3 ++- lib/helpers.js | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/lib/git-shell-out-strategy.js b/lib/git-shell-out-strategy.js index d201cb4fec..02e49d1af3 100644 --- a/lib/git-shell-out-strategy.js +++ b/lib/git-shell-out-strategy.js @@ -12,7 +12,7 @@ import GitPromptServer from './git-prompt-server'; import GitTempDir from './git-temp-dir'; import AsyncQueue from './async-queue'; import { - getDugitePath, getAtomHelperPath, + getDugitePath, getSharedModulePath, getAtomHelperPath, readFile, fileExists, fsStat, writeFile, isFileExecutable, isFileSymlink, isBinary, getRealPath, normalizeGitHelperPath, toNativePathSep, toGitPathSep, } from './helpers'; @@ -151,6 +151,7 @@ export default class GitShellOutStrategy { env.ATOM_GITHUB_WORKDIR_PATH = this.workingDir; env.ATOM_GITHUB_DUGITE_PATH = getDugitePath(); + env.ATOM_GITHUB_KEYTAR_STRATEGY_PATH = getSharedModulePath('keytar-strategy'); // "ssh" won't respect SSH_ASKPASS unless: // (a) it's running without a tty diff --git a/lib/helpers.js b/lib/helpers.js index 5bf3e91b59..f2cb84b678 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -51,6 +51,23 @@ export function getDugitePath() { return DUGITE_PATH; } +const SHARED_MODULE_PATHS = new Map(); +export function getSharedModulePath(relPath) { + let modulePath = SHARED_MODULE_PATHS.get(relPath); + if (!modulePath) { + modulePath = require.resolve(path.join(__dirname, 'shared', relPath)); + if (!path.isAbsolute(modulePath)) { + // Assume we're snapshotted + const {resourcePath} = atom.getLoadSettings(); + modulePath = path.join(resourcePath, modulePath); + } + + SHARED_MODULE_PATHS.set(relPath, modulePath); + } + + return modulePath; +} + export function isBinary(data) { for (let i = 0; i < 50; i++) { const code = data.charCodeAt(i); From 8296b7667606360fee36675c912f515182099efd Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 2 Mar 2018 16:11:54 -0500 Subject: [PATCH 0436/5882] Another log message --- bin/git-credential-atom.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bin/git-credential-atom.js b/bin/git-credential-atom.js index dc806a5775..01b5ef9702 100755 --- a/bin/git-credential-atom.js +++ b/bin/git-credential-atom.js @@ -180,11 +180,13 @@ async function fromKeytar(query) { log(`reading username from service "${metaService}" and account "username"`); const u = await strategy.getPassword(metaService, 'username'); if (u !== UNAUTHENTICATED) { + log('username found in keychain'); query.username = u; } } if (query.username) { + // Read git entry from OS keychain const gitService = `atom-github-git @ ${query.protocol}://${query.host}`; log(`reading service "${gitService}" and account "${query.username}"`); const gitPassword = await strategy.getPassword(gitService, query.username); From 48f69d1c73f8b116fbc3ee2ca2b27ed3418f6fe6 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 2 Mar 2018 16:12:14 -0500 Subject: [PATCH 0437/5882] Only try to read atom-github if atom-github-git is not found --- bin/git-credential-atom.js | 53 ++++++++++++++++++++------------------ 1 file changed, 28 insertions(+), 25 deletions(-) diff --git a/bin/git-credential-atom.js b/bin/git-credential-atom.js index 01b5ef9702..eb8fb0479e 100755 --- a/bin/git-credential-atom.js +++ b/bin/git-credential-atom.js @@ -196,33 +196,36 @@ async function fromKeytar(query) { } } - const githubHost = `${query.protocol}://api.${query.host}`; - log(`reading service "atom-github" and account "${githubHost}"`); - const githubPassword = await strategy.getPassword('atom-github', githubHost); - if (githubPassword !== UNAUTHENTICATED) { - try { - if (!query.username) { - const apiUrl = githubHost === 'https://api.github.com' ? `${githubHost}/graphql` : `${githubHost}/api/v3/graphql`; - const response = await fetch(apiUrl, { - method: 'POST', - headers: { - 'content-type': 'application/json', - 'Authorization': `bearer ${githubPassword}`, - 'Accept': 'application/vnd.github.graphql-profiling+json', - }, - body: JSON.stringify({ - query: 'query { viewer { login } }', - }), - }); - - query.username = response.json().data.viewer.login; + if (password === UNAUTHENTICATED) { + // Read GitHub tab token + const githubHost = `${query.protocol}://api.${query.host}`; + log(`reading service "atom-github" and account "${githubHost}"`); + const githubPassword = await strategy.getPassword('atom-github', githubHost); + if (githubPassword !== UNAUTHENTICATED) { + try { + if (!query.username) { + const apiUrl = githubHost === 'https://api.github.com' ? `${githubHost}/graphql` : `${githubHost}/api/v3/graphql`; + const response = await fetch(apiUrl, { + method: 'POST', + headers: { + 'content-type': 'application/json', + 'Authorization': `bearer ${githubPassword}`, + 'Accept': 'application/vnd.github.graphql-profiling+json', + }, + body: JSON.stringify({ + query: 'query { viewer { login } }', + }), + }); + + query.username = response.json().data.viewer.login; + } + } catch (e) { + log(`unable to acquire username from token: ${e.stack}`); + throw new Error('token found in keychain, but no username'); } - } catch (e) { - log(`unable to acquire username from token: ${e.stack}`); - throw new Error('token found in keychain, but no username'); - } - password = githubPassword; + password = githubPassword; + } } if (password !== UNAUTHENTICATED) { From 941c64549b16a0e0a7f9da94fb6757fb1170a85d Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 2 Mar 2018 16:12:37 -0500 Subject: [PATCH 0438/5882] Include remember checkbox on query payload --- bin/git-credential-atom.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/git-credential-atom.js b/bin/git-credential-atom.js index eb8fb0479e..96a144ce88 100755 --- a/bin/git-credential-atom.js +++ b/bin/git-credential-atom.js @@ -250,7 +250,7 @@ function dialog(query) { const prompt = 'Please enter your credentials for ' + url.format(query); const includeUsername = !query.username; - const payload = {prompt, includeUsername, pid: process.pid}; + const payload = {prompt, includeUsername, includeRemember: true, pid: process.pid}; return new Promise((resolve, reject) => { log('requesting dialog through Atom socket'); From 54ccaa85b1727ecb0ddab5546aa19f80b41b63e8 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 2 Mar 2018 16:41:48 -0500 Subject: [PATCH 0439/5882] No fetch in ELECTRON_RUN_AS_NODE, use https module instead --- bin/git-credential-atom.js | 55 +++++++++++++++++++++++++++++--------- 1 file changed, 42 insertions(+), 13 deletions(-) diff --git a/bin/git-credential-atom.js b/bin/git-credential-atom.js index 96a144ce88..82b0d3d4a9 100755 --- a/bin/git-credential-atom.js +++ b/bin/git-credential-atom.js @@ -3,6 +3,7 @@ const readline = require('readline'); const url = require('url'); const fs = require('fs'); const path = require('path'); +const https = require('https'); const {execFile} = require('child_process'); const {GitProcess} = require(process.env.ATOM_GITHUB_DUGITE_PATH); const {createStrategy, UNAUTHENTICATED} = require(process.env.ATOM_GITHUB_KEYTAR_STRATEGY_PATH); @@ -198,26 +199,54 @@ async function fromKeytar(query) { if (password === UNAUTHENTICATED) { // Read GitHub tab token - const githubHost = `${query.protocol}://api.${query.host}`; + const githubHost = query.host === 'github.com' + ? `${query.protocol}://api.${query.host}` + : `${query.protocol}://${query.host}`; log(`reading service "atom-github" and account "${githubHost}"`); const githubPassword = await strategy.getPassword('atom-github', githubHost); if (githubPassword !== UNAUTHENTICATED) { try { if (!query.username) { - const apiUrl = githubHost === 'https://api.github.com' ? `${githubHost}/graphql` : `${githubHost}/api/v3/graphql`; - const response = await fetch(apiUrl, { - method: 'POST', - headers: { - 'content-type': 'application/json', - 'Authorization': `bearer ${githubPassword}`, - 'Accept': 'application/vnd.github.graphql-profiling+json', - }, - body: JSON.stringify({ - query: 'query { viewer { login } }', - }), + const apiHost = query.host === 'github.com' ? 'api.github.com' : query.host; + const apiPath = query.host === 'github.com' ? '/graphql' : '/api/graphql'; + + const response = await new Promise((resolve, reject) => { + const postBody = JSON.stringify({query: 'query { viewer { login } }'}); + const req = https.request({ + protocol: query.protocol + ':', + hostname: apiHost, + method: 'POST', + path: apiPath, + headers: { + 'content-type': 'application/json', + 'content-length': Buffer.byteLength(postBody, 'utf8'), + 'Authorization': `bearer ${githubPassword}`, + 'Accept': 'application/vnd.github.graphql-profiling+json', + 'User-Agent': 'Atom git credential helper/1.0.0', + }, + }, res => { + const parts = []; + + res.setEncoding('utf8'); + res.on('data', chunk => parts.push(chunk)); + res.on('end', () => { + if (res.statusCode !== 200) { + reject(new Error(parts.join(''))); + } else { + resolve(parts.join('')); + } + }); + }); + + req.on('error', reject); + req.end(postBody); }); + log(`GraphQL response:\n${response}`); - query.username = response.json().data.viewer.login; + query.username = JSON.parse(response).data.viewer.login; + if (!query.username) { + throw new Error('Username missing from GraphQL response'); + } } } catch (e) { log(`unable to acquire username from token: ${e.stack}`); From bccc2d86ed08e86e155afc9196bf61f8e0d49aa6 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 2 Mar 2018 16:42:10 -0500 Subject: [PATCH 0440/5882] Always store credentials we needed to get from GraphQL or the GitHub tab --- bin/git-credential-atom.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/bin/git-credential-atom.js b/bin/git-credential-atom.js index 82b0d3d4a9..f2e3f83a2c 100755 --- a/bin/git-credential-atom.js +++ b/bin/git-credential-atom.js @@ -254,6 +254,13 @@ async function fromKeytar(query) { } password = githubPassword; + + // Always remember credentials we had to go to GraphQL to get + await new Promise((resolve, reject) => { + fs.writeFile(rememberFile, err => { + if (err) { reject(err); } else { resolve(); } + }); + }); } } From 49b53798023ba4849b939c787728d6a85f7b8b7c Mon Sep 17 00:00:00 2001 From: simurai Date: Mon, 5 Mar 2018 11:59:37 +0900 Subject: [PATCH 0441/5882] Style remember checkobx --- lib/views/credential-dialog.js | 4 ++-- styles/dialog.less | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/views/credential-dialog.js b/lib/views/credential-dialog.js index 8e08fe21a8..5de0d00132 100644 --- a/lib/views/credential-dialog.js +++ b/lib/views/credential-dialog.js @@ -71,9 +71,9 @@ export default class CredentialDialog extends React.Component {
        {this.props.includeRemember ? ( -
    +
      +
    • + as-cii + Antonio Scandurra +
    • +
    • + damieng + Damien Guard +
    • +
    • + daviwil + David Wilson +
    • +
    • + iolsen + Ian Olsen +
    • +
    • + maxbrunsfeld + Max Brunsfeld +
    • +
    • + jasonrudolph + Jason Rudolph +
    • +
    • + lee-dohm + Lee Dohm +
    • +
    • + nathansobo + Nathan Sobo +
    • +
    • + rsese + Robert Sese +
    • +
    {showAbortMergeButton && diff --git a/styles/commit-view.less b/styles/commit-view.less index 43b419516f..b23629efe4 100644 --- a/styles/commit-view.less +++ b/styles/commit-view.less @@ -99,6 +99,66 @@ border: 1px solid mix(@syntax-text-color, @syntax-background-color, 16%); background-color: mix(@syntax-text-color, @syntax-background-color, 8%); } + + &:focus + .github-CommitView-coAuthorEditor-selectList { + display: block; + } + + &-selectList { + display: none; // only show when coAuthorEditor has focus + position: absolute; + z-index: 1; + bottom: 100px; // TODO: Needs to be relative to coAuthorEditor + left: @component-padding; + right: @component-padding; + max-height: 112px; + overflow-x: hidden; + overflow-y: auto; + padding: 0; + margin: 0; + list-style: none; + border-radius: @component-border-radius; + border: 1px solid @overlay-border-color; + background-color: @overlay-background-color; + box-shadow: 0 2px 6px hsla(0,0%,0%,.15); + + &Item { + padding: @component-padding / 2; + padding-right: 10px; // space for scrollbar + border-top: 1px solid @overlay-border-color; + white-space: nowrap; + &:first-child { + border-top: none; + border-top-left-radius: inherit; + border-top-right-radius: inherit; + color: white; + background-color: @background-color-info; + } + &:last-child { + border-bottom-left-radius: inherit; + border-top-right-radius: inherit; + } + + &.is-selected { + color: white; + background-color: @background-color-info; + + .github-CommitView-coAuthorEditor-username, + .github-CommitView-coAuthorEditor-name { + color: inherit; + } + } + } + } + + &-username { + font-weight: 600; + margin-right: .75em; + color: @text-color-highlight; + } + &-name { + color: @text-color-subtle; + } } &-expandButton { From f9a226de9eb702021ff6868f806182e0dfa704c5 Mon Sep 17 00:00:00 2001 From: Markus Olsson Date: Wed, 14 Mar 2018 19:29:59 +0100 Subject: [PATCH 0490/5882] Add react-select dependency --- lib/github-package.js | 1 + package-lock.json | 98 +++++++++++++++++++++++++++++++++++++++++-- package.json | 4 +- 3 files changed, 97 insertions(+), 6 deletions(-) diff --git a/lib/github-package.js b/lib/github-package.js index 7e413e31d3..77952d0e19 100644 --- a/lib/github-package.js +++ b/lib/github-package.js @@ -30,6 +30,7 @@ const defaultState = { export default class GithubPackage { constructor(workspace, project, commandRegistry, notificationManager, tooltips, styles, grammars, confirm, config, deserializers, configDirPath, getLoadSettings) { + this.workspace = workspace; this.project = project; this.commandRegistry = commandRegistry; diff --git a/package-lock.json b/package-lock.json index da4ac9c9b4..c3c62441dd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4288,6 +4288,45 @@ "resolved": "https://registry.npmjs.org/react-input-autosize/-/react-input-autosize-2.2.1.tgz", "integrity": "sha512-3+K4CD13iE4lQQ2WlF8PuV5htfmTRLH6MDnfndHM6LuBRszuXnuyIfE7nhSKt8AzRBZ50bu0sAhkNMeS5pxQQA==" }, + "react-input-autosize": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/react-input-autosize/-/react-input-autosize-2.2.1.tgz", + "integrity": "sha512-3+K4CD13iE4lQQ2WlF8PuV5htfmTRLH6MDnfndHM6LuBRszuXnuyIfE7nhSKt8AzRBZ50bu0sAhkNMeS5pxQQA==", + "requires": { + "prop-types": "15.6.1" + }, + "dependencies": { + "core-js": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz", + "integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=" + }, + "fbjs": { + "version": "0.8.16", + "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.16.tgz", + "integrity": "sha1-XmdDL1UNxBtXK/VYR7ispk5TN9s=", + "requires": { + "core-js": "1.2.7", + "isomorphic-fetch": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz", + "loose-envify": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz", + "object-assign": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "promise": "7.3.1", + "setimmediate": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "ua-parser-js": "0.7.14" + } + }, + "prop-types": { + "version": "15.6.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.1.tgz", + "integrity": "sha512-4ec7bY1Y66LymSUOH/zARVYObB23AT2h8cf6e/O6ZALB/N0sqZFEx7rq6EYPX2MkOdKORuooI/H5k9TlR4q7kQ==", + "requires": { + "fbjs": "0.8.16", + "loose-envify": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz", + "object-assign": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz" + } + } + } + }, "react-relay": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/react-relay/-/react-relay-1.4.1.tgz", @@ -4296,7 +4335,48 @@ "react-select": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/react-select/-/react-select-1.2.1.tgz", - "integrity": "sha512-vaCgT2bEl+uTyE/uKOEgzE5Dc/wLtzhnBvoHCeuLoJWc4WuadN6WQDhoL42DW+TziniZK2Gaqe/wUXydI3NSaQ==" + "integrity": "sha512-vaCgT2bEl+uTyE/uKOEgzE5Dc/wLtzhnBvoHCeuLoJWc4WuadN6WQDhoL42DW+TziniZK2Gaqe/wUXydI3NSaQ==", + "requires": { + "classnames": "https://registry.npmjs.org/classnames/-/classnames-2.2.5.tgz", + "prop-types": "15.6.1", + "react-input-autosize": "2.2.1" + }, + "dependencies": { + "core-js": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz", + "integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=" + }, + "fbjs": { + "version": "0.8.16", + "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.16.tgz", + "integrity": "sha1-XmdDL1UNxBtXK/VYR7ispk5TN9s=", + "requires": { + "core-js": "1.2.7", + "isomorphic-fetch": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz", + "loose-envify": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz", + "object-assign": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "promise": "7.3.1", + "setimmediate": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "ua-parser-js": "0.7.14" + } + }, + "prop-types": { + "version": "15.6.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.1.tgz", + "integrity": "sha512-4ec7bY1Y66LymSUOH/zARVYObB23AT2h8cf6e/O6ZALB/N0sqZFEx7rq6EYPX2MkOdKORuooI/H5k9TlR4q7kQ==", + "requires": { + "fbjs": "0.8.16", + "loose-envify": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz", + "object-assign": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz" + } + } + } + }, + "react-static-container": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/react-static-container/-/react-static-container-1.0.1.tgz", + "integrity": "sha1-aUwN1oqJa4eVGa+1SDmcwZicmrA=" }, "react-test-renderer": { "version": "15.6.2", @@ -4820,9 +4900,19 @@ "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" }, "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" + "version": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha1-q5Pyeo3BPSjKyBXEYhQ6bZASrp4=", + "dev": true, + "requires": { + "is-fullwidth-code-point": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "strip-ansi": "4.0.0" + } }, "strip-ansi": { "version": "4.0.0", diff --git a/package.json b/package.json index 6a09540c6f..b884c2ae1d 100644 --- a/package.json +++ b/package.json @@ -61,10 +61,10 @@ "prop-types": "^15.5.8", "react": "^15.6.1", "react-dom": "^15.6.1", - "react-relay": "1.4.1", + "react-relay": "^1.2.0-rc.1", "react-select": "^1.2.1", "relay-compiler": "1.4.1", - "relay-runtime": "1.4.1", + "relay-runtime": "^1.2.0-rc.1", "temp": "^0.8.3", "tinycolor2": "^1.4.1", "tree-kill": "^1.1.0", From 5d51ca7cb2446e138c27a80790120ae7153eb1a6 Mon Sep 17 00:00:00 2001 From: Markus Olsson Date: Thu, 15 Mar 2018 01:17:29 +0100 Subject: [PATCH 0491/5882] Plumbing and wiring --- lib/views/commit-view.js | 100 ++++++++++++++++--------------- styles/commit-view.less | 124 ++++++++++++++++++++++++--------------- 2 files changed, 130 insertions(+), 94 deletions(-) diff --git a/lib/views/commit-view.js b/lib/views/commit-view.js index a602022984..ef6aec4130 100644 --- a/lib/views/commit-view.js +++ b/lib/views/commit-view.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import {CompositeDisposable} from 'event-kit'; import {autobind} from 'core-decorators'; import cx from 'classnames'; +import Select from 'react-select'; import Tooltip from './tooltip'; import AtomTextEditor from './atom-text-editor'; @@ -10,6 +11,13 @@ import {shortenSha} from '../helpers'; const LINE_ENDING_REGEX = /\r?\n/; +const temporaryAuthors = [ + { name: 'Katrina Uychaco', login: 'kuychaco', email: 'kuychaco@users.noreply.github.com' }, + { name: 'Neha Batra', login: 'nerdneha', email: 'neha@github.com' }, + { name: 'Markus Olsson', login: 'niik', email: 'j.markus.olsson@github.com' }, + { name: 'Josh Abernathy', login: 'joshaber', email: 'joshaber@github.com' }, +] + export default class CommitView extends React.Component { static focus = { EDITOR: Symbol('commit-editor'), @@ -45,7 +53,11 @@ export default class CommitView extends React.Component { constructor(props, context) { super(props, context); - this.state = {showWorking: false}; + this.state = { + showWorking: false, + selectedCoAuthors: [] + }; + this.timeoutHandle = null; this.subscriptions = new CompositeDisposable(); @@ -133,52 +145,20 @@ export default class CommitView extends React.Component { />
    -
    - Co-Authors -
      -
    • @kuychaco
    • -
    • @smashwilson
    • -
    • @binarymuse
    • -
    +
    console.log(e)} className="native-key-bindings"> + this.refCoAuthorSelect = c} className="github-CommitView-coAuthorEditor input-textarea native-key-bindings" placeholder="Co-Authors" arrowRenderer={null} From 5b8d2188c816e88a39b5eb7c234f5b99ab63025e Mon Sep 17 00:00:00 2001 From: Markus Olsson Date: Thu, 15 Mar 2018 02:33:31 +0100 Subject: [PATCH 0494/5882] Look at all this sadness --- keymaps/git.cson | 7 +++++++ lib/views/commit-view.js | 27 +++++++++++++++++++++++++-- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/keymaps/git.cson b/keymaps/git.cson index 6fd6175699..85cf305e2e 100644 --- a/keymaps/git.cson +++ b/keymaps/git.cson @@ -54,3 +54,10 @@ '.github-Dialog input': 'enter': 'core:confirm' + +'body .github-CommitView-coAuthorEditor': + 'enter': 'github:co-author:enter' + 'down': 'github:co-author:down' + 'up': 'github:co-author:up' + 'tab': 'github:co-author:tab' + 'backspace': 'github:co-author:backspace' diff --git a/lib/views/commit-view.js b/lib/views/commit-view.js index b0734952e7..432fcd2991 100644 --- a/lib/views/commit-view.js +++ b/lib/views/commit-view.js @@ -69,6 +69,25 @@ export default class CommitView extends React.Component { this.refCoAuthorSelect = null; } + proxyKeyCode(keyCode) { + return () => { + console.log('handler', keyCode) + if (!this.refCoAuthorSelect) { + return + } + + const fakeEvent = { + keyCode, + preventDefault: () => {}, + stopPropagation: () => {}, + } + + if (this.refCoAuthorSelect.handleKeyDown) { + this.refCoAuthorSelect.handleKeyDown(fakeEvent) + } + } + } + componentWillMount() { this.scheduleShowWorking(this.props); @@ -76,6 +95,12 @@ export default class CommitView extends React.Component { this.props.commandRegistry.add('atom-workspace', { 'github:commit': this.commit, 'github:toggle-expanded-commit-message-editor': this.toggleExpandedCommitMessageEditor, + + 'github:co-author:down': this.proxyKeyCode(40), + 'github:co-author:up': this.proxyKeyCode(38), + 'github:co-author:enter': this.proxyKeyCode(13), + 'github:co-author:tab': this.proxyKeyCode(9), + 'github:co-author:backspace': this.proxyKeyCode(8), }), this.props.config.onDidChange('github.automaticCommitMessageWrapping', () => this.forceUpdate()), ); @@ -146,7 +171,6 @@ export default class CommitView extends React.Component { />
    -
    console.log(e)} className="native-key-bindings"> this.refCoAuthorSelect = c} + ref={c => { this.refCoAuthorSelect = c; }} className="github-CommitView-coAuthorEditor input-textarea native-key-bindings" placeholder="Co-Authors" arrowRenderer={null} @@ -228,9 +222,11 @@ export default class CommitView extends React.Component { } renderCoAuthorToggleIcon() { + /* eslint-disable max-len */ + const svgPath = 'M9.875 2.125H12v1.75H9.875V6h-1.75V3.875H6v-1.75h2.125V0h1.75v2.125zM6 6.5a.5.5 0 0 1-.5.5h-5a.5.5 0 0 1-.5-.5V6c0-1.316 2-2 2-2s.114-.204 0-.5c-.42-.31-.472-.795-.5-2C1.587.293 2.434 0 3 0s1.413.293 1.5 1.5c-.028 1.205-.08 1.69-.5 2-.114.295 0 .5 0 .5s2 .684 2 2v.5z'; return ( - + ); } diff --git a/lib/views/git-tab-view.js b/lib/views/git-tab-view.js index e14c20de87..864160e0c8 100644 --- a/lib/views/git-tab-view.js +++ b/lib/views/git-tab-view.js @@ -34,6 +34,7 @@ export default class GitTabView extends React.Component { mergeConflicts: PropTypes.arrayOf(PropTypes.object), workingDirectoryPath: PropTypes.string, mergeMessage: PropTypes.string, + mentionableUsers: PropTypes.arrayOf(PropTypes.object).isRequired, workspace: PropTypes.object.isRequired, commandRegistry: PropTypes.object.isRequired, From 6897161d6fad043b46bc2612c6a4f47136e1835f Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Thu, 15 Mar 2018 04:57:44 +0100 Subject: [PATCH 0509/5882] :fire: comment --- lib/controllers/git-tab-controller.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/controllers/git-tab-controller.js b/lib/controllers/git-tab-controller.js index 998c5ca787..a565fbe05a 100644 --- a/lib/controllers/git-tab-controller.js +++ b/lib/controllers/git-tab-controller.js @@ -106,7 +106,6 @@ export default class GitTabController extends React.Component { this.refView = null; this.userStore = new UserStore({repository: this.props.repository}); - /* this.userStore = UserStore.empty() */ } serialize() { From 068c65367703cf3cf8e3c84d5add14fd012afaf4 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Thu, 15 Mar 2018 05:18:34 +0100 Subject: [PATCH 0510/5882] :fire: whitespace --- lib/controllers/git-tab-controller.js | 2 -- lib/git-shell-out-strategy.js | 3 --- lib/github-package.js | 1 - lib/models/repository-states/present.js | 4 ---- 4 files changed, 10 deletions(-) diff --git a/lib/controllers/git-tab-controller.js b/lib/controllers/git-tab-controller.js index a565fbe05a..7dcc86113f 100644 --- a/lib/controllers/git-tab-controller.js +++ b/lib/controllers/git-tab-controller.js @@ -176,12 +176,10 @@ export default class GitTabController extends React.Component { componentWillReceiveProps(newProps) { this.refreshResolutionProgress(false, false); - if (this.props.repository !== newProps.repository) { this.userStore = new UserStore({repository: newProps.repository}); this.userStore.populate(); } - } componentWillUnmount() { diff --git a/lib/git-shell-out-strategy.js b/lib/git-shell-out-strategy.js index d0f71358c7..388bc55e60 100644 --- a/lib/git-shell-out-strategy.js +++ b/lib/git-shell-out-strategy.js @@ -630,15 +630,12 @@ export default class GitShellOutStrategy { mergeTrailers(commitMessage, trailers, unfold) { const args = ['interpret-trailers']; - if (unfold) { args.push('--unfold'); } - for (const trailer of trailers) { args.push('--trailer', `${trailer.token}=${trailer.value}`); } - return this.exec(args, {stdin: commitMessage}); } diff --git a/lib/github-package.js b/lib/github-package.js index 77952d0e19..7e413e31d3 100644 --- a/lib/github-package.js +++ b/lib/github-package.js @@ -30,7 +30,6 @@ const defaultState = { export default class GithubPackage { constructor(workspace, project, commandRegistry, notificationManager, tooltips, styles, grammars, confirm, config, deserializers, configDirPath, getLoadSettings) { - this.workspace = workspace; this.project = project; this.commandRegistry = commandRegistry; diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js index db49c3a188..7fda147c97 100644 --- a/lib/models/repository-states/present.js +++ b/lib/models/repository-states/present.js @@ -59,10 +59,6 @@ export default class Present extends State { this.operationStates = new OperationStates({didUpdate: this.didUpdate.bind(this)}); - // this.userStore = new UserStore() - // const authors = this.getRepoAuthors() - // userStore.getUsers() - this.amending = false; this.amendingCommitMessage = ''; this.regularCommitMessage = ''; From 96c56b99f8e768e7a181d0e8f5f52dae51d86a7f Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Thu, 15 Mar 2018 06:12:32 +0100 Subject: [PATCH 0511/5882] Sort users by name rather than email --- lib/models/user-store.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/models/user-store.js b/lib/models/user-store.js index 419e672a51..6eeeb5993b 100644 --- a/lib/models/user-store.js +++ b/lib/models/user-store.js @@ -49,7 +49,9 @@ export default class UserStore { getUsers() { // don't actually do this. will change when we settle on best way to actually store data - return Object.keys(this.users).sort().map(email => ({email, name: this.users[email]})); + return Object.keys(this.users) + .map(email => ({email, name: this.users[email]})) + .sort((a, b) => a.name > b.name); } } From 8d258fa651379620cb52cbccf4f1fa7940062861 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Thu, 15 Mar 2018 06:12:52 +0100 Subject: [PATCH 0512/5882] Add email to user select list --- lib/views/commit-view.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/views/commit-view.js b/lib/views/commit-view.js index 514e66ae73..7f4ec18830 100644 --- a/lib/views/commit-view.js +++ b/lib/views/commit-view.js @@ -392,6 +392,7 @@ export default class CommitView extends React.Component {
    {author.login} {author.name} + {author.email}
    ); } From 20be8fc44132af25ad8281c83600508a740e2945 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Thu, 15 Mar 2018 06:13:32 +0100 Subject: [PATCH 0513/5882] Make search case insensitive. Include email in search values --- 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 7f4ec18830..f9bdd316d2 100644 --- a/lib/views/commit-view.js +++ b/lib/views/commit-view.js @@ -384,7 +384,7 @@ export default class CommitView extends React.Component { matchAuthors(authors, filterText, currentValues) { return authors .filter(x => !currentValues || currentValues.indexOf(x) === -1) - .filter(x => `${x.name}${x.login}`.indexOf(filterText) !== -1); + .filter(x => `${x.name}${x.login}${x.email}`.toLowerCase().indexOf(filterText.toLowerCase()) !== -1); } renderCoAuthorListItem(author) { From e493894d09012cdad2873f241757f960d2c55b8a Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Thu, 15 Mar 2018 06:29:23 +0100 Subject: [PATCH 0514/5882] Destructure regex result only if there is a match --- lib/git-shell-out-strategy.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/git-shell-out-strategy.js b/lib/git-shell-out-strategy.js index 388bc55e60..1e78ba6614 100644 --- a/lib/git-shell-out-strategy.js +++ b/lib/git-shell-out-strategy.js @@ -621,8 +621,10 @@ export default class GitShellOutStrategy { acc[email] = name; }); } else if (line !== '') { - const [_, name, email] = line.match(/(.*) (.*@.*)/); // eslint-disable-line no-unused-vars - acc[email] = name; + const matches = line.match(/(.*) (.*@.*)/); // eslint-disable-line no-unused-vars + if (matches) { + acc[matches[2]] = matches[1]; + } } return acc; }, {}); From 51ca98c60976e9b578be21ad2df38e4908c50a2a Mon Sep 17 00:00:00 2001 From: simurai Date: Thu, 15 Mar 2018 18:56:10 +0900 Subject: [PATCH 0515/5882] Colorize placeholder --- styles/commit-view.less | 1 + 1 file changed, 1 insertion(+) diff --git a/styles/commit-view.less b/styles/commit-view.less index 53f812ccbc..84ae380d87 100644 --- a/styles/commit-view.less +++ b/styles/commit-view.less @@ -93,6 +93,7 @@ .Select-placeholder { line-height: @component-line-height; padding: 0; + color: @text-color-subtle; } .Select-input { From 7c66ded45075b21607d552285548e82d9d9bdf92 Mon Sep 17 00:00:00 2001 From: simurai Date: Thu, 15 Mar 2018 19:11:22 +0900 Subject: [PATCH 0516/5882] Remove un-used styles --- styles/commit-view.less | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/styles/commit-view.less b/styles/commit-view.less index 84ae380d87..79b655f33e 100644 --- a/styles/commit-view.less +++ b/styles/commit-view.less @@ -155,32 +155,6 @@ vertical-align: middle; } - &-label { - display: inline-block; - margin: @component-padding / 4; - line-height: 1; - color: mix(@syntax-text-color, @syntax-background-color, 50%); - cursor: default; - } - - &-selectList { - display: none; // only show when coAuthorEditor has focus - position: absolute; - z-index: 1; - bottom: 100px; // TODO: Needs to be relative to coAuthorEditor - left: @component-padding; - right: @component-padding; - max-height: 112px; - overflow-x: hidden; - overflow-y: auto; - padding: 0; - margin: 0; - list-style: none; - border-radius: @component-border-radius; - border: 1px solid @overlay-border-color; - background-color: @overlay-background-color; - } - &-username { font-weight: 600; margin-right: .75em; From 0d93fb4cc2ef466fb6fe25a8e1b442090df8b787 Mon Sep 17 00:00:00 2001 From: simurai Date: Thu, 15 Mar 2018 19:18:51 +0900 Subject: [PATCH 0517/5882] Reorg styles --- styles/commit-view.less | 41 +++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/styles/commit-view.less b/styles/commit-view.less index 79b655f33e..613078ac9f 100644 --- a/styles/commit-view.less +++ b/styles/commit-view.less @@ -108,6 +108,27 @@ } } + // Token + .Select-value { + margin: 0; + margin-right: .5em; + vertical-align: middle; + } + + // Popup + .Select-menu-outer { + top: auto; + left: -1px; + right: -1px; + width: auto; + bottom: 100%; + border-radius: @component-border-radius; + border: 1px solid @overlay-border-color; + background-color: @overlay-background-color; + box-shadow: 0 2px 6px hsla(0,0%,0%,.15); + overflow: hidden; + } + .Select-option { display: inline-block; margin: 0; @@ -135,26 +156,6 @@ } } } - - .Select-menu-outer { - top: auto; - left: -1px; - right: -1px; - width: auto; - bottom: 100%; - border-radius: @component-border-radius; - border: 1px solid @overlay-border-color; - background-color: @overlay-background-color; - box-shadow: 0 2px 6px hsla(0,0%,0%,.15); - overflow: hidden; - } - - .Select-value { - margin: 0; - margin-right: .5em; - vertical-align: middle; - } - &-username { font-weight: 600; margin-right: .75em; From 5329c8fd4fb7f32b6f81086f6a425ba9adb1dd04 Mon Sep 17 00:00:00 2001 From: simurai Date: Thu, 15 Mar 2018 20:20:10 +0900 Subject: [PATCH 0518/5882] Style token and popup --- styles/commit-view.less | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/styles/commit-view.less b/styles/commit-view.less index 613078ac9f..200cffc8d4 100644 --- a/styles/commit-view.less +++ b/styles/commit-view.less @@ -113,6 +113,14 @@ margin: 0; margin-right: .5em; vertical-align: middle; + line-height: 1.1; + color: mix(@text-color-highlight, @text-color-info, 70%); + border-color: fade(@background-color-info, 15%); + background-color: fade(@background-color-info, 10%); + + &-icon { + border-color: inherit; + } } // Popup @@ -121,11 +129,11 @@ left: -1px; right: -1px; width: auto; - bottom: 100%; + bottom: calc(100% ~'+' 2px); border-radius: @component-border-radius; border: 1px solid @overlay-border-color; background-color: @overlay-background-color; - box-shadow: 0 2px 6px hsla(0,0%,0%,.15); + box-shadow: 0 2px 4px hsla(0,0%,0%,.15); overflow: hidden; } From a31b8c64ad692e81d85719abe1acdc547a20c125 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 16 Mar 2018 15:10:21 +0100 Subject: [PATCH 0519/5882] Have UserStore update GitTabController once users have been updated --- lib/controllers/git-tab-controller.js | 18 +++++++++++++++--- lib/models/user-store.js | 14 +++++++------- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/lib/controllers/git-tab-controller.js b/lib/controllers/git-tab-controller.js index 7dcc86113f..4c4fd26583 100644 --- a/lib/controllers/git-tab-controller.js +++ b/lib/controllers/git-tab-controller.js @@ -105,7 +105,14 @@ export default class GitTabController extends React.Component { this.refView = null; - this.userStore = new UserStore({repository: this.props.repository}); + this.state = {mentionableUsers: []}; + + this.userStore = new UserStore({ + repository: this.props.repository, + onDidUpdate: users => { + this.setState({mentionableUsers: users}); + }, + }); } serialize() { @@ -135,7 +142,7 @@ export default class GitTabController extends React.Component { mergeConflicts={this.props.mergeConflicts} workingDirectoryPath={this.props.workingDirectoryPath} mergeMessage={this.props.mergeMessage} - mentionableUsers={this.userStore.getUsers()} + mentionableUsers={this.state.mentionableUsers} resolutionProgress={this.props.resolutionProgress} workspace={this.props.workspace} @@ -177,7 +184,12 @@ export default class GitTabController extends React.Component { this.refreshResolutionProgress(false, false); if (this.props.repository !== newProps.repository) { - this.userStore = new UserStore({repository: newProps.repository}); + this.userStore = new UserStore({ + repository: newProps.repository, + onDidUpdate: users => { + this.setState({mentionableUsers: users}); + }, + }); this.userStore.populate(); } } diff --git a/lib/models/user-store.js b/lib/models/user-store.js index 6eeeb5993b..0020ca522b 100644 --- a/lib/models/user-store.js +++ b/lib/models/user-store.js @@ -1,12 +1,10 @@ // TODO: create proper User class export default class UserStore { - constructor({repository}) { + constructor({repository, onDidUpdate}) { this.repository = repository; + this.onDidUpdate = onDidUpdate || (() => {}); this.users = {}; - this.usersLoadedPromise = new Promise(res => { - this.resolveUsersLoadedPromise = res; - }); } populate() { @@ -22,8 +20,6 @@ export default class UserStore { } }); } - - return this.usersLoadedPromise; } loadUsers() { @@ -35,7 +31,6 @@ export default class UserStore { async loadUsersFromLocalRepo() { const users = await this.repository.getAuthors({max: 5000}); this.addUsers(users); - this.resolveUsersLoadedPromise(); } addUsersFromGraphQL(response) { @@ -45,6 +40,11 @@ export default class UserStore { addUsers(users) { this.users = {...this.users, ...users}; + this.didUpdate(); + } + + didUpdate() { + this.onDidUpdate(this.getUsers()); } getUsers() { From 04e5ee1a9e464d86fc160fafc40004fcf499c141 Mon Sep 17 00:00:00 2001 From: Markus Olsson Date: Fri, 16 Mar 2018 17:29:49 +0100 Subject: [PATCH 0520/5882] Wire up the co author input toggle Co-Authored-By: Katrina Uychaco --- lib/views/commit-view.js | 52 +++++++++++++++++++++++++++------------- 1 file changed, 36 insertions(+), 16 deletions(-) diff --git a/lib/views/commit-view.js b/lib/views/commit-view.js index f9bdd316d2..008e7182c8 100644 --- a/lib/views/commit-view.js +++ b/lib/views/commit-view.js @@ -57,6 +57,7 @@ export default class CommitView extends React.Component { this.state = { showWorking: false, selectedCoAuthors: [], + showCoAuthorInput: false, }; this.timeoutHandle = null; @@ -140,7 +141,8 @@ export default class CommitView extends React.Component { />
    - { this.refCoAuthorSelect = c; }} + className="github-CommitView-coAuthorEditor input-textarea native-key-bindings" + placeholder="Co-Authors" + arrowRenderer={null} + options={this.props.mentionableUsers} + labelKey="name" + valueKey="email" + filterOptions={this.matchAuthors} + optionRenderer={this.renderCoAuthorListItem} + valueRenderer={this.renderCoAuthorValue} + onChange={this.onSelectedCoAuthorsChanged} + value={this.state.selectedCoAuthors} + multi={true} + /> + ); + } + renderHardWrapIcon() { const singleLineMessage = this.editor && this.editor.getText().split(LINE_ENDING_REGEX).length === 1; const hardWrap = this.props.config.get('github.automaticCommitMessageWrapping'); @@ -298,6 +311,13 @@ export default class CommitView extends React.Component { this.props.config.set('github.automaticCommitMessageWrapping', !currentSetting); } + @autobind + toggleCoAuthorInput() { + this.setState({ + showCoAuthorInput: !this.state.showCoAuthorInput + }) + } + @autobind abortMerge() { this.props.abortMerge(); From c9f50ce94429997a2f88b28803c201008677302e Mon Sep 17 00:00:00 2001 From: Markus Olsson Date: Fri, 16 Mar 2018 17:45:32 +0100 Subject: [PATCH 0521/5882] Conditionally render author fields Co-Authored-By: Katrina Uychaco --- lib/views/commit-view.js | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/lib/views/commit-view.js b/lib/views/commit-view.js index 008e7182c8..d9c916a975 100644 --- a/lib/views/commit-view.js +++ b/lib/views/commit-view.js @@ -407,12 +407,23 @@ export default class CommitView extends React.Component { .filter(x => `${x.name}${x.login}${x.email}`.toLowerCase().indexOf(filterText.toLowerCase()) !== -1); } + renderCoAuthorListItemField(fieldName, value) { + if (!value || value.length === 0) { + return null + } + + return ( + {value} + ) + } + + @autobind renderCoAuthorListItem(author) { return (
    - {author.login} - {author.name} - {author.email} + {this.renderCoAuthorListItemField('username', author.login)} + {this.renderCoAuthorListItemField('name', author.name)} + {this.renderCoAuthorListItemField('email', author.email)}
    ); } From 0f4a5000575e12614d155f0edaa10e9f0545feba Mon Sep 17 00:00:00 2001 From: Markus Olsson Date: Fri, 16 Mar 2018 17:45:45 +0100 Subject: [PATCH 0522/5882] Style select list items based on position Co-Authored-By: Katrina Uychaco --- styles/commit-view.less | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/styles/commit-view.less b/styles/commit-view.less index 200cffc8d4..efb2bd059c 100644 --- a/styles/commit-view.less +++ b/styles/commit-view.less @@ -158,19 +158,24 @@ color: white; background-color: @background-color-info; - .github-CommitView-coAuthorEditor-username, - .github-CommitView-coAuthorEditor-name { + .github-CommitView-coAuthorEditor-selectListItem > span { color: inherit; } } } - &-username { - font-weight: 600; - margin-right: .75em; - color: @text-color-highlight; - } - &-name { - color: @text-color-subtle; + + &-selectListItem { + &> span:first-child { + font-weight: 600; + color: @text-color-highlight; + } + &> span { + color: @text-color-subtle; + margin-right: .75em; + } + &> span:last-child { + margin-right: 0; + } } } From bcd360df4694289d61a18260f500f6f9b3604fe1 Mon Sep 17 00:00:00 2001 From: Markus Olsson Date: Fri, 16 Mar 2018 17:46:28 +0100 Subject: [PATCH 0523/5882] Truncate autocomplete items Co-Authored-By: Katrina Uychaco --- styles/commit-view.less | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/styles/commit-view.less b/styles/commit-view.less index efb2bd059c..d81fafdd39 100644 --- a/styles/commit-view.less +++ b/styles/commit-view.less @@ -165,6 +165,10 @@ } &-selectListItem { + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + &> span:first-child { font-weight: 600; color: @text-color-highlight; From 3510fbd231c92bc5b0d0a03d6a650a1be9580b91 Mon Sep 17 00:00:00 2001 From: Markus Olsson Date: Fri, 16 Mar 2018 17:50:36 +0100 Subject: [PATCH 0524/5882] :fire: whitespace Co-Authored-By: Katrina Uychaco --- styles/commit-view.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/styles/commit-view.less b/styles/commit-view.less index d81fafdd39..615199ad43 100644 --- a/styles/commit-view.less +++ b/styles/commit-view.less @@ -168,7 +168,7 @@ text-overflow: ellipsis; white-space: nowrap; overflow: hidden; - + &> span:first-child { font-weight: 600; color: @text-color-highlight; From f68d63a5bf28d7c4c0786df95b8aa2e6c0223c33 Mon Sep 17 00:00:00 2001 From: Markus Olsson Date: Fri, 16 Mar 2018 18:33:38 +0100 Subject: [PATCH 0525/5882] Bump dugite for --trailers unfold,only support Co-Authored-By: Katrina Uychaco --- package-lock.json | 2395 ++++++++++++++++++++++++++++++++++++--------- package.json | 2 +- 2 files changed, 1926 insertions(+), 471 deletions(-) diff --git a/package-lock.json b/package-lock.json index c3c62441dd..bd4eee7132 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2,6 +2,7 @@ "name": "github", "version": "0.12.0", "lockfileVersion": 1, + "requires": true, "dependencies": { "7zip": { "version": "0.0.6", @@ -12,13 +13,16 @@ "acorn": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.1.1.tgz", - "integrity": "sha512-vOk6uEMctu0vQrvuSqFdJyqj1Q0S5VTDL79qtjo+DhRr+1mmaD+tluFSCZqhvi/JUhXSzoZN2BhtstaPEeE8cw==", + "integrity": "sha1-U/4WERH5EquZnuiHqQoLxSgi/XU=", "dev": true }, "acorn-jsx": { "version": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz", "integrity": "sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=", "dev": true, + "requires": { + "acorn": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz" + }, "dependencies": { "acorn": { "version": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", @@ -27,6 +31,17 @@ } } }, + "ajv": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", + "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", + "requires": { + "co": "4.6.0", + "fast-deep-equal": "1.1.0", + "fast-json-stable-stringify": "2.0.0", + "json-schema-traverse": "0.3.1" + } + }, "ansi-regex": { "version": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" @@ -70,6 +85,11 @@ "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + }, "assign-symbols": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", @@ -93,19 +113,32 @@ "atom-babel6-transpiler": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/atom-babel6-transpiler/-/atom-babel6-transpiler-1.1.3.tgz", - "integrity": "sha1-1wKxDpDrzx+R4apcSnm5zqmKm6Y=" + "integrity": "sha1-1wKxDpDrzx+R4apcSnm5zqmKm6Y=", + "requires": { + "babel-core": "6.26.0" + } }, "atom-mocha-test-runner": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/atom-mocha-test-runner/-/atom-mocha-test-runner-1.2.0.tgz", "integrity": "sha1-qPZQm40pqAn8tv9H8FiEthLNxqk=", "dev": true, + "requires": { + "etch": "0.8.0", + "grim": "2.0.2", + "less": "2.7.3", + "mocha": "3.4.2", + "tmp": "0.0.31" + }, "dependencies": { "etch": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/etch/-/etch-0.8.0.tgz", "integrity": "sha1-VPYZV0NG+KPueXP1T7vQG1YnItY=", - "dev": true + "dev": true, + "requires": { + "virtual-dom": "2.1.1" + } }, "grim": { "version": "2.0.2", @@ -118,6 +151,11 @@ } } }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + }, "aws4": { "version": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=" @@ -130,6 +168,27 @@ "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.26.0.tgz", "integrity": "sha1-rzL3izGm/O8RnIew/Y2XU/A6C7g=", + "requires": { + "babel-code-frame": "6.26.0", + "babel-generator": "6.26.0", + "babel-helpers": "6.24.1", + "babel-messages": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", + "babel-register": "6.26.0", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0", + "babylon": "6.18.0", + "convert-source-map": "1.5.1", + "debug": "2.6.8", + "json5": "0.5.1", + "lodash": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", + "minimatch": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "path-is-absolute": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "private": "0.1.8", + "slash": "1.0.0", + "source-map": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz" + }, "dependencies": { "babel-code-frame": { "version": "6.26.0", @@ -210,6 +269,12 @@ "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-7.2.3.tgz", "integrity": "sha1-sv4tgBJkcPXBlELcdXJTqJdxCCc=", "dev": true, + "requires": { + "babel-code-frame": "6.26.0", + "babel-traverse": "6.25.0", + "babel-types": "6.25.0", + "babylon": "6.17.4" + }, "dependencies": { "ansi-regex": { "version": "2.1.1", @@ -289,6 +354,16 @@ "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.0.tgz", "integrity": "sha1-rBriAHC3n248odMmlhMFN3TyDcU=", + "requires": { + "babel-messages": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", + "babel-runtime": "6.26.0", + "babel-types": "6.26.0", + "detect-indent": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz", + "jsesc": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", + "lodash": "4.17.5", + "source-map": "0.5.7", + "trim-right": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz" + }, "dependencies": { "babel-runtime": { "version": "6.26.0", @@ -326,6 +401,11 @@ "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-helper-builder-react-jsx/-/babel-helper-builder-react-jsx-6.26.0.tgz", "integrity": "sha1-Of+DE7dci2Xc7/HzHTg+D/KkCKA=", + "requires": { + "babel-runtime": "6.26.0", + "babel-types": "6.26.0", + "esutils": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz" + }, "dependencies": { "babel-runtime": { "version": "6.26.0", @@ -358,6 +438,12 @@ "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-helper-define-map/-/babel-helper-define-map-6.26.0.tgz", "integrity": "sha1-pfVtq0GiX5fstJjH66ypgZ+Vvl8=", + "requires": { + "babel-helper-function-name": "https://registry.npmjs.org/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz", + "babel-runtime": "6.26.0", + "babel-types": "6.26.0", + "lodash": "4.17.5" + }, "dependencies": { "babel-runtime": { "version": "6.26.0", @@ -382,41 +468,78 @@ "regenerator-runtime": { "version": "0.11.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", - "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" + "integrity": "sha1-vgWtf5v30i4Fb5cmzuUBf78Z4uk=" } } }, "babel-helper-function-name": { "version": "https://registry.npmjs.org/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz", - "integrity": "sha1-00dbjAPtmCQqJbSDUasYOZ01gKk=" + "integrity": "sha1-00dbjAPtmCQqJbSDUasYOZ01gKk=", + "requires": { + "babel-helper-get-function-arity": "https://registry.npmjs.org/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz", + "babel-runtime": "6.25.0", + "babel-template": "6.25.0", + "babel-traverse": "6.25.0", + "babel-types": "6.25.0" + } }, "babel-helper-get-function-arity": { "version": "https://registry.npmjs.org/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz", - "integrity": "sha1-j3eCqpNAfEHTqlCQj4mwMbG2hT0=" + "integrity": "sha1-j3eCqpNAfEHTqlCQj4mwMbG2hT0=", + "requires": { + "babel-runtime": "6.25.0", + "babel-types": "6.25.0" + } }, "babel-helper-optimise-call-expression": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.24.1.tgz", - "integrity": "sha1-96E0J7qfc/j0+pk8VKl4gtEkQlc=" + "integrity": "sha1-96E0J7qfc/j0+pk8VKl4gtEkQlc=", + "requires": { + "babel-runtime": "6.25.0", + "babel-types": "6.25.0" + } }, "babel-helper-remap-async-to-generator": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-helper-remap-async-to-generator/-/babel-helper-remap-async-to-generator-6.24.1.tgz", - "integrity": "sha1-XsWBgnrXI/7N04HxySg5BnbkVRs=" + "integrity": "sha1-XsWBgnrXI/7N04HxySg5BnbkVRs=", + "requires": { + "babel-helper-function-name": "https://registry.npmjs.org/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz", + "babel-runtime": "6.25.0", + "babel-template": "6.25.0", + "babel-traverse": "6.25.0", + "babel-types": "6.25.0" + } }, "babel-helper-replace-supers": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-helper-replace-supers/-/babel-helper-replace-supers-6.24.1.tgz", - "integrity": "sha1-v22/5Dk40XNpohPKiov3S2qQqxo=" + "integrity": "sha1-v22/5Dk40XNpohPKiov3S2qQqxo=", + "requires": { + "babel-helper-optimise-call-expression": "6.24.1", + "babel-messages": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", + "babel-runtime": "6.25.0", + "babel-template": "6.25.0", + "babel-traverse": "6.25.0", + "babel-types": "6.25.0" + } }, "babel-helpers": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-helpers/-/babel-helpers-6.24.1.tgz", - "integrity": "sha1-NHHenK7DiOXIUOWX5Yom3fN2ArI=" + "integrity": "sha1-NHHenK7DiOXIUOWX5Yom3fN2ArI=", + "requires": { + "babel-runtime": "6.25.0", + "babel-template": "6.25.0" + } }, "babel-messages": { "version": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", - "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=" + "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=", + "requires": { + "babel-runtime": "6.25.0" + } }, "babel-plugin-chai-assert-async": { "version": "0.1.0", @@ -426,17 +549,28 @@ "babel-plugin-check-es2015-constants": { "version": "6.22.0", "resolved": "https://registry.npmjs.org/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz", - "integrity": "sha1-NRV7EBQm/S/9PaP3XH0ekYNbv4o=" + "integrity": "sha1-NRV7EBQm/S/9PaP3XH0ekYNbv4o=", + "requires": { + "babel-runtime": "6.25.0" + } }, "babel-plugin-relay": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/babel-plugin-relay/-/babel-plugin-relay-1.4.1.tgz", "integrity": "sha1-isVivLH9KzVl0nGqztMr5CyCc1M=", + "requires": { + "babel-runtime": "6.25.0", + "babel-types": "6.25.0", + "graphql": "0.11.7" + }, "dependencies": { "graphql": { "version": "0.11.7", "resolved": "https://registry.npmjs.org/graphql/-/graphql-0.11.7.tgz", - "integrity": "sha1-5auqnLe3zMuE6fCDa/Q3DSaHUMY=" + "integrity": "sha1-5auqnLe3zMuE6fCDa/Q3DSaHUMY=", + "requires": { + "iterall": "1.1.3" + } } } }, @@ -473,12 +607,23 @@ "babel-plugin-transform-async-to-generator": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.24.1.tgz", - "integrity": "sha1-ZTbjeK/2yx1VF6wOQOs+n8jQh2E=" + "integrity": "sha1-ZTbjeK/2yx1VF6wOQOs+n8jQh2E=", + "requires": { + "babel-helper-remap-async-to-generator": "6.24.1", + "babel-plugin-syntax-async-functions": "6.13.0", + "babel-runtime": "6.25.0" + } }, "babel-plugin-transform-class-properties": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-plugin-transform-class-properties/-/babel-plugin-transform-class-properties-6.24.1.tgz", "integrity": "sha1-anl2PqYdM9NvN7YRqp3vgagbRqw=", + "requires": { + "babel-helper-function-name": "6.24.1", + "babel-plugin-syntax-class-properties": "6.13.0", + "babel-runtime": "6.25.0", + "babel-template": "6.25.0" + }, "dependencies": { "babel-helper-function-name": { "version": "6.24.1", @@ -512,6 +657,11 @@ "version": "1.3.4", "resolved": "https://registry.npmjs.org/babel-plugin-transform-decorators-legacy/-/babel-plugin-transform-decorators-legacy-1.3.4.tgz", "integrity": "sha1-dBtY9sW86eYCfgiC2cmU8E82aSU=", + "requires": { + "babel-plugin-syntax-decorators": "6.13.0", + "babel-runtime": "6.25.0", + "babel-template": "6.25.0" + }, "dependencies": { "babel-plugin-syntax-decorators": { "version": "6.13.0", @@ -523,17 +673,30 @@ "babel-plugin-transform-es2015-arrow-functions": { "version": "6.22.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz", - "integrity": "sha1-RSaSy3EdX3ncf4XkQM5BufJE0iE=" + "integrity": "sha1-RSaSy3EdX3ncf4XkQM5BufJE0iE=", + "requires": { + "babel-runtime": "6.25.0" + } }, "babel-plugin-transform-es2015-block-scoped-functions": { "version": "6.22.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz", - "integrity": "sha1-u8UbSflk1wy42OC5ToICRs46YUE=" + "integrity": "sha1-u8UbSflk1wy42OC5ToICRs46YUE=", + "requires": { + "babel-runtime": "6.25.0" + } }, "babel-plugin-transform-es2015-block-scoping": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.26.0.tgz", "integrity": "sha1-1w9SmcEwjQXBL0Y4E7CgnnOxiV8=", + "requires": { + "babel-runtime": "6.26.0", + "babel-template": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0", + "lodash": "4.17.5" + }, "dependencies": { "babel-code-frame": { "version": "6.26.0", @@ -596,7 +759,7 @@ "babylon": { "version": "6.18.0", "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", - "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==" + "integrity": "sha1-ry87iPpvXB5MY00aD46sT1WzleM=" }, "invariant": { "version": "2.2.3", @@ -609,44 +772,79 @@ "regenerator-runtime": { "version": "0.11.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", - "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" + "integrity": "sha1-vgWtf5v30i4Fb5cmzuUBf78Z4uk=" } } }, "babel-plugin-transform-es2015-classes": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.24.1.tgz", - "integrity": "sha1-WkxYpQyclGHlZLSyo7+ryXolhNs=" + "integrity": "sha1-WkxYpQyclGHlZLSyo7+ryXolhNs=", + "requires": { + "babel-helper-define-map": "6.26.0", + "babel-helper-function-name": "https://registry.npmjs.org/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz", + "babel-helper-optimise-call-expression": "6.24.1", + "babel-helper-replace-supers": "6.24.1", + "babel-messages": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", + "babel-runtime": "6.25.0", + "babel-template": "6.25.0", + "babel-traverse": "6.25.0", + "babel-types": "6.25.0" + } }, "babel-plugin-transform-es2015-computed-properties": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz", - "integrity": "sha1-b+Ko0WiV1WNPTNmZttNICjCBWbM=" + "integrity": "sha1-b+Ko0WiV1WNPTNmZttNICjCBWbM=", + "requires": { + "babel-runtime": "6.25.0", + "babel-template": "6.25.0" + } }, "babel-plugin-transform-es2015-destructuring": { "version": "6.23.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz", - "integrity": "sha1-mXux8auWf2gtKwh2/jWNYOdlxW0=" + "integrity": "sha1-mXux8auWf2gtKwh2/jWNYOdlxW0=", + "requires": { + "babel-runtime": "6.25.0" + } }, "babel-plugin-transform-es2015-for-of": { "version": "6.23.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz", - "integrity": "sha1-9HyVsrYT3x0+zC/bdXNiPHUkhpE=" + "integrity": "sha1-9HyVsrYT3x0+zC/bdXNiPHUkhpE=", + "requires": { + "babel-runtime": "6.25.0" + } }, "babel-plugin-transform-es2015-function-name": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.24.1.tgz", - "integrity": "sha1-g0yJhTvDaxrw86TF26qU/Y6sqos=" + "integrity": "sha1-g0yJhTvDaxrw86TF26qU/Y6sqos=", + "requires": { + "babel-helper-function-name": "https://registry.npmjs.org/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz", + "babel-runtime": "6.25.0", + "babel-types": "6.25.0" + } }, "babel-plugin-transform-es2015-literals": { "version": "6.22.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.22.0.tgz", - "integrity": "sha1-T1SgLWzWbPkVKAAZox0xklN3yi4=" + "integrity": "sha1-T1SgLWzWbPkVKAAZox0xklN3yi4=", + "requires": { + "babel-runtime": "6.25.0" + } }, "babel-plugin-transform-es2015-modules-commonjs": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.0.tgz", "integrity": "sha1-DYOUApt9xqvhqX7xgeAHWN0uXYo=", + "requires": { + "babel-plugin-transform-strict-mode": "6.24.1", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0", + "babel-types": "6.26.0" + }, "dependencies": { "babel-code-frame": { "version": "6.26.0", @@ -721,12 +919,24 @@ "babel-plugin-transform-es2015-object-super": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz", - "integrity": "sha1-JM72muIcuDp/hgPa0CH1cusnj40=" + "integrity": "sha1-JM72muIcuDp/hgPa0CH1cusnj40=", + "requires": { + "babel-helper-replace-supers": "6.24.1", + "babel-runtime": "6.25.0" + } }, "babel-plugin-transform-es2015-parameters": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz", "integrity": "sha1-V6w1GrScrxSpfNE7CfZv3wpiXys=", + "requires": { + "babel-helper-call-delegate": "6.24.1", + "babel-helper-get-function-arity": "6.24.1", + "babel-runtime": "6.25.0", + "babel-template": "6.25.0", + "babel-traverse": "6.25.0", + "babel-types": "6.25.0" + }, "dependencies": { "babel-helper-call-delegate": { "version": "6.24.1", @@ -762,37 +972,61 @@ "babel-plugin-transform-es2015-shorthand-properties": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz", - "integrity": "sha1-JPh11nIch2YbvZmkYi5R8U3jiqA=" + "integrity": "sha1-JPh11nIch2YbvZmkYi5R8U3jiqA=", + "requires": { + "babel-runtime": "6.25.0", + "babel-types": "6.25.0" + } }, "babel-plugin-transform-es2015-spread": { "version": "6.22.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.22.0.tgz", - "integrity": "sha1-1taKmfia7cRTbIGlQujdnxdG+NE=" + "integrity": "sha1-1taKmfia7cRTbIGlQujdnxdG+NE=", + "requires": { + "babel-runtime": "6.25.0" + } }, "babel-plugin-transform-es2015-template-literals": { "version": "6.22.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.22.0.tgz", - "integrity": "sha1-qEs0UPfp+PH2g51taH2oS7EjbY0=" + "integrity": "sha1-qEs0UPfp+PH2g51taH2oS7EjbY0=", + "requires": { + "babel-runtime": "6.25.0" + } }, "babel-plugin-transform-es3-member-expression-literals": { "version": "6.22.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es3-member-expression-literals/-/babel-plugin-transform-es3-member-expression-literals-6.22.0.tgz", - "integrity": "sha1-cz00RPPsxBvvjtGmpOCWV7iWnrs=" + "integrity": "sha1-cz00RPPsxBvvjtGmpOCWV7iWnrs=", + "requires": { + "babel-runtime": "6.25.0" + } }, "babel-plugin-transform-es3-property-literals": { "version": "6.22.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es3-property-literals/-/babel-plugin-transform-es3-property-literals-6.22.0.tgz", - "integrity": "sha1-sgeNWELiKr9A9z6M3pzTcRq9V1g=" + "integrity": "sha1-sgeNWELiKr9A9z6M3pzTcRq9V1g=", + "requires": { + "babel-runtime": "6.25.0" + } }, "babel-plugin-transform-flow-strip-types": { "version": "6.22.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-flow-strip-types/-/babel-plugin-transform-flow-strip-types-6.22.0.tgz", - "integrity": "sha1-hMtnKTXUNxT9wyvOhFaNh0Qc988=" + "integrity": "sha1-hMtnKTXUNxT9wyvOhFaNh0Qc988=", + "requires": { + "babel-plugin-syntax-flow": "6.18.0", + "babel-runtime": "6.25.0" + } }, "babel-plugin-transform-object-rest-spread": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.26.0.tgz", "integrity": "sha1-DzZpLVD+9rfi1LOsFHgTepY7ewY=", + "requires": { + "babel-plugin-syntax-object-rest-spread": "6.13.0", + "babel-runtime": "6.26.0" + }, "dependencies": { "babel-runtime": { "version": "6.26.0", @@ -813,22 +1047,39 @@ "babel-plugin-transform-react-display-name": { "version": "6.25.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-display-name/-/babel-plugin-transform-react-display-name-6.25.0.tgz", - "integrity": "sha1-Z+K/Hx6ck6sI25Z5LgU5K/LMKNE=" + "integrity": "sha1-Z+K/Hx6ck6sI25Z5LgU5K/LMKNE=", + "requires": { + "babel-runtime": "6.25.0" + } }, "babel-plugin-transform-react-jsx": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-jsx/-/babel-plugin-transform-react-jsx-6.24.1.tgz", - "integrity": "sha1-hAoCjn30YN/DotKfDA2R9jduZqM=" + "integrity": "sha1-hAoCjn30YN/DotKfDA2R9jduZqM=", + "requires": { + "babel-helper-builder-react-jsx": "6.26.0", + "babel-plugin-syntax-jsx": "6.18.0", + "babel-runtime": "6.25.0" + } }, "babel-plugin-transform-strict-mode": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz", - "integrity": "sha1-1fr3qleKZbvlkc9e2uBKDGcCB1g=" + "integrity": "sha1-1fr3qleKZbvlkc9e2uBKDGcCB1g=", + "requires": { + "babel-runtime": "6.25.0", + "babel-types": "6.25.0" + } }, "babel-polyfill": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-polyfill/-/babel-polyfill-6.26.0.tgz", "integrity": "sha1-N5k3q8Z9eJWXCtxiHyhM2WbPIVM=", + "requires": { + "babel-runtime": "6.26.0", + "core-js": "2.5.0", + "regenerator-runtime": "0.10.5" + }, "dependencies": { "babel-runtime": { "version": "6.26.0", @@ -856,12 +1107,50 @@ "babel-preset-fbjs": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/babel-preset-fbjs/-/babel-preset-fbjs-2.1.4.tgz", - "integrity": "sha1-IvNY5mVAc6z2HkegUqd317zPA68=" + "integrity": "sha1-IvNY5mVAc6z2HkegUqd317zPA68=", + "requires": { + "babel-plugin-check-es2015-constants": "6.22.0", + "babel-plugin-syntax-class-properties": "6.13.0", + "babel-plugin-syntax-flow": "6.18.0", + "babel-plugin-syntax-jsx": "6.18.0", + "babel-plugin-syntax-object-rest-spread": "6.13.0", + "babel-plugin-syntax-trailing-function-commas": "6.22.0", + "babel-plugin-transform-class-properties": "6.24.1", + "babel-plugin-transform-es2015-arrow-functions": "6.22.0", + "babel-plugin-transform-es2015-block-scoped-functions": "6.22.0", + "babel-plugin-transform-es2015-block-scoping": "6.26.0", + "babel-plugin-transform-es2015-classes": "6.24.1", + "babel-plugin-transform-es2015-computed-properties": "6.24.1", + "babel-plugin-transform-es2015-destructuring": "6.23.0", + "babel-plugin-transform-es2015-for-of": "6.23.0", + "babel-plugin-transform-es2015-function-name": "6.24.1", + "babel-plugin-transform-es2015-literals": "6.22.0", + "babel-plugin-transform-es2015-modules-commonjs": "6.26.0", + "babel-plugin-transform-es2015-object-super": "6.24.1", + "babel-plugin-transform-es2015-parameters": "6.24.1", + "babel-plugin-transform-es2015-shorthand-properties": "6.24.1", + "babel-plugin-transform-es2015-spread": "6.22.0", + "babel-plugin-transform-es2015-template-literals": "6.22.0", + "babel-plugin-transform-es3-member-expression-literals": "6.22.0", + "babel-plugin-transform-es3-property-literals": "6.22.0", + "babel-plugin-transform-flow-strip-types": "6.22.0", + "babel-plugin-transform-object-rest-spread": "6.26.0", + "babel-plugin-transform-react-display-name": "6.25.0", + "babel-plugin-transform-react-jsx": "6.24.1" + } }, "babel-preset-react": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-preset-react/-/babel-preset-react-6.24.1.tgz", "integrity": "sha1-umnfrqRfw+xjm2pOzqbhdwLJE4A=", + "requires": { + "babel-plugin-syntax-jsx": "6.18.0", + "babel-plugin-transform-react-display-name": "6.25.0", + "babel-plugin-transform-react-jsx": "6.24.1", + "babel-plugin-transform-react-jsx-self": "6.22.0", + "babel-plugin-transform-react-jsx-source": "6.22.0", + "babel-preset-flow": "6.23.0" + }, "dependencies": { "babel-helper-builder-react-jsx": { "version": "6.26.0", @@ -987,6 +1276,15 @@ "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-register/-/babel-register-6.26.0.tgz", "integrity": "sha1-btAhFz4vy0htestFxgCahW9kcHE=", + "requires": { + "babel-core": "6.26.0", + "babel-runtime": "6.26.0", + "core-js": "2.5.0", + "home-or-tmp": "2.0.0", + "lodash": "4.17.5", + "mkdirp": "0.5.1", + "source-map-support": "0.4.18" + }, "dependencies": { "babel-runtime": { "version": "6.26.0", @@ -1007,22 +1305,50 @@ "babel-runtime": { "version": "6.25.0", "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.25.0.tgz", - "integrity": "sha1-M7mOql1IK7AajRqmtDetKwGuxBw=" + "integrity": "sha1-M7mOql1IK7AajRqmtDetKwGuxBw=", + "requires": { + "core-js": "2.5.0", + "regenerator-runtime": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz" + } }, "babel-template": { "version": "6.25.0", "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.25.0.tgz", - "integrity": "sha1-ZlJBFmt8KqTGGdceGSlpVSsQwHE=" + "integrity": "sha1-ZlJBFmt8KqTGGdceGSlpVSsQwHE=", + "requires": { + "babel-runtime": "6.25.0", + "babel-traverse": "6.25.0", + "babel-types": "6.25.0", + "babylon": "6.17.4", + "lodash": "4.17.5" + } }, "babel-traverse": { "version": "6.25.0", "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.25.0.tgz", - "integrity": "sha1-IldJfi/NGbie3BPEyROB+VEklvE=" + "integrity": "sha1-IldJfi/NGbie3BPEyROB+VEklvE=", + "requires": { + "babel-code-frame": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.22.0.tgz", + "babel-messages": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", + "babel-runtime": "6.25.0", + "babel-types": "6.25.0", + "babylon": "6.17.4", + "debug": "2.6.8", + "globals": "9.18.0", + "invariant": "https://registry.npmjs.org/invariant/-/invariant-2.2.2.tgz", + "lodash": "4.17.5" + } }, "babel-types": { "version": "6.25.0", "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.25.0.tgz", - "integrity": "sha1-cK+ySNVmDl0Y+BHZHIMDtUE0oY4=" + "integrity": "sha1-cK+ySNVmDl0Y+BHZHIMDtUE0oY4=", + "requires": { + "babel-runtime": "6.25.0", + "esutils": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "lodash": "4.17.5", + "to-fast-properties": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz" + } }, "babylon": { "version": "6.17.4", @@ -1038,34 +1364,57 @@ "version": "0.11.2", "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", "integrity": "sha1-e95c7RRbbVUakNuH+DxVi060io8=", + "requires": { + "cache-base": "1.0.1", + "class-utils": "0.3.6", + "component-emitter": "1.2.1", + "define-property": "1.0.0", + "isobject": "3.0.1", + "mixin-deep": "1.3.1", + "pascalcase": "0.1.1" + }, "dependencies": { "define-property": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=" + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "requires": { + "is-descriptor": "1.0.2" + } } } }, "bash-glob": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bash-glob/-/bash-glob-1.0.2.tgz", - "integrity": "sha1-laxWMf3XqPxWnyZxZ6hOuDGXmhs=" + "integrity": "sha1-laxWMf3XqPxWnyZxZ6hOuDGXmhs=", + "requires": { + "async-each": "1.0.1", + "bash-path": "1.0.3", + "component-emitter": "1.2.1", + "cross-spawn": "5.1.0", + "extend-shallow": "2.0.1", + "is-extglob": "2.1.1", + "is-glob": "4.0.0" + } }, "bash-path": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/bash-path/-/bash-path-1.0.3.tgz", - "integrity": "sha1-28nvvfGLHBFBPctZuWDmqlbIQlg=" + "integrity": "sha1-28nvvfGLHBFBPctZuWDmqlbIQlg=", + "requires": { + "arr-union": "3.1.0", + "is-windows": "1.0.2" + } }, "bcrypt-pbkdf": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", - "optional": true - }, - "block-stream": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", - "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=" + "optional": true, + "requires": { + "tweetnacl": "0.14.5" + } }, "boolbase": { "version": "1.0.0", @@ -1073,20 +1422,49 @@ "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", "dev": true }, + "boom": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz", + "integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=", + "requires": { + "hoek": "4.2.1" + } + }, "brace-expansion": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", - "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=" + "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", + "requires": { + "balanced-match": "1.0.0", + "concat-map": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" + } }, "braces": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.1.tgz", "integrity": "sha1-cIbJE7TloI2+N6wO5qJQDEumkbs=", + "requires": { + "arr-flatten": "1.1.0", + "array-unique": "0.3.2", + "define-property": "1.0.0", + "extend-shallow": "2.0.1", + "fill-range": "4.0.0", + "isobject": "3.0.1", + "kind-of": "6.0.2", + "repeat-element": "1.1.2", + "snapdragon": "0.8.1", + "snapdragon-node": "2.1.1", + "split-string": "3.1.0", + "to-regex": "3.0.2" + }, "dependencies": { "define-property": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=" + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "requires": { + "is-descriptor": "1.0.2" + } } } }, @@ -1104,7 +1482,10 @@ "bser": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/bser/-/bser-2.0.0.tgz", - "integrity": "sha1-mseNPtXZFYBP2HrLFYvHlxR6Fxk=" + "integrity": "sha1-mseNPtXZFYBP2HrLFYvHlxR6Fxk=", + "requires": { + "node-int64": "0.4.0" + } }, "builtin-modules": { "version": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", @@ -1113,7 +1494,18 @@ "cache-base": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", - "integrity": "sha1-Cn9GQWgxyLZi7jb+TnxZ129marI=" + "integrity": "sha1-Cn9GQWgxyLZi7jb+TnxZ129marI=", + "requires": { + "collection-visit": "1.0.0", + "component-emitter": "1.2.1", + "get-value": "2.0.6", + "has-value": "1.0.0", + "isobject": "3.0.1", + "set-value": "2.0.0", + "to-object-path": "0.3.0", + "union-value": "1.0.0", + "unset-value": "1.0.0" + } }, "call-me-maybe": { "version": "1.0.1", @@ -1141,6 +1533,11 @@ "resolved": "https://registry.npmjs.org/chai/-/chai-3.5.0.tgz", "integrity": "sha1-TQJjewZ/6Vi9v906QOxW/vc3Mkc=", "dev": true, + "requires": { + "assertion-error": "1.1.0", + "deep-eql": "0.1.3", + "type-detect": "1.0.0" + }, "dependencies": { "assertion-error": { "version": "1.1.0", @@ -1181,23 +1578,62 @@ }, "chalk": { "version": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=" + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "escape-string-regexp": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "has-ansi": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "strip-ansi": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "supports-color": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz" + } }, "checksum": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/checksum/-/checksum-0.1.1.tgz", - "integrity": "sha1-3GUn1MkL6FYNvR7Uzs8yl9Uo6ek=" + "integrity": "sha1-3GUn1MkL6FYNvR7Uzs8yl9Uo6ek=", + "requires": { + "optimist": "0.3.7" + } }, "cheerio": { "version": "0.22.0", "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-0.22.0.tgz", "integrity": "sha1-qbqoYKP5tZWmuBsahocxIe06Jp4=", - "dev": true + "dev": true, + "requires": { + "css-select": "1.2.0", + "dom-serializer": "0.1.0", + "entities": "1.1.1", + "htmlparser2": "3.9.2", + "lodash.assignin": "4.2.0", + "lodash.bind": "4.2.1", + "lodash.defaults": "4.2.0", + "lodash.filter": "4.6.0", + "lodash.flatten": "4.4.0", + "lodash.foreach": "4.5.0", + "lodash.map": "4.6.0", + "lodash.merge": "4.6.0", + "lodash.pick": "4.4.0", + "lodash.reduce": "4.6.0", + "lodash.reject": "4.6.0", + "lodash.some": "4.6.0" + } + }, + "chownr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.0.1.tgz", + "integrity": "sha1-4qdQQqlVGQi+vSW4Uj1fl2nXkYE=" }, "class-utils": { "version": "0.3.6", "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", "integrity": "sha1-+TNprouafOAv1B+q0MqDAzGQxGM=", + "requires": { + "arr-union": "3.1.0", + "define-property": "0.2.5", + "isobject": "3.0.1", + "static-extend": "0.1.2" + }, "dependencies": { "define-property": { "version": "0.2.5", @@ -1208,11 +1644,17 @@ "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "requires": { + "kind-of": "3.2.2" + }, "dependencies": { "kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=" + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "1.1.6" + } } } }, @@ -1220,11 +1662,17 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "requires": { + "kind-of": "3.2.2" + }, "dependencies": { "kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=" + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "1.1.6" + } } } }, @@ -1249,14 +1697,17 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", + "requires": { + "string-width": "1.0.2", + "strip-ansi": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "wrap-ansi": "2.1.0" + }, "dependencies": { "string-width": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", "strip-ansi": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz" } } @@ -1270,12 +1721,27 @@ "collection-visit": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", - "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=" + "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "requires": { + "map-visit": "1.0.0", + "object-visit": "1.0.1" + } + }, + "combined-stream": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", + "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", + "requires": { + "delayed-stream": "1.0.0" + } }, "commander": { "version": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", "integrity": "sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q=", - "dev": true + "dev": true, + "requires": { + "graceful-readlink": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz" + } }, "compare-sets": { "version": "1.0.1", @@ -1323,21 +1789,27 @@ "create-react-class": { "version": "15.6.2", "resolved": "https://registry.npmjs.org/create-react-class/-/create-react-class-15.6.2.tgz", - "integrity": "sha1-zx7RXxKq1/FO9fLf4F5sQvke8Co=" + "integrity": "sha1-zx7RXxKq1/FO9fLf4F5sQvke8Co=", + "requires": { + "fbjs": "0.8.14", + "loose-envify": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz", + "object-assign": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz" + } }, "cross-spawn": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", + "requires": { + "lru-cache": "4.1.2", + "shebang-command": "1.2.0", + "which": "1.3.0" + }, "dependencies": { "lru-cache": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.2.tgz", - "integrity": "sha512-wgeVXhrDwAWnIF/yZARsFnMBtdFXOg1b8RIrhilp+0iDYN4mdQcNZElDZ0e4B64BhaxeQ5zN7PMyvu7we1kPeQ==", - "requires": { - "pseudomap": "1.0.2", - "yallist": "2.1.2" - } + "integrity": "sha512-wgeVXhrDwAWnIF/yZARsFnMBtdFXOg1b8RIrhilp+0iDYN4mdQcNZElDZ0e4B64BhaxeQ5zN7PMyvu7we1kPeQ==" } } }, @@ -1347,11 +1819,35 @@ "integrity": "sha1-UYO8R6CVWb78+YzEZXlkmZNZNy8=", "dev": true }, + "cryptiles": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz", + "integrity": "sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4=", + "requires": { + "boom": "5.2.0" + }, + "dependencies": { + "boom": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", + "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==", + "requires": { + "hoek": "4.2.1" + } + } + } + }, "css-select": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", "integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=", - "dev": true + "dev": true, + "requires": { + "boolbase": "1.0.0", + "css-what": "2.1.0", + "domutils": "1.5.1", + "nth-check": "1.0.1" + } }, "css-what": { "version": "2.1.0", @@ -1363,6 +1859,9 @@ "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "requires": { + "assert-plus": "1.0.0" + }, "dependencies": { "assert-plus": { "version": "1.0.0", @@ -1374,7 +1873,10 @@ "debug": { "version": "2.6.8", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz", - "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=" + "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=", + "requires": { + "ms": "2.0.0" + } }, "decamelize": { "version": "1.2.0", @@ -1396,16 +1898,32 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.2.tgz", "integrity": "sha1-g6c/L+pWmJj7c3GTyPhzyvbUXJQ=", - "dev": true + "dev": true, + "requires": { + "foreach": "2.0.5", + "object-keys": "1.0.11" + } }, "define-property": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha1-1Flono1lS6d+AqgX+HENcCyxbp0=" + "integrity": "sha1-1Flono1lS6d+AqgX+HENcCyxbp0=", + "requires": { + "is-descriptor": "1.0.2", + "isobject": "3.0.1" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" }, "detect-indent": { "version": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz", - "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=" + "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=", + "requires": { + "repeating": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz" + } }, "diff": { "version": "3.2.0", @@ -1422,6 +1940,10 @@ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz", "integrity": "sha1-BzxpdUbOB4DOI75KKOKT5AvDDII=", "dev": true, + "requires": { + "domelementtype": "1.1.3", + "entities": "1.1.1" + }, "dependencies": { "domelementtype": { "version": "1.1.3", @@ -1447,7 +1969,10 @@ "version": "2.4.1", "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.1.tgz", "integrity": "sha1-iS5HAAqZvlW783dP/qBWHYh5wlk=", - "dev": true + "dev": true, + "requires": { + "domelementtype": "1.3.0" + } }, "dompurify": { "version": "1.0.3", @@ -1458,203 +1983,52 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", - "dev": true + "dev": true, + "requires": { + "dom-serializer": "0.1.0", + "domelementtype": "1.3.0" + } }, "dugite": { - "version": "1.49.0", - "resolved": "https://registry.npmjs.org/dugite/-/dugite-1.49.0.tgz", - "integrity": "sha1-b1zkraF0jwyL+bCOYj9foZKYA0w=", - "dependencies": { - "ajv": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.3.0.tgz", - "integrity": "sha1-RBT/dKUIecII7l/cgm4ywwNUnto=", - "requires": { - "co": "4.6.0", - "fast-deep-equal": "1.0.0", - "fast-json-stable-stringify": "2.0.0", - "json-schema-traverse": "0.3.1" - } - }, - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" - }, - "boom": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz", - "integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=", - "requires": { - "hoek": "4.2.0" - } - }, - "cryptiles": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz", - "integrity": "sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4=", - "requires": { - "boom": "5.2.0" - }, - "dependencies": { - "boom": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", - "integrity": "sha1-XdnabuOl8wIHdDYpDLcX0/SlTgI=", - "requires": { - "hoek": "4.2.0" - } - } - } - }, - "form-data": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.1.tgz", - "integrity": "sha1-b7lPvXGIUwbXPRXMSX/kzE7NRL8=", - "requires": { - "asynckit": "0.4.0", - "combined-stream": "1.0.6", - "mime-types": "2.1.17" - } - }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" - }, - "har-validator": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", - "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", - "requires": { - "ajv": "5.3.0", - "har-schema": "2.0.0" - } - }, - "hawk": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz", - "integrity": "sha1-r02RTrBl+bXOTZ0RwcshJu7MMDg=", - "requires": { - "boom": "4.3.1", - "cryptiles": "3.1.2", - "hoek": "4.2.0", - "sntp": "2.1.0" - } - }, - "hoek": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.0.tgz", - "integrity": "sha1-ctnQdU9/4lyi0BrY+PmpRJqJUm0=" - }, - "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "requires": { - "assert-plus": "1.0.0", - "jsprim": "1.4.1", - "sshpk": "1.13.1" - } - }, - "mime-db": { - "version": "1.30.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.30.0.tgz", - "integrity": "sha1-dMZD2i3Z1qRTmZY0ZbJtXKfXHwE=" - }, - "mime-types": { - "version": "2.1.17", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz", - "integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=", - "requires": { - "mime-db": "1.30.0" - } - }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" - }, - "qs": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", - "integrity": "sha1-NJzfbu+J7EXBLX1es/wMhwNDptg=" - }, - "request": { - "version": "2.83.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.83.0.tgz", - "integrity": "sha1-ygtl2gLtYpNYh4COb1EDgQNOM1Y=", - "requires": { - "aws-sign2": "0.7.0", - "aws4": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", - "caseless": "0.12.0", - "combined-stream": "1.0.6", - "extend": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", - "forever-agent": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "form-data": "2.3.1", - "har-validator": "5.0.3", - "hawk": "6.0.2", - "http-signature": "1.2.0", - "is-typedarray": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "isstream": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "json-stringify-safe": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "mime-types": "2.1.17", - "oauth-sign": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", - "performance-now": "2.1.0", - "qs": "6.5.1", - "safe-buffer": "5.1.1", - "stringstream": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", - "tough-cookie": "2.3.3", - "tunnel-agent": "0.6.0", - "uuid": "3.1.0" - } - }, - "rimraf": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", - "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", - "requires": { - "glob": "7.1.2" - } - }, - "sntp": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/sntp/-/sntp-2.1.0.tgz", - "integrity": "sha1-LGzsFP7cIiJznK+bXD2F0cxaLMg=", - "requires": { - "hoek": "4.2.0" - } - }, - "tough-cookie": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.3.tgz", - "integrity": "sha1-C2GKVWW23qkL80JdBNVe3EdadWE=", - "requires": { - "punycode": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz" - } - } + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/dugite/-/dugite-1.60.0.tgz", + "integrity": "sha512-HP5Qfx867ESOkrrEhXtxi72UQoNWaCSEcahGCmnBM/v+ldgbFu94s9CvG3tDcBtz1HJkvYEtkBd8V8Vb63nfUw==", + "requires": { + "checksum": "0.1.1", + "mkdirp": "0.5.1", + "progress": "2.0.0", + "request": "2.85.0", + "rimraf": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.1.tgz", + "tar": "4.4.0" } }, "ecc-jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", - "optional": true + "optional": true, + "requires": { + "jsbn": "0.1.1" + } }, "electron-devtools-installer": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/electron-devtools-installer/-/electron-devtools-installer-2.2.3.tgz", "integrity": "sha1-WLmk7FBzd7xG4JHNQ3FBiODDab4=", - "dev": true + "dev": true, + "requires": { + "7zip": "0.0.6", + "cross-unzip": "0.0.2", + "rimraf": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.1.tgz", + "semver": "5.4.1" + } }, "encoding": { "version": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", - "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=" + "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=", + "requires": { + "iconv-lite": "0.4.18" + } }, "entities": { "version": "1.1.1", @@ -1666,30 +2040,60 @@ "version": "2.9.1", "resolved": "https://registry.npmjs.org/enzyme/-/enzyme-2.9.1.tgz", "integrity": "sha1-B9XOaRJBJA+4F78sSxjW5TAkDfY=", - "dev": true + "dev": true, + "requires": { + "cheerio": "0.22.0", + "function.prototype.name": "1.0.3", + "is-subset": "0.1.1", + "lodash": "4.17.5", + "object-is": "1.0.1", + "object.assign": "4.0.4", + "object.entries": "1.0.4", + "object.values": "1.0.4", + "prop-types": "15.6.1", + "uuid": "3.1.0" + } }, "errno": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.4.tgz", "integrity": "sha1-uJbiOp5ei6M4cfyZar02NfyaHH0=", "dev": true, - "optional": true + "optional": true, + "requires": { + "prr": "0.0.0" + } }, "error": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/error/-/error-4.4.0.tgz", "integrity": "sha1-v2n/JR+0onnBmtzNqmth6Q2b8So=", - "dev": true + "dev": true, + "requires": { + "camelize": "1.0.0", + "string-template": "0.2.1", + "xtend": "4.0.1" + } }, "error-ex": { "version": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz", - "integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=" + "integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=", + "requires": { + "is-arrayish": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz" + } }, "es-abstract": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.9.0.tgz", - "integrity": "sha512-kk3IJoKo7A3pWJc0OV8yZ/VEX2oSUytfekrJiqoxBlKJMFAJVJVpGdHClCCTdv+Fn2zHfpDHHIelMFhZVfef3Q==", + "integrity": "sha1-aQgpoHyuNrIi5/2bdcDQVz6yUic=", "dev": true, + "requires": { + "es-to-primitive": "1.1.1", + "function-bind": "1.1.1", + "has": "https://registry.npmjs.org/has/-/has-1.0.1.tgz", + "is-callable": "1.1.3", + "is-regex": "1.0.4" + }, "dependencies": { "function-bind": { "version": "1.1.1", @@ -1703,7 +2107,12 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.1.1.tgz", "integrity": "sha1-RTVSSKiJeQNLZ5Lhm7gfK3l13Q0=", - "dev": true + "dev": true, + "requires": { + "is-callable": "1.1.3", + "is-date-object": "1.0.1", + "is-symbol": "1.0.1" + } }, "es6-promise": { "version": "4.2.4", @@ -1719,6 +2128,43 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-3.19.0.tgz", "integrity": "sha1-yPxiAcf0DdCJQbh8CFdnOGpnmsw=", "dev": true, + "requires": { + "babel-code-frame": "6.26.0", + "chalk": "1.1.3", + "concat-stream": "1.6.1", + "debug": "2.6.8", + "doctrine": "2.1.0", + "escope": "3.6.0", + "espree": "3.5.0", + "esquery": "1.0.0", + "estraverse": "4.2.0", + "esutils": "2.0.2", + "file-entry-cache": "2.0.0", + "glob": "7.1.2", + "globals": "9.18.0", + "ignore": "3.3.3", + "imurmurhash": "0.1.4", + "inquirer": "0.12.0", + "is-my-json-valid": "2.17.2", + "is-resolvable": "1.1.0", + "js-yaml": "3.9.1", + "json-stable-stringify": "1.0.1", + "levn": "0.3.0", + "lodash": "4.17.5", + "mkdirp": "0.5.1", + "natural-compare": "1.4.0", + "optionator": "0.8.2", + "path-is-inside": "1.0.2", + "pluralize": "1.2.1", + "progress": "1.1.8", + "require-uncached": "1.0.3", + "shelljs": "0.7.8", + "strip-bom": "3.0.0", + "strip-json-comments": "2.0.1", + "table": "3.8.3", + "text-table": "0.2.0", + "user-home": "2.0.0" + }, "dependencies": { "ajv": { "version": "4.11.8", @@ -1850,10 +2296,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz", "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=", - "dev": true, - "requires": { - "es5-ext": "0.10.26" - } + "dev": true }, "deep-is": { "version": "0.1.3", @@ -1915,7 +2358,6 @@ "dev": true, "requires": { "d": "1.0.0", - "es5-ext": "0.10.26", "es6-iterator": "2.0.3", "es6-set": "0.1.5", "es6-symbol": "3.1.1", @@ -1929,7 +2371,6 @@ "dev": true, "requires": { "d": "1.0.0", - "es5-ext": "0.10.26", "es6-iterator": "2.0.3", "es6-symbol": "3.1.1", "event-emitter": "0.3.5" @@ -1941,8 +2382,7 @@ "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=", "dev": true, "requires": { - "d": "1.0.0", - "es5-ext": "0.10.26" + "d": "1.0.0" } }, "es6-weak-map": { @@ -1952,7 +2392,6 @@ "dev": true, "requires": { "d": "1.0.0", - "es5-ext": "0.10.26", "es6-iterator": "2.0.3", "es6-symbol": "3.1.1" } @@ -1971,7 +2410,6 @@ "requires": { "es6-map": "0.1.5", "es6-weak-map": "2.0.2", - "esrecurse": "4.2.0", "estraverse": "4.2.0" } }, @@ -2002,8 +2440,7 @@ "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=", "dev": true, "requires": { - "d": "1.0.0", - "es5-ext": "0.10.26" + "d": "1.0.0" } }, "exit-hook": { @@ -2044,7 +2481,6 @@ "integrity": "sha1-0wMLMrOBVPTjt+nHCfSQ9++XxIE=", "dev": true, "requires": { - "circular-json": "0.3.3", "del": "2.2.2", "graceful-fs": "4.1.11", "write": "0.2.1" @@ -2144,7 +2580,6 @@ "requires": { "generate-function": "2.0.0", "generate-object-property": "1.2.0", - "is-my-ip-valid": "1.0.0", "jsonpointer": "4.0.1", "xtend": "4.0.1" } @@ -2553,7 +2988,11 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.1.1.tgz", "integrity": "sha1-q67IJBd2E7ipWymWOeG2+s9HNEk=", - "dev": true + "dev": true, + "requires": { + "debug": "2.6.8", + "pkg-dir": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-1.0.0.tgz" + } }, "eslint-plugin-babel": { "version": "4.1.2", @@ -2565,13 +3004,28 @@ "version": "2.34.1", "resolved": "https://registry.npmjs.org/eslint-plugin-flowtype/-/eslint-plugin-flowtype-2.34.1.tgz", "integrity": "sha512-xwXpTW7Xv+wfuQdfPILmFl9HWBdWbDjE1aZWWQ4EgCpQtMzymEkDQfyD1ME0VA8C0HTXV7cufypQRvLi+Hk/og==", - "dev": true + "dev": true, + "requires": { + "lodash": "4.17.5" + } }, "eslint-plugin-import": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.6.1.tgz", "integrity": "sha512-aAMb32eHCQaQmgdb1MOG1hfu/rPiNgGur2IF71VJeDfTXdLpPiKALKWlzxMdcxQOZZ2CmYVKabAxCvjACxH1uQ==", "dev": true, + "requires": { + "builtin-modules": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "contains-path": "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz", + "debug": "2.6.8", + "doctrine": "1.5.0", + "eslint-import-resolver-node": "0.3.1", + "eslint-module-utils": "2.1.1", + "has": "https://registry.npmjs.org/has/-/has-1.0.1.tgz", + "lodash.cond": "https://registry.npmjs.org/lodash.cond/-/lodash.cond-4.5.2.tgz", + "minimatch": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "read-pkg-up": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz" + }, "dependencies": { "debug": { "version": "2.6.8", @@ -2621,11 +3075,17 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-5.2.1.tgz", "integrity": "sha1-gN8yU8TXkBBF7If6ZgooTjK9yik=", "dev": true, + "requires": { + "ignore": "3.3.7", + "minimatch": "3.0.4", + "resolve": "1.4.0", + "semver": "5.3.0" + }, "dependencies": { "ignore": { "version": "3.3.7", "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.7.tgz", - "integrity": "sha512-YGG3ejvBNHRqu0559EOxxNFihD0AjpvHlC/pdGKd3X3ofe+CoJkYazwNJYTNebqpPKN+VVQbh4ZFn1DivMNuHA==", + "integrity": "sha1-YSKJv7PCIOGGpYEYYY1b6MG6sCE=", "dev": true }, "minimatch": { @@ -2661,7 +3121,12 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.1.0.tgz", "integrity": "sha1-J3cKzzn1/UnNCvQIPOWBBOs5DUw=", - "dev": true + "dev": true, + "requires": { + "doctrine": "https://registry.npmjs.org/doctrine/-/doctrine-2.0.0.tgz", + "has": "https://registry.npmjs.org/has/-/has-1.0.1.tgz", + "jsx-ast-utils": "1.4.1" + } }, "eslint-plugin-standard": { "version": "3.0.1", @@ -2673,7 +3138,11 @@ "version": "3.5.0", "resolved": "https://registry.npmjs.org/espree/-/espree-3.5.0.tgz", "integrity": "sha1-mDWGJb3QVYYeon4oZ+pyn69GPY0=", - "dev": true + "dev": true, + "requires": { + "acorn": "5.1.1", + "acorn-jsx": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz" + } }, "esprima": { "version": "4.0.0", @@ -2694,7 +3163,10 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/ev-store/-/ev-store-7.0.0.tgz", "integrity": "sha1-GrDH+CE2UF3XSzHRdwHLK+bSZVg=", - "dev": true + "dev": true, + "requires": { + "individual": "3.0.0" + } }, "event-kit": { "version": "2.4.0", @@ -2704,27 +3176,54 @@ "execa": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", - "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=" + "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", + "requires": { + "cross-spawn": "5.1.0", + "get-stream": "3.0.0", + "is-stream": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "npm-run-path": "2.0.2", + "p-finally": "1.0.0", + "signal-exit": "3.0.2", + "strip-eof": "1.0.0" + } }, "expand-brackets": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "requires": { + "debug": "2.6.8", + "define-property": "0.2.5", + "extend-shallow": "2.0.1", + "posix-character-classes": "0.1.1", + "regex-not": "1.0.2", + "snapdragon": "0.8.1", + "to-regex": "3.0.2" + }, "dependencies": { "define-property": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=" + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "0.1.6" + } }, "is-accessor-descriptor": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "requires": { + "kind-of": "3.2.2" + }, "dependencies": { "kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=" + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "1.1.6" + } } } }, @@ -2732,18 +3231,29 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "requires": { + "kind-of": "3.2.2" + }, "dependencies": { "kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=" + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "1.1.6" + } } } }, "is-descriptor": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha1-Nm2CQN3kh8pRgjsaufB6EKeCUco=" + "integrity": "sha1-Nm2CQN3kh8pRgjsaufB6EKeCUco=", + "requires": { + "is-accessor-descriptor": "0.1.6", + "is-data-descriptor": "0.1.4", + "kind-of": "5.1.0" + } }, "kind-of": { "version": "5.1.0", @@ -2759,17 +3269,33 @@ "extend-shallow": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=" + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "0.1.1" + } }, "extglob": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", "integrity": "sha1-rQD+TcYSqSMuhxhxHcXLWrAoVUM=", + "requires": { + "array-unique": "0.3.2", + "define-property": "1.0.0", + "expand-brackets": "2.1.4", + "extend-shallow": "2.0.1", + "fragment-cache": "0.2.1", + "regex-not": "1.0.2", + "snapdragon": "0.8.1", + "to-regex": "3.0.2" + }, "dependencies": { "define-property": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=" + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "requires": { + "is-descriptor": "1.0.2" + } } } }, @@ -2778,20 +3304,48 @@ "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" }, + "fast-deep-equal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", + "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=" + }, "fast-glob": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-1.0.1.tgz", - "integrity": "sha1-MPmxEg/Ven8XI2SmRY+9vZgYezw=" + "integrity": "sha1-MPmxEg/Ven8XI2SmRY+9vZgYezw=", + "requires": { + "bash-glob": "1.0.2", + "glob-parent": "3.1.0", + "micromatch": "3.1.9", + "readdir-enhanced": "1.5.2" + } + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" }, "fb-watchman": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.0.tgz", - "integrity": "sha1-VOmr99+i8mzZsWNsWIwa/AXeXVg=" + "integrity": "sha1-VOmr99+i8mzZsWNsWIwa/AXeXVg=", + "requires": { + "bser": "2.0.0" + } }, "fbjs": { "version": "0.8.14", "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.14.tgz", "integrity": "sha1-0dviviVMNakeCfMfnNUKQLKg7Rw=", + "requires": { + "core-js": "1.2.7", + "isomorphic-fetch": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz", + "loose-envify": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz", + "object-assign": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "promise": "7.3.1", + "setimmediate": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "ua-parser-js": "0.7.14" + }, "dependencies": { "core-js": { "version": "1.2.7", @@ -2803,7 +3357,13 @@ "fill-range": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=" + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "requires": { + "extend-shallow": "2.0.1", + "is-number": "3.0.0", + "repeat-string": "1.6.1", + "to-regex-range": "2.1.1" + } }, "find-up": { "version": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", @@ -2825,39 +3385,54 @@ "version": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" }, + "form-data": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz", + "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", + "requires": { + "asynckit": "0.4.0", + "combined-stream": "1.0.6", + "mime-types": "2.1.16" + } + }, "formatio": { "version": "https://registry.npmjs.org/formatio/-/formatio-1.2.0.tgz", "integrity": "sha1-87IWfZBoxGmKjVH092CjmlTYGOs=", - "dev": true + "dev": true, + "requires": { + "samsam": "https://registry.npmjs.org/samsam/-/samsam-1.2.1.tgz" + } }, "fragment-cache": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", - "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=" + "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "requires": { + "map-cache": "0.2.2" + } }, "fs-extra": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-5.0.0.tgz", - "integrity": "sha1-QU0BEM3QZwVzTQVWUsVBEmDDGr0=" + "integrity": "sha1-QU0BEM3QZwVzTQVWUsVBEmDDGr0=", + "requires": { + "graceful-fs": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "jsonfile": "4.0.0", + "universalify": "0.1.1" + } + }, + "fs-minipass": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.5.tgz", + "integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==", + "requires": { + "minipass": "2.2.1" + } }, "fs.realpath": { "version": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - }, - "fstream": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz", - "integrity": "sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=", - "dependencies": { - "rimraf": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", - "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", - "requires": { - "glob": "7.1.2" - } - } - } + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true }, "function-bind": { "version": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.0.tgz", @@ -2867,8 +3442,13 @@ "function.prototype.name": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.0.3.tgz", - "integrity": "sha512-5EblxZUdioXi2JiMZ9FUbwYj40eQ9MFHyzFLBSPdlRl3SO8l7SLWuAnQ/at/1Wi4hjJwME/C5WpF2ZfAc8nGNw==", + "integrity": "sha1-AJmuVXLp3W8DyX0CP9krzF5jnqw=", "dev": true, + "requires": { + "define-properties": "1.1.2", + "function-bind": "1.1.1", + "is-callable": "1.1.3" + }, "dependencies": { "function-bind": { "version": "1.1.1", @@ -2897,6 +3477,9 @@ "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "requires": { + "assert-plus": "1.0.0" + }, "dependencies": { "assert-plus": { "version": "1.0.0", @@ -2908,17 +3491,33 @@ "glob": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=" + "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=", + "dev": true, + "requires": { + "fs.realpath": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "inflight": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "inherits": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "minimatch": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "once": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "path-is-absolute": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" + } }, "glob-parent": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "requires": { + "is-glob": "3.1.0", + "path-dirname": "1.0.2" + }, "dependencies": { "is-glob": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=" + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "requires": { + "is-extglob": "2.1.1" + } } } }, @@ -2931,7 +3530,11 @@ "version": "4.3.2", "resolved": "https://registry.npmjs.org/global/-/global-4.3.2.tgz", "integrity": "sha1-52mJJopsdMOJCLEwWxD8DjlOnQ8=", - "dev": true + "dev": true, + "requires": { + "min-document": "2.19.0", + "process": "0.5.2" + } }, "globals": { "version": "9.18.0", @@ -2951,6 +3554,9 @@ "version": "0.10.3", "resolved": "https://registry.npmjs.org/graphql/-/graphql-0.10.3.tgz", "integrity": "sha1-wxOv1VGOZzNRvuGPtj4qDkh0B6s=", + "requires": { + "iterall": "1.1.1" + }, "dependencies": { "iterall": { "version": "1.1.1", @@ -2964,37 +3570,83 @@ "integrity": "sha1-Dqd0NxXbjY3ixe3hd14bRayFwC8=", "dev": true }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + }, + "har-validator": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", + "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", + "requires": { + "ajv": "5.5.2", + "har-schema": "2.0.0" + } + }, "has": { "version": "https://registry.npmjs.org/has/-/has-1.0.1.tgz", "integrity": "sha1-hGFzP1OLCDfJNh45qauelwTcLyg=", - "dev": true + "dev": true, + "requires": { + "function-bind": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.0.tgz" + } }, "has-ansi": { "version": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=" + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "requires": { + "ansi-regex": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz" + } }, "has-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", - "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=" + "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "requires": { + "get-value": "2.0.6", + "has-values": "1.0.0", + "isobject": "3.0.1" + } }, "has-values": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", + "requires": { + "is-number": "3.0.0", + "kind-of": "4.0.0" + }, "dependencies": { "kind-of": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=" + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "requires": { + "is-buffer": "1.1.6" + } } } }, + "hawk": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz", + "integrity": "sha512-miowhl2+U7Qle4vdLqDdPt9m09K6yZhkLDTWGoUiUzrQCn+mHHSmfJgAyGaLRZbPmTqfFFjRV1QWCW0VWUJBbQ==", + "requires": { + "boom": "4.3.1", + "cryptiles": "3.1.2", + "hoek": "4.2.1", + "sntp": "2.1.0" + } + }, "hock": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/hock/-/hock-1.3.2.tgz", "integrity": "sha1-btPovkK0ZnmBGNEhUKqA6NbvIhk=", "dev": true, + "requires": { + "deep-equal": "0.2.1" + }, "dependencies": { "deep-equal": { "version": "0.2.1", @@ -3004,6 +3656,11 @@ } } }, + "hoek": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz", + "integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==" + }, "hoist-non-react-statics": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-1.2.0.tgz", @@ -3012,7 +3669,11 @@ "home-or-tmp": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz", - "integrity": "sha1-42w/LSyufXRqhX440Y1fMqeILbg=" + "integrity": "sha1-42w/LSyufXRqhX440Y1fMqeILbg=", + "requires": { + "os-homedir": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "os-tmpdir": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz" + } }, "hosted-git-info": { "version": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.5.0.tgz", @@ -3022,12 +3683,30 @@ "version": "3.9.2", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.9.2.tgz", "integrity": "sha1-G9+HrMoPP55T+k/M6w9LTLsAszg=", - "dev": true + "dev": true, + "requires": { + "domelementtype": "1.3.0", + "domhandler": "2.4.1", + "domutils": "1.5.1", + "entities": "1.1.1", + "inherits": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "readable-stream": "2.3.3" + } }, - "iconv-lite": { - "version": "0.4.18", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.18.tgz", - "integrity": "sha512-sr1ZQph3UwHTR0XftSbK85OvBbxe/abLGzEnPENCQwmHf7sck8Oyu4ob3LgBxWWxRoM+QszeUyl7jbqapu2TqA==" + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "requires": { + "assert-plus": "1.0.0", + "jsprim": "1.4.1", + "sshpk": "1.13.1" + } + }, + "iconv-lite": { + "version": "0.4.18", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.18.tgz", + "integrity": "sha1-I9hlaxaq5nQqwpcy6o8DNqR4nPI=" }, "ignore": { "version": "3.3.3", @@ -3055,11 +3734,17 @@ }, "inflight": { "version": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=" + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "wrappy": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" + } }, "inherits": { "version": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true }, "interpret": { "version": "https://registry.npmjs.org/interpret/-/interpret-1.0.3.tgz", @@ -3078,7 +3763,10 @@ "is-accessor-descriptor": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha1-FpwvbT3x+ZJhgHI2XJsOofaHhlY=" + "integrity": "sha1-FpwvbT3x+ZJhgHI2XJsOofaHhlY=", + "requires": { + "kind-of": "6.0.2" + } }, "is-arrayish": { "version": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -3091,7 +3779,10 @@ }, "is-builtin-module": { "version": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", - "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=" + "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", + "requires": { + "builtin-modules": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz" + } }, "is-callable": { "version": "1.1.3", @@ -3102,7 +3793,10 @@ "is-data-descriptor": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha1-2Eh2Mh0Oet0DmQQGq7u9NrqSaMc=" + "integrity": "sha1-2Eh2Mh0Oet0DmQQGq7u9NrqSaMc=", + "requires": { + "kind-of": "6.0.2" + } }, "is-date-object": { "version": "1.0.1", @@ -3113,7 +3807,12 @@ "is-descriptor": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha1-OxWXRqZmBLBPjIFSS6NlxfFNhuw=" + "integrity": "sha1-OxWXRqZmBLBPjIFSS6NlxfFNhuw=", + "requires": { + "is-accessor-descriptor": "1.0.0", + "is-data-descriptor": "1.0.0", + "kind-of": "6.0.2" + } }, "is-extendable": { "version": "0.1.1", @@ -3127,22 +3826,34 @@ }, "is-finite": { "version": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", - "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=" + "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", + "requires": { + "number-is-nan": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz" + } }, "is-glob": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.0.tgz", - "integrity": "sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=" + "integrity": "sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=", + "requires": { + "is-extglob": "2.1.1" + } }, "is-number": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "requires": { + "kind-of": "3.2.2" + }, "dependencies": { "kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=" + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "1.1.6" + } } } }, @@ -3156,6 +3867,9 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-odd/-/is-odd-2.0.0.tgz", "integrity": "sha1-dkZiRnH9fqVYzNmieVGC8pWPGyQ=", + "requires": { + "is-number": "4.0.0" + }, "dependencies": { "is-number": { "version": "4.0.0", @@ -3167,13 +3881,19 @@ "is-plain-object": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha1-LBY7P6+xtgbZ0Xko8FwqHDjgdnc=" + "integrity": "sha1-LBY7P6+xtgbZ0Xko8FwqHDjgdnc=", + "requires": { + "isobject": "3.0.1" + } }, "is-regex": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", - "dev": true + "dev": true, + "requires": { + "has": "https://registry.npmjs.org/has/-/has-1.0.1.tgz" + } }, "is-stream": { "version": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", @@ -3216,7 +3936,11 @@ }, "isomorphic-fetch": { "version": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz", - "integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=" + "integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=", + "requires": { + "node-fetch": "1.7.1", + "whatwg-fetch": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz" + } }, "isstream": { "version": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", @@ -3232,6 +3956,10 @@ "resolved": "https://registry.npmjs.org/jade/-/jade-0.26.3.tgz", "integrity": "sha1-jxDXl32NefL2/4YqgbBRPMslaGw=", "dev": true, + "requires": { + "commander": "0.6.1", + "mkdirp": "0.3.0" + }, "dependencies": { "commander": { "version": "0.6.1", @@ -3255,8 +3983,12 @@ "js-yaml": { "version": "3.9.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.9.1.tgz", - "integrity": "sha512-CbcG379L1e+mWBnLvHWWeLs8GyV/EMw862uLI3c+GxVyDHWZcjZinwuBd3iW2pgxgIlksW/1vNJa4to+RvDOww==", - "dev": true + "integrity": "sha1-CHdc69/dNZIJ8NKs04PI+GppBKA=", + "dev": true, + "requires": { + "argparse": "https://registry.npmjs.org/argparse/-/argparse-1.0.9.tgz", + "esprima": "4.0.0" + } }, "jsbn": { "version": "0.1.1", @@ -3273,11 +4005,19 @@ "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" }, + "json-schema-traverse": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", + "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" + }, "json-stable-stringify": { "version": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", "dev": true, - "optional": true + "optional": true, + "requires": { + "jsonify": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz" + } }, "json-stringify-safe": { "version": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", @@ -3296,7 +4036,10 @@ "jsonfile": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=" + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "requires": { + "graceful-fs": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz" + } }, "jsonify": { "version": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", @@ -3308,6 +4051,12 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + }, "dependencies": { "assert-plus": { "version": "1.0.0", @@ -3325,7 +4074,10 @@ "keytar": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/keytar/-/keytar-4.1.0.tgz", - "integrity": "sha512-L3KqiSMtpVitlug4uuI+K5XLne9SAVEFWE8SCQIhQiH0IA/CTbon5v5prVLKK0Ken54o2O8V9HceKagpwJum+Q==" + "integrity": "sha1-njkz5InWVt4aho4Sk3CTEwRJidc=", + "requires": { + "nan": "2.5.1" + } }, "kind-of": { "version": "6.0.2", @@ -3335,25 +4087,45 @@ "lazy-cache": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-2.0.2.tgz", - "integrity": "sha1-uRkKT5EzVGlIQIWfio9whNiCImQ=" + "integrity": "sha1-uRkKT5EzVGlIQIWfio9whNiCImQ=", + "requires": { + "set-getter": "0.1.0" + } }, "lcid": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", - "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=" + "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", + "requires": { + "invert-kv": "1.0.0" + } }, "less": { "version": "2.7.3", "resolved": "https://registry.npmjs.org/less/-/less-2.7.3.tgz", "integrity": "sha1-zBJg9RyQCp7A2R+2mYE54CUHtjs=", "dev": true, + "requires": { + "errno": "0.1.4", + "graceful-fs": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "image-size": "0.5.5", + "mime": "1.4.1", + "mkdirp": "0.5.1", + "promise": "7.3.1", + "request": "2.81.0", + "source-map": "0.5.7" + }, "dependencies": { "ajv": { "version": "4.11.8", "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=", "dev": true, - "optional": true + "optional": true, + "requires": { + "co": "4.6.0", + "json-stable-stringify": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz" + } }, "assert-plus": { "version": "0.2.0", @@ -3373,14 +4145,20 @@ "version": "2.10.1", "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", - "dev": true + "dev": true, + "requires": { + "hoek": "2.16.3" + } }, "cryptiles": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=", "dev": true, - "optional": true + "optional": true, + "requires": { + "boom": "2.10.1" + } }, "form-data": { "version": "2.1.4", @@ -3388,6 +4166,11 @@ "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=", "dev": true, "optional": true, + "requires": { + "asynckit": "0.4.0", + "combined-stream": "1.0.6", + "mime-types": "2.1.16" + }, "dependencies": { "combined-stream": { "version": "1.0.6", @@ -3413,14 +4196,24 @@ "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-4.2.1.tgz", "integrity": "sha1-M0gdDxu/9gDdID11gSpqX7oALio=", "dev": true, - "optional": true + "optional": true, + "requires": { + "ajv": "4.11.8", + "har-schema": "1.0.5" + } }, "hawk": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=", "dev": true, - "optional": true + "optional": true, + "requires": { + "boom": "2.10.1", + "cryptiles": "2.0.5", + "hoek": "2.16.3", + "sntp": "1.0.9" + } }, "hoek": { "version": "2.16.3", @@ -3433,7 +4226,12 @@ "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=", "dev": true, - "optional": true + "optional": true, + "requires": { + "assert-plus": "0.2.0", + "jsprim": "1.4.1", + "sshpk": "1.13.1" + } }, "mime": { "version": "1.4.1", @@ -3462,6 +4260,30 @@ "integrity": "sha1-xpKJRqDgbF+Nb4qTM0af/aRimKA=", "dev": true, "optional": true, + "requires": { + "aws-sign2": "0.6.0", + "aws4": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", + "caseless": "0.12.0", + "combined-stream": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", + "extend": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", + "forever-agent": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "form-data": "2.1.4", + "har-validator": "4.2.1", + "hawk": "3.1.3", + "http-signature": "1.1.1", + "is-typedarray": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "isstream": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "json-stringify-safe": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "mime-types": "2.1.16", + "oauth-sign": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", + "performance-now": "0.2.0", + "qs": "6.4.0", + "safe-buffer": "5.1.1", + "stringstream": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", + "tough-cookie": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.2.tgz", + "tunnel-agent": "0.6.0", + "uuid": "3.1.0" + }, "dependencies": { "combined-stream": { "version": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", @@ -3478,7 +4300,7 @@ "dev": true, "optional": true, "requires": { - "punycode": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz" + "punycode": "1.4.1" } } } @@ -3488,7 +4310,10 @@ "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=", "dev": true, - "optional": true + "optional": true, + "requires": { + "hoek": "2.16.3" + } }, "source-map": { "version": "0.5.7", @@ -3501,7 +4326,13 @@ }, "load-json-file": { "version": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", - "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=" + "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", + "requires": { + "graceful-fs": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "parse-json": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "pify": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "strip-bom": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz" + } }, "lodash": { "version": "4.17.5", @@ -3511,7 +4342,11 @@ "lodash._baseassign": { "version": "https://registry.npmjs.org/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz", "integrity": "sha1-jDigmVAPIVrQnlnxci/QxSv+Ck4=", - "dev": true + "dev": true, + "requires": { + "lodash._basecopy": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz", + "lodash.keys": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz" + } }, "lodash._basecopy": { "version": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz", @@ -3553,7 +4388,12 @@ "lodash.create": { "version": "https://registry.npmjs.org/lodash.create/-/lodash.create-3.1.1.tgz", "integrity": "sha1-1/KEnw29p+BGgruM1yqwIkYd6+c=", - "dev": true + "dev": true, + "requires": { + "lodash._baseassign": "https://registry.npmjs.org/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz", + "lodash._basecreate": "https://registry.npmjs.org/lodash._basecreate/-/lodash._basecreate-3.0.3.tgz", + "lodash._isiterateecall": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz" + } }, "lodash.defaults": { "version": "4.2.0", @@ -3597,7 +4437,12 @@ "lodash.keys": { "version": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=", - "dev": true + "dev": true, + "requires": { + "lodash._getnative": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", + "lodash.isarguments": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "lodash.isarray": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz" + } }, "lodash.map": { "version": "4.6.0", @@ -3647,7 +4492,10 @@ }, "loose-envify": { "version": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz", - "integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=" + "integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=", + "requires": { + "js-tokens": "3.0.2" + } }, "lru-cache": { "version": "2.7.3", @@ -3663,41 +4511,70 @@ "map-visit": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", - "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=" + "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "requires": { + "object-visit": "1.0.1" + } }, "mem": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/mem/-/mem-1.1.0.tgz", - "integrity": "sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y=" + "integrity": "sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y=", + "requires": { + "mimic-fn": "1.2.0" + } }, "micromatch": { "version": "3.1.9", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.9.tgz", "integrity": "sha1-FdyTF1rjnlLpMIeEcJbv/HPvz4k=", + "requires": { + "arr-diff": "4.0.0", + "array-unique": "0.3.2", + "braces": "2.3.1", + "define-property": "2.0.2", + "extend-shallow": "3.0.2", + "extglob": "2.0.4", + "fragment-cache": "0.2.1", + "kind-of": "6.0.2", + "nanomatch": "1.2.9", + "object.pick": "1.3.0", + "regex-not": "1.0.2", + "snapdragon": "0.8.1", + "to-regex": "3.0.2" + }, "dependencies": { "extend-shallow": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=" + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "requires": { + "assign-symbols": "1.0.0", + "is-extendable": "1.0.1" + } }, "is-extendable": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha1-p0cPnkJnM9gb2B4RVSZOOjUHyrQ=" + "integrity": "sha1-p0cPnkJnM9gb2B4RVSZOOjUHyrQ=", + "requires": { + "is-plain-object": "2.0.4" + } } } }, "mime-db": { "version": "1.29.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.29.0.tgz", - "integrity": "sha1-SNJtI1WJZRcErFkWygYAGRQmaHg=", - "dev": true + "integrity": "sha1-SNJtI1WJZRcErFkWygYAGRQmaHg=" }, "mime-types": { "version": "2.1.16", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.16.tgz", "integrity": "sha1-K4WKUuXs1RbbiXrCvodIeDBpjiM=", - "dev": true + "requires": { + "mime-db": "1.29.0" + } }, "mimic-fn": { "version": "1.2.0", @@ -3708,21 +4585,50 @@ "version": "2.19.0", "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz", "integrity": "sha1-e9KC4/WELtKVu3SM3Z8f+iyCRoU=", - "dev": true + "dev": true, + "requires": { + "dom-walk": "0.1.1" + } }, "minimatch": { "version": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=" + "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=", + "requires": { + "brace-expansion": "1.1.8" + } + }, + "minipass": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.2.1.tgz", + "integrity": "sha512-u1aUllxPJUI07cOqzR7reGmQxmCqlH88uIIsf6XZFEWgw7gXKpJdR+5R9Y3KEDmWYkdIz9wXZs3C0jOPxejk/Q==", + "requires": { + "yallist": "3.0.2" + } + }, + "minizlib": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.1.0.tgz", + "integrity": "sha512-4T6Ur/GctZ27nHfpt9THOdRZNgyJ9FZchYO1ceg5S8Q3DNLCKYy44nCZzgCJgcvx2UM8czmqak5BCxJMrq37lA==", + "requires": { + "minipass": "2.2.1" + } }, "mixin-deep": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz", "integrity": "sha1-pJ5yaNzhoNlpjkUybFYm3zVD0P4=", + "requires": { + "for-in": "1.0.2", + "is-extendable": "1.0.1" + }, "dependencies": { "is-extendable": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha1-p0cPnkJnM9gb2B4RVSZOOjUHyrQ=" + "integrity": "sha1-p0cPnkJnM9gb2B4RVSZOOjUHyrQ=", + "requires": { + "is-plain-object": "2.0.4" + } } } }, @@ -3730,6 +4636,9 @@ "version": "0.5.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "requires": { + "minimist": "0.0.8" + }, "dependencies": { "minimist": { "version": "0.0.8", @@ -3743,6 +4652,19 @@ "resolved": "https://registry.npmjs.org/mocha/-/mocha-3.4.2.tgz", "integrity": "sha1-0O9NMyEm2/GNDWQMmzgt1IvpdZQ=", "dev": true, + "requires": { + "browser-stdout": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.0.tgz", + "commander": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", + "debug": "2.6.0", + "diff": "3.2.0", + "escape-string-regexp": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "glob": "7.1.1", + "growl": "https://registry.npmjs.org/growl/-/growl-1.9.2.tgz", + "json3": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz", + "lodash.create": "https://registry.npmjs.org/lodash.create/-/lodash.create-3.1.1.tgz", + "mkdirp": "0.5.1", + "supports-color": "3.1.2" + }, "dependencies": { "debug": { "version": "2.6.0", @@ -3779,7 +4701,15 @@ "integrity": "sha1-cqJiiU2dQIuVbKBf83su2KbiotU=", "dev": true, "requires": { - "has-flag": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz" + "has-flag": "1.0.0" + }, + "dependencies": { + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", + "dev": true + } } } } @@ -3789,6 +4719,9 @@ "resolved": "https://registry.npmjs.org/mocha-appveyor-reporter/-/mocha-appveyor-reporter-0.4.0.tgz", "integrity": "sha1-gpOC/8Bla2Z+e+ZQoJSgba69Uk8=", "dev": true, + "requires": { + "request-json": "0.6.3" + }, "dependencies": { "depd": { "version": "1.1.2", @@ -3802,8 +4735,7 @@ "integrity": "sha512-5TVnMD3LaeK0GRCyFlsNgJf5Fjg8J8j7VEfsoJESSWZlWRgPIf7IojsBLbTHcg2798JrrRkJ6L3k1+wj4sglgw==", "dev": true, "requires": { - "depd": "1.1.2", - "request": "2.83.0" + "depd": "1.1.2" } } } @@ -3813,6 +4745,12 @@ "resolved": "https://registry.npmjs.org/mocha-junit-and-console-reporter/-/mocha-junit-and-console-reporter-1.6.0.tgz", "integrity": "sha1-kBmEuev51g9xXvDo4EwG5KsSJFc=", "dev": true, + "requires": { + "debug": "2.6.8", + "mkdirp": "0.5.1", + "mocha": "2.5.3", + "xml": "1.0.1" + }, "dependencies": { "commander": { "version": "2.3.0", @@ -3845,25 +4783,48 @@ "version": "3.2.11", "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz", "integrity": "sha1-Spc/Y1uRkPcV0QmH1cAP0oFevj0=", - "dev": true + "dev": true, + "requires": { + "inherits": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "minimatch": "0.3.0" + } }, "minimatch": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz", "integrity": "sha1-J12O2qxPG7MyZHIInnlJyDlGmd0=", - "dev": true + "dev": true, + "requires": { + "lru-cache": "2.7.3", + "sigmund": "1.0.1" + } }, "mocha": { "version": "2.5.3", "resolved": "https://registry.npmjs.org/mocha/-/mocha-2.5.3.tgz", "integrity": "sha1-FhvlvetJZ3HrmzV0UFC2IrWu/Fg=", "dev": true, + "requires": { + "commander": "2.3.0", + "debug": "2.2.0", + "diff": "1.4.0", + "escape-string-regexp": "1.0.2", + "glob": "3.2.11", + "growl": "https://registry.npmjs.org/growl/-/growl-1.9.2.tgz", + "jade": "0.26.3", + "mkdirp": "0.5.1", + "supports-color": "1.2.0", + "to-iso-string": "0.0.2" + }, "dependencies": { "debug": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", - "dev": true + "dev": true, + "requires": { + "ms": "0.7.1" + } }, "ms": { "version": "0.7.1", @@ -3917,16 +4878,37 @@ "version": "1.2.9", "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.9.tgz", "integrity": "sha1-h59xUMstq3pHElkGbBBO7m4Pp8I=", + "requires": { + "arr-diff": "4.0.0", + "array-unique": "0.3.2", + "define-property": "2.0.2", + "extend-shallow": "3.0.2", + "fragment-cache": "0.2.1", + "is-odd": "2.0.0", + "is-windows": "1.0.2", + "kind-of": "6.0.2", + "object.pick": "1.3.0", + "regex-not": "1.0.2", + "snapdragon": "0.8.1", + "to-regex": "3.0.2" + }, "dependencies": { "extend-shallow": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=" + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "requires": { + "assign-symbols": "1.0.0", + "is-extendable": "1.0.1" + } }, "is-extendable": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha1-p0cPnkJnM9gb2B4RVSZOOjUHyrQ=" + "integrity": "sha1-p0cPnkJnM9gb2B4RVSZOOjUHyrQ=", + "requires": { + "is-plain-object": "2.0.4" + } } } }, @@ -3944,7 +4926,11 @@ "node-fetch": { "version": "1.7.1", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.1.tgz", - "integrity": "sha512-j8XsFGCLw79vWXkZtMSmmLaOk9z5SQ9bV/tkbZVCqvgwzrjAGq66igobLofHtF63NvMTp2WjytpsNTGKa+XRIQ==" + "integrity": "sha512-j8XsFGCLw79vWXkZtMSmmLaOk9z5SQ9bV/tkbZVCqvgwzrjAGq66igobLofHtF63NvMTp2WjytpsNTGKa+XRIQ==", + "requires": { + "encoding": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", + "is-stream": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz" + } }, "node-int64": { "version": "0.4.0", @@ -3953,18 +4939,30 @@ }, "normalize-package-data": { "version": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", - "integrity": "sha1-EvlaMH1YNSB1oEkHuErIvpisAS8=" + "integrity": "sha1-EvlaMH1YNSB1oEkHuErIvpisAS8=", + "requires": { + "hosted-git-info": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.5.0.tgz", + "is-builtin-module": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", + "semver": "5.4.1", + "validate-npm-package-license": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz" + } }, "npm-run-path": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=" + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "requires": { + "path-key": "2.0.1" + } }, "nth-check": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.1.tgz", "integrity": "sha1-mSms32KPwsQQmN6rgqxYDPFJquQ=", - "dev": true + "dev": true, + "requires": { + "boolbase": "1.0.0" + } }, "number-is-nan": { "version": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", @@ -3982,26 +4980,45 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "requires": { + "copy-descriptor": "0.1.1", + "define-property": "0.2.5", + "kind-of": "3.2.2" + }, "dependencies": { "define-property": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=" + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "0.1.6" + } }, "is-accessor-descriptor": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=" + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "requires": { + "kind-of": "3.2.2" + } }, "is-data-descriptor": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=" + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "requires": { + "kind-of": "3.2.2" + } }, "is-descriptor": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", "integrity": "sha1-Nm2CQN3kh8pRgjsaufB6EKeCUco=", + "requires": { + "is-accessor-descriptor": "0.1.6", + "is-data-descriptor": "0.1.4", + "kind-of": "5.1.0" + }, "dependencies": { "kind-of": { "version": "5.1.0", @@ -4013,7 +5030,10 @@ "kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=" + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "1.1.6" + } } } }, @@ -4032,13 +5052,21 @@ "object-visit": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", - "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=" + "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "requires": { + "isobject": "3.0.1" + } }, "object.assign": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.0.4.tgz", "integrity": "sha1-scnMBE7xuf5jYG/BQau7MuFHMMw=", "dev": true, + "requires": { + "define-properties": "1.1.2", + "function-bind": "1.1.1", + "object-keys": "1.0.11" + }, "dependencies": { "function-bind": { "version": "1.1.1", @@ -4053,6 +5081,12 @@ "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.0.4.tgz", "integrity": "sha1-G/mk3SKI9bM/Opk9JXZh8F0WGl8=", "dev": true, + "requires": { + "define-properties": "1.1.2", + "es-abstract": "1.9.0", + "function-bind": "1.1.1", + "has": "https://registry.npmjs.org/has/-/has-1.0.1.tgz" + }, "dependencies": { "function-bind": { "version": "1.1.1", @@ -4065,13 +5099,22 @@ "object.pick": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=" + "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "requires": { + "isobject": "3.0.1" + } }, "object.values": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.0.4.tgz", "integrity": "sha1-5STaCbT2b/Bd9FdUbscqyZ8TBpo=", "dev": true, + "requires": { + "define-properties": "1.1.2", + "es-abstract": "1.9.0", + "function-bind": "1.1.1", + "has": "https://registry.npmjs.org/has/-/has-1.0.1.tgz" + }, "dependencies": { "function-bind": { "version": "1.1.1", @@ -4083,12 +5126,19 @@ }, "once": { "version": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=" + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" + } }, "optimist": { "version": "0.3.7", "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.3.7.tgz", - "integrity": "sha1-yQlBrVnkJzMokjB00s8ufLxuwNk=" + "integrity": "sha1-yQlBrVnkJzMokjB00s8ufLxuwNk=", + "requires": { + "wordwrap": "0.0.3" + } }, "os-homedir": { "version": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", @@ -4097,7 +5147,12 @@ "os-locale": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-2.1.0.tgz", - "integrity": "sha1-QrwpAKa1uL0XN2yOiCtlr8zyS/I=" + "integrity": "sha1-QrwpAKa1uL0XN2yOiCtlr8zyS/I=", + "requires": { + "execa": "0.7.0", + "lcid": "1.0.0", + "mem": "1.1.0" + } }, "os-tmpdir": { "version": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", @@ -4110,7 +5165,10 @@ }, "parse-json": { "version": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", - "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=" + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "requires": { + "error-ex": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz" + } }, "pascalcase": { "version": "0.1.1", @@ -4146,6 +5204,9 @@ "version": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", "dev": true, + "requires": { + "isarray": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" + }, "dependencies": { "isarray": { "version": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", @@ -4156,7 +5217,15 @@ }, "path-type": { "version": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", - "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=" + "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", + "requires": { + "pify": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz" + } + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" }, "pify": { "version": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", @@ -4164,18 +5233,22 @@ }, "pinkie": { "version": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", - "dev": true + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=" }, "pinkie-promise": { "version": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", - "dev": true + "requires": { + "pinkie": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz" + } }, "pkg-dir": { "version": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-1.0.0.tgz", "integrity": "sha1-ektQio1bstYp1EcFb/TpyTFM89Q=", - "dev": true + "dev": true, + "requires": { + "find-up": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz" + } }, "posix-character-classes": { "version": "0.1.1", @@ -4206,12 +5279,20 @@ "promise": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", - "integrity": "sha1-BktyYCsY+Q8pGSuLG8QY/9Hr078=" + "integrity": "sha1-BktyYCsY+Q8pGSuLG8QY/9Hr078=", + "requires": { + "asap": "2.0.6" + } }, "prop-types": { "version": "15.6.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.1.tgz", "integrity": "sha1-NmREU1ZCVd3aORGR+zoSXL32VMo=", + "requires": { + "fbjs": "0.8.16", + "loose-envify": "1.3.1", + "object-assign": "4.1.1" + }, "dependencies": { "core-js": { "version": "1.2.7", @@ -4273,20 +5354,38 @@ "dev": true, "optional": true }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + }, + "qs": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", + "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" + }, "react": { "version": "15.6.2", "resolved": "https://registry.npmjs.org/react/-/react-15.6.2.tgz", - "integrity": "sha1-26BDSrQ5z+gvEI8PURZjkIF5qnI=" + "integrity": "sha1-26BDSrQ5z+gvEI8PURZjkIF5qnI=", + "requires": { + "create-react-class": "15.6.2", + "fbjs": "0.8.14", + "loose-envify": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz", + "object-assign": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "prop-types": "15.6.1" + } }, "react-dom": { "version": "15.6.2", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-15.6.2.tgz", - "integrity": "sha1-Qc+t9pO3V/rycIRDodH9WgK+9zA=" - }, - "react-input-autosize": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/react-input-autosize/-/react-input-autosize-2.2.1.tgz", - "integrity": "sha512-3+K4CD13iE4lQQ2WlF8PuV5htfmTRLH6MDnfndHM6LuBRszuXnuyIfE7nhSKt8AzRBZ50bu0sAhkNMeS5pxQQA==" + "integrity": "sha1-Qc+t9pO3V/rycIRDodH9WgK+9zA=", + "requires": { + "fbjs": "0.8.14", + "loose-envify": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz", + "object-assign": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "prop-types": "15.6.1" + } }, "react-input-autosize": { "version": "2.2.1", @@ -4330,14 +5429,20 @@ "react-relay": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/react-relay/-/react-relay-1.4.1.tgz", - "integrity": "sha1-Yfe1gC1wRG4VRJD50PzuyRUOvaU=" + "integrity": "sha1-Yfe1gC1wRG4VRJD50PzuyRUOvaU=", + "requires": { + "babel-runtime": "6.25.0", + "fbjs": "0.8.14", + "prop-types": "15.6.1", + "relay-runtime": "1.4.1" + } }, "react-select": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/react-select/-/react-select-1.2.1.tgz", "integrity": "sha512-vaCgT2bEl+uTyE/uKOEgzE5Dc/wLtzhnBvoHCeuLoJWc4WuadN6WQDhoL42DW+TziniZK2Gaqe/wUXydI3NSaQ==", "requires": { - "classnames": "https://registry.npmjs.org/classnames/-/classnames-2.2.5.tgz", + "classnames": "2.2.5", "prop-types": "15.6.1", "react-input-autosize": "2.2.1" }, @@ -4382,40 +5487,67 @@ "version": "15.6.2", "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-15.6.2.tgz", "integrity": "sha1-0DM0NPwsQ4CSaWyncNpe1IA376g=", - "dev": true + "dev": true, + "requires": { + "fbjs": "0.8.14", + "object-assign": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz" + } }, "read-pkg": { "version": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", - "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=" + "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", + "requires": { + "load-json-file": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", + "normalize-package-data": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", + "path-type": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz" + } }, "read-pkg-up": { "version": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", + "requires": { + "find-up": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "read-pkg": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz" + }, "dependencies": { "find-up": { "version": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==", - "requires": { - "locate-path": "2.0.0" - } + "integrity": "sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==" } } }, "readable-stream": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", - "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", - "dev": true + "integrity": "sha1-No8lEtefnUb9/HE0mueHi7weuVw=", + "dev": true, + "requires": { + "core-util-is": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "inherits": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "isarray": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "process-nextick-args": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "safe-buffer": "5.1.1", + "string_decoder": "1.0.3", + "util-deprecate": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" + } }, "readdir-enhanced": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/readdir-enhanced/-/readdir-enhanced-1.5.2.tgz", - "integrity": "sha1-YUYwSGkKxqRVt1ti+nioj43IXlM=" + "integrity": "sha1-YUYwSGkKxqRVt1ti+nioj43IXlM=", + "requires": { + "call-me-maybe": "1.0.1", + "es6-promise": "4.2.4", + "glob-to-regexp": "0.3.0" + } }, "rechoir": { "version": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", - "dev": true + "dev": true, + "requires": { + "resolve": "1.4.0" + } }, "regenerator-runtime": { "version": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz", @@ -4425,16 +5557,27 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", "integrity": "sha1-H07OJ+ALC2XgJHpoEOaoXYOldSw=", + "requires": { + "extend-shallow": "3.0.2", + "safe-regex": "1.1.0" + }, "dependencies": { "extend-shallow": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=" + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "requires": { + "assign-symbols": "1.0.0", + "is-extendable": "1.0.1" + } }, "is-extendable": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha1-p0cPnkJnM9gb2B4RVSZOOjUHyrQ=" + "integrity": "sha1-p0cPnkJnM9gb2B4RVSZOOjUHyrQ=", + "requires": { + "is-plain-object": "2.0.4" + } } } }, @@ -4442,6 +5585,24 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/relay-compiler/-/relay-compiler-1.4.1.tgz", "integrity": "sha1-EOg/D13o2z0ACFGkwOQ1590d65U=", + "requires": { + "babel-generator": "6.26.0", + "babel-polyfill": "6.26.0", + "babel-preset-fbjs": "2.1.4", + "babel-runtime": "6.25.0", + "babel-traverse": "6.26.0", + "babel-types": "6.25.0", + "babylon": "6.18.0", + "chalk": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "fast-glob": "1.0.1", + "fb-watchman": "2.0.0", + "fbjs": "0.8.14", + "graphql": "0.11.7", + "immutable": "3.7.6", + "relay-runtime": "1.4.1", + "signedsource": "1.0.0", + "yargs": "9.0.1" + }, "dependencies": { "babel-code-frame": { "version": "6.26.0", @@ -4494,12 +5655,15 @@ "babylon": { "version": "6.18.0", "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", - "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==" + "integrity": "sha1-ry87iPpvXB5MY00aD46sT1WzleM=" }, "graphql": { "version": "0.11.7", "resolved": "https://registry.npmjs.org/graphql/-/graphql-0.11.7.tgz", - "integrity": "sha1-5auqnLe3zMuE6fCDa/Q3DSaHUMY=" + "integrity": "sha1-5auqnLe3zMuE6fCDa/Q3DSaHUMY=", + "requires": { + "iterall": "1.1.3" + } }, "invariant": { "version": "2.2.3", @@ -4512,7 +5676,7 @@ "regenerator-runtime": { "version": "0.11.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", - "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" + "integrity": "sha1-vgWtf5v30i4Fb5cmzuUBf78Z4uk=" } } }, @@ -4524,7 +5688,12 @@ "relay-runtime": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/relay-runtime/-/relay-runtime-1.4.1.tgz", - "integrity": "sha1-+I3NCkInAKBFY/KR9XDkzjaONtA=" + "integrity": "sha1-+I3NCkInAKBFY/KR9XDkzjaONtA=", + "requires": { + "babel-runtime": "6.25.0", + "fbjs": "0.8.14", + "relay-debugger-react-native-runtime": "0.0.10" + } }, "repeat-element": { "version": "1.1.2", @@ -4538,7 +5707,39 @@ }, "repeating": { "version": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", - "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=" + "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", + "requires": { + "is-finite": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz" + } + }, + "request": { + "version": "2.85.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.85.0.tgz", + "integrity": "sha512-8H7Ehijd4js+s6wuVPLjwORxD4zeuyjYugprdOXlPSqaApmL/QOy+EB/beICHVCHkGMKNh5rvihb5ov+IDw4mg==", + "requires": { + "aws-sign2": "0.7.0", + "aws4": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", + "caseless": "0.12.0", + "combined-stream": "1.0.6", + "extend": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", + "forever-agent": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "form-data": "2.3.2", + "har-validator": "5.0.3", + "hawk": "6.0.2", + "http-signature": "1.2.0", + "is-typedarray": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "isstream": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "json-stringify-safe": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "mime-types": "2.1.16", + "oauth-sign": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", + "performance-now": "2.1.0", + "qs": "6.5.1", + "safe-buffer": "5.1.1", + "stringstream": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", + "tough-cookie": "2.3.4", + "tunnel-agent": "0.6.0", + "uuid": "3.1.0" + } }, "require-directory": { "version": "2.1.1", @@ -4553,8 +5754,11 @@ "resolve": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.4.0.tgz", - "integrity": "sha512-aW7sVKPufyHqOmyyLzg/J+8606v5nevBgaliIlV7nUpVMsDnoBGV/cbSLNjZAg9q0Cfd/+easKVKQ8vOu8fn1Q==", - "dev": true + "integrity": "sha1-p1vgHFPaJdk0qY69DkxKcxL5KoY=", + "dev": true, + "requires": { + "path-parse": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz" + } }, "resolve-url": { "version": "0.2.1", @@ -4579,7 +5783,10 @@ "safe-regex": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", - "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=" + "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "requires": { + "ret": "0.1.15" + } }, "samsam": { "version": "https://registry.npmjs.org/samsam/-/samsam-1.2.1.tgz", @@ -4589,7 +5796,7 @@ "semver": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", - "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==" + "integrity": "sha1-4FnAnYVx8FQII3M0M1BdOi8AsY4=" }, "set-blocking": { "version": "2.0.0", @@ -4599,12 +5806,21 @@ "set-getter": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/set-getter/-/set-getter-0.1.0.tgz", - "integrity": "sha1-12nBgsnVpR9AkUXy+6guXoboA3Y=" + "integrity": "sha1-12nBgsnVpR9AkUXy+6guXoboA3Y=", + "requires": { + "to-object-path": "0.3.0" + } }, "set-value": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz", - "integrity": "sha1-ca5KiPD+77v1LR6mBPP7MV67YnQ=" + "integrity": "sha1-ca5KiPD+77v1LR6mBPP7MV67YnQ=", + "requires": { + "extend-shallow": "2.0.1", + "is-extendable": "0.1.1", + "is-plain-object": "2.0.4", + "split-string": "3.1.0" + } }, "setimmediate": { "version": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", @@ -4613,7 +5829,10 @@ "shebang-command": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=" + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "requires": { + "shebang-regex": "1.0.0" + } }, "shebang-regex": { "version": "1.0.0", @@ -4624,7 +5843,12 @@ "version": "0.7.8", "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.7.8.tgz", "integrity": "sha1-3svPh0sNHl+3LhSxZKloMEjprLM=", - "dev": true + "dev": true, + "requires": { + "glob": "7.1.2", + "interpret": "https://registry.npmjs.org/interpret/-/interpret-1.0.3.tgz", + "rechoir": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz" + } }, "sigmund": { "version": "1.0.1", @@ -4653,6 +5877,16 @@ "resolved": "https://registry.npmjs.org/sinon/-/sinon-2.3.6.tgz", "integrity": "sha1-lTeOfg+XapcS6bRZH/WznnPcPd4=", "dev": true, + "requires": { + "diff": "3.2.0", + "formatio": "https://registry.npmjs.org/formatio/-/formatio-1.2.0.tgz", + "lolex": "https://registry.npmjs.org/lolex/-/lolex-1.6.0.tgz", + "native-promise-only": "https://registry.npmjs.org/native-promise-only/-/native-promise-only-0.8.1.tgz", + "path-to-regexp": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", + "samsam": "https://registry.npmjs.org/samsam/-/samsam-1.2.1.tgz", + "text-encoding": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.6.4.tgz", + "type-detect": "4.0.3" + }, "dependencies": { "type-detect": { "version": "4.0.3", @@ -4671,6 +5905,16 @@ "version": "0.8.1", "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.1.tgz", "integrity": "sha1-4StUh/re0+PeoKyR6UAL91tAE3A=", + "requires": { + "base": "0.11.2", + "debug": "2.6.8", + "define-property": "0.2.5", + "extend-shallow": "2.0.1", + "map-cache": "0.2.2", + "source-map": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", + "source-map-resolve": "0.5.1", + "use": "2.0.2" + }, "dependencies": { "define-property": { "version": "0.2.5", @@ -4737,11 +5981,19 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", "integrity": "sha1-bBdfhv8UvbByRWPo88GwIaKGhTs=", + "requires": { + "define-property": "1.0.0", + "isobject": "3.0.1", + "snapdragon-util": "3.0.1" + }, "dependencies": { "define-property": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=" + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "requires": { + "is-descriptor": "1.0.2" + } } } }, @@ -4749,14 +6001,28 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", "integrity": "sha1-+VZHlIbyrNeXAGk/b3uAXkWrVuI=", + "requires": { + "kind-of": "3.2.2" + }, "dependencies": { "kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=" + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "1.1.6" + } } } }, + "sntp": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-2.1.0.tgz", + "integrity": "sha512-FL1b58BDrqS3A11lJ0zEdnJ3UOKqVxawAkF3k7F0CVN7VQ34aZrV+G8BZ1WC9ZL7NyrwsW0oviwsWDgRuVYtJg==", + "requires": { + "hoek": "4.2.1" + } + }, "source-map": { "version": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", "integrity": "sha1-dc449SvwczxafwwRjYEzSiu19BI=" @@ -4764,12 +6030,22 @@ "source-map-resolve": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.1.tgz", - "integrity": "sha1-etD1k/IoFZjoVN+A8ZquS5LXoRo=" + "integrity": "sha1-etD1k/IoFZjoVN+A8ZquS5LXoRo=", + "requires": { + "atob": "2.0.3", + "decode-uri-component": "0.2.0", + "resolve-url": "0.2.1", + "source-map-url": "0.4.0", + "urix": "0.1.0" + } }, "source-map-support": { "version": "0.4.18", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", - "integrity": "sha1-Aoam3ovkJkEzhZTpfM6nXwosWF8=" + "integrity": "sha1-Aoam3ovkJkEzhZTpfM6nXwosWF8=", + "requires": { + "source-map": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz" + } }, "source-map-url": { "version": "0.4.0", @@ -4794,35 +6070,57 @@ "split": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", - "integrity": "sha1-YFvZvjA6pZ+zX5Ip++oN3snqB9k=" + "integrity": "sha1-YFvZvjA6pZ+zX5Ip++oN3snqB9k=", + "requires": { + "through": "https://registry.npmjs.org/through/-/through-2.3.8.tgz" + } }, "split-string": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", "integrity": "sha1-fLCd2jqGWFcFxks5pkZgOGguj+I=", + "requires": { + "extend-shallow": "3.0.2" + }, "dependencies": { "extend-shallow": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=" + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "requires": { + "assign-symbols": "1.0.0", + "is-extendable": "1.0.1" + } }, "is-extendable": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha1-p0cPnkJnM9gb2B4RVSZOOjUHyrQ=" + "integrity": "sha1-p0cPnkJnM9gb2B4RVSZOOjUHyrQ=", + "requires": { + "is-plain-object": "2.0.4" + } } } }, "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", - "dev": true + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" }, "sshpk": { "version": "1.13.1", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz", "integrity": "sha1-US322mKHFEMW3EwY/hzx2UBzm+M=", + "requires": { + "asn1": "0.2.3", + "assert-plus": "1.0.0", + "bcrypt-pbkdf": "1.0.1", + "dashdash": "1.14.1", + "ecc-jsbn": "0.1.1", + "getpass": "0.1.7", + "jsbn": "0.1.1", + "tweetnacl": "0.14.5" + }, "dependencies": { "assert-plus": { "version": "1.0.0", @@ -4835,21 +6133,34 @@ "version": "0.1.2", "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "requires": { + "define-property": "0.2.5", + "object-copy": "0.1.0" + }, "dependencies": { "define-property": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=" + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "0.1.6" + } }, "is-accessor-descriptor": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "requires": { + "kind-of": "3.2.2" + }, "dependencies": { "kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=" + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "1.1.6" + } } } }, @@ -4857,18 +6168,29 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "requires": { + "kind-of": "3.2.2" + }, "dependencies": { "kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=" + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "1.1.6" + } } } }, "is-descriptor": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha1-Nm2CQN3kh8pRgjsaufB6EKeCUco=" + "integrity": "sha1-Nm2CQN3kh8pRgjsaufB6EKeCUco=", + "requires": { + "is-accessor-descriptor": "0.1.6", + "is-data-descriptor": "0.1.4", + "kind-of": "5.1.0" + } }, "kind-of": { "version": "5.1.0", @@ -4877,12 +6199,6 @@ } } }, - "string_decoder": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", - "integrity": "sha1-D8Z9fBQYJd6UKC3VNr7GubzoYKs=", - "dev": true - }, "string-template": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/string-template/-/string-template-0.2.1.tgz", @@ -4893,6 +6209,10 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "requires": { + "is-fullwidth-code-point": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "strip-ansi": "4.0.0" + }, "dependencies": { "ansi-regex": { "version": "3.0.0", @@ -4901,14 +6221,12 @@ }, "is-fullwidth-code-point": { "version": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" }, "string-width": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", "integrity": "sha1-q5Pyeo3BPSjKyBXEYhQ6bZASrp4=", - "dev": true, "requires": { "is-fullwidth-code-point": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", "strip-ansi": "4.0.0" @@ -4924,13 +6242,25 @@ } } }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha1-D8Z9fBQYJd6UKC3VNr7GubzoYKs=", + "dev": true, + "requires": { + "safe-buffer": "5.1.1" + } + }, "stringstream": { "version": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=" }, "strip-ansi": { "version": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=" + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz" + } }, "strip-bom": { "version": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -4946,14 +6276,26 @@ "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" }, "tar": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz", - "integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=" + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.0.tgz", + "integrity": "sha512-gJlTiiErwo96K904FnoYWl+5+FBgS+FimU6GMh66XLdLa55al8+d4jeDfPoGwSNHdtWI5FJP6xurmVqhBuGJpQ==", + "requires": { + "chownr": "1.0.1", + "fs-minipass": "1.2.5", + "minipass": "2.2.1", + "minizlib": "1.1.0", + "mkdirp": "0.5.1", + "yallist": "3.0.2" + } }, "temp": { "version": "0.8.3", "resolved": "https://registry.npmjs.org/temp/-/temp-0.8.3.tgz", "integrity": "sha1-4Ma8TSa5AxJEEOT+2BEDAU38H1k=", + "requires": { + "os-tmpdir": "1.0.2", + "rimraf": "2.2.8" + }, "dependencies": { "os-tmpdir": { "version": "1.0.2", @@ -4991,7 +6333,10 @@ "version": "0.0.31", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.31.tgz", "integrity": "sha1-jzirlDjhcxXl29izZX6L+yd65Kc=", - "dev": true + "dev": true, + "requires": { + "os-tmpdir": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz" + } }, "to-fast-properties": { "version": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", @@ -5007,11 +6352,17 @@ "version": "0.3.0", "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "requires": { + "kind-of": "3.2.2" + }, "dependencies": { "kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=" + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "1.1.6" + } } } }, @@ -5019,23 +6370,48 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", "integrity": "sha1-E8/dmzNlUvMLUfM6iuG0Knp1mc4=", + "requires": { + "define-property": "2.0.2", + "extend-shallow": "3.0.2", + "regex-not": "1.0.2", + "safe-regex": "1.1.0" + }, "dependencies": { "extend-shallow": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=" + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "requires": { + "assign-symbols": "1.0.0", + "is-extendable": "1.0.1" + } }, "is-extendable": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha1-p0cPnkJnM9gb2B4RVSZOOjUHyrQ=" + "integrity": "sha1-p0cPnkJnM9gb2B4RVSZOOjUHyrQ=", + "requires": { + "is-plain-object": "2.0.4" + } } } }, "to-regex-range": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=" + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "requires": { + "is-number": "3.0.0", + "repeat-string": "1.6.1" + } + }, + "tough-cookie": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", + "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==", + "requires": { + "punycode": "1.4.1" + } }, "tree-kill": { "version": "1.2.0", @@ -5049,7 +6425,10 @@ "tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=" + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "requires": { + "safe-buffer": "5.1.1" + } }, "tweetnacl": { "version": "0.14.5", @@ -5066,11 +6445,23 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=", + "requires": { + "arr-union": "3.1.0", + "get-value": "2.0.6", + "is-extendable": "0.1.1", + "set-value": "0.4.3" + }, "dependencies": { "set-value": { "version": "0.4.3", "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz", - "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=" + "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=", + "requires": { + "extend-shallow": "2.0.1", + "is-extendable": "0.1.1", + "is-plain-object": "2.0.4", + "to-object-path": "0.3.0" + } } } }, @@ -5083,16 +6474,28 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", + "requires": { + "has-value": "0.3.1", + "isobject": "3.0.1" + }, "dependencies": { "has-value": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", + "requires": { + "get-value": "2.0.6", + "has-values": "0.1.4", + "isobject": "2.1.0" + }, "dependencies": { "isobject": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=" + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "requires": { + "isarray": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz" + } } } }, @@ -5112,6 +6515,11 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/use/-/use-2.0.2.tgz", "integrity": "sha1-riig1y+TvyJCKhii43mZMRLeyOg=", + "requires": { + "define-property": "0.2.5", + "isobject": "3.0.1", + "lazy-cache": "2.0.2" + }, "dependencies": { "define-property": { "version": "0.2.5", @@ -5182,7 +6590,7 @@ "uuid": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", - "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==" + "integrity": "sha1-PdPT55Crwk17DToDT/q6vijrvAQ=" }, "validate-npm-package-license": { "version": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz", @@ -5192,6 +6600,11 @@ "version": "1.10.0", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "requires": { + "assert-plus": "1.0.0", + "core-util-is": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "extsprintf": "1.3.0" + }, "dependencies": { "assert-plus": { "version": "1.0.0", @@ -5204,7 +6617,17 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/virtual-dom/-/virtual-dom-2.1.1.tgz", "integrity": "sha1-gO2i1IG57eDASRGM78tKBfIdE3U=", - "dev": true + "dev": true, + "requires": { + "browser-split": "0.0.1", + "error": "4.4.0", + "ev-store": "7.0.0", + "global": "4.3.2", + "is-object": "1.0.1", + "next-tick": "0.2.2", + "x-is-array": "0.1.0", + "x-is-string": "0.1.0" + } }, "what-the-diff": { "version": "0.4.0", @@ -5214,7 +6637,10 @@ "what-the-status": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/what-the-status/-/what-the-status-1.0.3.tgz", - "integrity": "sha1-lP3NAR/7U6Ijnnb6+NrL78mHdRA=" + "integrity": "sha1-lP3NAR/7U6Ijnnb6+NrL78mHdRA=", + "requires": { + "split": "1.0.1" + } }, "whatwg-fetch": { "version": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz", @@ -5223,7 +6649,10 @@ "which": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/which/-/which-1.3.0.tgz", - "integrity": "sha1-/wS9/AEO5UfXgL7DjhrBwnd9JTo=" + "integrity": "sha1-/wS9/AEO5UfXgL7DjhrBwnd9JTo=", + "requires": { + "isexe": "2.0.0" + } }, "which-module": { "version": "2.0.0", @@ -5239,14 +6668,16 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "requires": { + "string-width": "1.0.2", + "strip-ansi": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz" + }, "dependencies": { "string-width": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", "strip-ansi": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz" } } @@ -5254,7 +6685,8 @@ }, "wrappy": { "version": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true }, "x-is-array": { "version": "0.1.0", @@ -5285,15 +6717,38 @@ "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=" }, + "yallist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.2.tgz", + "integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=" + }, "yargs": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/yargs/-/yargs-9.0.1.tgz", - "integrity": "sha1-UqzCP+7Kw0BCB47njAwAf1CF20w=" + "integrity": "sha1-UqzCP+7Kw0BCB47njAwAf1CF20w=", + "requires": { + "camelcase": "4.1.0", + "cliui": "3.2.0", + "decamelize": "1.2.0", + "get-caller-file": "1.0.2", + "os-locale": "2.1.0", + "read-pkg-up": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", + "require-directory": "2.1.1", + "require-main-filename": "1.0.1", + "set-blocking": "2.0.0", + "string-width": "2.1.1", + "which-module": "2.0.0", + "y18n": "3.2.1", + "yargs-parser": "7.0.0" + } }, "yargs-parser": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-7.0.0.tgz", - "integrity": "sha1-jQrELxbqVd69MyyvTEA4s+P139k=" + "integrity": "sha1-jQrELxbqVd69MyyvTEA4s+P139k=", + "requires": { + "camelcase": "4.1.0" + } }, "yubikiri": { "version": "1.0.0", diff --git a/package.json b/package.json index 6a09540c6f..cd9deac118 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "core-decorators": "^0.19.0", "diff": "3.2.0", "dompurify": "^1.0.3", - "dugite": "1.49.0", + "dugite": "^1.60.0", "etch": "^0.12.4", "event-kit": "^2.3.0", "fs-extra": "^5.0.0", From 2e5237bf80d9b039c82fef13b5b0819c20576c3e Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 16 Mar 2018 19:47:56 +0100 Subject: [PATCH 0526/5882] Actually sort alphabetically --- lib/models/user-store.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/models/user-store.js b/lib/models/user-store.js index 0020ca522b..e04caeecdb 100644 --- a/lib/models/user-store.js +++ b/lib/models/user-store.js @@ -51,7 +51,11 @@ export default class UserStore { // don't actually do this. will change when we settle on best way to actually store data return Object.keys(this.users) .map(email => ({email, name: this.users[email]})) - .sort((a, b) => a.name > b.name); + .sort((a, b) => { + if(a.name < b.name) return -1; + if(a.name > b.name) return 1; + return 0; + }); } } From ccc0e5db6768720ac2d6ac493fa613eecad9afa5 Mon Sep 17 00:00:00 2001 From: Markus Olsson Date: Fri, 16 Mar 2018 20:31:45 +0100 Subject: [PATCH 0527/5882] Only parse co-author trailers (not signed-off by and friends) in getAuthors Co-Authored-By: Katrina Uychaco --- lib/git-shell-out-strategy.js | 43 ++++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/lib/git-shell-out-strategy.js b/lib/git-shell-out-strategy.js index 1e78ba6614..ce49181a7f 100644 --- a/lib/git-shell-out-strategy.js +++ b/lib/git-shell-out-strategy.js @@ -602,31 +602,38 @@ export default class GitShellOutStrategy { const {max, ref} = {max: 1, ref: 'HEAD', ...options}; // https://git-scm.com/docs/git-log#_pretty_formats - // %x00 - null byte + // %x1F - field separator byte // %an - author name // %ae - author email // %cn - committer name // %ce - committer email - // %(trailers) - display the trailers of the body - // TODO figure out why %(trailers:only,unfold) doesn't work + // %(trailers:unfold,only) - the commit message trailers, separated + // by newlines and unfolded (i.e. properly + // formatted and one trailer per line). + + const delimiter = '1F' + const delimiterString = String.fromCharCode(parseInt(delimiter, 16)) + const fields = [ "%an", "%ae", "%cn", "%ce", "%(trailers:unfold,only)" ] + const format = fields.join(`%x${delimiter}`); const output = await this.exec([ - 'log', '--pretty=format:%an %ae%x00%cn %ce%x00%(trailers)', '-z', '-n', max, ref, '--', + 'log', `--format=${format}`, '-z', '-n', max, ref, '--', ]); - return output.split('\0').reduce((acc, line) => { - if (line.match(/^co-authored-by: /i)) { - const coAuthorLines = line.trim().split(LINE_ENDING_REGEX); - coAuthorLines.forEach(coAuthor => { - const [_, name, email] = coAuthor.match(/co-authored-by: (.*) <(.*)>/i); // eslint-disable-line no-unused-vars - acc[email] = name; - }); - } else if (line !== '') { - const matches = line.match(/(.*) (.*@.*)/); // eslint-disable-line no-unused-vars - if (matches) { - acc[matches[2]] = matches[1]; - } - } - return acc; + + return output.split('\0') + .filter(l => l.length > 0) + .map(line => line.split(delimiterString)) + .reduce((acc, [ an, ae, cn, ce, trailers ]) => { + trailers + .split('\n') + .map(trailer => trailer.match(/^co-authored-by. (.+?) <(.+?)>$/i)) + .filter(match => match !== null) + .forEach(([ _, name, email ]) => acc[email] = name) + + acc[ae] = an + acc[ce] = cn + + return acc; }, {}); } From 3fbb45da4c3417f7b62b03bd419f01386bf91017 Mon Sep 17 00:00:00 2001 From: iPherian Date: Mon, 19 Mar 2018 12:05:11 -0700 Subject: [PATCH 0528/5882] :bug: Use unambiguous branch name when pushing --- lib/git-shell-out-strategy.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/git-shell-out-strategy.js b/lib/git-shell-out-strategy.js index f00153814c..859606fad1 100644 --- a/lib/git-shell-out-strategy.js +++ b/lib/git-shell-out-strategy.js @@ -658,7 +658,7 @@ export default class GitShellOutStrategy { } push(remoteName, branchName, options = {}) { - const args = ['push', remoteName || 'origin', branchName]; + const args = ['push', remoteName || 'origin', `refs/heads/${branchName}`]; if (options.setUpstream) { args.push('--set-upstream'); } if (options.force) { args.push('--force'); } return this.exec(args, {useGitPromptServer: true, writeOperation: true}); From 79c45b4ef4e846cd12741f5f561ffd2e4de31b00 Mon Sep 17 00:00:00 2001 From: Matt Date: Sat, 24 Feb 2018 14:17:21 -0800 Subject: [PATCH 0529/5882] Tell eslint to only use 2 spaces --- .eslintrc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.eslintrc b/.eslintrc index 850db65d1e..8538ab389d 100644 --- a/.eslintrc +++ b/.eslintrc @@ -14,7 +14,8 @@ }, "extends": ["fbjs/opensource"], "rules": { - "linebreak-style": 0 + "linebreak-style": 0, + "indent": ["error", 2] }, "globals": { "atom": true, From 311a87deb76bb8022afa769a821a5cdb214cab02 Mon Sep 17 00:00:00 2001 From: Matt Date: Sun, 11 Feb 2018 13:23:07 -0800 Subject: [PATCH 0530/5882] WIP Push and pull straight from status bar --- lib/controllers/status-bar-tile-controller.js | 33 +++++--------- lib/views/pull-view.js | 43 ++++++++++++++++++ lib/views/push-view.js | 44 +++++++++++++++++++ styles/push-pull-view.less | 12 ++++- 4 files changed, 110 insertions(+), 22 deletions(-) create mode 100644 lib/views/pull-view.js create mode 100644 lib/views/push-view.js diff --git a/lib/controllers/status-bar-tile-controller.js b/lib/controllers/status-bar-tile-controller.js index 5f86e934f7..40ed64bbbc 100644 --- a/lib/controllers/status-bar-tile-controller.js +++ b/lib/controllers/status-bar-tile-controller.js @@ -5,8 +5,8 @@ import ObserveModelDecorator from '../decorators/observe-model'; import {BranchPropType, RemotePropType} from '../prop-types'; import BranchView from '../views/branch-view'; import BranchMenuView from '../views/branch-menu-view'; -import PushPullView from '../views/push-pull-view'; -import PushPullMenuView from '../views/push-pull-menu-view'; +import PullView from '../views/pull-view'; +import PushView from '../views/push-view'; import ChangedFilesCountView from '../views/changed-files-count-view'; import Tooltip from '../views/tooltip'; import Commands, {Command} from '../views/commands'; @@ -147,28 +147,19 @@ export default class StatusBarTileController extends React.Component { {...repoProps} /> - { this.pushPullView = e; }} - pushInProgress={pushInProgress} + { this.pullView = e; }} fetchInProgress={fetchInProgress || pullInProgress} + pull={this.pull} + fetch={this.fetch} + {...repoProps} + /> + { this.pushView = e; }} + pushInProgress={pushInProgress} + push={this.push} {...repoProps} /> - this.pushPullView} - trigger="click" - className="github-StatusBarTileController-tooltipMenu"> - - ); } diff --git a/lib/views/pull-view.js b/lib/views/pull-view.js new file mode 100644 index 0000000000..b1fdf1248c --- /dev/null +++ b/lib/views/pull-view.js @@ -0,0 +1,43 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import cx from 'classnames'; + +export default class PushPullView extends React.Component { + static propTypes = { + pull: PropTypes.func.isRequired, + fetch: PropTypes.func.isRequired, + fetchInProgress: PropTypes.bool, + behindCount: PropTypes.number, + } + + static defaultProps = { + fetchInProgress: false, + behindCount: 0, + } + + onClick = clickEvent => { + if (clickEvent.ctrlKey || clickEvent.shiftKey || clickEvent.altKey || clickEvent.metaKey) { + this.props.fetch(); + } else { + this.props.pull(); + } + } + + render() { + const pulling = this.props.fetchInProgress; + const pullClasses = cx('github-PushPull-icon', 'icon', {'icon-arrow-down': !pulling, 'icon-sync': pulling}); + return ( +
    { this.element = e; }} + onClick={this.onClick} + // TODO: This should be a blue Atom tooltip + title="Click to pull, Cmd + Click to fetch"> + + {this.props.behindCount > 0 && + `${this.props.behindCount}` + } +
    + ); + } +} diff --git a/lib/views/push-view.js b/lib/views/push-view.js new file mode 100644 index 0000000000..a00b0a7ffe --- /dev/null +++ b/lib/views/push-view.js @@ -0,0 +1,44 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import cx from 'classnames'; + +import {RemotePropType} from '../prop-types'; + +export default class PushView extends React.Component { + static propTypes = { + currentRemote: RemotePropType.isRequired, + push: PropTypes.func.isRequired, + pushInProgress: PropTypes.bool, + aheadCount: PropTypes.number, + } + + static defaultProps = { + pushInProgress: false, + aheadCount: 0, + } + + onClick = clickEvent => { + this.props.push({ + force: clickEvent.metaKey || clickEvent.ctrlKey, + setUpstream: !this.props.currentRemote.isPresent(), + }); + } + + render() { + const pushing = this.props.pushInProgress; + const pushClasses = cx('github-PushPull-icon', 'icon', {'icon-arrow-up': !pushing, 'icon-sync': pushing}); + return ( +
    { this.element = e; }} + onClick={this.onClick} + // TODO: This should be a blue Atom tooltip + title="Click to push, Cmd + Click to force push"> + + + {this.props.aheadCount ? `${this.props.aheadCount}` : ''} + +
    + ); + } +} diff --git a/styles/push-pull-view.less b/styles/push-pull-view.less index 4b42e688d5..1c0bf60a60 100644 --- a/styles/push-pull-view.less +++ b/styles/push-pull-view.less @@ -3,7 +3,6 @@ // Used in the status-bar .github-PushPull { - &-icon.icon.icon.icon { margin-right: 0; text-align: center; @@ -13,4 +12,15 @@ margin-right: .4em; } + status-bar.status-bar &-icon::before { + margin-right: 0px; + } +} + +status-bar .github-Push.inline-block { + padding-left: 0.375em; +} + +status-bar .github-Pull.inline-block { + padding-right: 0.375em; } From e6199e2e8b9849f68cc5f51c1780ffc6889b92a9 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 20 Feb 2018 01:07:45 -0800 Subject: [PATCH 0531/5882] Implement designs from #311 Also covers the remaining 2.6% edge case of not being on a branch. --- lib/controllers/status-bar-tile-controller.js | 15 +-- lib/get-repo-pipeline-manager.js | 2 +- lib/views/pull-view.js | 43 ------ lib/views/push-pull-view.js | 127 +++++++++++++++--- lib/views/push-view.js | 44 ------ menus/git.cson | 21 +++ styles/push-pull-view.less | 15 +-- styles/status-bar-tile-controller.less | 4 +- 8 files changed, 139 insertions(+), 132 deletions(-) delete mode 100644 lib/views/pull-view.js delete mode 100644 lib/views/push-view.js diff --git a/lib/controllers/status-bar-tile-controller.js b/lib/controllers/status-bar-tile-controller.js index 40ed64bbbc..5b78cee018 100644 --- a/lib/controllers/status-bar-tile-controller.js +++ b/lib/controllers/status-bar-tile-controller.js @@ -5,8 +5,7 @@ import ObserveModelDecorator from '../decorators/observe-model'; import {BranchPropType, RemotePropType} from '../prop-types'; import BranchView from '../views/branch-view'; import BranchMenuView from '../views/branch-menu-view'; -import PullView from '../views/pull-view'; -import PushView from '../views/push-view'; +import PushPullView from '../views/push-pull-view'; import ChangedFilesCountView from '../views/changed-files-count-view'; import Tooltip from '../views/tooltip'; import Commands, {Command} from '../views/commands'; @@ -147,19 +146,13 @@ export default class StatusBarTileController extends React.Component { {...repoProps} /> - { this.pullView = e; }} - fetchInProgress={fetchInProgress || pullInProgress} + - { this.pushView = e; }} - pushInProgress={pushInProgress} - push={this.push} - {...repoProps} - /> ); } diff --git a/lib/get-repo-pipeline-manager.js b/lib/get-repo-pipeline-manager.js index 8158295ada..366cf49c30 100644 --- a/lib/get-repo-pipeline-manager.js +++ b/lib/get-repo-pipeline-manager.js @@ -46,7 +46,7 @@ export default function({confirm, notificationManager, workspace}) { if (/rejected[\s\S]*failed to push/.test(error.stdErr)) { notificationManager.addError('Push rejected', { description: 'The tip of your current branch is behind its remote counterpart.' + - ' Try pulling before pushing again. Or, to force push, hold `cmd` or `ctrl` while clicking.', + ' Try pulling before pushing.
    To force push, hold `cmd` or `ctrl` while clicking.', dismissable: true, }); } else { diff --git a/lib/views/pull-view.js b/lib/views/pull-view.js deleted file mode 100644 index b1fdf1248c..0000000000 --- a/lib/views/pull-view.js +++ /dev/null @@ -1,43 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import cx from 'classnames'; - -export default class PushPullView extends React.Component { - static propTypes = { - pull: PropTypes.func.isRequired, - fetch: PropTypes.func.isRequired, - fetchInProgress: PropTypes.bool, - behindCount: PropTypes.number, - } - - static defaultProps = { - fetchInProgress: false, - behindCount: 0, - } - - onClick = clickEvent => { - if (clickEvent.ctrlKey || clickEvent.shiftKey || clickEvent.altKey || clickEvent.metaKey) { - this.props.fetch(); - } else { - this.props.pull(); - } - } - - render() { - const pulling = this.props.fetchInProgress; - const pullClasses = cx('github-PushPull-icon', 'icon', {'icon-arrow-down': !pulling, 'icon-sync': pulling}); - return ( -
    { this.element = e; }} - onClick={this.onClick} - // TODO: This should be a blue Atom tooltip - title="Click to pull, Cmd + Click to fetch"> - - {this.props.behindCount > 0 && - `${this.props.behindCount}` - } -
    - ); - } -} diff --git a/lib/views/push-pull-view.js b/lib/views/push-pull-view.js index 6e30bdeebd..1639c8eea6 100644 --- a/lib/views/push-pull-view.js +++ b/lib/views/push-pull-view.js @@ -2,37 +2,128 @@ import React from 'react'; import PropTypes from 'prop-types'; import cx from 'classnames'; +import {RemotePropType, BranchPropType} from '../prop-types'; + + +function getIconClass(icon, isSyncing = false) { + return cx( + 'github-PushPull-icon', + 'icon', + isSyncing ? 'icon-sync' : `icon-${icon}`, + {'animate-rotate': isSyncing}, + ); +} + export default class PushPullView extends React.Component { static propTypes = { - pushInProgress: PropTypes.bool, - fetchInProgress: PropTypes.bool, + currentBranch: BranchPropType.isRequired, + currentRemote: RemotePropType.isRequired, + isSyncing: PropTypes.bool, behindCount: PropTypes.number, aheadCount: PropTypes.number, + push: PropTypes.func.isRequired, + pull: PropTypes.func.isRequired, + fetch: PropTypes.func.isRequired, } static defaultProps = { - pushInProgress: false, - fetchInProgress: false, + isSyncing: false, behindCount: 0, aheadCount: 0, } + onClickPush = clickEvent => { + this.props.push({ + force: clickEvent.metaKey || clickEvent.ctrlKey, + setUpstream: !this.props.currentRemote.isPresent(), + }); + } + + onClickPull = clickEvent => { + this.props.pull(); + } + + onClickPushPull = clickEvent => { + if (clickEvent.metaKey || clickEvent.ctrlKey) { + this.props.push({ + force: true, + }); + } else { + this.props.pull(); + } + } + + onClickPublish = clickEvent => { + this.props.push({ + setUpstream: !this.props.currentRemote.isPresent(), + }); + } + + onClickFetch = clickEvent => { + this.props.fetch(); + } + render() { - const pushing = this.props.pushInProgress; - const pulling = this.props.fetchInProgress; - const pushClasses = cx('github-PushPull-icon', 'icon', {'icon-arrow-up': !pushing, 'icon-sync': pushing}); - const pullClasses = cx('github-PushPull-icon', 'icon', {'icon-arrow-down': !pulling, 'icon-sync': pulling}); - return ( -
    { this.element = e; }}> - - - {this.props.behindCount ? `${this.props.behindCount}` : ''} + const isAhead = this.props.aheadCount > 0; + const isBehind = this.props.behindCount > 0; + const isUnpublished = !this.props.currentRemote.isPresent(); + const isDetached = !!this.props.currentBranch.detached; + const isSyncing = this.props.isSyncing; + + return (
    + {isAhead && !isBehind && !isUnpublished && ( + + + Push {this.props.aheadCount} + + )} + + {isBehind && !isAhead && !isUnpublished && ( + + + Pull {this.props.behindCount} - - - {this.props.aheadCount ? `${this.props.aheadCount}` : ''} + )} + + {isBehind && isAhead && !isUnpublished && !isSyncing && ( + + + + {this.props.aheadCount}{' '} + + + Pull {this.props.behindCount} + + )} + + {isBehind && isAhead && !isUnpublished && isSyncing && ( + + + Pull {this.props.behindCount} + + )} + + {!isBehind && !isAhead && !isUnpublished && !isDetached && ( + + + Fetch + + )} + + {isUnpublished && !isDetached && ( + + + Publish + + )} + + {isDetached && ( + + + Not on branch -
    - ); + )} +
    ); } } diff --git a/lib/views/push-view.js b/lib/views/push-view.js deleted file mode 100644 index a00b0a7ffe..0000000000 --- a/lib/views/push-view.js +++ /dev/null @@ -1,44 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import cx from 'classnames'; - -import {RemotePropType} from '../prop-types'; - -export default class PushView extends React.Component { - static propTypes = { - currentRemote: RemotePropType.isRequired, - push: PropTypes.func.isRequired, - pushInProgress: PropTypes.bool, - aheadCount: PropTypes.number, - } - - static defaultProps = { - pushInProgress: false, - aheadCount: 0, - } - - onClick = clickEvent => { - this.props.push({ - force: clickEvent.metaKey || clickEvent.ctrlKey, - setUpstream: !this.props.currentRemote.isPresent(), - }); - } - - render() { - const pushing = this.props.pushInProgress; - const pushClasses = cx('github-PushPull-icon', 'icon', {'icon-arrow-up': !pushing, 'icon-sync': pushing}); - return ( -
    { this.element = e; }} - onClick={this.onClick} - // TODO: This should be a blue Atom tooltip - title="Click to push, Cmd + Click to force push"> - - - {this.props.aheadCount ? `${this.props.aheadCount}` : ''} - -
    - ); - } -} diff --git a/menus/git.cson b/menus/git.cson index 63be16c747..a8ce5c3a97 100644 --- a/menus/git.cson +++ b/menus/git.cson @@ -115,3 +115,24 @@ 'command': 'github:view-staged-changes-for-current-file' } ] + '.github-PushPull': [ + { + 'label': 'Fetch', + 'command': 'github:fetch' + } + { + 'label': 'Pull', + 'command': 'github:pull' + } + { + 'type': 'separator' + } + { + 'label': 'Push', + 'command': 'github:push' + } + { + 'label': 'Force Push', + 'command': 'github:force-push' + } + ] diff --git a/styles/push-pull-view.less b/styles/push-pull-view.less index 1c0bf60a60..47687925d2 100644 --- a/styles/push-pull-view.less +++ b/styles/push-pull-view.less @@ -8,19 +8,8 @@ text-align: center; } - &-label.is-pull { - margin-right: .4em; + .secondary { + color: @text-color-subtle; } - status-bar.status-bar &-icon::before { - margin-right: 0px; - } -} - -status-bar .github-Push.inline-block { - padding-left: 0.375em; -} - -status-bar .github-Pull.inline-block { - padding-right: 0.375em; } diff --git a/styles/status-bar-tile-controller.less b/styles/status-bar-tile-controller.less index bd6b14cfc2..abf24ec2be 100644 --- a/styles/status-bar-tile-controller.less +++ b/styles/status-bar-tile-controller.less @@ -34,11 +34,11 @@ } // Sync animation - .icon-sync::before { + .animate-rotate.icon-sync::before { @keyframes github-StatusBarSync-animation { 100% { transform: rotate(360deg); } } - animation: github-StatusBarSync-animation 2s linear 30; // limit to 1min in case something gets stuck + animation: github-StatusBarSync-animation 1s linear infinite; } // Merge conflict icon From cf8b0af12d5deb58b7c345a07051f206a9dc829f Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 19 Mar 2018 16:55:49 +0100 Subject: [PATCH 0532/5882] Add tooltips to push-pull-view in status-bar --- CONTRIBUTING.md | 5 ++ lib/views/push-pull-view.js | 95 +++++++++++++++++++++++++++++-------- 2 files changed, 81 insertions(+), 19 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2dd522fb8d..f904256ccc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -26,6 +26,11 @@ Please check in the generated `graphql/schema.graphql`. In addition, if you make any changes to any of the GraphQL queries or fragments (inside the `graphql` tagged template literals), you will need to run `npm run relay` to regenerate the statically-generated query files. +## Testing +To run tests, open the command palette and select "Run Package Specs". This will open a new window running "GitHub Package Tests". If the window stays blank for more than a few seconds, open DevTools and check for error messages. + +To re-run tests, you can refresh that window by pressing `Cmd + R` in DevTools. + ### Async Tests Sometimes it's necessary to test async operations. For example, imagine the following test: diff --git a/lib/views/push-pull-view.js b/lib/views/push-pull-view.js index 1639c8eea6..39fa5ae679 100644 --- a/lib/views/push-pull-view.js +++ b/lib/views/push-pull-view.js @@ -32,7 +32,28 @@ export default class PushPullView extends React.Component { aheadCount: 0, } + + componentDidMount() { + this.setTooltip(); + } + + componentDidUpdate() { + this.tooltip.dispose(); + this.setTooltip(); + } + + setTooltip = () => { + this.tooltip = atom.tooltips.add(this.node, { + title: `
    ${this.clickNode.dataset.tooltip}
    `, + delay: {show: 200, hide: 100}, + html: true, + }); + } + onClickPush = clickEvent => { + if (this.props.isSyncing) { + return; + } this.props.push({ force: clickEvent.metaKey || clickEvent.ctrlKey, setUpstream: !this.props.currentRemote.isPresent(), @@ -40,10 +61,16 @@ export default class PushPullView extends React.Component { } onClickPull = clickEvent => { + if (this.props.isSyncing) { + return; + } this.props.pull(); } onClickPushPull = clickEvent => { + if (this.props.isSyncing) { + return; + } if (clickEvent.metaKey || clickEvent.ctrlKey) { this.props.push({ force: true, @@ -54,12 +81,18 @@ export default class PushPullView extends React.Component { } onClickPublish = clickEvent => { + if (this.props.isSyncing) { + return; + } this.props.push({ setUpstream: !this.props.currentRemote.isPresent(), }); } onClickFetch = clickEvent => { + if (this.props.isSyncing) { + return; + } this.props.fetch(); } @@ -71,55 +104,79 @@ export default class PushPullView extends React.Component { const isSyncing = this.props.isSyncing; return (
    (this.node = node)} className={cx('github-PushPull', 'inline-block', {'github-branch-detached': isDetached})}> + {isAhead && !isBehind && !isUnpublished && ( - + (this.clickNode = node)} + onClick={this.onClickPush} + className="push-pull-target" + data-tooltip="Click to push
    Cmd-click to force push
    Right-click for more"> Push {this.props.aheadCount} - - )} +
    + )} {isBehind && !isAhead && !isUnpublished && ( - - + (this.clickNode = node)} + onClick={this.onClickPull} + className="push-pull-target" + data-tooltip="Click to pull
    Right-click for more"> + Pull {this.props.behindCount} - +
    )} {isBehind && isAhead && !isUnpublished && !isSyncing && ( - + (this.clickNode = node)} + onClick={this.onClickPushPull} + className="push-pull-target" + data-tooltip="Click to push
    Cmd-click to force push
    Right-click for more"> {this.props.aheadCount}{' '} Pull {this.props.behindCount} - - )} +
    + )} {isBehind && isAhead && !isUnpublished && isSyncing && ( - - + + Pull {this.props.behindCount} - - )} + + )} {!isBehind && !isAhead && !isUnpublished && !isDetached && ( - - + (this.clickNode = node)} + onClick={this.onClickFetch} + className="push-pull-target" + data-tooltip="Click to fetch
    Right-click for more"> + Fetch - +
    )} {isUnpublished && !isDetached && ( - + (this.clickNode = node)} + onClick={this.onClickPublish} + className="push-pull-target" + data-tooltip="Click to publish
    Right-click for more"> Publish - +
    )} {isDetached && ( - + (this.clickNode = node)} + className="push-pull-target" + data-tooltip={'Create a branch if you wish to push your work anywhere'}> Not on branch From 8ba4e8a87e19cb7f134e39f5a5dc3666ba154da7 Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 19 Mar 2018 16:56:17 +0100 Subject: [PATCH 0533/5882] Write basic tests for push-pull-view --- .../status-bar-tile-controller.test.js | 396 +++++++++++++----- 1 file changed, 282 insertions(+), 114 deletions(-) diff --git a/test/controllers/status-bar-tile-controller.test.js b/test/controllers/status-bar-tile-controller.test.js index c32f7628c0..9212c5f863 100644 --- a/test/controllers/status-bar-tile-controller.test.js +++ b/test/controllers/status-bar-tile-controller.test.js @@ -267,176 +267,344 @@ describe('StatusBarTileController', function() { }); }); - describe('pushing and pulling', function() { - it('shows and hides the PushPullView', async function() { - const {localRepoPath} = await setUpLocalAndRemoteRepositories(); - const repository = await buildRepository(localRepoPath); + describe.only('pushing and pulling', function() { - const wrapper = mount(React.cloneElement(component, {repository})); - await wrapper.instance().refreshModelData(); - - assert.lengthOf(document.querySelectorAll('.github-PushPullMenuView'), 0); - wrapper.find(PushPullView).node.element.click(); - assert.lengthOf(document.querySelectorAll('.github-PushPullMenuView'), 1); - wrapper.find(PushPullView).node.element.click(); - assert.lengthOf(document.querySelectorAll('.github-PushPullMenuView'), 0); - }); + describe('status bar tile state', function() { - it('indicates the ahead and behind counts', async function() { - const {localRepoPath} = await setUpLocalAndRemoteRepositories(); - const repository = await buildRepository(localRepoPath); + describe('when there is no remote tracking branch', function() { + let repository; + let statusBarTile; - const wrapper = mount(React.cloneElement(component, {repository})); - await wrapper.instance().refreshModelData(); + beforeEach(async function() { + const {localRepoPath} = await setUpLocalAndRemoteRepositories(); + repository = await buildRepository(localRepoPath); + await repository.git.exec(['checkout', '-b', 'new-branch']); - const tip = getTooltipNode(wrapper, PushPullView); + statusBarTile = mount(React.cloneElement(component, {repository})); + await statusBarTile.instance().refreshModelData(); - assert.equal(tip.querySelector('.github-PushPullMenuView-pull').textContent.trim(), 'Pull'); - assert.equal(tip.querySelector('.github-PushPullMenuView-push').textContent.trim(), 'Push'); + sinon.spy(repository, 'fetch'); + sinon.spy(repository, 'push'); + sinon.spy(repository, 'pull'); + }); - await repository.git.exec(['reset', '--hard', 'HEAD~2']); - repository.refresh(); - await wrapper.instance().refreshModelData(); + it('gives the option to publish the current branch', function() { + assert.equal(statusBarTile.find('.github-PushPull').text().trim(), 'Publish'); + }); - assert.equal(tip.querySelector('.github-PushPullMenuView-pull').textContent.trim(), 'Pull (2)'); - assert.equal(tip.querySelector('.github-PushPullMenuView-push').textContent.trim(), 'Push'); + it('pushes the current branch when clicked', function() { + statusBarTile.find('.push-pull-target').simulate('click'); + assert.isTrue(repository.push.called); + }); - await repository.git.commit('new local commit', {allowEmpty: true}); - repository.refresh(); - await wrapper.instance().refreshModelData(); + it('does nothing when clicked and currently pushing', function() { + repository.getOperationStates().setPushInProgress(true); + statusBarTile = mount(React.cloneElement(component, {repository})); - assert.equal(tip.querySelector('.github-PushPullMenuView-pull').textContent.trim(), 'Pull (2)'); - assert.equal(tip.querySelector('.github-PushPullMenuView-push').textContent.trim(), 'Push (1)'); - }); + statusBarTile.find('.push-pull-target').simulate('click'); + assert.isFalse(repository.fetch.called); + assert.isFalse(repository.push.called); + assert.isFalse(repository.pull.called); + }); + }); - describe('the push/pull menu', function() { - describe('when there is no remote tracking branch', function() { + describe('when there is a remote with nothing to pull or push', function() { let repository; + let statusBarTile; beforeEach(async function() { const {localRepoPath} = await setUpLocalAndRemoteRepositories(); repository = await buildRepository(localRepoPath); - await repository.git.exec(['checkout', '-b', 'new-branch']); + + statusBarTile = mount(React.cloneElement(component, {repository})); + await statusBarTile.instance().refreshModelData(); + + sinon.spy(repository, 'fetch'); + sinon.spy(repository, 'push'); + sinon.spy(repository, 'pull'); }); - it('disables the fetch and pull buttons and displays an informative message', async function() { - const wrapper = mount(React.cloneElement(component, {repository})); - await wrapper.instance().refreshModelData(); + it('gives the option to fetch from remote', function() { + assert.equal(statusBarTile.find('.github-PushPull').text().trim(), 'Fetch'); + }); - const tip = getTooltipNode(wrapper, PushPullView); + it('fetches from remote when clicked', function() { + statusBarTile.find('.push-pull-target').simulate('click'); + assert.isTrue(repository.fetch.called); + }); - const pullButton = tip.querySelector('button.github-PushPullMenuView-pull'); - const pushButton = tip.querySelector('button.github-PushPullMenuView-push'); - const message = tip.querySelector('.github-PushPullMenuView-message'); + it('does nothing when clicked and currently fetching', function() { + repository.getOperationStates().setFetchInProgress(true); + statusBarTile = mount(React.cloneElement(component, {repository})); - assert.isTrue(pullButton.disabled); - assert.isFalse(pushButton.disabled); - assert.match(message.innerHTML, /No remote detected.*Pushing will set up a remote tracking branch/); - - pushButton.click(); - await until(async fail => { - try { - repository.refresh(); - await wrapper.instance().refreshModelData(); - - assert.isFalse(pullButton.disabled); - assert.isFalse(pushButton.disabled); - assert.equal(message.textContent, ''); - return true; - } catch (err) { - return fail(err); - } - }); + statusBarTile.find('.push-pull-target').simulate('click'); + assert.isFalse(repository.fetch.called); + assert.isFalse(repository.push.called); + assert.isFalse(repository.pull.called); }); + }); - describe('when there is no remote named "origin"', function() { - beforeEach(async function() { - await repository.git.exec(['remote', 'remove', 'origin']); - }); + describe('when there is a remote and we are ahead', function() { + let repository; + let statusBarTile; - it('additionally disables the push button and displays an informative message', async function() { - const wrapper = mount(React.cloneElement(component, {repository})); - await wrapper.instance().refreshModelData(); + beforeEach(async function() { + const {localRepoPath} = await setUpLocalAndRemoteRepositories(); + repository = await buildRepository(localRepoPath); + await repository.git.commit('new local commit', {allowEmpty: true}); - const tip = getTooltipNode(wrapper, PushPullView); + statusBarTile = mount(React.cloneElement(component, {repository})); + await statusBarTile.instance().refreshModelData(); - const pullButton = tip.querySelector('button.github-PushPullMenuView-pull'); - const pushButton = tip.querySelector('button.github-PushPullMenuView-push'); - const message = tip.querySelector('.github-PushPullMenuView-message'); + sinon.spy(repository, 'fetch'); + sinon.spy(repository, 'push'); + sinon.spy(repository, 'pull'); + }); - assert.isTrue(pullButton.disabled); - assert.isTrue(pushButton.disabled); - assert.match(message.innerHTML, /No remote detected.*no remote named "origin"/); - }); + it('gives the option to push with ahead count', function() { + assert.equal(statusBarTile.find('.github-PushPull').text().trim(), 'Push 1'); + }); + + it('pushes when clicked', function() { + statusBarTile.find('.push-pull-target').simulate('click'); + assert.isTrue(repository.push.called); + }); + + it('does nothing when clicked and is currently pushing', function() { + repository.getOperationStates().setPushInProgress(true); + statusBarTile = mount(React.cloneElement(component, {repository})); + + statusBarTile.find('.push-pull-target').simulate('click'); + assert.isFalse(repository.fetch.called); + assert.isFalse(repository.push.called); + assert.isFalse(repository.pull.called); }); }); - it('displays an error message if push fails', async function() { - const {localRepoPath} = await setUpLocalAndRemoteRepositories(); - const repository = await buildRepositoryWithPipeline(localRepoPath, {confirm, notificationManager, workspace}); - await repository.git.exec(['reset', '--hard', 'HEAD~2']); - await repository.git.commit('another commit', {allowEmpty: true}); + describe('when there is a remote and we are behind', function() { + let repository; + let statusBarTile; - const wrapper = mount(React.cloneElement(component, {repository})); - await wrapper.instance().refreshModelData(); + beforeEach(async function() { + const {localRepoPath} = await setUpLocalAndRemoteRepositories(); + repository = await buildRepository(localRepoPath); + await repository.git.exec(['reset', '--hard', 'HEAD~2']); - const tip = getTooltipNode(wrapper, PushPullView); + statusBarTile = mount(React.cloneElement(component, {repository})); + await statusBarTile.instance().refreshModelData(); - const pullButton = tip.querySelector('button.github-PushPullMenuView-pull'); - const pushButton = tip.querySelector('button.github-PushPullMenuView-push'); + sinon.spy(repository, 'fetch'); + sinon.spy(repository, 'push'); + sinon.spy(repository, 'pull'); + }); - sinon.stub(notificationManager, 'addError'); + it('gives the option to push with behind count', function() { + assert.equal(statusBarTile.find('.github-PushPull').text().trim(), 'Pull 2'); + }); - assert.equal(pushButton.textContent.trim(), 'Push (1)'); - assert.equal(pullButton.textContent.trim(), 'Pull (2)'); + it('pulls when clicked', function() { + statusBarTile.find('.push-pull-target').simulate('click'); + assert.isTrue(repository.pull.called); + }); - try { - await wrapper.instance().getWrappedComponentInstance().push(); - } catch (e) { - assert(e, 'is error'); - } - await wrapper.instance().refreshModelData(); + it('does nothing when clicked and is currently pulling', function() { + repository.getOperationStates().setPullInProgress(true); + statusBarTile = mount(React.cloneElement(component, {repository})); - await assert.async.isTrue(notificationManager.addError.called); - const notificationArgs = notificationManager.addError.args[0]; - assert.equal(notificationArgs[0], 'Push rejected'); - assert.match(notificationArgs[1].description, /Try pulling before pushing again/); + statusBarTile.find('.push-pull-target').simulate('click'); + assert.isFalse(repository.fetch.called); + assert.isFalse(repository.push.called); + assert.isFalse(repository.pull.called); + }); + }); - await wrapper.instance().refreshModelData(); + describe('when there is a remote and we are ahead and behind', function() { + let repository; + let statusBarTile; + + beforeEach(async function() { + const {localRepoPath} = await setUpLocalAndRemoteRepositories(); + repository = await buildRepository(localRepoPath); + await repository.git.exec(['reset', '--hard', 'HEAD~2']); + await repository.git.commit('new local commit', {allowEmpty: true}); - await assert.async.equal(pushButton.textContent.trim(), 'Push (1)'); - await assert.async.equal(pullButton.textContent.trim(), 'Pull (2)'); - wrapper.unmount(); + statusBarTile = mount(React.cloneElement(component, {repository})); + await statusBarTile.instance().refreshModelData(); + + sinon.spy(repository, 'fetch'); + sinon.spy(repository, 'push'); + sinon.spy(repository, 'pull'); + }); + + it('gives the option to push with ahead and behind count', function() { + assert.equal(statusBarTile.find('.github-PushPull').text().trim(), '1 Pull 2'); + }); + + it('pulls when clicked', function() { + statusBarTile.find('.push-pull-target').simulate('click'); + assert.isTrue(repository.pull.called); + assert.isFalse(repository.fetch.called); + assert.isFalse(repository.push.called); + }); + + it('does nothing when clicked and is currently pulling', function() { + repository.getOperationStates().setPullInProgress(true); + statusBarTile = mount(React.cloneElement(component, {repository})); + + statusBarTile.find('.push-pull-target').simulate('click'); + assert.isFalse(repository.fetch.called); + assert.isFalse(repository.push.called); + assert.isFalse(repository.pull.called); + }); }); - describe('with a detached HEAD', function() { - let wrapper; + describe('when there is a remote and we are detached HEAD', function() { + let repository; + let statusBarTile; beforeEach(async function() { - const workdirPath = await cloneRepository('multiple-commits'); - const repository = await buildRepository(workdirPath); + const {localRepoPath} = await setUpLocalAndRemoteRepositories(); + repository = await buildRepository(localRepoPath); await repository.checkout('HEAD~2'); - wrapper = mount(React.cloneElement(component, {repository})); - await wrapper.instance().refreshModelData(); + statusBarTile = mount(React.cloneElement(component, {repository})); + await statusBarTile.instance().refreshModelData(); + + sinon.spy(repository, 'fetch'); + sinon.spy(repository, 'push'); + sinon.spy(repository, 'pull'); }); - it('disables the fetch, pull, and push buttons', function() { - const tip = getTooltipNode(wrapper, PushPullView); + it('gives the option to push with ahead and behind count', function() { + assert.equal(statusBarTile.find('.github-PushPull').text().trim(), 'Not on branch'); + }); + + it('does nothing when clicked', function() { + statusBarTile.find('.push-pull-target').simulate('click'); + assert.isFalse(repository.fetch.called); + assert.isFalse(repository.push.called); + assert.isFalse(repository.pull.called); + }); + }); + + }); + + describe('when there is no remote tracking branch', function() { + let repository; + + beforeEach(async function() { + const {localRepoPath} = await setUpLocalAndRemoteRepositories(); + repository = await buildRepository(localRepoPath); + await repository.git.exec(['checkout', '-b', 'new-branch']); + }); + + it('disables the fetch and pull buttons and displays an informative message', async function() { + const wrapper = mount(React.cloneElement(component, {repository})); + await wrapper.instance().refreshModelData(); + + const tip = getTooltipNode(wrapper, PushPullView); + + const pullButton = tip.querySelector('button.github-PushPullMenuView-pull'); + const pushButton = tip.querySelector('button.github-PushPullMenuView-push'); + const message = tip.querySelector('.github-PushPullMenuView-message'); + + assert.isTrue(pullButton.disabled); + assert.isFalse(pushButton.disabled); + assert.match(message.innerHTML, /No remote detected.*Pushing will set up a remote tracking branch/); - assert.isTrue(tip.querySelector('button.github-PushPullMenuView-pull').disabled); - assert.isTrue(tip.querySelector('button.github-PushPullMenuView-push').disabled); + pushButton.click(); + await until(async fail => { + try { + repository.refresh(); + await wrapper.instance().refreshModelData(); + + assert.isFalse(pullButton.disabled); + assert.isFalse(pushButton.disabled); + assert.equal(message.textContent, ''); + return true; + } catch (err) { + return fail(err); + } }); + }); + + describe('when there is no remote named "origin"', function() { + beforeEach(async function() { + await repository.git.exec(['remote', 'remove', 'origin']); + }); + + it('additionally disables the push button and displays an informative message', async function() { + const wrapper = mount(React.cloneElement(component, {repository})); + await wrapper.instance().refreshModelData(); - it('displays an appropriate explanation', function() { const tip = getTooltipNode(wrapper, PushPullView); + const pullButton = tip.querySelector('button.github-PushPullMenuView-pull'); + const pushButton = tip.querySelector('button.github-PushPullMenuView-push'); const message = tip.querySelector('.github-PushPullMenuView-message'); - assert.match(message.textContent, /not on a branch/); + + assert.isTrue(pullButton.disabled); + assert.isTrue(pushButton.disabled); + assert.match(message.innerHTML, /No remote detected.*no remote named "origin"/); }); }); }); + it('displays an error message if push fails', async function() { + const {localRepoPath} = await setUpLocalAndRemoteRepositories(); + const repository = await buildRepositoryWithPipeline(localRepoPath, {confirm, notificationManager, workspace}); + await repository.git.exec(['reset', '--hard', 'HEAD~2']); + await repository.git.commit('another commit', {allowEmpty: true}); + + const wrapper = mount(React.cloneElement(component, {repository})); + await wrapper.instance().refreshModelData(); + + sinon.stub(notificationManager, 'addError'); + + try { + await wrapper.instance().getWrappedComponentInstance().push(); + } catch (e) { + assert(e, 'is error'); + } + await wrapper.instance().refreshModelData(); + + await assert.async.isTrue(notificationManager.addError.called); + const notificationArgs = notificationManager.addError.args[0]; + assert.equal(notificationArgs[0], 'Push rejected'); + assert.match(notificationArgs[1].description, /Try pulling before pushing/); + + await wrapper.instance().refreshModelData(); + + wrapper.unmount(); + }); + + describe('with a detached HEAD', function() { + let wrapper; + + beforeEach(async function() { + const workdirPath = await cloneRepository('multiple-commits'); + const repository = await buildRepository(workdirPath); + await repository.checkout('HEAD~2'); + + wrapper = mount(React.cloneElement(component, {repository})); + await wrapper.instance().refreshModelData(); + }); + + it('disables the fetch, pull, and push buttons', function() { + const tip = getTooltipNode(wrapper, PushPullView); + + assert.isTrue(tip.querySelector('button.github-PushPullMenuView-pull').disabled); + assert.isTrue(tip.querySelector('button.github-PushPullMenuView-push').disabled); + }); + + it('displays an appropriate explanation', function() { + const tip = getTooltipNode(wrapper, PushPullView); + + const message = tip.querySelector('.github-PushPullMenuView-message'); + assert.match(message.textContent, /not on a branch/); + }); + }); + describe('fetch and pull commands', function() { it('fetches when github:fetch is triggered', async function() { const {localRepoPath} = await setUpLocalAndRemoteRepositories('multiple-commits', {remoteAhead: true}); From f9b975f02f3e02e13aca129fb39b8493d805d798 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 20 Mar 2018 00:50:24 +0100 Subject: [PATCH 0534/5882] Handle and test case of no "origin" remote --- lib/controllers/status-bar-tile-controller.js | 1 + lib/views/push-pull-view.js | 73 +++++++------ .../status-bar-tile-controller.test.js | 102 ++++-------------- 3 files changed, 64 insertions(+), 112 deletions(-) diff --git a/lib/controllers/status-bar-tile-controller.js b/lib/controllers/status-bar-tile-controller.js index 5b78cee018..0e17a02957 100644 --- a/lib/controllers/status-bar-tile-controller.js +++ b/lib/controllers/status-bar-tile-controller.js @@ -88,6 +88,7 @@ export default class StatusBarTileController extends React.Component { currentRemote: this.props.currentRemote, aheadCount: this.props.aheadCount, behindCount: this.props.behindCount, + originExists: this.props.originExists, changedFilesCount, mergeConflictsPresent, }; diff --git a/lib/views/push-pull-view.js b/lib/views/push-pull-view.js index 39fa5ae679..4a398a256d 100644 --- a/lib/views/push-pull-view.js +++ b/lib/views/push-pull-view.js @@ -24,6 +24,7 @@ export default class PushPullView extends React.Component { push: PropTypes.func.isRequired, pull: PropTypes.func.isRequired, fetch: PropTypes.func.isRequired, + originExists: PropTypes.bool, } static defaultProps = { @@ -43,8 +44,8 @@ export default class PushPullView extends React.Component { } setTooltip = () => { - this.tooltip = atom.tooltips.add(this.node, { - title: `
    ${this.clickNode.dataset.tooltip}
    `, + this.tooltip = atom.tooltips.add(this.tileNode, { + title: `
    ${this.tooltipTarget.dataset.tooltip}
    `, delay: {show: 200, hide: 100}, html: true, }); @@ -102,36 +103,37 @@ export default class PushPullView extends React.Component { const isUnpublished = !this.props.currentRemote.isPresent(); const isDetached = !!this.props.currentBranch.detached; const isSyncing = this.props.isSyncing; + const hasOrigin = !!this.props.originExists; return (
    (this.node = node)} + ref={node => (this.tileNode = node)} className={cx('github-PushPull', 'inline-block', {'github-branch-detached': isDetached})}> {isAhead && !isBehind && !isUnpublished && ( (this.clickNode = node)} + ref={node => (this.tooltipTarget = node)} onClick={this.onClickPush} className="push-pull-target" data-tooltip="Click to push
    Cmd-click to force push
    Right-click for more"> Push {this.props.aheadCount} -
    - )} + + )} {isBehind && !isAhead && !isUnpublished && ( - (this.clickNode = node)} - onClick={this.onClickPull} - className="push-pull-target" - data-tooltip="Click to pull
    Right-click for more"> - +
    (this.tooltipTarget = node)} + onClick={this.onClickPull} + className="push-pull-target" + data-tooltip="Click to pull
    Right-click for more"> + Pull {this.props.behindCount} -
    + )} {isBehind && isAhead && !isUnpublished && !isSyncing && ( (this.clickNode = node)} + ref={node => (this.tooltipTarget = node)} onClick={this.onClickPushPull} className="push-pull-target" data-tooltip="Click to push
    Cmd-click to force push
    Right-click for more"> @@ -141,43 +143,52 @@ export default class PushPullView extends React.Component { Pull {this.props.behindCount} -
    - )} + + )} {isBehind && isAhead && !isUnpublished && isSyncing && ( - - + + Pull {this.props.behindCount} - - )} + + )} {!isBehind && !isAhead && !isUnpublished && !isDetached && ( - (this.clickNode = node)} - onClick={this.onClickFetch} - className="push-pull-target" - data-tooltip="Click to fetch
    Right-click for more"> - +
    (this.tooltipTarget = node)} + onClick={this.onClickFetch} + className="push-pull-target" + data-tooltip="Click to fetch
    Right-click for more"> + Fetch
    )} - {isUnpublished && !isDetached && ( + {isUnpublished && !isDetached && hasOrigin && ( (this.clickNode = node)} + ref={node => (this.tooltipTarget = node)} onClick={this.onClickPublish} className="push-pull-target" - data-tooltip="Click to publish
    Right-click for more"> + data-tooltip="Click to set up a remote tracking branch
    Right-click for more"> Publish
    )} + {isUnpublished && !isDetached && !hasOrigin && ( + (this.tooltipTarget = node)} + className="push-pull-target" + data-tooltip={'There is no remote named "origin"'}> + + No remote + + )} + {isDetached && ( - (this.clickNode = node)} + (this.tooltipTarget = node)} className="push-pull-target" data-tooltip={'Create a branch if you wish to push your work anywhere'}> - + Not on branch )} diff --git a/test/controllers/status-bar-tile-controller.test.js b/test/controllers/status-bar-tile-controller.test.js index 9212c5f863..55d6013469 100644 --- a/test/controllers/status-bar-tile-controller.test.js +++ b/test/controllers/status-bar-tile-controller.test.js @@ -10,7 +10,6 @@ import {getTempDir} from '../../lib/helpers'; import Repository from '../../lib/models/repository'; import StatusBarTileController from '../../lib/controllers/status-bar-tile-controller'; import BranchView from '../../lib/views/branch-view'; -import PushPullView from '../../lib/views/push-pull-view'; import ChangedFilesCountView from '../../lib/views/changed-files-count-view'; describe('StatusBarTileController', function() { @@ -267,7 +266,7 @@ describe('StatusBarTileController', function() { }); }); - describe.only('pushing and pulling', function() { + describe('pushing and pulling', function() { describe('status bar tile state', function() { @@ -487,67 +486,35 @@ describe('StatusBarTileController', function() { }); }); - }); - - describe('when there is no remote tracking branch', function() { - let repository; - - beforeEach(async function() { - const {localRepoPath} = await setUpLocalAndRemoteRepositories(); - repository = await buildRepository(localRepoPath); - await repository.git.exec(['checkout', '-b', 'new-branch']); - }); - - it('disables the fetch and pull buttons and displays an informative message', async function() { - const wrapper = mount(React.cloneElement(component, {repository})); - await wrapper.instance().refreshModelData(); - - const tip = getTooltipNode(wrapper, PushPullView); - - const pullButton = tip.querySelector('button.github-PushPullMenuView-pull'); - const pushButton = tip.querySelector('button.github-PushPullMenuView-push'); - const message = tip.querySelector('.github-PushPullMenuView-message'); - - assert.isTrue(pullButton.disabled); - assert.isFalse(pushButton.disabled); - assert.match(message.innerHTML, /No remote detected.*Pushing will set up a remote tracking branch/); - - pushButton.click(); - await until(async fail => { - try { - repository.refresh(); - await wrapper.instance().refreshModelData(); - - assert.isFalse(pullButton.disabled); - assert.isFalse(pushButton.disabled); - assert.equal(message.textContent, ''); - return true; - } catch (err) { - return fail(err); - } - }); - }); - describe('when there is no remote named "origin"', function() { + let repository; + let statusBarTile; + beforeEach(async function() { + const {localRepoPath} = await setUpLocalAndRemoteRepositories(); + repository = await buildRepository(localRepoPath); await repository.git.exec(['remote', 'remove', 'origin']); - }); - it('additionally disables the push button and displays an informative message', async function() { - const wrapper = mount(React.cloneElement(component, {repository})); - await wrapper.instance().refreshModelData(); + statusBarTile = mount(React.cloneElement(component, {repository})); + await statusBarTile.instance().refreshModelData(); - const tip = getTooltipNode(wrapper, PushPullView); + sinon.spy(repository, 'fetch'); + sinon.spy(repository, 'push'); + sinon.spy(repository, 'pull'); + }); - const pullButton = tip.querySelector('button.github-PushPullMenuView-pull'); - const pushButton = tip.querySelector('button.github-PushPullMenuView-push'); - const message = tip.querySelector('.github-PushPullMenuView-message'); + it('gives the option to push with ahead and behind count', function() { + assert.equal(statusBarTile.find('.github-PushPull').text().trim(), 'No remote'); + }); - assert.isTrue(pullButton.disabled); - assert.isTrue(pushButton.disabled); - assert.match(message.innerHTML, /No remote detected.*no remote named "origin"/); + it('does nothing when clicked', function() { + statusBarTile.find('.push-pull-target').simulate('click'); + assert.isFalse(repository.fetch.called); + assert.isFalse(repository.push.called); + assert.isFalse(repository.pull.called); }); }); + }); it('displays an error message if push fails', async function() { @@ -578,33 +545,6 @@ describe('StatusBarTileController', function() { wrapper.unmount(); }); - describe('with a detached HEAD', function() { - let wrapper; - - beforeEach(async function() { - const workdirPath = await cloneRepository('multiple-commits'); - const repository = await buildRepository(workdirPath); - await repository.checkout('HEAD~2'); - - wrapper = mount(React.cloneElement(component, {repository})); - await wrapper.instance().refreshModelData(); - }); - - it('disables the fetch, pull, and push buttons', function() { - const tip = getTooltipNode(wrapper, PushPullView); - - assert.isTrue(tip.querySelector('button.github-PushPullMenuView-pull').disabled); - assert.isTrue(tip.querySelector('button.github-PushPullMenuView-push').disabled); - }); - - it('displays an appropriate explanation', function() { - const tip = getTooltipNode(wrapper, PushPullView); - - const message = tip.querySelector('.github-PushPullMenuView-message'); - assert.match(message.textContent, /not on a branch/); - }); - }); - describe('fetch and pull commands', function() { it('fetches when github:fetch is triggered', async function() { const {localRepoPath} = await setUpLocalAndRemoteRepositories('multiple-commits', {remoteAhead: true}); From d98120c2e891c6e5b38fa9538b01f11bc5ee9d3d Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 20 Mar 2018 00:51:37 +0100 Subject: [PATCH 0535/5882] Remove now-unused push-pull-menu-view --- lib/views/push-pull-menu-view.js | 148 ------------------ .../status-bar-tile-controller.test.js | 1 - 2 files changed, 149 deletions(-) delete mode 100644 lib/views/push-pull-menu-view.js diff --git a/lib/views/push-pull-menu-view.js b/lib/views/push-pull-menu-view.js deleted file mode 100644 index 7ec3971229..0000000000 --- a/lib/views/push-pull-menu-view.js +++ /dev/null @@ -1,148 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import {autobind} from 'core-decorators'; - -import {BranchPropType, RemotePropType} from '../prop-types'; -import {GitError} from '../git-shell-out-strategy'; - -export default class PushPullMenuView extends React.Component { - static propTypes = { - currentBranch: BranchPropType.isRequired, - currentRemote: RemotePropType.isRequired, - inProgress: PropTypes.bool, - aheadCount: PropTypes.number, - behindCount: PropTypes.number, - onMarkSpecialClick: PropTypes.func.isRequired, - fetch: PropTypes.func.isRequired, - push: PropTypes.func.isRequired, - pull: PropTypes.func.isRequired, - originExists: PropTypes.bool.isRequired, - } - - static defaultProps = { - inProgress: false, - onMarkSpecialClick: () => {}, - } - - constructor(props, context) { - super(props, context); - - this.state = { - errorMessage: '', - }; - } - - render() { - const errorMessage = this.getErrorMessage(); - const fetchDisabled = !this.props.currentRemote.isPresent() - || this.props.inProgress; - const pullDisabled = !this.props.currentRemote.isPresent() - || this.props.currentBranch.isDetached() - || this.props.inProgress; - const pushDisabled = this.props.currentBranch.isDetached() - || (!this.props.currentRemote.isPresent() && !this.props.originExists) - || this.props.inProgress; - - return ( -
    -
    - - - -
    - - -
    -
    -
    - {errorMessage} -
    -
    - ); - } - - getErrorMessage() { - if (this.state.errorMessage !== '') { - return this.state.errorMessage; - } - - if (this.props.currentBranch.isDetached()) { - return 'Note: you are not on a branch. Please create one if you wish to push your work anywhere.'; - } - - if (!this.props.currentRemote.isPresent()) { - if (this.props.originExists) { - return `Note: No remote detected for branch ${this.props.currentBranch.getName()}. ` + - 'Pushing will set up a remote tracking branch on remote repo "origin"'; - } else { - return `Note: No remote detected for branch ${this.props.currentBranch.getName()}. ` + - 'Cannot push because there is no remote named "origin" for which to create a remote tracking branch.'; - } - } - - return ''; - } - - @autobind - handleIconClick(evt) { - if (evt.shiftKey) { - this.props.onMarkSpecialClick(); - } - } - - @autobind - fetch() { - try { - return this.props.fetch(); - } catch (error) { - if (error instanceof GitError) { - // eslint-disable-next-line no-console - console.warn('Non-fatal', error); - } else { - throw error; - } - return null; - } - } - - @autobind - pull() { - try { - return this.props.pull(); - } catch (error) { - if (error instanceof GitError) { - // eslint-disable-next-line no-console - console.warn('Non-fatal', error); - } else { - throw error; - } - return null; - } - } - - @autobind - async push(evt) { - try { - await this.props.push({force: evt.metaKey || evt.ctrlKey, setUpstream: !this.props.currentRemote.isPresent()}); - } catch (error) { - if (error instanceof GitError) { - // eslint-disable-next-line no-console - console.warn('Non-fatal', error); - } else { - throw error; - } - } - } -} diff --git a/test/controllers/status-bar-tile-controller.test.js b/test/controllers/status-bar-tile-controller.test.js index 55d6013469..19329b4815 100644 --- a/test/controllers/status-bar-tile-controller.test.js +++ b/test/controllers/status-bar-tile-controller.test.js @@ -679,7 +679,6 @@ describe('StatusBarTileController', function() { assert.isFalse(wrapper.find('BranchView').exists()); assert.isFalse(wrapper.find('BranchMenuView').exists()); assert.isFalse(wrapper.find('PushPullView').exists()); - assert.isFalse(wrapper.find('PushPullMenuView').exists()); assert.isTrue(wrapper.find('ChangedFilesCountView').exists()); }); }); From 659709d406f53ea46d9dfffb4510a13b66b93c31 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 20 Mar 2018 20:33:55 -0700 Subject: [PATCH 0536/5882] Correct test case descriptions for push-pull-view --- test/controllers/status-bar-tile-controller.test.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/controllers/status-bar-tile-controller.test.js b/test/controllers/status-bar-tile-controller.test.js index 19329b4815..2b5ee8b441 100644 --- a/test/controllers/status-bar-tile-controller.test.js +++ b/test/controllers/status-bar-tile-controller.test.js @@ -397,7 +397,7 @@ describe('StatusBarTileController', function() { sinon.spy(repository, 'pull'); }); - it('gives the option to push with behind count', function() { + it('gives the option to pull with behind count', function() { assert.equal(statusBarTile.find('.github-PushPull').text().trim(), 'Pull 2'); }); @@ -435,7 +435,7 @@ describe('StatusBarTileController', function() { sinon.spy(repository, 'pull'); }); - it('gives the option to push with ahead and behind count', function() { + it('gives the option to pull with ahead and behind count', function() { assert.equal(statusBarTile.find('.github-PushPull').text().trim(), '1 Pull 2'); }); @@ -474,7 +474,7 @@ describe('StatusBarTileController', function() { sinon.spy(repository, 'pull'); }); - it('gives the option to push with ahead and behind count', function() { + it('gives a hint that we are not on a branch', function() { assert.equal(statusBarTile.find('.github-PushPull').text().trim(), 'Not on branch'); }); @@ -503,7 +503,7 @@ describe('StatusBarTileController', function() { sinon.spy(repository, 'pull'); }); - it('gives the option to push with ahead and behind count', function() { + it('gives that there is no remote', function() { assert.equal(statusBarTile.find('.github-PushPull').text().trim(), 'No remote'); }); From 95d92a48eaee0b4e5074641a9255c03bfb0fe4ae Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 20 Mar 2018 20:35:20 -0700 Subject: [PATCH 0537/5882] Fix unrelated indentations to make tests pass --- .../cross-referenced-events-container.js | 16 +++---- lib/github-package.js | 20 ++++----- lib/models/file-patch.js | 20 ++++----- lib/models/hunk-line.js | 44 +++++++++---------- 4 files changed, 50 insertions(+), 50 deletions(-) diff --git a/lib/containers/timeline-items/cross-referenced-events-container.js b/lib/containers/timeline-items/cross-referenced-events-container.js index 339244a395..fa26266fbe 100644 --- a/lib/containers/timeline-items/cross-referenced-events-container.js +++ b/lib/containers/timeline-items/cross-referenced-events-container.js @@ -51,14 +51,14 @@ export class CrossReferencedEvents extends React.Component { } else { let type = null; switch (first.source.__typename) { - case 'PullRequest': - type = 'pull request'; - break; - case 'Issue': - type = 'issue'; - break; - default: - throw new Error(`Invalid type: ${first.source.__typename}`); + case 'PullRequest': + type = 'pull request'; + break; + case 'Issue': + type = 'issue'; + break; + default: + throw new Error(`Invalid type: ${first.source.__typename}`); } let xrefClause = ''; if (first.isCrossRepository) { diff --git a/lib/github-package.js b/lib/github-package.js index 7e413e31d3..59f56ee447 100644 --- a/lib/github-package.js +++ b/lib/github-package.js @@ -342,16 +342,16 @@ export default class GithubPackage { // always return an empty stub // but only set it as the active item for a tab type // if it doesn't already exist - case 'atom-github://dock-item/git': - item = this.createGitTabControllerStub(uri); - this.gitTabStubItem = this.gitTabStubItem || item; - break; - case 'atom-github://dock-item/github': - item = this.createGithubTabControllerStub(uri); - this.githubTabStubItem = this.githubTabStubItem || item; - break; - default: - throw new Error(`Invalid DockItem stub URI: ${uri}`); + case 'atom-github://dock-item/git': + item = this.createGitTabControllerStub(uri); + this.gitTabStubItem = this.gitTabStubItem || item; + break; + case 'atom-github://dock-item/github': + item = this.createGithubTabControllerStub(uri); + this.githubTabStubItem = this.githubTabStubItem || item; + break; + default: + throw new Error(`Invalid DockItem stub URI: ${uri}`); } if (this.controller) { diff --git a/lib/models/file-patch.js b/lib/models/file-patch.js index 909289bdc1..beb1e76b88 100644 --- a/lib/models/file-patch.js +++ b/lib/models/file-patch.js @@ -226,16 +226,16 @@ export default class FilePatch { getUnstagePatch() { let invertedStatus; switch (this.getStatus()) { - case 'modified': - invertedStatus = 'modified'; - break; - case 'added': - invertedStatus = 'deleted'; - break; - case 'deleted': - invertedStatus = 'added'; - break; - default: + case 'modified': + invertedStatus = 'modified'; + break; + case 'added': + invertedStatus = 'deleted'; + break; + case 'deleted': + invertedStatus = 'added'; + break; + default: // throw new Error(`Unknown Status: ${this.getStatus()}`); } const invertedHunks = this.getHunks().map(h => h.invert()); diff --git a/lib/models/hunk-line.js b/lib/models/hunk-line.js index f75ce7f4b8..439ac90e8e 100644 --- a/lib/models/hunk-line.js +++ b/lib/models/hunk-line.js @@ -45,34 +45,34 @@ export default class HunkLine { getOrigin() { switch (this.getStatus()) { - case 'added': - return '+'; - case 'deleted': - return '-'; - case 'unchanged': - return ' '; - case 'nonewline': - return '\\'; - default: - return ''; + case 'added': + return '+'; + case 'deleted': + return '-'; + case 'unchanged': + return ' '; + case 'nonewline': + return '\\'; + default: + return ''; } } invert() { let invertedStatus; switch (this.getStatus()) { - case 'added': - invertedStatus = 'deleted'; - break; - case 'deleted': - invertedStatus = 'added'; - break; - case 'unchanged': - invertedStatus = 'unchanged'; - break; - case 'nonewline': - invertedStatus = 'nonewline'; - break; + case 'added': + invertedStatus = 'deleted'; + break; + case 'deleted': + invertedStatus = 'added'; + break; + case 'unchanged': + invertedStatus = 'unchanged'; + break; + case 'nonewline': + invertedStatus = 'nonewline'; + break; } return new HunkLine( From 27f66e057611d75e0742d8d0fc26da02e85fd99c Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Wed, 21 Mar 2018 15:08:29 -0700 Subject: [PATCH 0538/5882] Populate user store in constructor Co-authored-by: Tilde Ann Thurium --- lib/controllers/git-tab-controller.js | 1 - lib/models/user-store.js | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/controllers/git-tab-controller.js b/lib/controllers/git-tab-controller.js index 4c4fd26583..5394ffe97d 100644 --- a/lib/controllers/git-tab-controller.js +++ b/lib/controllers/git-tab-controller.js @@ -190,7 +190,6 @@ export default class GitTabController extends React.Component { this.setState({mentionableUsers: users}); }, }); - this.userStore.populate(); } } diff --git a/lib/models/user-store.js b/lib/models/user-store.js index e04caeecdb..653a0251bc 100644 --- a/lib/models/user-store.js +++ b/lib/models/user-store.js @@ -5,6 +5,7 @@ export default class UserStore { this.repository = repository; this.onDidUpdate = onDidUpdate || (() => {}); this.users = {}; + this.populate(); } populate() { From 9cb3597d9ceed7d3a007737760af385cb677d05a Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Wed, 21 Mar 2018 15:26:20 -0700 Subject: [PATCH 0539/5882] Handle edge case for getAuthors where repo has no commits Co-Authored-By: Tilde Ann Thurium --- lib/git-shell-out-strategy.js | 46 ++++++++++++++++++++--------------- lib/models/user-store.js | 34 ++++---------------------- test/git-strategies.test.js | 8 ++++++ 3 files changed, 40 insertions(+), 48 deletions(-) diff --git a/lib/git-shell-out-strategy.js b/lib/git-shell-out-strategy.js index ce49181a7f..7e9904c6cc 100644 --- a/lib/git-shell-out-strategy.js +++ b/lib/git-shell-out-strategy.js @@ -616,25 +616,33 @@ export default class GitShellOutStrategy { const fields = [ "%an", "%ae", "%cn", "%ce", "%(trailers:unfold,only)" ] const format = fields.join(`%x${delimiter}`); - const output = await this.exec([ - 'log', `--format=${format}`, '-z', '-n', max, ref, '--', - ]); - - return output.split('\0') - .filter(l => l.length > 0) - .map(line => line.split(delimiterString)) - .reduce((acc, [ an, ae, cn, ce, trailers ]) => { - trailers - .split('\n') - .map(trailer => trailer.match(/^co-authored-by. (.+?) <(.+?)>$/i)) - .filter(match => match !== null) - .forEach(([ _, name, email ]) => acc[email] = name) - - acc[ae] = an - acc[ce] = cn - - return acc; - }, {}); + try { + const output = await this.exec([ + 'log', `--format=${format}`, '-z', '-n', max, ref, '--', + ]); + + return output.split('\0') + .filter(l => l.length > 0) + .map(line => line.split(delimiterString)) + .reduce((acc, [ an, ae, cn, ce, trailers ]) => { + trailers + .split('\n') + .map(trailer => trailer.match(/^co-authored-by. (.+?) <(.+?)>$/i)) + .filter(match => match !== null) + .forEach(([ _, name, email ]) => acc[email] = name) + + acc[ae] = an + acc[ce] = cn + + return acc; + }, {}); + } catch (err) { + if (/unknown revision/.test(err.stdErr) || /bad revision 'HEAD'/.test(err.stdErr)) { + return []; + } else { + throw err; + } + } } mergeTrailers(commitMessage, trailers, unfold) { diff --git a/lib/models/user-store.js b/lib/models/user-store.js index 653a0251bc..1cf8d9e29f 100644 --- a/lib/models/user-store.js +++ b/lib/models/user-store.js @@ -1,4 +1,5 @@ -// TODO: create proper User class +// This is a guess about what a reasonable value is. Can adjust if performance is poor. +const MAX_COMMITS = 5000; export default class UserStore { constructor({repository, onDidUpdate}) { @@ -9,11 +10,8 @@ export default class UserStore { } populate() { - // TODO: check conditional branches - // if repo present, get info if (this.repository.isPresent()) { this.loadUsers(); - // else, add listener to do so when repo is present } else { this.repository.onDidChangeState(({from, to}) => { if (!from.isPresent() && to.isPresent()) { @@ -24,13 +22,13 @@ export default class UserStore { } loadUsers() { - // TODO: also get users from GraphQL API if available. Will need to reshape the data accordingly - // look into using Dexie + // TODO: [ku 3/2018] also get users from GraphQL API if available. Will need to reshape the data accordingly + // Consider using Dexie (indexDB wrapper) like Desktop this.loadUsersFromLocalRepo(); } async loadUsersFromLocalRepo() { - const users = await this.repository.getAuthors({max: 5000}); + const users = await this.repository.getAuthors({max: MAX_COMMITS}); this.addUsers(users); } @@ -59,25 +57,3 @@ export default class UserStore { }); } } - -// class User { -// constructor({email, name, githubLogin}) { -// -// } -// -// isGitHubUser() { -// return !!this.githubLogin; -// } -// -// displayName() { -// if (this.githubLogin) { -// return `@${this.githubLogin}`; -// } else { -// return `${this.name}`; -// } -// } -// -// coAuthorTrailer() { -// return `Co-authored-by: ${this.email}`; -// } -// } diff --git a/test/git-strategies.test.js b/test/git-strategies.test.js index bdd1478471..ec3ec8c23a 100644 --- a/test/git-strategies.test.js +++ b/test/git-strategies.test.js @@ -263,6 +263,14 @@ import {normalizeGitHelperPath, getTempDir} from '../lib/helpers'; 'yet-another@example.com': 'yet another name', }); }); + + it('returns an empty array when there are no commits', async function() { + const workingDirPath = await initRepository(); + const git = createTestStrategy(workingDirPath); + + const authors = await git.getAuthors({max: 1}); + assert.deepEqual(authors, []) + }); }); describe('diffFileStatus', function() { From 0629af0a55943e5e027bd43019a0637a824edc7f Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Wed, 21 Mar 2018 15:44:53 -0700 Subject: [PATCH 0540/5882] :memo: User Store Co-Authored-By: Tilde Ann Thurium --- lib/models/user-store.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/models/user-store.js b/lib/models/user-store.js index 1cf8d9e29f..dc0b77860c 100644 --- a/lib/models/user-store.js +++ b/lib/models/user-store.js @@ -5,6 +5,7 @@ export default class UserStore { constructor({repository, onDidUpdate}) { this.repository = repository; this.onDidUpdate = onDidUpdate || (() => {}); + // TODO: [ku 3/2018] Consider using Dexie (indexDB wrapper) like Desktop and persist users across sessions this.users = {}; this.populate(); } @@ -22,8 +23,6 @@ export default class UserStore { } loadUsers() { - // TODO: [ku 3/2018] also get users from GraphQL API if available. Will need to reshape the data accordingly - // Consider using Dexie (indexDB wrapper) like Desktop this.loadUsersFromLocalRepo(); } @@ -33,7 +32,8 @@ export default class UserStore { } addUsersFromGraphQL(response) { - // this gets called in query renderer callback + // TODO: [ku 3/2018] also get users from GraphQL API if available. Will need to reshape the data accordingly + // This will get called in relay query renderer callback // this.addUsers(users); } @@ -47,7 +47,8 @@ export default class UserStore { } getUsers() { - // don't actually do this. will change when we settle on best way to actually store data + // TODO: [ku 3/2018] consider sorting based on most recent authors or commit frequency + // This is obviously not the most performant. Test out on repo with many users to see if we need to optimize return Object.keys(this.users) .map(email => ({email, name: this.users[email]})) .sort((a, b) => { From 20e54f49a45c4f7cafd3e2bcf81cf29d9809a482 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Wed, 21 Mar 2018 16:06:39 -0700 Subject: [PATCH 0541/5882] Add authors cache key Co-Authored-By: Tilde Ann Thurium --- lib/models/repository-states/present.js | 11 +++++++++-- lib/models/user-store.js | 3 +++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js index 7fda147c97..9bc86b9380 100644 --- a/lib/models/repository-states/present.js +++ b/lib/models/repository-states/present.js @@ -151,6 +151,7 @@ export default class Present extends State { keys.add(Keys.recentCommits); keys.add(Keys.statusBundle); keys.add(Keys.headDescription); + keys.add(Keys.authors); continue; } @@ -159,6 +160,7 @@ export default class Present extends State { keys.add(Keys.lastCommit); keys.add(Keys.recentCommits); keys.add(Keys.headDescription); + keys.add(Keys.authors); continue; } @@ -316,6 +318,7 @@ export default class Present extends State { Keys.stagedChangesSinceParentCommit, Keys.lastCommit, Keys.recentCommits, + Keys.authors, Keys.statusBundle, Keys.index.all, ...Keys.filePatch.eachWithOpts({staged: true, amending: true}), @@ -623,8 +626,9 @@ export default class Present extends State { // Author information getAuthors(options) { - // TODO: caching - return this.git().getAuthors(options); + return this.cache.getOrSet(Keys.authors, async () => { + return this.git().getAuthors(options); + }); } // Branches @@ -981,6 +985,8 @@ const Keys = { recentCommits: new CacheKey('recent-commits'), + authors: new CacheKey('authors'), + branches: new CacheKey('branches'), headDescription: new CacheKey('head-description'), @@ -1026,6 +1032,7 @@ const Keys = { Keys.stagedChangesSinceParentCommit, Keys.lastCommit, Keys.recentCommits, + Keys.authors, Keys.statusBundle, ], }; diff --git a/lib/models/user-store.js b/lib/models/user-store.js index dc0b77860c..55d8a94587 100644 --- a/lib/models/user-store.js +++ b/lib/models/user-store.js @@ -4,6 +4,9 @@ const MAX_COMMITS = 5000; export default class UserStore { constructor({repository, onDidUpdate}) { this.repository = repository; + this.repository.onDidUpdate(() => { + this.loadUsers(); + }) this.onDidUpdate = onDidUpdate || (() => {}); // TODO: [ku 3/2018] Consider using Dexie (indexDB wrapper) like Desktop and persist users across sessions this.users = {}; From a9d4a9b5674c7721a51edc5340df22c7e6fc2ad9 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Wed, 21 Mar 2018 16:30:24 -0700 Subject: [PATCH 0542/5882] Add title to toggle co-authors icon for accessibility Co-Authored-By: Tilde Ann Thurium --- lib/views/commit-view.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/views/commit-view.js b/lib/views/commit-view.js index d9c916a975..e0851289c4 100644 --- a/lib/views/commit-view.js +++ b/lib/views/commit-view.js @@ -214,6 +214,7 @@ export default class CommitView extends React.Component { const svgPath = 'M9.875 2.125H12v1.75H9.875V6h-1.75V3.875H6v-1.75h2.125V0h1.75v2.125zM6 6.5a.5.5 0 0 1-.5.5h-5a.5.5 0 0 1-.5-.5V6c0-1.316 2-2 2-2s.114-.204 0-.5c-.42-.31-.472-.795-.5-2C1.587.293 2.434 0 3 0s1.413.293 1.5 1.5c-.028 1.205-.08 1.69-.5 2-.114.295 0 .5 0 .5s2 .684 2 2v.5z'; return ( + Toggle co-authors ); From e6d23f486b2eed393aa0a960305b474a7e009df2 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Wed, 21 Mar 2018 16:33:27 -0700 Subject: [PATCH 0543/5882] :fire: login until we add github user info Co-Authored-By: Tilde Ann Thurium --- lib/views/commit-view.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/views/commit-view.js b/lib/views/commit-view.js index e0851289c4..e4f509b432 100644 --- a/lib/views/commit-view.js +++ b/lib/views/commit-view.js @@ -405,7 +405,7 @@ export default class CommitView extends React.Component { matchAuthors(authors, filterText, currentValues) { return authors .filter(x => !currentValues || currentValues.indexOf(x) === -1) - .filter(x => `${x.name}${x.login}${x.email}`.toLowerCase().indexOf(filterText.toLowerCase()) !== -1); + .filter(x => `${x.name}${x.email}`.toLowerCase().indexOf(filterText.toLowerCase()) !== -1); } renderCoAuthorListItemField(fieldName, value) { @@ -422,7 +422,6 @@ export default class CommitView extends React.Component { renderCoAuthorListItem(author) { return (
    - {this.renderCoAuthorListItemField('username', author.login)} {this.renderCoAuthorListItemField('name', author.name)} {this.renderCoAuthorListItemField('email', author.email)}
    @@ -430,9 +429,8 @@ export default class CommitView extends React.Component { } renderCoAuthorValue(author) { - const user = author.login ? `@${author.login}` : author.name; return ( - {user} + {author.name} ); } From 46ef9cd9722312b87537e7762d92c592ac9d9a0d Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Wed, 21 Mar 2018 16:41:17 -0700 Subject: [PATCH 0544/5882] Don't call UserStore#populate in tests since it happens upon construction Co-Authored-By: Tilde Ann Thurium --- test/models/user-store.test.js | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/test/models/user-store.test.js b/test/models/user-store.test.js index ace0f1e09b..859a113e6d 100644 --- a/test/models/user-store.test.js +++ b/test/models/user-store.test.js @@ -3,23 +3,20 @@ import UserStore from '../../lib/models/user-store'; import {cloneRepository, buildRepository} from '../helpers'; describe('UserStore', function() { - describe('populate', function() { - it('loads store with users in repo', async function() { - const workdirPath = await cloneRepository('multiple-commits'); - const repository = await buildRepository(workdirPath); - const store = new UserStore({repository}); + it('loads store with users in repo upon construction', async function() { + const workdirPath = await cloneRepository('multiple-commits'); + const repository = await buildRepository(workdirPath); + const store = new UserStore({repository}); - assert.deepEqual(store.getUsers(), []); + assert.deepEqual(store.getUsers(), []); - await store.populate(); - const users = store.getUsers(); - assert.deepEqual(users, [ - { - email: 'kuychaco@github.com', - name: 'Katrina Uychaco', - }, - ]); - }); + // Store is populated asynchronously + await assert.async.deepEqual(store.getUsers(), [ + { + email: 'kuychaco@github.com', + name: 'Katrina Uychaco', + }, + ]); }); describe('addUsers', function() { @@ -28,13 +25,14 @@ describe('UserStore', function() { const repository = await buildRepository(workdirPath); const store = new UserStore({repository}); - await store.populate(); + await assert.async.lengthOf(store.getUsers(), 1); + store.addUsers({ 'mona@lisa.com': 'Mona Lisa', 'hubot@github.com': 'Hubot Robot', }); - assert.deepEqual(store.getUsers(), [ + await assert.async.deepEqual(store.getUsers(), [ { name: 'Hubot Robot', email: 'hubot@github.com', From 8a6ca824d02812efb771f631d4f0c63c16942e99 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Wed, 21 Mar 2018 17:34:30 -0700 Subject: [PATCH 0545/5882] Add User Store test for refetching data when HEAD changes Co-Authored-By: Tilde Ann Thurium --- test/models/user-store.test.js | 39 +++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/test/models/user-store.test.js b/test/models/user-store.test.js index 859a113e6d..4d8be260ff 100644 --- a/test/models/user-store.test.js +++ b/test/models/user-store.test.js @@ -1,3 +1,5 @@ +import dedent from 'dedent-js' + import UserStore from '../../lib/models/user-store'; import {cloneRepository, buildRepository} from '../helpers'; @@ -20,7 +22,7 @@ describe('UserStore', function() { }); describe('addUsers', function() { - it('adds specified user', async function() { + it('adds specified users and does not overwrite existing users', async function() { const workdirPath = await cloneRepository('multiple-commits'); const repository = await buildRepository(workdirPath); const store = new UserStore({repository}); @@ -48,4 +50,39 @@ describe('UserStore', function() { ]); }); }); + + it('refetches users when HEAD changes', async function() { + const workdirPath = await cloneRepository('multiple-commits'); + const repository = await buildRepository(workdirPath); + await repository.checkout('new-branch', {createNew: true}); + await repository.commit('commit 1', {allowEmpty: true}); + await repository.commit('commit 2', {allowEmpty: true}); + await repository.checkout('master'); + + const store = new UserStore({repository}); + await assert.async.deepEqual(store.getUsers(), [ + { + email: 'kuychaco@github.com', + name: 'Katrina Uychaco', + }, + ]); + + sinon.spy(store, 'addUsers') + + // Head changes due to new commit + await repository.commit(dedent` + New commit + + Co-authored-by: New Author + `, {allowEmpty: true}); + + await assert.async.equal(store.addUsers.callCount, 1) + assert.isOk(store.getUsers().find(user => { + return user.name === 'New Author' && user.email === 'new-author@email.com'; + })); + + // Change head due to branch checkout + await repository.checkout('new-branch') + await assert.async.equal(store.addUsers.callCount, 2) + }); }); From c93f83a25636034553730624f2e248ef21ca9bc0 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Wed, 21 Mar 2018 18:10:01 -0700 Subject: [PATCH 0546/5882] Fix test Co-Authored-By: Tilde Ann Thurium --- test/git-strategies.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/git-strategies.test.js b/test/git-strategies.test.js index ec3ec8c23a..428f0f4c77 100644 --- a/test/git-strategies.test.js +++ b/test/git-strategies.test.js @@ -251,8 +251,8 @@ import {normalizeGitHelperPath, getTempDir} from '../lib/helpers'; Implemented feature collaboratively Co-authored-by: name - Co-authored-by: another name " - Co-authored-by: yet another name " + Co-authored-by: another name + Co-authored-by: yet another name `, {allowEmpty: true}); const authors = await git.getAuthors({max: 1}); From 5cdee60dcd4778902e48065c640d4e96ccc9c903 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Wed, 21 Mar 2018 18:26:02 -0700 Subject: [PATCH 0547/5882] Trim message before committing and default coAuthors to empty array (to fix tests) Co-Authored-By: Tilde Ann Thurium --- lib/controllers/commit-controller.js | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/lib/controllers/commit-controller.js b/lib/controllers/commit-controller.js index 952c066a73..176d9a89c8 100644 --- a/lib/controllers/commit-controller.js +++ b/lib/controllers/commit-controller.js @@ -139,7 +139,7 @@ export default class CommitController extends React.Component { } @autobind - async commit(message, coAuthors) { + async commit(message, coAuthors = []) { let msg; if (this.getCommitMessageEditors().length > 0) { @@ -149,15 +149,17 @@ export default class CommitController extends React.Component { msg = wrapMessage ? wrapCommitMessage(message) : message; } - // TODO: ensure that expanded editor commit functionality still works - const trailers = coAuthors.map(author => { - return { - token: 'Co-Authored-By', - value: `${author.name} <${author.email}>`, - }; - }); - const msgWithTrailers = await this.props.repository.addTrailersToCommitMessage(msg, trailers); - await this.props.commit(msgWithTrailers); + if (coAuthors.length) { + // TODO: ensure that expanded editor commit functionality still works + const trailers = coAuthors.map(author => { + return { + token: 'Co-Authored-By', + value: `${author.name} <${author.email}>`, + }; + }); + msg = await this.props.repository.addTrailersToCommitMessage(msg, trailers); + } + return this.props.commit(msg.trim()); } getCommitMessage() { From 7cb00e98e6d066fb03165c39205694a8a0e03b1e Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Wed, 21 Mar 2018 18:37:59 -0700 Subject: [PATCH 0548/5882] Add comment about caching Co-Authored-By: Tilde Ann Thurium --- lib/models/repository-states/present.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js index 9bc86b9380..cf6ede5e2f 100644 --- a/lib/models/repository-states/present.js +++ b/lib/models/repository-states/present.js @@ -626,6 +626,9 @@ export default class Present extends State { // Author information getAuthors(options) { + // For now we'll do the naive thing and invalidate anytime HEAD moves. This ensures that we get new authors + // introduced by newly created commits or pulled commits. + // This means that we are constantly re-fetching data. If performance becomes a concern we can optimize return this.cache.getOrSet(Keys.authors, async () => { return this.git().getAuthors(options); }); From 5329c861ebf7c3cfcbf573aab537452c1d786980 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Wed, 21 Mar 2018 23:04:23 -0700 Subject: [PATCH 0549/5882] :shirt: --- lib/git-shell-out-strategy.js | 16 +- lib/models/repository-states/present.js | 2 +- lib/models/user-store.js | 6 +- lib/views/commit-view.js | 8 +- package-lock.json | 3815 +++++------------------ package.json | 5 +- test/git-strategies.test.js | 2 +- test/models/user-store.test.js | 10 +- 8 files changed, 781 insertions(+), 3083 deletions(-) diff --git a/lib/git-shell-out-strategy.js b/lib/git-shell-out-strategy.js index 7e9904c6cc..9c6770c1a6 100644 --- a/lib/git-shell-out-strategy.js +++ b/lib/git-shell-out-strategy.js @@ -611,9 +611,9 @@ export default class GitShellOutStrategy { // by newlines and unfolded (i.e. properly // formatted and one trailer per line). - const delimiter = '1F' - const delimiterString = String.fromCharCode(parseInt(delimiter, 16)) - const fields = [ "%an", "%ae", "%cn", "%ce", "%(trailers:unfold,only)" ] + const delimiter = '1F'; + const delimiterString = String.fromCharCode(parseInt(delimiter, 16)); + const fields = ['%an', '%ae', '%cn', '%ce', '%(trailers:unfold,only)']; const format = fields.join(`%x${delimiter}`); try { @@ -624,18 +624,18 @@ export default class GitShellOutStrategy { return output.split('\0') .filter(l => l.length > 0) .map(line => line.split(delimiterString)) - .reduce((acc, [ an, ae, cn, ce, trailers ]) => { + .reduce((acc, [an, ae, cn, ce, trailers]) => { trailers .split('\n') .map(trailer => trailer.match(/^co-authored-by. (.+?) <(.+?)>$/i)) .filter(match => match !== null) - .forEach(([ _, name, email ]) => acc[email] = name) + .forEach(([_, name, email]) => { acc[email] = name; }); - acc[ae] = an - acc[ce] = cn + acc[ae] = an; + acc[ce] = cn; return acc; - }, {}); + }, {}); } catch (err) { if (/unknown revision/.test(err.stdErr) || /bad revision 'HEAD'/.test(err.stdErr)) { return []; diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js index cf6ede5e2f..d1f871d31c 100644 --- a/lib/models/repository-states/present.js +++ b/lib/models/repository-states/present.js @@ -629,7 +629,7 @@ export default class Present extends State { // For now we'll do the naive thing and invalidate anytime HEAD moves. This ensures that we get new authors // introduced by newly created commits or pulled commits. // This means that we are constantly re-fetching data. If performance becomes a concern we can optimize - return this.cache.getOrSet(Keys.authors, async () => { + return this.cache.getOrSet(Keys.authors, () => { return this.git().getAuthors(options); }); } diff --git a/lib/models/user-store.js b/lib/models/user-store.js index 55d8a94587..532edb5926 100644 --- a/lib/models/user-store.js +++ b/lib/models/user-store.js @@ -6,7 +6,7 @@ export default class UserStore { this.repository = repository; this.repository.onDidUpdate(() => { this.loadUsers(); - }) + }); this.onDidUpdate = onDidUpdate || (() => {}); // TODO: [ku 3/2018] Consider using Dexie (indexDB wrapper) like Desktop and persist users across sessions this.users = {}; @@ -55,8 +55,8 @@ export default class UserStore { return Object.keys(this.users) .map(email => ({email, name: this.users[email]})) .sort((a, b) => { - if(a.name < b.name) return -1; - if(a.name > b.name) return 1; + if (a.name < b.name) { return -1; } + if (a.name > b.name) { return 1; } return 0; }); } diff --git a/lib/views/commit-view.js b/lib/views/commit-view.js index e4f509b432..8fd762ebec 100644 --- a/lib/views/commit-view.js +++ b/lib/views/commit-view.js @@ -315,8 +315,8 @@ export default class CommitView extends React.Component { @autobind toggleCoAuthorInput() { this.setState({ - showCoAuthorInput: !this.state.showCoAuthorInput - }) + showCoAuthorInput: !this.state.showCoAuthorInput, + }); } @autobind @@ -410,12 +410,12 @@ export default class CommitView extends React.Component { renderCoAuthorListItemField(fieldName, value) { if (!value || value.length === 0) { - return null + return null; } return ( {value} - ) + ); } @autobind diff --git a/package-lock.json b/package-lock.json index bd4eee7132..ee6363dbfd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2,7 +2,6 @@ "name": "github", "version": "0.12.0", "lockfileVersion": 1, - "requires": true, "dependencies": { "7zip": { "version": "0.0.6", @@ -13,16 +12,13 @@ "acorn": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.1.1.tgz", - "integrity": "sha1-U/4WERH5EquZnuiHqQoLxSgi/XU=", + "integrity": "sha512-vOk6uEMctu0vQrvuSqFdJyqj1Q0S5VTDL79qtjo+DhRr+1mmaD+tluFSCZqhvi/JUhXSzoZN2BhtstaPEeE8cw==", "dev": true }, "acorn-jsx": { "version": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz", "integrity": "sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=", "dev": true, - "requires": { - "acorn": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz" - }, "dependencies": { "acorn": { "version": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", @@ -34,13 +30,19 @@ "ajv": { "version": "5.5.2", "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", - "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", - "requires": { - "co": "4.6.0", - "fast-deep-equal": "1.1.0", - "fast-json-stable-stringify": "2.0.0", - "json-schema-traverse": "0.3.1" - } + "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=" + }, + "ajv-keywords": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-1.5.1.tgz", + "integrity": "sha1-MU3QpLM2j609/NxU7eYXG4htrzw=", + "dev": true + }, + "ansi-escapes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz", + "integrity": "sha1-06ioOzGapneTZisT52HHkRQiMG4=", + "dev": true }, "ansi-regex": { "version": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", @@ -70,11 +72,29 @@ "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=" }, + "array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", + "dev": true + }, + "array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", + "dev": true + }, "array-unique": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" }, + "arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", + "dev": true + }, "asap": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", @@ -113,41 +133,25 @@ "atom-babel6-transpiler": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/atom-babel6-transpiler/-/atom-babel6-transpiler-1.1.3.tgz", - "integrity": "sha1-1wKxDpDrzx+R4apcSnm5zqmKm6Y=", - "requires": { - "babel-core": "6.26.0" - } + "integrity": "sha1-1wKxDpDrzx+R4apcSnm5zqmKm6Y=" }, "atom-mocha-test-runner": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/atom-mocha-test-runner/-/atom-mocha-test-runner-1.2.0.tgz", "integrity": "sha1-qPZQm40pqAn8tv9H8FiEthLNxqk=", "dev": true, - "requires": { - "etch": "0.8.0", - "grim": "2.0.2", - "less": "2.7.3", - "mocha": "3.4.2", - "tmp": "0.0.31" - }, "dependencies": { "etch": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/etch/-/etch-0.8.0.tgz", "integrity": "sha1-VPYZV0NG+KPueXP1T7vQG1YnItY=", - "dev": true, - "requires": { - "virtual-dom": "2.1.1" - } + "dev": true }, "grim": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/grim/-/grim-2.0.2.tgz", "integrity": "sha512-Qj7hTJRfd87E/gUgfvM0YIH/g2UA2SV6niv6BYXk1o6w4mhgv+QyYM1EjOJQljvzgEj4SqSsRWldXIeKHz3e3Q==", - "dev": true, - "requires": { - "event-kit": "2.4.0" - } + "dev": true } } }, @@ -168,85 +172,31 @@ "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.26.0.tgz", "integrity": "sha1-rzL3izGm/O8RnIew/Y2XU/A6C7g=", - "requires": { - "babel-code-frame": "6.26.0", - "babel-generator": "6.26.0", - "babel-helpers": "6.24.1", - "babel-messages": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", - "babel-register": "6.26.0", - "babel-runtime": "6.26.0", - "babel-template": "6.26.0", - "babel-traverse": "6.26.0", - "babel-types": "6.26.0", - "babylon": "6.18.0", - "convert-source-map": "1.5.1", - "debug": "2.6.8", - "json5": "0.5.1", - "lodash": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", - "minimatch": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "path-is-absolute": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "private": "0.1.8", - "slash": "1.0.0", - "source-map": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz" - }, "dependencies": { "babel-code-frame": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", - "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", - "requires": { - "chalk": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "esutils": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "js-tokens": "3.0.2" - } + "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=" }, "babel-runtime": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", - "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", - "requires": { - "core-js": "2.5.0", - "regenerator-runtime": "0.11.1" - } + "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=" }, "babel-template": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz", - "integrity": "sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=", - "requires": { - "babel-runtime": "6.26.0", - "babel-traverse": "6.26.0", - "babel-types": "6.26.0", - "babylon": "6.18.0", - "lodash": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz" - } + "integrity": "sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=" }, "babel-traverse": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", - "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=", - "requires": { - "babel-code-frame": "6.26.0", - "babel-messages": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", - "babel-runtime": "6.26.0", - "babel-types": "6.26.0", - "babylon": "6.18.0", - "debug": "2.6.8", - "globals": "9.18.0", - "invariant": "https://registry.npmjs.org/invariant/-/invariant-2.2.2.tgz", - "lodash": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz" - } + "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=" }, "babel-types": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", - "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", - "requires": { - "babel-runtime": "6.26.0", - "esutils": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "lodash": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", - "to-fast-properties": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz" - } + "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=" }, "babylon": { "version": "6.18.0", @@ -269,12 +219,6 @@ "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-7.2.3.tgz", "integrity": "sha1-sv4tgBJkcPXBlELcdXJTqJdxCCc=", "dev": true, - "requires": { - "babel-code-frame": "6.26.0", - "babel-traverse": "6.25.0", - "babel-types": "6.25.0", - "babylon": "6.17.4" - }, "dependencies": { "ansi-regex": { "version": "2.1.1", @@ -292,25 +236,13 @@ "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", - "dev": true, - "requires": { - "chalk": "1.1.3", - "esutils": "2.0.2", - "js-tokens": "3.0.2" - } + "dev": true }, "chalk": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "2.2.1", - "escape-string-regexp": "1.0.5", - "has-ansi": "2.0.0", - "strip-ansi": "3.0.1", - "supports-color": "2.0.0" - } + "dev": true }, "escape-string-regexp": { "version": "1.0.5", @@ -328,19 +260,13 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "dev": true, - "requires": { - "ansi-regex": "2.1.1" - } + "dev": true }, "strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "2.1.1" - } + "dev": true }, "supports-color": { "version": "2.0.0", @@ -354,36 +280,16 @@ "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.0.tgz", "integrity": "sha1-rBriAHC3n248odMmlhMFN3TyDcU=", - "requires": { - "babel-messages": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", - "babel-runtime": "6.26.0", - "babel-types": "6.26.0", - "detect-indent": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz", - "jsesc": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", - "lodash": "4.17.5", - "source-map": "0.5.7", - "trim-right": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz" - }, "dependencies": { "babel-runtime": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", - "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", - "requires": { - "core-js": "2.5.0", - "regenerator-runtime": "0.11.0" - } + "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=" }, "babel-types": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", - "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", - "requires": { - "babel-runtime": "6.26.0", - "esutils": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "lodash": "4.17.5", - "to-fast-properties": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz" - } + "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=" }, "regenerator-runtime": { "version": "0.11.0", @@ -401,31 +307,16 @@ "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-helper-builder-react-jsx/-/babel-helper-builder-react-jsx-6.26.0.tgz", "integrity": "sha1-Of+DE7dci2Xc7/HzHTg+D/KkCKA=", - "requires": { - "babel-runtime": "6.26.0", - "babel-types": "6.26.0", - "esutils": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz" - }, "dependencies": { "babel-runtime": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", - "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", - "requires": { - "core-js": "2.5.0", - "regenerator-runtime": "0.11.1" - } + "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=" }, "babel-types": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", - "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", - "requires": { - "babel-runtime": "6.26.0", - "esutils": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "lodash": "4.17.5", - "to-fast-properties": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz" - } + "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=" }, "regenerator-runtime": { "version": "0.11.1", @@ -438,108 +329,55 @@ "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-helper-define-map/-/babel-helper-define-map-6.26.0.tgz", "integrity": "sha1-pfVtq0GiX5fstJjH66ypgZ+Vvl8=", - "requires": { - "babel-helper-function-name": "https://registry.npmjs.org/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz", - "babel-runtime": "6.26.0", - "babel-types": "6.26.0", - "lodash": "4.17.5" - }, "dependencies": { "babel-runtime": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", - "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", - "requires": { - "core-js": "2.5.0", - "regenerator-runtime": "0.11.1" - } + "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=" }, "babel-types": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", - "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", - "requires": { - "babel-runtime": "6.26.0", - "esutils": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "lodash": "4.17.5", - "to-fast-properties": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz" - } + "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=" }, "regenerator-runtime": { "version": "0.11.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", - "integrity": "sha1-vgWtf5v30i4Fb5cmzuUBf78Z4uk=" + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" } } }, "babel-helper-function-name": { "version": "https://registry.npmjs.org/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz", - "integrity": "sha1-00dbjAPtmCQqJbSDUasYOZ01gKk=", - "requires": { - "babel-helper-get-function-arity": "https://registry.npmjs.org/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz", - "babel-runtime": "6.25.0", - "babel-template": "6.25.0", - "babel-traverse": "6.25.0", - "babel-types": "6.25.0" - } + "integrity": "sha1-00dbjAPtmCQqJbSDUasYOZ01gKk=" }, "babel-helper-get-function-arity": { "version": "https://registry.npmjs.org/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz", - "integrity": "sha1-j3eCqpNAfEHTqlCQj4mwMbG2hT0=", - "requires": { - "babel-runtime": "6.25.0", - "babel-types": "6.25.0" - } + "integrity": "sha1-j3eCqpNAfEHTqlCQj4mwMbG2hT0=" }, "babel-helper-optimise-call-expression": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.24.1.tgz", - "integrity": "sha1-96E0J7qfc/j0+pk8VKl4gtEkQlc=", - "requires": { - "babel-runtime": "6.25.0", - "babel-types": "6.25.0" - } + "integrity": "sha1-96E0J7qfc/j0+pk8VKl4gtEkQlc=" }, "babel-helper-remap-async-to-generator": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-helper-remap-async-to-generator/-/babel-helper-remap-async-to-generator-6.24.1.tgz", - "integrity": "sha1-XsWBgnrXI/7N04HxySg5BnbkVRs=", - "requires": { - "babel-helper-function-name": "https://registry.npmjs.org/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz", - "babel-runtime": "6.25.0", - "babel-template": "6.25.0", - "babel-traverse": "6.25.0", - "babel-types": "6.25.0" - } + "integrity": "sha1-XsWBgnrXI/7N04HxySg5BnbkVRs=" }, "babel-helper-replace-supers": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-helper-replace-supers/-/babel-helper-replace-supers-6.24.1.tgz", - "integrity": "sha1-v22/5Dk40XNpohPKiov3S2qQqxo=", - "requires": { - "babel-helper-optimise-call-expression": "6.24.1", - "babel-messages": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", - "babel-runtime": "6.25.0", - "babel-template": "6.25.0", - "babel-traverse": "6.25.0", - "babel-types": "6.25.0" - } + "integrity": "sha1-v22/5Dk40XNpohPKiov3S2qQqxo=" }, "babel-helpers": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-helpers/-/babel-helpers-6.24.1.tgz", - "integrity": "sha1-NHHenK7DiOXIUOWX5Yom3fN2ArI=", - "requires": { - "babel-runtime": "6.25.0", - "babel-template": "6.25.0" - } + "integrity": "sha1-NHHenK7DiOXIUOWX5Yom3fN2ArI=" }, "babel-messages": { "version": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", - "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=", - "requires": { - "babel-runtime": "6.25.0" - } + "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=" }, "babel-plugin-chai-assert-async": { "version": "0.1.0", @@ -549,28 +387,17 @@ "babel-plugin-check-es2015-constants": { "version": "6.22.0", "resolved": "https://registry.npmjs.org/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz", - "integrity": "sha1-NRV7EBQm/S/9PaP3XH0ekYNbv4o=", - "requires": { - "babel-runtime": "6.25.0" - } + "integrity": "sha1-NRV7EBQm/S/9PaP3XH0ekYNbv4o=" }, "babel-plugin-relay": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/babel-plugin-relay/-/babel-plugin-relay-1.4.1.tgz", "integrity": "sha1-isVivLH9KzVl0nGqztMr5CyCc1M=", - "requires": { - "babel-runtime": "6.25.0", - "babel-types": "6.25.0", - "graphql": "0.11.7" - }, "dependencies": { "graphql": { "version": "0.11.7", "resolved": "https://registry.npmjs.org/graphql/-/graphql-0.11.7.tgz", - "integrity": "sha1-5auqnLe3zMuE6fCDa/Q3DSaHUMY=", - "requires": { - "iterall": "1.1.3" - } + "integrity": "sha1-5auqnLe3zMuE6fCDa/Q3DSaHUMY=" } } }, @@ -607,44 +434,22 @@ "babel-plugin-transform-async-to-generator": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.24.1.tgz", - "integrity": "sha1-ZTbjeK/2yx1VF6wOQOs+n8jQh2E=", - "requires": { - "babel-helper-remap-async-to-generator": "6.24.1", - "babel-plugin-syntax-async-functions": "6.13.0", - "babel-runtime": "6.25.0" - } + "integrity": "sha1-ZTbjeK/2yx1VF6wOQOs+n8jQh2E=" }, "babel-plugin-transform-class-properties": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-plugin-transform-class-properties/-/babel-plugin-transform-class-properties-6.24.1.tgz", "integrity": "sha1-anl2PqYdM9NvN7YRqp3vgagbRqw=", - "requires": { - "babel-helper-function-name": "6.24.1", - "babel-plugin-syntax-class-properties": "6.13.0", - "babel-runtime": "6.25.0", - "babel-template": "6.25.0" - }, "dependencies": { "babel-helper-function-name": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz", - "integrity": "sha1-00dbjAPtmCQqJbSDUasYOZ01gKk=", - "requires": { - "babel-helper-get-function-arity": "6.24.1", - "babel-runtime": "6.25.0", - "babel-template": "6.25.0", - "babel-traverse": "6.25.0", - "babel-types": "6.25.0" - } + "integrity": "sha1-00dbjAPtmCQqJbSDUasYOZ01gKk=" }, "babel-helper-get-function-arity": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz", - "integrity": "sha1-j3eCqpNAfEHTqlCQj4mwMbG2hT0=", - "requires": { - "babel-runtime": "6.25.0", - "babel-types": "6.25.0" - } + "integrity": "sha1-j3eCqpNAfEHTqlCQj4mwMbG2hT0=" }, "babel-plugin-syntax-class-properties": { "version": "6.13.0", @@ -657,11 +462,6 @@ "version": "1.3.4", "resolved": "https://registry.npmjs.org/babel-plugin-transform-decorators-legacy/-/babel-plugin-transform-decorators-legacy-1.3.4.tgz", "integrity": "sha1-dBtY9sW86eYCfgiC2cmU8E82aSU=", - "requires": { - "babel-plugin-syntax-decorators": "6.13.0", - "babel-runtime": "6.25.0", - "babel-template": "6.25.0" - }, "dependencies": { "babel-plugin-syntax-decorators": { "version": "6.13.0", @@ -673,236 +473,119 @@ "babel-plugin-transform-es2015-arrow-functions": { "version": "6.22.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz", - "integrity": "sha1-RSaSy3EdX3ncf4XkQM5BufJE0iE=", - "requires": { - "babel-runtime": "6.25.0" - } + "integrity": "sha1-RSaSy3EdX3ncf4XkQM5BufJE0iE=" }, "babel-plugin-transform-es2015-block-scoped-functions": { "version": "6.22.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz", - "integrity": "sha1-u8UbSflk1wy42OC5ToICRs46YUE=", - "requires": { - "babel-runtime": "6.25.0" - } + "integrity": "sha1-u8UbSflk1wy42OC5ToICRs46YUE=" }, "babel-plugin-transform-es2015-block-scoping": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.26.0.tgz", "integrity": "sha1-1w9SmcEwjQXBL0Y4E7CgnnOxiV8=", - "requires": { - "babel-runtime": "6.26.0", - "babel-template": "6.26.0", - "babel-traverse": "6.26.0", - "babel-types": "6.26.0", - "lodash": "4.17.5" - }, "dependencies": { "babel-code-frame": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", - "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", - "requires": { - "chalk": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "esutils": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "js-tokens": "3.0.2" - } + "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=" }, "babel-runtime": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", - "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", - "requires": { - "core-js": "2.5.0", - "regenerator-runtime": "0.11.1" - } + "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=" }, "babel-template": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz", - "integrity": "sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=", - "requires": { - "babel-runtime": "6.26.0", - "babel-traverse": "6.26.0", - "babel-types": "6.26.0", - "babylon": "6.18.0", - "lodash": "4.17.5" - } + "integrity": "sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=" }, "babel-traverse": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", - "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=", - "requires": { - "babel-code-frame": "6.26.0", - "babel-messages": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", - "babel-runtime": "6.26.0", - "babel-types": "6.26.0", - "babylon": "6.18.0", - "debug": "2.6.8", - "globals": "9.18.0", - "invariant": "2.2.3", - "lodash": "4.17.5" - } + "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=" }, "babel-types": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", - "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", - "requires": { - "babel-runtime": "6.26.0", - "esutils": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "lodash": "4.17.5", - "to-fast-properties": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz" - } + "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=" }, "babylon": { "version": "6.18.0", "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", - "integrity": "sha1-ry87iPpvXB5MY00aD46sT1WzleM=" + "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==" }, "invariant": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.3.tgz", - "integrity": "sha512-7Z5PPegwDTyjbaeCnV0efcyS6vdKAU51kpEmS7QFib3P4822l8ICYyMn7qvJnc+WzLoDsuI9gPMKbJ8pCu8XtA==", - "requires": { - "loose-envify": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz" - } + "integrity": "sha512-7Z5PPegwDTyjbaeCnV0efcyS6vdKAU51kpEmS7QFib3P4822l8ICYyMn7qvJnc+WzLoDsuI9gPMKbJ8pCu8XtA==" }, "regenerator-runtime": { "version": "0.11.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", - "integrity": "sha1-vgWtf5v30i4Fb5cmzuUBf78Z4uk=" + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" } } }, "babel-plugin-transform-es2015-classes": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.24.1.tgz", - "integrity": "sha1-WkxYpQyclGHlZLSyo7+ryXolhNs=", - "requires": { - "babel-helper-define-map": "6.26.0", - "babel-helper-function-name": "https://registry.npmjs.org/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz", - "babel-helper-optimise-call-expression": "6.24.1", - "babel-helper-replace-supers": "6.24.1", - "babel-messages": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", - "babel-runtime": "6.25.0", - "babel-template": "6.25.0", - "babel-traverse": "6.25.0", - "babel-types": "6.25.0" - } + "integrity": "sha1-WkxYpQyclGHlZLSyo7+ryXolhNs=" }, "babel-plugin-transform-es2015-computed-properties": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz", - "integrity": "sha1-b+Ko0WiV1WNPTNmZttNICjCBWbM=", - "requires": { - "babel-runtime": "6.25.0", - "babel-template": "6.25.0" - } + "integrity": "sha1-b+Ko0WiV1WNPTNmZttNICjCBWbM=" }, "babel-plugin-transform-es2015-destructuring": { "version": "6.23.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz", - "integrity": "sha1-mXux8auWf2gtKwh2/jWNYOdlxW0=", - "requires": { - "babel-runtime": "6.25.0" - } + "integrity": "sha1-mXux8auWf2gtKwh2/jWNYOdlxW0=" }, "babel-plugin-transform-es2015-for-of": { "version": "6.23.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz", - "integrity": "sha1-9HyVsrYT3x0+zC/bdXNiPHUkhpE=", - "requires": { - "babel-runtime": "6.25.0" - } + "integrity": "sha1-9HyVsrYT3x0+zC/bdXNiPHUkhpE=" }, "babel-plugin-transform-es2015-function-name": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.24.1.tgz", - "integrity": "sha1-g0yJhTvDaxrw86TF26qU/Y6sqos=", - "requires": { - "babel-helper-function-name": "https://registry.npmjs.org/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz", - "babel-runtime": "6.25.0", - "babel-types": "6.25.0" - } + "integrity": "sha1-g0yJhTvDaxrw86TF26qU/Y6sqos=" }, "babel-plugin-transform-es2015-literals": { "version": "6.22.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.22.0.tgz", - "integrity": "sha1-T1SgLWzWbPkVKAAZox0xklN3yi4=", - "requires": { - "babel-runtime": "6.25.0" - } + "integrity": "sha1-T1SgLWzWbPkVKAAZox0xklN3yi4=" }, "babel-plugin-transform-es2015-modules-commonjs": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.0.tgz", "integrity": "sha1-DYOUApt9xqvhqX7xgeAHWN0uXYo=", - "requires": { - "babel-plugin-transform-strict-mode": "6.24.1", - "babel-runtime": "6.26.0", - "babel-template": "6.26.0", - "babel-types": "6.26.0" - }, "dependencies": { "babel-code-frame": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", - "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", - "requires": { - "chalk": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "esutils": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "js-tokens": "3.0.2" - } + "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=" }, "babel-runtime": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", - "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", - "requires": { - "core-js": "2.5.0", - "regenerator-runtime": "0.11.0" - } + "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=" }, "babel-template": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz", - "integrity": "sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=", - "requires": { - "babel-runtime": "6.26.0", - "babel-traverse": "6.26.0", - "babel-types": "6.26.0", - "babylon": "6.18.0", - "lodash": "4.17.5" - } + "integrity": "sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=" }, "babel-traverse": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", - "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=", - "requires": { - "babel-code-frame": "6.26.0", - "babel-messages": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", - "babel-runtime": "6.26.0", - "babel-types": "6.26.0", - "babylon": "6.18.0", - "debug": "2.6.8", - "globals": "9.18.0", - "invariant": "https://registry.npmjs.org/invariant/-/invariant-2.2.2.tgz", - "lodash": "4.17.5" - } + "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=" }, "babel-types": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", - "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", - "requires": { - "babel-runtime": "6.26.0", - "esutils": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "lodash": "4.17.5", - "to-fast-properties": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz" - } + "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=" }, "babylon": { "version": "6.18.0", @@ -919,123 +602,69 @@ "babel-plugin-transform-es2015-object-super": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz", - "integrity": "sha1-JM72muIcuDp/hgPa0CH1cusnj40=", - "requires": { - "babel-helper-replace-supers": "6.24.1", - "babel-runtime": "6.25.0" - } + "integrity": "sha1-JM72muIcuDp/hgPa0CH1cusnj40=" }, "babel-plugin-transform-es2015-parameters": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz", "integrity": "sha1-V6w1GrScrxSpfNE7CfZv3wpiXys=", - "requires": { - "babel-helper-call-delegate": "6.24.1", - "babel-helper-get-function-arity": "6.24.1", - "babel-runtime": "6.25.0", - "babel-template": "6.25.0", - "babel-traverse": "6.25.0", - "babel-types": "6.25.0" - }, "dependencies": { "babel-helper-call-delegate": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz", - "integrity": "sha1-7Oaqzdx25Bw0YfiL/Fdb0Nqi340=", - "requires": { - "babel-helper-hoist-variables": "6.24.1", - "babel-runtime": "6.25.0", - "babel-traverse": "6.25.0", - "babel-types": "6.25.0" - } + "integrity": "sha1-7Oaqzdx25Bw0YfiL/Fdb0Nqi340=" }, "babel-helper-get-function-arity": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz", - "integrity": "sha1-j3eCqpNAfEHTqlCQj4mwMbG2hT0=", - "requires": { - "babel-runtime": "6.25.0", - "babel-types": "6.25.0" - } + "integrity": "sha1-j3eCqpNAfEHTqlCQj4mwMbG2hT0=" }, "babel-helper-hoist-variables": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.24.1.tgz", - "integrity": "sha1-HssnaJydJVE+rbyZFKc/VAi+enY=", - "requires": { - "babel-runtime": "6.25.0", - "babel-types": "6.25.0" - } + "integrity": "sha1-HssnaJydJVE+rbyZFKc/VAi+enY=" } } }, "babel-plugin-transform-es2015-shorthand-properties": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz", - "integrity": "sha1-JPh11nIch2YbvZmkYi5R8U3jiqA=", - "requires": { - "babel-runtime": "6.25.0", - "babel-types": "6.25.0" - } + "integrity": "sha1-JPh11nIch2YbvZmkYi5R8U3jiqA=" }, "babel-plugin-transform-es2015-spread": { "version": "6.22.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.22.0.tgz", - "integrity": "sha1-1taKmfia7cRTbIGlQujdnxdG+NE=", - "requires": { - "babel-runtime": "6.25.0" - } + "integrity": "sha1-1taKmfia7cRTbIGlQujdnxdG+NE=" }, "babel-plugin-transform-es2015-template-literals": { "version": "6.22.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.22.0.tgz", - "integrity": "sha1-qEs0UPfp+PH2g51taH2oS7EjbY0=", - "requires": { - "babel-runtime": "6.25.0" - } + "integrity": "sha1-qEs0UPfp+PH2g51taH2oS7EjbY0=" }, "babel-plugin-transform-es3-member-expression-literals": { "version": "6.22.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es3-member-expression-literals/-/babel-plugin-transform-es3-member-expression-literals-6.22.0.tgz", - "integrity": "sha1-cz00RPPsxBvvjtGmpOCWV7iWnrs=", - "requires": { - "babel-runtime": "6.25.0" - } + "integrity": "sha1-cz00RPPsxBvvjtGmpOCWV7iWnrs=" }, "babel-plugin-transform-es3-property-literals": { "version": "6.22.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es3-property-literals/-/babel-plugin-transform-es3-property-literals-6.22.0.tgz", - "integrity": "sha1-sgeNWELiKr9A9z6M3pzTcRq9V1g=", - "requires": { - "babel-runtime": "6.25.0" - } + "integrity": "sha1-sgeNWELiKr9A9z6M3pzTcRq9V1g=" }, "babel-plugin-transform-flow-strip-types": { "version": "6.22.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-flow-strip-types/-/babel-plugin-transform-flow-strip-types-6.22.0.tgz", - "integrity": "sha1-hMtnKTXUNxT9wyvOhFaNh0Qc988=", - "requires": { - "babel-plugin-syntax-flow": "6.18.0", - "babel-runtime": "6.25.0" - } + "integrity": "sha1-hMtnKTXUNxT9wyvOhFaNh0Qc988=" }, "babel-plugin-transform-object-rest-spread": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.26.0.tgz", "integrity": "sha1-DzZpLVD+9rfi1LOsFHgTepY7ewY=", - "requires": { - "babel-plugin-syntax-object-rest-spread": "6.13.0", - "babel-runtime": "6.26.0" - }, "dependencies": { "babel-runtime": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", - "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", - "requires": { - "core-js": "2.5.0", - "regenerator-runtime": "0.11.0" - } + "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=" }, "regenerator-runtime": { "version": "0.11.0", @@ -1047,48 +676,27 @@ "babel-plugin-transform-react-display-name": { "version": "6.25.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-display-name/-/babel-plugin-transform-react-display-name-6.25.0.tgz", - "integrity": "sha1-Z+K/Hx6ck6sI25Z5LgU5K/LMKNE=", - "requires": { - "babel-runtime": "6.25.0" - } + "integrity": "sha1-Z+K/Hx6ck6sI25Z5LgU5K/LMKNE=" }, "babel-plugin-transform-react-jsx": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-jsx/-/babel-plugin-transform-react-jsx-6.24.1.tgz", - "integrity": "sha1-hAoCjn30YN/DotKfDA2R9jduZqM=", - "requires": { - "babel-helper-builder-react-jsx": "6.26.0", - "babel-plugin-syntax-jsx": "6.18.0", - "babel-runtime": "6.25.0" - } + "integrity": "sha1-hAoCjn30YN/DotKfDA2R9jduZqM=" }, "babel-plugin-transform-strict-mode": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz", - "integrity": "sha1-1fr3qleKZbvlkc9e2uBKDGcCB1g=", - "requires": { - "babel-runtime": "6.25.0", - "babel-types": "6.25.0" - } + "integrity": "sha1-1fr3qleKZbvlkc9e2uBKDGcCB1g=" }, "babel-polyfill": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-polyfill/-/babel-polyfill-6.26.0.tgz", "integrity": "sha1-N5k3q8Z9eJWXCtxiHyhM2WbPIVM=", - "requires": { - "babel-runtime": "6.26.0", - "core-js": "2.5.0", - "regenerator-runtime": "0.10.5" - }, "dependencies": { "babel-runtime": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", - "requires": { - "core-js": "2.5.0", - "regenerator-runtime": "0.11.1" - }, "dependencies": { "regenerator-runtime": { "version": "0.11.1", @@ -1107,69 +715,22 @@ "babel-preset-fbjs": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/babel-preset-fbjs/-/babel-preset-fbjs-2.1.4.tgz", - "integrity": "sha1-IvNY5mVAc6z2HkegUqd317zPA68=", - "requires": { - "babel-plugin-check-es2015-constants": "6.22.0", - "babel-plugin-syntax-class-properties": "6.13.0", - "babel-plugin-syntax-flow": "6.18.0", - "babel-plugin-syntax-jsx": "6.18.0", - "babel-plugin-syntax-object-rest-spread": "6.13.0", - "babel-plugin-syntax-trailing-function-commas": "6.22.0", - "babel-plugin-transform-class-properties": "6.24.1", - "babel-plugin-transform-es2015-arrow-functions": "6.22.0", - "babel-plugin-transform-es2015-block-scoped-functions": "6.22.0", - "babel-plugin-transform-es2015-block-scoping": "6.26.0", - "babel-plugin-transform-es2015-classes": "6.24.1", - "babel-plugin-transform-es2015-computed-properties": "6.24.1", - "babel-plugin-transform-es2015-destructuring": "6.23.0", - "babel-plugin-transform-es2015-for-of": "6.23.0", - "babel-plugin-transform-es2015-function-name": "6.24.1", - "babel-plugin-transform-es2015-literals": "6.22.0", - "babel-plugin-transform-es2015-modules-commonjs": "6.26.0", - "babel-plugin-transform-es2015-object-super": "6.24.1", - "babel-plugin-transform-es2015-parameters": "6.24.1", - "babel-plugin-transform-es2015-shorthand-properties": "6.24.1", - "babel-plugin-transform-es2015-spread": "6.22.0", - "babel-plugin-transform-es2015-template-literals": "6.22.0", - "babel-plugin-transform-es3-member-expression-literals": "6.22.0", - "babel-plugin-transform-es3-property-literals": "6.22.0", - "babel-plugin-transform-flow-strip-types": "6.22.0", - "babel-plugin-transform-object-rest-spread": "6.26.0", - "babel-plugin-transform-react-display-name": "6.25.0", - "babel-plugin-transform-react-jsx": "6.24.1" - } + "integrity": "sha1-IvNY5mVAc6z2HkegUqd317zPA68=" }, "babel-preset-react": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-preset-react/-/babel-preset-react-6.24.1.tgz", "integrity": "sha1-umnfrqRfw+xjm2pOzqbhdwLJE4A=", - "requires": { - "babel-plugin-syntax-jsx": "6.18.0", - "babel-plugin-transform-react-display-name": "6.25.0", - "babel-plugin-transform-react-jsx": "6.24.1", - "babel-plugin-transform-react-jsx-self": "6.22.0", - "babel-plugin-transform-react-jsx-source": "6.22.0", - "babel-preset-flow": "6.23.0" - }, "dependencies": { "babel-helper-builder-react-jsx": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-helper-builder-react-jsx/-/babel-helper-builder-react-jsx-6.26.0.tgz", "integrity": "sha1-Of+DE7dci2Xc7/HzHTg+D/KkCKA=", - "requires": { - "babel-runtime": "6.26.0", - "babel-types": "6.26.0", - "esutils": "2.0.2" - }, "dependencies": { "babel-runtime": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", - "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", - "requires": { - "core-js": "2.5.0", - "regenerator-runtime": "0.11.1" - } + "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=" } } }, @@ -1186,67 +747,37 @@ "babel-plugin-transform-flow-strip-types": { "version": "6.22.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-flow-strip-types/-/babel-plugin-transform-flow-strip-types-6.22.0.tgz", - "integrity": "sha1-hMtnKTXUNxT9wyvOhFaNh0Qc988=", - "requires": { - "babel-plugin-syntax-flow": "6.18.0", - "babel-runtime": "6.25.0" - } + "integrity": "sha1-hMtnKTXUNxT9wyvOhFaNh0Qc988=" }, "babel-plugin-transform-react-jsx": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-jsx/-/babel-plugin-transform-react-jsx-6.24.1.tgz", - "integrity": "sha1-hAoCjn30YN/DotKfDA2R9jduZqM=", - "requires": { - "babel-helper-builder-react-jsx": "6.26.0", - "babel-plugin-syntax-jsx": "6.18.0", - "babel-runtime": "6.25.0" - } + "integrity": "sha1-hAoCjn30YN/DotKfDA2R9jduZqM=" }, "babel-plugin-transform-react-jsx-self": { "version": "6.22.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-jsx-self/-/babel-plugin-transform-react-jsx-self-6.22.0.tgz", - "integrity": "sha1-322AqdomEqEh5t3XVYvL7PBuY24=", - "requires": { - "babel-plugin-syntax-jsx": "6.18.0", - "babel-runtime": "6.25.0" - } + "integrity": "sha1-322AqdomEqEh5t3XVYvL7PBuY24=" }, "babel-plugin-transform-react-jsx-source": { "version": "6.22.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-jsx-source/-/babel-plugin-transform-react-jsx-source-6.22.0.tgz", - "integrity": "sha1-ZqwSFT9c0tF7PBkmj0vwGX9E7NY=", - "requires": { - "babel-plugin-syntax-jsx": "6.18.0", - "babel-runtime": "6.25.0" - } + "integrity": "sha1-ZqwSFT9c0tF7PBkmj0vwGX9E7NY=" }, "babel-preset-flow": { "version": "6.23.0", "resolved": "https://registry.npmjs.org/babel-preset-flow/-/babel-preset-flow-6.23.0.tgz", - "integrity": "sha1-5xIYiHCFrpoktb5Baa/7WZgWxJ0=", - "requires": { - "babel-plugin-transform-flow-strip-types": "6.22.0" - } + "integrity": "sha1-5xIYiHCFrpoktb5Baa/7WZgWxJ0=" }, "babel-types": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", - "requires": { - "babel-runtime": "6.26.0", - "esutils": "2.0.2", - "lodash": "4.17.5", - "to-fast-properties": "1.0.3" - }, "dependencies": { "babel-runtime": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", - "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", - "requires": { - "core-js": "2.5.0", - "regenerator-runtime": "0.11.1" - } + "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=" } } }, @@ -1276,24 +807,11 @@ "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-register/-/babel-register-6.26.0.tgz", "integrity": "sha1-btAhFz4vy0htestFxgCahW9kcHE=", - "requires": { - "babel-core": "6.26.0", - "babel-runtime": "6.26.0", - "core-js": "2.5.0", - "home-or-tmp": "2.0.0", - "lodash": "4.17.5", - "mkdirp": "0.5.1", - "source-map-support": "0.4.18" - }, "dependencies": { "babel-runtime": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", - "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", - "requires": { - "core-js": "2.5.0", - "regenerator-runtime": "0.11.1" - } + "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=" }, "regenerator-runtime": { "version": "0.11.1", @@ -1305,50 +823,22 @@ "babel-runtime": { "version": "6.25.0", "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.25.0.tgz", - "integrity": "sha1-M7mOql1IK7AajRqmtDetKwGuxBw=", - "requires": { - "core-js": "2.5.0", - "regenerator-runtime": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz" - } + "integrity": "sha1-M7mOql1IK7AajRqmtDetKwGuxBw=" }, "babel-template": { "version": "6.25.0", "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.25.0.tgz", - "integrity": "sha1-ZlJBFmt8KqTGGdceGSlpVSsQwHE=", - "requires": { - "babel-runtime": "6.25.0", - "babel-traverse": "6.25.0", - "babel-types": "6.25.0", - "babylon": "6.17.4", - "lodash": "4.17.5" - } + "integrity": "sha1-ZlJBFmt8KqTGGdceGSlpVSsQwHE=" }, "babel-traverse": { "version": "6.25.0", "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.25.0.tgz", - "integrity": "sha1-IldJfi/NGbie3BPEyROB+VEklvE=", - "requires": { - "babel-code-frame": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.22.0.tgz", - "babel-messages": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", - "babel-runtime": "6.25.0", - "babel-types": "6.25.0", - "babylon": "6.17.4", - "debug": "2.6.8", - "globals": "9.18.0", - "invariant": "https://registry.npmjs.org/invariant/-/invariant-2.2.2.tgz", - "lodash": "4.17.5" - } + "integrity": "sha1-IldJfi/NGbie3BPEyROB+VEklvE=" }, "babel-types": { "version": "6.25.0", "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.25.0.tgz", - "integrity": "sha1-cK+ySNVmDl0Y+BHZHIMDtUE0oY4=", - "requires": { - "babel-runtime": "6.25.0", - "esutils": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "lodash": "4.17.5", - "to-fast-properties": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz" - } + "integrity": "sha1-cK+ySNVmDl0Y+BHZHIMDtUE0oY4=" }, "babylon": { "version": "6.17.4", @@ -1364,57 +854,29 @@ "version": "0.11.2", "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", "integrity": "sha1-e95c7RRbbVUakNuH+DxVi060io8=", - "requires": { - "cache-base": "1.0.1", - "class-utils": "0.3.6", - "component-emitter": "1.2.1", - "define-property": "1.0.0", - "isobject": "3.0.1", - "mixin-deep": "1.3.1", - "pascalcase": "0.1.1" - }, "dependencies": { "define-property": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "requires": { - "is-descriptor": "1.0.2" - } + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=" } } }, "bash-glob": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bash-glob/-/bash-glob-1.0.2.tgz", - "integrity": "sha1-laxWMf3XqPxWnyZxZ6hOuDGXmhs=", - "requires": { - "async-each": "1.0.1", - "bash-path": "1.0.3", - "component-emitter": "1.2.1", - "cross-spawn": "5.1.0", - "extend-shallow": "2.0.1", - "is-extglob": "2.1.1", - "is-glob": "4.0.0" - } + "integrity": "sha1-laxWMf3XqPxWnyZxZ6hOuDGXmhs=" }, "bash-path": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/bash-path/-/bash-path-1.0.3.tgz", - "integrity": "sha1-28nvvfGLHBFBPctZuWDmqlbIQlg=", - "requires": { - "arr-union": "3.1.0", - "is-windows": "1.0.2" - } + "integrity": "sha1-28nvvfGLHBFBPctZuWDmqlbIQlg=" }, "bcrypt-pbkdf": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", - "optional": true, - "requires": { - "tweetnacl": "0.14.5" - } + "optional": true }, "boolbase": { "version": "1.0.0", @@ -1425,46 +887,22 @@ "boom": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz", - "integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=", - "requires": { - "hoek": "4.2.1" - } + "integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=" }, "brace-expansion": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", - "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", - "requires": { - "balanced-match": "1.0.0", - "concat-map": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" - } + "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=" }, "braces": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.1.tgz", "integrity": "sha1-cIbJE7TloI2+N6wO5qJQDEumkbs=", - "requires": { - "arr-flatten": "1.1.0", - "array-unique": "0.3.2", - "define-property": "1.0.0", - "extend-shallow": "2.0.1", - "fill-range": "4.0.0", - "isobject": "3.0.1", - "kind-of": "6.0.2", - "repeat-element": "1.1.2", - "snapdragon": "0.8.1", - "snapdragon-node": "2.1.1", - "split-string": "3.1.0", - "to-regex": "3.0.2" - }, "dependencies": { "define-property": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "requires": { - "is-descriptor": "1.0.2" - } + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=" } } }, @@ -1482,10 +920,13 @@ "bser": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/bser/-/bser-2.0.0.tgz", - "integrity": "sha1-mseNPtXZFYBP2HrLFYvHlxR6Fxk=", - "requires": { - "node-int64": "0.4.0" - } + "integrity": "sha1-mseNPtXZFYBP2HrLFYvHlxR6Fxk=" + }, + "buffer-from": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.0.0.tgz", + "integrity": "sha512-83apNb8KK0Se60UE1+4Ukbe3HbfELJ6UlI4ldtOGs7So4KD26orJM8hIY9lxdzP+UpItH1Yh/Y8GUvNFWFFRxA==", + "dev": true }, "builtin-modules": { "version": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", @@ -1494,24 +935,25 @@ "cache-base": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", - "integrity": "sha1-Cn9GQWgxyLZi7jb+TnxZ129marI=", - "requires": { - "collection-visit": "1.0.0", - "component-emitter": "1.2.1", - "get-value": "2.0.6", - "has-value": "1.0.0", - "isobject": "3.0.1", - "set-value": "2.0.0", - "to-object-path": "0.3.0", - "union-value": "1.0.0", - "unset-value": "1.0.0" - } + "integrity": "sha1-Cn9GQWgxyLZi7jb+TnxZ129marI=" }, "call-me-maybe": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz", "integrity": "sha1-JtII6onje1y95gJQoV8DHBak1ms=" }, + "caller-path": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz", + "integrity": "sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8=", + "dev": true + }, + "callsites": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz", + "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=", + "dev": true + }, "camelcase": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", @@ -1533,11 +975,6 @@ "resolved": "https://registry.npmjs.org/chai/-/chai-3.5.0.tgz", "integrity": "sha1-TQJjewZ/6Vi9v906QOxW/vc3Mkc=", "dev": true, - "requires": { - "assertion-error": "1.1.0", - "deep-eql": "0.1.3", - "type-detect": "1.0.0" - }, "dependencies": { "assertion-error": { "version": "1.1.0", @@ -1550,9 +987,6 @@ "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-0.1.3.tgz", "integrity": "sha1-71WKyrjeJSBs1xOQbXTlaTDrafI=", "dev": true, - "requires": { - "type-detect": "0.1.1" - }, "dependencies": { "type-detect": { "version": "0.1.1", @@ -1578,62 +1012,34 @@ }, "chalk": { "version": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "requires": { - "ansi-styles": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "escape-string-regexp": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "has-ansi": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "strip-ansi": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "supports-color": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz" - } + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=" }, "checksum": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/checksum/-/checksum-0.1.1.tgz", - "integrity": "sha1-3GUn1MkL6FYNvR7Uzs8yl9Uo6ek=", - "requires": { - "optimist": "0.3.7" - } + "integrity": "sha1-3GUn1MkL6FYNvR7Uzs8yl9Uo6ek=" }, "cheerio": { "version": "0.22.0", "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-0.22.0.tgz", "integrity": "sha1-qbqoYKP5tZWmuBsahocxIe06Jp4=", - "dev": true, - "requires": { - "css-select": "1.2.0", - "dom-serializer": "0.1.0", - "entities": "1.1.1", - "htmlparser2": "3.9.2", - "lodash.assignin": "4.2.0", - "lodash.bind": "4.2.1", - "lodash.defaults": "4.2.0", - "lodash.filter": "4.6.0", - "lodash.flatten": "4.4.0", - "lodash.foreach": "4.5.0", - "lodash.map": "4.6.0", - "lodash.merge": "4.6.0", - "lodash.pick": "4.4.0", - "lodash.reduce": "4.6.0", - "lodash.reject": "4.6.0", - "lodash.some": "4.6.0" - } + "dev": true }, "chownr": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.0.1.tgz", "integrity": "sha1-4qdQQqlVGQi+vSW4Uj1fl2nXkYE=" }, + "circular-json": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz", + "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==", + "dev": true + }, "class-utils": { "version": "0.3.6", "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", "integrity": "sha1-+TNprouafOAv1B+q0MqDAzGQxGM=", - "requires": { - "arr-union": "3.1.0", - "define-property": "0.2.5", - "isobject": "3.0.1", - "static-extend": "0.1.2" - }, "dependencies": { "define-property": { "version": "0.2.5", @@ -1644,17 +1050,11 @@ "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "requires": { - "kind-of": "3.2.2" - }, "dependencies": { "kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "1.1.6" - } + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=" } } }, @@ -1662,17 +1062,11 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "requires": { - "kind-of": "3.2.2" - }, "dependencies": { "kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "1.1.6" - } + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=" } } }, @@ -1693,23 +1087,27 @@ "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.5.tgz", "integrity": "sha1-+zgB1FNGdknvNgPH1hoCvRKb3m0=" }, + "cli-cursor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz", + "integrity": "sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=", + "dev": true + }, + "cli-width": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", + "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", + "dev": true + }, "cliui": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", - "requires": { - "string-width": "1.0.2", - "strip-ansi": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "wrap-ansi": "2.1.0" - }, "dependencies": { "string-width": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "requires": { - "strip-ansi": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz" - } + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=" } } }, @@ -1718,30 +1116,25 @@ "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" + }, "collection-visit": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", - "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", - "requires": { - "map-visit": "1.0.0", - "object-visit": "1.0.1" - } + "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=" }, "combined-stream": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", - "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", - "requires": { - "delayed-stream": "1.0.0" - } + "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=" }, "commander": { "version": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", "integrity": "sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q=", - "dev": true, - "requires": { - "graceful-readlink": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz" - } + "dev": true }, "compare-sets": { "version": "1.0.1", @@ -1757,6 +1150,12 @@ "version": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "dev": true + }, "contains-path": { "version": "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz", "integrity": "sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo=", @@ -1789,22 +1188,12 @@ "create-react-class": { "version": "15.6.2", "resolved": "https://registry.npmjs.org/create-react-class/-/create-react-class-15.6.2.tgz", - "integrity": "sha1-zx7RXxKq1/FO9fLf4F5sQvke8Co=", - "requires": { - "fbjs": "0.8.14", - "loose-envify": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz", - "object-assign": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz" - } + "integrity": "sha1-zx7RXxKq1/FO9fLf4F5sQvke8Co=" }, "cross-spawn": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", - "requires": { - "lru-cache": "4.1.2", - "shebang-command": "1.2.0", - "which": "1.3.0" - }, "dependencies": { "lru-cache": { "version": "4.1.2", @@ -1823,17 +1212,11 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz", "integrity": "sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4=", - "requires": { - "boom": "5.2.0" - }, "dependencies": { "boom": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", - "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==", - "requires": { - "hoek": "4.2.1" - } + "integrity": "sha1-XdnabuOl8wIHdDYpDLcX0/SlTgI=" } } }, @@ -1841,13 +1224,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", "integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=", - "dev": true, - "requires": { - "boolbase": "1.0.0", - "css-what": "2.1.0", - "domutils": "1.5.1", - "nth-check": "1.0.1" - } + "dev": true }, "css-what": { "version": "2.1.0", @@ -1855,13 +1232,16 @@ "integrity": "sha1-lGfQMsOM+u+58teVASUwYvh/ob0=", "dev": true }, + "d": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz", + "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=", + "dev": true + }, "dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "requires": { - "assert-plus": "1.0.0" - }, "dependencies": { "assert-plus": { "version": "1.0.0", @@ -1873,10 +1253,7 @@ "debug": { "version": "2.6.8", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz", - "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=", - "requires": { - "ms": "2.0.0" - } + "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=" }, "decamelize": { "version": "1.2.0", @@ -1894,24 +1271,28 @@ "integrity": "sha1-vuX7fJ5yfYXf+iRZDRDsGrElUwU=", "dev": true }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, "define-properties": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.2.tgz", "integrity": "sha1-g6c/L+pWmJj7c3GTyPhzyvbUXJQ=", - "dev": true, - "requires": { - "foreach": "2.0.5", - "object-keys": "1.0.11" - } + "dev": true }, "define-property": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha1-1Flono1lS6d+AqgX+HENcCyxbp0=", - "requires": { - "is-descriptor": "1.0.2", - "isobject": "3.0.1" - } + "integrity": "sha1-1Flono1lS6d+AqgX+HENcCyxbp0=" + }, + "del": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/del/-/del-2.2.2.tgz", + "integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=", + "dev": true }, "delayed-stream": { "version": "1.0.0", @@ -1920,10 +1301,7 @@ }, "detect-indent": { "version": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz", - "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=", - "requires": { - "repeating": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz" - } + "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=" }, "diff": { "version": "3.2.0", @@ -1940,10 +1318,6 @@ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz", "integrity": "sha1-BzxpdUbOB4DOI75KKOKT5AvDDII=", "dev": true, - "requires": { - "domelementtype": "1.1.3", - "entities": "1.1.1" - }, "dependencies": { "domelementtype": { "version": "1.1.3", @@ -1969,10 +1343,7 @@ "version": "2.4.1", "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.1.tgz", "integrity": "sha1-iS5HAAqZvlW783dP/qBWHYh5wlk=", - "dev": true, - "requires": { - "domelementtype": "1.3.0" - } + "dev": true }, "dompurify": { "version": "1.0.3", @@ -1983,52 +1354,28 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", - "dev": true, - "requires": { - "dom-serializer": "0.1.0", - "domelementtype": "1.3.0" - } + "dev": true }, "dugite": { "version": "1.60.0", "resolved": "https://registry.npmjs.org/dugite/-/dugite-1.60.0.tgz", - "integrity": "sha512-HP5Qfx867ESOkrrEhXtxi72UQoNWaCSEcahGCmnBM/v+ldgbFu94s9CvG3tDcBtz1HJkvYEtkBd8V8Vb63nfUw==", - "requires": { - "checksum": "0.1.1", - "mkdirp": "0.5.1", - "progress": "2.0.0", - "request": "2.85.0", - "rimraf": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.1.tgz", - "tar": "4.4.0" - } + "integrity": "sha512-HP5Qfx867ESOkrrEhXtxi72UQoNWaCSEcahGCmnBM/v+ldgbFu94s9CvG3tDcBtz1HJkvYEtkBd8V8Vb63nfUw==" }, "ecc-jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", - "optional": true, - "requires": { - "jsbn": "0.1.1" - } + "optional": true }, "electron-devtools-installer": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/electron-devtools-installer/-/electron-devtools-installer-2.2.3.tgz", "integrity": "sha1-WLmk7FBzd7xG4JHNQ3FBiODDab4=", - "dev": true, - "requires": { - "7zip": "0.0.6", - "cross-unzip": "0.0.2", - "rimraf": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.1.tgz", - "semver": "5.4.1" - } + "dev": true }, "encoding": { "version": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", - "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=", - "requires": { - "iconv-lite": "0.4.18" - } + "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=" }, "entities": { "version": "1.1.1", @@ -2040,60 +1387,30 @@ "version": "2.9.1", "resolved": "https://registry.npmjs.org/enzyme/-/enzyme-2.9.1.tgz", "integrity": "sha1-B9XOaRJBJA+4F78sSxjW5TAkDfY=", - "dev": true, - "requires": { - "cheerio": "0.22.0", - "function.prototype.name": "1.0.3", - "is-subset": "0.1.1", - "lodash": "4.17.5", - "object-is": "1.0.1", - "object.assign": "4.0.4", - "object.entries": "1.0.4", - "object.values": "1.0.4", - "prop-types": "15.6.1", - "uuid": "3.1.0" - } + "dev": true }, "errno": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.4.tgz", "integrity": "sha1-uJbiOp5ei6M4cfyZar02NfyaHH0=", "dev": true, - "optional": true, - "requires": { - "prr": "0.0.0" - } + "optional": true }, "error": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/error/-/error-4.4.0.tgz", "integrity": "sha1-v2n/JR+0onnBmtzNqmth6Q2b8So=", - "dev": true, - "requires": { - "camelize": "1.0.0", - "string-template": "0.2.1", - "xtend": "4.0.1" - } + "dev": true }, "error-ex": { "version": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz", - "integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=", - "requires": { - "is-arrayish": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz" - } + "integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=" }, "es-abstract": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.9.0.tgz", - "integrity": "sha1-aQgpoHyuNrIi5/2bdcDQVz6yUic=", + "integrity": "sha512-kk3IJoKo7A3pWJc0OV8yZ/VEX2oSUytfekrJiqoxBlKJMFAJVJVpGdHClCCTdv+Fn2zHfpDHHIelMFhZVfef3Q==", "dev": true, - "requires": { - "es-to-primitive": "1.1.1", - "function-bind": "1.1.1", - "has": "https://registry.npmjs.org/has/-/has-1.0.1.tgz", - "is-callable": "1.1.3", - "is-regex": "1.0.4" - }, "dependencies": { "function-bind": { "version": "1.1.1", @@ -2107,868 +1424,78 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.1.1.tgz", "integrity": "sha1-RTVSSKiJeQNLZ5Lhm7gfK3l13Q0=", + "dev": true + }, + "es5-ext": { + "version": "0.10.41", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.41.tgz", + "integrity": "sha512-MYK02wXfwTMie5TEJWPolgOsXEmz7wKCQaGzgmRjZOoV6VLG8I5dSv2bn6AOClXhK64gnSQTQ9W9MKvx87J4gw==", "dev": true, - "requires": { - "is-callable": "1.1.3", - "is-date-object": "1.0.1", - "is-symbol": "1.0.1" + "dependencies": { + "next-tick": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", + "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=", + "dev": true + } } }, + "es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", + "dev": true + }, + "es6-map": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/es6-map/-/es6-map-0.1.5.tgz", + "integrity": "sha1-kTbgUD3MBqMBaQ8LsU/042TpSfA=", + "dev": true + }, "es6-promise": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.4.tgz", "integrity": "sha1-3EIhwrFlGHYL2MOaUtjzVvwA7Sk=" }, + "es6-set": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/es6-set/-/es6-set-0.1.5.tgz", + "integrity": "sha1-0rPsXU2ADO2BjbU40ol02wpzzLE=", + "dev": true + }, + "es6-symbol": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz", + "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=", + "dev": true + }, + "es6-weak-map": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.2.tgz", + "integrity": "sha1-XjqzIlH/0VOKH45f+hNXdy+S2W8=", + "dev": true + }, "escape-string-regexp": { "version": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, + "escope": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/escope/-/escope-3.6.0.tgz", + "integrity": "sha1-4Bl16BJ4GhY6ba392AOY3GTIicM=", + "dev": true + }, "eslint": { "version": "3.19.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-3.19.0.tgz", "integrity": "sha1-yPxiAcf0DdCJQbh8CFdnOGpnmsw=", "dev": true, - "requires": { - "babel-code-frame": "6.26.0", - "chalk": "1.1.3", - "concat-stream": "1.6.1", - "debug": "2.6.8", - "doctrine": "2.1.0", - "escope": "3.6.0", - "espree": "3.5.0", - "esquery": "1.0.0", - "estraverse": "4.2.0", - "esutils": "2.0.2", - "file-entry-cache": "2.0.0", - "glob": "7.1.2", - "globals": "9.18.0", - "ignore": "3.3.3", - "imurmurhash": "0.1.4", - "inquirer": "0.12.0", - "is-my-json-valid": "2.17.2", - "is-resolvable": "1.1.0", - "js-yaml": "3.9.1", - "json-stable-stringify": "1.0.1", - "levn": "0.3.0", - "lodash": "4.17.5", - "mkdirp": "0.5.1", - "natural-compare": "1.4.0", - "optionator": "0.8.2", - "path-is-inside": "1.0.2", - "pluralize": "1.2.1", - "progress": "1.1.8", - "require-uncached": "1.0.3", - "shelljs": "0.7.8", - "strip-bom": "3.0.0", - "strip-json-comments": "2.0.1", - "table": "3.8.3", - "text-table": "0.2.0", - "user-home": "2.0.0" - }, "dependencies": { - "ajv": { - "version": "4.11.8", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", - "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=", - "dev": true, - "requires": { - "co": "4.6.0", - "json-stable-stringify": "1.0.1" - } - }, - "ajv-keywords": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-1.5.1.tgz", - "integrity": "sha1-MU3QpLM2j609/NxU7eYXG4htrzw=", - "dev": true - }, - "ansi-escapes": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz", - "integrity": "sha1-06ioOzGapneTZisT52HHkRQiMG4=", + "progress": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/progress/-/progress-1.1.8.tgz", + "integrity": "sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=", "dev": true - }, - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "array-union": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", - "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", - "dev": true, - "requires": { - "array-uniq": "1.0.3" - } - }, - "array-uniq": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", - "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", - "dev": true - }, - "arrify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", - "dev": true - }, - "babel-code-frame": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", - "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", - "dev": true, - "requires": { - "chalk": "1.1.3", - "esutils": "2.0.2", - "js-tokens": "3.0.2" - } - }, - "caller-path": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz", - "integrity": "sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8=", - "dev": true, - "requires": { - "callsites": "0.2.0" - } - }, - "callsites": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz", - "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=", - "dev": true - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "2.2.1", - "escape-string-regexp": "1.0.5", - "has-ansi": "2.0.0", - "strip-ansi": "3.0.1", - "supports-color": "2.0.0" - } - }, - "cli-cursor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz", - "integrity": "sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=", - "dev": true, - "requires": { - "restore-cursor": "1.0.1" - } - }, - "cli-width": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", - "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", - "dev": true - }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true - }, - "concat-stream": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.1.tgz", - "integrity": "sha512-gslSSJx03QKa59cIKqeJO9HQ/WZMotvYJCuaUULrLpjj8oG40kV2Z+gz82pVxlTkOADi4PJxQPPfhl1ELYrrXw==", - "dev": true, - "requires": { - "inherits": "2.0.3", - "readable-stream": "2.3.3", - "typedarray": "0.0.6" - } - }, - "d": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz", - "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=", - "dev": true - }, - "deep-is": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", - "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", - "dev": true - }, - "del": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/del/-/del-2.2.2.tgz", - "integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=", - "dev": true, - "requires": { - "globby": "5.0.0", - "is-path-cwd": "1.0.0", - "is-path-in-cwd": "1.0.0", - "object-assign": "4.1.1", - "pify": "2.3.0", - "pinkie-promise": "2.0.1", - "rimraf": "2.6.2" - } - }, - "doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "requires": { - "esutils": "2.0.2" - } - }, - "es6-iterator": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", - "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", - "dev": true, - "requires": { - "d": "1.0.0", - "es5-ext": "0.10.40", - "es6-symbol": "3.1.1" - }, - "dependencies": { - "es5-ext": { - "version": "0.10.40", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.40.tgz", - "integrity": "sha512-S9Fh3oya5OOvYSNGvPZJ+vyrs6VYpe1IXPowVe3N1OhaiwVaGlwfn3Zf5P5klYcWOA0toIwYQW8XEv/QqhdHvQ==", - "dev": true, - "requires": { - "es6-iterator": "2.0.3", - "es6-symbol": "3.1.1" - } - } - } - }, - "es6-map": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/es6-map/-/es6-map-0.1.5.tgz", - "integrity": "sha1-kTbgUD3MBqMBaQ8LsU/042TpSfA=", - "dev": true, - "requires": { - "d": "1.0.0", - "es6-iterator": "2.0.3", - "es6-set": "0.1.5", - "es6-symbol": "3.1.1", - "event-emitter": "0.3.5" - } - }, - "es6-set": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/es6-set/-/es6-set-0.1.5.tgz", - "integrity": "sha1-0rPsXU2ADO2BjbU40ol02wpzzLE=", - "dev": true, - "requires": { - "d": "1.0.0", - "es6-iterator": "2.0.3", - "es6-symbol": "3.1.1", - "event-emitter": "0.3.5" - } - }, - "es6-symbol": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz", - "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=", - "dev": true, - "requires": { - "d": "1.0.0" - } - }, - "es6-weak-map": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.2.tgz", - "integrity": "sha1-XjqzIlH/0VOKH45f+hNXdy+S2W8=", - "dev": true, - "requires": { - "d": "1.0.0", - "es6-iterator": "2.0.3", - "es6-symbol": "3.1.1" - } - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true - }, - "escope": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/escope/-/escope-3.6.0.tgz", - "integrity": "sha1-4Bl16BJ4GhY6ba392AOY3GTIicM=", - "dev": true, - "requires": { - "es6-map": "0.1.5", - "es6-weak-map": "2.0.2", - "estraverse": "4.2.0" - } - }, - "esquery": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.0.tgz", - "integrity": "sha1-z7qLV9f7qT8XKYqKAGoEzaE9gPo=", - "dev": true, - "requires": { - "estraverse": "4.2.0" - } - }, - "estraverse": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", - "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", - "dev": true - }, - "esutils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", - "dev": true - }, - "event-emitter": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", - "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=", - "dev": true, - "requires": { - "d": "1.0.0" - } - }, - "exit-hook": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz", - "integrity": "sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g=", - "dev": true - }, - "fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", - "dev": true - }, - "figures": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", - "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", - "dev": true, - "requires": { - "escape-string-regexp": "1.0.5", - "object-assign": "4.1.1" - } - }, - "file-entry-cache": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-2.0.0.tgz", - "integrity": "sha1-w5KZDD5oR4PYOLjISkXYoEhFg2E=", - "dev": true, - "requires": { - "flat-cache": "1.3.0", - "object-assign": "4.1.1" - } - }, - "flat-cache": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.3.0.tgz", - "integrity": "sha1-0wMLMrOBVPTjt+nHCfSQ9++XxIE=", - "dev": true, - "requires": { - "del": "2.2.2", - "graceful-fs": "4.1.11", - "write": "0.2.1" - } - }, - "generate-function": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz", - "integrity": "sha1-aFj+fAlpt9TpCTM3ZHrHn2DfvnQ=", - "dev": true - }, - "generate-object-property": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", - "integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=", - "dev": true, - "requires": { - "is-property": "1.0.2" - } - }, - "globby": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz", - "integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=", - "dev": true, - "requires": { - "array-union": "1.0.2", - "arrify": "1.0.1", - "glob": "7.1.2", - "object-assign": "4.1.1", - "pify": "2.3.0", - "pinkie-promise": "2.0.1" - } - }, - "graceful-fs": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", - "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", - "dev": true - }, - "has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "dev": true, - "requires": { - "ansi-regex": "2.1.1" - } - }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", - "dev": true - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - }, - "inquirer": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-0.12.0.tgz", - "integrity": "sha1-HvK/1jUE3wvHV4X/+MLEHfEvB34=", - "dev": true, - "requires": { - "ansi-escapes": "1.4.0", - "ansi-regex": "2.1.1", - "chalk": "1.1.3", - "cli-cursor": "1.0.2", - "cli-width": "2.2.0", - "figures": "1.7.0", - "lodash": "4.17.5", - "readline2": "1.0.1", - "run-async": "0.1.0", - "rx-lite": "3.1.2", - "string-width": "1.0.2", - "strip-ansi": "3.0.1", - "through": "2.3.8" - } - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, - "requires": { - "number-is-nan": "1.0.1" - } - }, - "is-my-json-valid": { - "version": "2.17.2", - "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.17.2.tgz", - "integrity": "sha512-IBhBslgngMQN8DDSppmgDv7RNrlFotuuDsKcrCP3+HbFaVivIBU7u9oiiErw8sH4ynx3+gOGQ3q2otkgiSi6kg==", - "dev": true, - "requires": { - "generate-function": "2.0.0", - "generate-object-property": "1.2.0", - "jsonpointer": "4.0.1", - "xtend": "4.0.1" - } - }, - "is-path-cwd": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", - "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=", - "dev": true - }, - "is-path-in-cwd": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.0.tgz", - "integrity": "sha1-ZHdYK4IU1gI0YJRWcAO+ip6sBNw=", - "dev": true, - "requires": { - "is-path-inside": "1.0.1" - } - }, - "is-path-inside": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", - "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", - "dev": true, - "requires": { - "path-is-inside": "1.0.2" - } - }, - "is-property": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", - "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=", - "dev": true - }, - "is-resolvable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz", - "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==", - "dev": true - }, - "json-stable-stringify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", - "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", - "dev": true, - "requires": { - "jsonify": "0.0.0" - } - }, - "jsonify": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", - "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", - "dev": true - }, - "jsonpointer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz", - "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=", - "dev": true - }, - "levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", - "dev": true, - "requires": { - "prelude-ls": "1.1.2", - "type-check": "0.3.2" - } - }, - "lodash": { - "version": "4.17.5", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.5.tgz", - "integrity": "sha512-svL3uiZf1RwhH+cWrfZn3A4+U58wbP0tGVTLQPbjplZxZ8ROD9VLuNgsRniTlLe7OlSqR79RUehXgpBW/s0IQw==", - "dev": true - }, - "mute-stream": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.5.tgz", - "integrity": "sha1-j7+rsKmKJT0xhDMfno3rc3L6xsA=", - "dev": true - }, - "natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", - "dev": true - }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "requires": { - "wrappy": "1.0.2" - } - }, - "onetime": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", - "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=", - "dev": true - }, - "optionator": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", - "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", - "dev": true, - "requires": { - "deep-is": "0.1.3", - "fast-levenshtein": "2.0.6", - "levn": "0.3.0", - "prelude-ls": "1.1.2", - "type-check": "0.3.2", - "wordwrap": "1.0.0" - } - }, - "os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", - "dev": true - }, - "path-is-inside": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", - "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", - "dev": true - }, - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true - }, - "pinkie": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", - "dev": true - }, - "pinkie-promise": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", - "dev": true, - "requires": { - "pinkie": "2.0.4" - } - }, - "pluralize": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-1.2.1.tgz", - "integrity": "sha1-0aIUg/0iu0HlihL6NCGCMUCJfEU=", - "dev": true - }, - "prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", - "dev": true - }, - "progress": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/progress/-/progress-1.1.8.tgz", - "integrity": "sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=", - "dev": true - }, - "readline2": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/readline2/-/readline2-1.0.1.tgz", - "integrity": "sha1-QQWWCP/BVHV7cV2ZidGZ/783LjU=", - "dev": true, - "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "mute-stream": "0.0.5" - } - }, - "require-uncached": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz", - "integrity": "sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM=", - "dev": true, - "requires": { - "caller-path": "0.1.0", - "resolve-from": "1.0.1" - } - }, - "resolve-from": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz", - "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=", - "dev": true - }, - "restore-cursor": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz", - "integrity": "sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=", - "dev": true, - "requires": { - "exit-hook": "1.1.1", - "onetime": "1.1.0" - } - }, - "rimraf": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", - "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", - "dev": true, - "requires": { - "glob": "7.1.2" - } - }, - "run-async": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-0.1.0.tgz", - "integrity": "sha1-yK1KXhEGYeQCp9IbUw4AnyX444k=", - "dev": true, - "requires": { - "once": "1.4.0" - } - }, - "rx-lite": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-3.1.2.tgz", - "integrity": "sha1-Gc5QLKVyZl87ZHsQk5+X/RYV8QI=", - "dev": true - }, - "slice-ansi": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz", - "integrity": "sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=", - "dev": true - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "strip-ansi": "3.0.1" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "2.1.1" - } - }, - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", - "dev": true - }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", - "dev": true - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - }, - "table": { - "version": "3.8.3", - "resolved": "https://registry.npmjs.org/table/-/table-3.8.3.tgz", - "integrity": "sha1-K7xULw/amGGnVdOUf+/Ys/UThV8=", - "dev": true, - "requires": { - "ajv": "4.11.8", - "ajv-keywords": "1.5.1", - "chalk": "1.1.3", - "lodash": "4.17.5", - "slice-ansi": "0.0.4", - "string-width": "2.1.1" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "requires": { - "is-fullwidth-code-point": "2.0.0", - "strip-ansi": "4.0.0" - } - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "3.0.0" - } - } - } - }, - "text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", - "dev": true - }, - "through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", - "dev": true - }, - "type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", - "dev": true, - "requires": { - "prelude-ls": "1.1.2" - } - }, - "typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", - "dev": true - }, - "user-home": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/user-home/-/user-home-2.0.0.tgz", - "integrity": "sha1-nHC/2Babwdy/SGBODwS4tJzenp8=", - "dev": true, - "requires": { - "os-homedir": "1.0.2" - } - }, - "wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", - "dev": true - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true - }, - "write": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/write/-/write-0.2.1.tgz", - "integrity": "sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c=", - "dev": true, - "requires": { - "mkdirp": "0.5.1" - } } } }, @@ -2988,11 +1515,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.1.1.tgz", "integrity": "sha1-q67IJBd2E7ipWymWOeG2+s9HNEk=", - "dev": true, - "requires": { - "debug": "2.6.8", - "pkg-dir": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-1.0.0.tgz" - } + "dev": true }, "eslint-plugin-babel": { "version": "4.1.2", @@ -3004,57 +1527,31 @@ "version": "2.34.1", "resolved": "https://registry.npmjs.org/eslint-plugin-flowtype/-/eslint-plugin-flowtype-2.34.1.tgz", "integrity": "sha512-xwXpTW7Xv+wfuQdfPILmFl9HWBdWbDjE1aZWWQ4EgCpQtMzymEkDQfyD1ME0VA8C0HTXV7cufypQRvLi+Hk/og==", - "dev": true, - "requires": { - "lodash": "4.17.5" - } + "dev": true }, "eslint-plugin-import": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.6.1.tgz", "integrity": "sha512-aAMb32eHCQaQmgdb1MOG1hfu/rPiNgGur2IF71VJeDfTXdLpPiKALKWlzxMdcxQOZZ2CmYVKabAxCvjACxH1uQ==", "dev": true, - "requires": { - "builtin-modules": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", - "contains-path": "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz", - "debug": "2.6.8", - "doctrine": "1.5.0", - "eslint-import-resolver-node": "0.3.1", - "eslint-module-utils": "2.1.1", - "has": "https://registry.npmjs.org/has/-/has-1.0.1.tgz", - "lodash.cond": "https://registry.npmjs.org/lodash.cond/-/lodash.cond-4.5.2.tgz", - "minimatch": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "read-pkg-up": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz" - }, "dependencies": { "debug": { "version": "2.6.8", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz", "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=", - "dev": true, - "requires": { - "ms": "2.0.0" - } + "dev": true }, "doctrine": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=", - "dev": true, - "requires": { - "esutils": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "isarray": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz" - } + "dev": true }, "eslint-import-resolver-node": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.1.tgz", "integrity": "sha512-yUtXS15gIcij68NmXmP9Ni77AQuCN0itXbCc/jWd8C6/yKZaSNXicpC8cgvjnxVdmfsosIXrjpzFq7GcDryb6A==", - "dev": true, - "requires": { - "debug": "2.6.8", - "resolve": "1.4.0" - } + "dev": true }, "ms": { "version": "2.0.0", @@ -3075,27 +1572,18 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-5.2.1.tgz", "integrity": "sha1-gN8yU8TXkBBF7If6ZgooTjK9yik=", "dev": true, - "requires": { - "ignore": "3.3.7", - "minimatch": "3.0.4", - "resolve": "1.4.0", - "semver": "5.3.0" - }, "dependencies": { "ignore": { "version": "3.3.7", "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.7.tgz", - "integrity": "sha1-YSKJv7PCIOGGpYEYYY1b6MG6sCE=", + "integrity": "sha512-YGG3ejvBNHRqu0559EOxxNFihD0AjpvHlC/pdGKd3X3ofe+CoJkYazwNJYTNebqpPKN+VVQbh4ZFn1DivMNuHA==", "dev": true }, "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "1.1.8" - } + "dev": true }, "semver": { "version": "5.3.0", @@ -3121,12 +1609,7 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.1.0.tgz", "integrity": "sha1-J3cKzzn1/UnNCvQIPOWBBOs5DUw=", - "dev": true, - "requires": { - "doctrine": "https://registry.npmjs.org/doctrine/-/doctrine-2.0.0.tgz", - "has": "https://registry.npmjs.org/has/-/has-1.0.1.tgz", - "jsx-ast-utils": "1.4.1" - } + "dev": true }, "eslint-plugin-standard": { "version": "3.0.1", @@ -3138,11 +1621,7 @@ "version": "3.5.0", "resolved": "https://registry.npmjs.org/espree/-/espree-3.5.0.tgz", "integrity": "sha1-mDWGJb3QVYYeon4oZ+pyn69GPY0=", - "dev": true, - "requires": { - "acorn": "5.1.1", - "acorn-jsx": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz" - } + "dev": true }, "esprima": { "version": "4.0.0", @@ -3150,6 +1629,24 @@ "integrity": "sha1-RJnt3NERDgshi6zy+n9/WfVcqAQ=", "dev": true }, + "esquery": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.0.tgz", + "integrity": "sha1-z7qLV9f7qT8XKYqKAGoEzaE9gPo=", + "dev": true + }, + "esrecurse": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", + "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", + "dev": true + }, + "estraverse": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", + "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", + "dev": true + }, "esutils": { "version": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=" @@ -3163,10 +1660,13 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/ev-store/-/ev-store-7.0.0.tgz", "integrity": "sha1-GrDH+CE2UF3XSzHRdwHLK+bSZVg=", - "dev": true, - "requires": { - "individual": "3.0.0" - } + "dev": true + }, + "event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=", + "dev": true }, "event-kit": { "version": "2.4.0", @@ -3176,54 +1676,33 @@ "execa": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", - "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", - "requires": { - "cross-spawn": "5.1.0", - "get-stream": "3.0.0", - "is-stream": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "npm-run-path": "2.0.2", - "p-finally": "1.0.0", - "signal-exit": "3.0.2", - "strip-eof": "1.0.0" - } + "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=" + }, + "exit-hook": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz", + "integrity": "sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g=", + "dev": true }, "expand-brackets": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "requires": { - "debug": "2.6.8", - "define-property": "0.2.5", - "extend-shallow": "2.0.1", - "posix-character-classes": "0.1.1", - "regex-not": "1.0.2", - "snapdragon": "0.8.1", - "to-regex": "3.0.2" - }, "dependencies": { "define-property": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "0.1.6" - } + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=" }, "is-accessor-descriptor": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "requires": { - "kind-of": "3.2.2" - }, "dependencies": { "kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "1.1.6" - } + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=" } } }, @@ -3231,29 +1710,18 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "requires": { - "kind-of": "3.2.2" - }, "dependencies": { "kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "1.1.6" - } + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=" } } }, "is-descriptor": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha1-Nm2CQN3kh8pRgjsaufB6EKeCUco=", - "requires": { - "is-accessor-descriptor": "0.1.6", - "is-data-descriptor": "0.1.4", - "kind-of": "5.1.0" - } + "integrity": "sha1-Nm2CQN3kh8pRgjsaufB6EKeCUco=" }, "kind-of": { "version": "5.1.0", @@ -3269,33 +1737,17 @@ "extend-shallow": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "0.1.1" - } + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=" }, "extglob": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", "integrity": "sha1-rQD+TcYSqSMuhxhxHcXLWrAoVUM=", - "requires": { - "array-unique": "0.3.2", - "define-property": "1.0.0", - "expand-brackets": "2.1.4", - "extend-shallow": "2.0.1", - "fragment-cache": "0.2.1", - "regex-not": "1.0.2", - "snapdragon": "0.8.1", - "to-regex": "3.0.2" - }, "dependencies": { "define-property": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "requires": { - "is-descriptor": "1.0.2" - } + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=" } } }, @@ -3312,40 +1764,28 @@ "fast-glob": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-1.0.1.tgz", - "integrity": "sha1-MPmxEg/Ven8XI2SmRY+9vZgYezw=", - "requires": { - "bash-glob": "1.0.2", - "glob-parent": "3.1.0", - "micromatch": "3.1.9", - "readdir-enhanced": "1.5.2" - } + "integrity": "sha1-MPmxEg/Ven8XI2SmRY+9vZgYezw=" }, "fast-json-stable-stringify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, "fb-watchman": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.0.tgz", - "integrity": "sha1-VOmr99+i8mzZsWNsWIwa/AXeXVg=", - "requires": { - "bser": "2.0.0" - } + "integrity": "sha1-VOmr99+i8mzZsWNsWIwa/AXeXVg=" }, "fbjs": { "version": "0.8.14", "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.14.tgz", "integrity": "sha1-0dviviVMNakeCfMfnNUKQLKg7Rw=", - "requires": { - "core-js": "1.2.7", - "isomorphic-fetch": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz", - "loose-envify": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz", - "object-assign": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "promise": "7.3.1", - "setimmediate": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "ua-parser-js": "0.7.14" - }, "dependencies": { "core-js": { "version": "1.2.7", @@ -3354,22 +1794,34 @@ } } }, + "figures": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", + "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", + "dev": true + }, + "file-entry-cache": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-2.0.0.tgz", + "integrity": "sha1-w5KZDD5oR4PYOLjISkXYoEhFg2E=", + "dev": true + }, "fill-range": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "requires": { - "extend-shallow": "2.0.1", - "is-number": "3.0.0", - "repeat-string": "1.6.1", - "to-regex-range": "2.1.1" - } + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=" }, "find-up": { "version": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", "dev": true }, + "flat-cache": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.3.0.tgz", + "integrity": "sha1-0wMLMrOBVPTjt+nHCfSQ9++XxIE=", + "dev": true + }, "for-in": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", @@ -3388,51 +1840,31 @@ "form-data": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz", - "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", - "requires": { - "asynckit": "0.4.0", - "combined-stream": "1.0.6", - "mime-types": "2.1.16" - } + "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=" }, "formatio": { "version": "https://registry.npmjs.org/formatio/-/formatio-1.2.0.tgz", "integrity": "sha1-87IWfZBoxGmKjVH092CjmlTYGOs=", - "dev": true, - "requires": { - "samsam": "https://registry.npmjs.org/samsam/-/samsam-1.2.1.tgz" - } + "dev": true }, "fragment-cache": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", - "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", - "requires": { - "map-cache": "0.2.2" - } + "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=" }, "fs-extra": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-5.0.0.tgz", - "integrity": "sha1-QU0BEM3QZwVzTQVWUsVBEmDDGr0=", - "requires": { - "graceful-fs": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", - "jsonfile": "4.0.0", - "universalify": "0.1.1" - } + "integrity": "sha1-QU0BEM3QZwVzTQVWUsVBEmDDGr0=" }, "fs-minipass": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.5.tgz", - "integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==", - "requires": { - "minipass": "2.2.1" - } + "integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==" }, "fs.realpath": { "version": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, "function-bind": { "version": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.0.tgz", @@ -3442,13 +1874,8 @@ "function.prototype.name": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.0.3.tgz", - "integrity": "sha1-AJmuVXLp3W8DyX0CP9krzF5jnqw=", + "integrity": "sha512-5EblxZUdioXi2JiMZ9FUbwYj40eQ9MFHyzFLBSPdlRl3SO8l7SLWuAnQ/at/1Wi4hjJwME/C5WpF2ZfAc8nGNw==", "dev": true, - "requires": { - "define-properties": "1.1.2", - "function-bind": "1.1.1", - "is-callable": "1.1.3" - }, "dependencies": { "function-bind": { "version": "1.1.1", @@ -3458,6 +1885,18 @@ } } }, + "generate-function": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz", + "integrity": "sha1-aFj+fAlpt9TpCTM3ZHrHn2DfvnQ=", + "dev": true + }, + "generate-object-property": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", + "integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=", + "dev": true + }, "get-caller-file": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.2.tgz", @@ -3477,9 +1916,6 @@ "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "requires": { - "assert-plus": "1.0.0" - }, "dependencies": { "assert-plus": { "version": "1.0.0", @@ -3491,33 +1927,17 @@ "glob": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=", - "dev": true, - "requires": { - "fs.realpath": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "inflight": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "inherits": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "minimatch": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "once": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "path-is-absolute": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" - } + "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=" }, "glob-parent": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", - "requires": { - "is-glob": "3.1.0", - "path-dirname": "1.0.2" - }, "dependencies": { "is-glob": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "requires": { - "is-extglob": "2.1.1" - } + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=" } } }, @@ -3530,17 +1950,19 @@ "version": "4.3.2", "resolved": "https://registry.npmjs.org/global/-/global-4.3.2.tgz", "integrity": "sha1-52mJJopsdMOJCLEwWxD8DjlOnQ8=", - "dev": true, - "requires": { - "min-document": "2.19.0", - "process": "0.5.2" - } + "dev": true }, "globals": { "version": "9.18.0", "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", "integrity": "sha1-qjiWs+abSH8X4x7SFD1pqOMMLYo=" }, + "globby": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz", + "integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=", + "dev": true + }, "graceful-fs": { "version": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" @@ -3554,9 +1976,6 @@ "version": "0.10.3", "resolved": "https://registry.npmjs.org/graphql/-/graphql-0.10.3.tgz", "integrity": "sha1-wxOv1VGOZzNRvuGPtj4qDkh0B6s=", - "requires": { - "iterall": "1.1.1" - }, "dependencies": { "iterall": { "version": "1.1.1", @@ -3578,75 +1997,44 @@ "har-validator": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", - "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", - "requires": { - "ajv": "5.5.2", - "har-schema": "2.0.0" - } + "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=" }, "has": { "version": "https://registry.npmjs.org/has/-/has-1.0.1.tgz", "integrity": "sha1-hGFzP1OLCDfJNh45qauelwTcLyg=", - "dev": true, - "requires": { - "function-bind": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.0.tgz" - } + "dev": true }, "has-ansi": { "version": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "requires": { - "ansi-regex": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz" - } + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=" }, "has-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", - "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", - "requires": { - "get-value": "2.0.6", - "has-values": "1.0.0", - "isobject": "3.0.1" - } + "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=" }, "has-values": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", - "requires": { - "is-number": "3.0.0", - "kind-of": "4.0.0" - }, "dependencies": { "kind-of": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", - "requires": { - "is-buffer": "1.1.6" - } + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=" } } }, "hawk": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz", - "integrity": "sha512-miowhl2+U7Qle4vdLqDdPt9m09K6yZhkLDTWGoUiUzrQCn+mHHSmfJgAyGaLRZbPmTqfFFjRV1QWCW0VWUJBbQ==", - "requires": { - "boom": "4.3.1", - "cryptiles": "3.1.2", - "hoek": "4.2.1", - "sntp": "2.1.0" - } + "integrity": "sha1-r02RTrBl+bXOTZ0RwcshJu7MMDg=" }, "hock": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/hock/-/hock-1.3.2.tgz", "integrity": "sha1-btPovkK0ZnmBGNEhUKqA6NbvIhk=", "dev": true, - "requires": { - "deep-equal": "0.2.1" - }, "dependencies": { "deep-equal": { "version": "0.2.1", @@ -3659,7 +2047,7 @@ "hoek": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz", - "integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==" + "integrity": "sha1-ljRQKqEsRF3Vp8VzS1cruHOKrLs=" }, "hoist-non-react-statics": { "version": "1.2.0", @@ -3669,11 +2057,7 @@ "home-or-tmp": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz", - "integrity": "sha1-42w/LSyufXRqhX440Y1fMqeILbg=", - "requires": { - "os-homedir": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "os-tmpdir": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz" - } + "integrity": "sha1-42w/LSyufXRqhX440Y1fMqeILbg=" }, "hosted-git-info": { "version": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.5.0.tgz", @@ -3683,30 +2067,17 @@ "version": "3.9.2", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.9.2.tgz", "integrity": "sha1-G9+HrMoPP55T+k/M6w9LTLsAszg=", - "dev": true, - "requires": { - "domelementtype": "1.3.0", - "domhandler": "2.4.1", - "domutils": "1.5.1", - "entities": "1.1.1", - "inherits": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "readable-stream": "2.3.3" - } + "dev": true }, "http-signature": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "requires": { - "assert-plus": "1.0.0", - "jsprim": "1.4.1", - "sshpk": "1.13.1" - } + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=" }, "iconv-lite": { "version": "0.4.18", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.18.tgz", - "integrity": "sha1-I9hlaxaq5nQqwpcy6o8DNqR4nPI=" + "integrity": "sha512-sr1ZQph3UwHTR0XftSbK85OvBbxe/abLGzEnPENCQwmHf7sck8Oyu4ob3LgBxWWxRoM+QszeUyl7jbqapu2TqA==" }, "ignore": { "version": "3.3.3", @@ -3726,6 +2097,12 @@ "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.7.6.tgz", "integrity": "sha1-E7TTyxK++hVIKib+Gy665kAHHks=" }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, "individual": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/individual/-/individual-3.0.0.tgz", @@ -3734,17 +2111,25 @@ }, "inflight": { "version": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, - "requires": { - "once": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "wrappy": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" - } + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=" }, "inherits": { "version": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "inquirer": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-0.12.0.tgz", + "integrity": "sha1-HvK/1jUE3wvHV4X/+MLEHfEvB34=", + "dev": true, + "dependencies": { + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true + } + } }, "interpret": { "version": "https://registry.npmjs.org/interpret/-/interpret-1.0.3.tgz", @@ -3763,10 +2148,7 @@ "is-accessor-descriptor": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha1-FpwvbT3x+ZJhgHI2XJsOofaHhlY=", - "requires": { - "kind-of": "6.0.2" - } + "integrity": "sha1-FpwvbT3x+ZJhgHI2XJsOofaHhlY=" }, "is-arrayish": { "version": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -3779,10 +2161,7 @@ }, "is-builtin-module": { "version": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", - "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", - "requires": { - "builtin-modules": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz" - } + "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=" }, "is-callable": { "version": "1.1.3", @@ -3793,10 +2172,7 @@ "is-data-descriptor": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha1-2Eh2Mh0Oet0DmQQGq7u9NrqSaMc=", - "requires": { - "kind-of": "6.0.2" - } + "integrity": "sha1-2Eh2Mh0Oet0DmQQGq7u9NrqSaMc=" }, "is-date-object": { "version": "1.0.1", @@ -3807,12 +2183,7 @@ "is-descriptor": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha1-OxWXRqZmBLBPjIFSS6NlxfFNhuw=", - "requires": { - "is-accessor-descriptor": "1.0.0", - "is-data-descriptor": "1.0.0", - "kind-of": "6.0.2" - } + "integrity": "sha1-OxWXRqZmBLBPjIFSS6NlxfFNhuw=" }, "is-extendable": { "version": "0.1.1", @@ -3826,34 +2197,39 @@ }, "is-finite": { "version": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", - "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", - "requires": { - "number-is-nan": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz" - } + "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=" + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=" }, "is-glob": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.0.tgz", - "integrity": "sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=", - "requires": { - "is-extglob": "2.1.1" - } + "integrity": "sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=" + }, + "is-my-ip-valid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz", + "integrity": "sha512-gmh/eWXROncUzRnIa1Ubrt5b8ep/MGSnfAUI3aRp+sqTCs1tv1Isl8d8F6JmkN3dXKc3ehZMrtiPN9eL03NuaQ==", + "dev": true + }, + "is-my-json-valid": { + "version": "2.17.2", + "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.17.2.tgz", + "integrity": "sha512-IBhBslgngMQN8DDSppmgDv7RNrlFotuuDsKcrCP3+HbFaVivIBU7u9oiiErw8sH4ynx3+gOGQ3q2otkgiSi6kg==", + "dev": true }, "is-number": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "requires": { - "kind-of": "3.2.2" - }, "dependencies": { "kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "1.1.6" - } + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=" } } }, @@ -3867,9 +2243,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-odd/-/is-odd-2.0.0.tgz", "integrity": "sha1-dkZiRnH9fqVYzNmieVGC8pWPGyQ=", - "requires": { - "is-number": "4.0.0" - }, "dependencies": { "is-number": { "version": "4.0.0", @@ -3878,22 +2251,46 @@ } } }, + "is-path-cwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", + "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=", + "dev": true + }, + "is-path-in-cwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.0.tgz", + "integrity": "sha1-ZHdYK4IU1gI0YJRWcAO+ip6sBNw=", + "dev": true + }, + "is-path-inside": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", + "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", + "dev": true + }, "is-plain-object": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha1-LBY7P6+xtgbZ0Xko8FwqHDjgdnc=", - "requires": { - "isobject": "3.0.1" - } + "integrity": "sha1-LBY7P6+xtgbZ0Xko8FwqHDjgdnc=" + }, + "is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=", + "dev": true }, "is-regex": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", - "dev": true, - "requires": { - "has": "https://registry.npmjs.org/has/-/has-1.0.1.tgz" - } + "dev": true + }, + "is-resolvable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz", + "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==", + "dev": true }, "is-stream": { "version": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", @@ -3936,11 +2333,7 @@ }, "isomorphic-fetch": { "version": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz", - "integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=", - "requires": { - "node-fetch": "1.7.1", - "whatwg-fetch": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz" - } + "integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=" }, "isstream": { "version": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", @@ -3956,10 +2349,6 @@ "resolved": "https://registry.npmjs.org/jade/-/jade-0.26.3.tgz", "integrity": "sha1-jxDXl32NefL2/4YqgbBRPMslaGw=", "dev": true, - "requires": { - "commander": "0.6.1", - "mkdirp": "0.3.0" - }, "dependencies": { "commander": { "version": "0.6.1", @@ -3983,12 +2372,8 @@ "js-yaml": { "version": "3.9.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.9.1.tgz", - "integrity": "sha1-CHdc69/dNZIJ8NKs04PI+GppBKA=", - "dev": true, - "requires": { - "argparse": "https://registry.npmjs.org/argparse/-/argparse-1.0.9.tgz", - "esprima": "4.0.0" - } + "integrity": "sha512-CbcG379L1e+mWBnLvHWWeLs8GyV/EMw862uLI3c+GxVyDHWZcjZinwuBd3iW2pgxgIlksW/1vNJa4to+RvDOww==", + "dev": true }, "jsbn": { "version": "0.1.1", @@ -4013,11 +2398,7 @@ "json-stable-stringify": { "version": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", - "dev": true, - "optional": true, - "requires": { - "jsonify": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz" - } + "dev": true }, "json-stringify-safe": { "version": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", @@ -4036,27 +2417,23 @@ "jsonfile": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", - "requires": { - "graceful-fs": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz" - } + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=" }, "jsonify": { "version": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", - "dev": true, - "optional": true + "dev": true + }, + "jsonpointer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz", + "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=", + "dev": true }, "jsprim": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" - }, "dependencies": { "assert-plus": { "version": "1.0.0", @@ -4074,10 +2451,7 @@ "keytar": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/keytar/-/keytar-4.1.0.tgz", - "integrity": "sha1-njkz5InWVt4aho4Sk3CTEwRJidc=", - "requires": { - "nan": "2.5.1" - } + "integrity": "sha512-L3KqiSMtpVitlug4uuI+K5XLne9SAVEFWE8SCQIhQiH0IA/CTbon5v5prVLKK0Ken54o2O8V9HceKagpwJum+Q==" }, "kind-of": { "version": "6.0.2", @@ -4087,45 +2461,25 @@ "lazy-cache": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-2.0.2.tgz", - "integrity": "sha1-uRkKT5EzVGlIQIWfio9whNiCImQ=", - "requires": { - "set-getter": "0.1.0" - } + "integrity": "sha1-uRkKT5EzVGlIQIWfio9whNiCImQ=" }, "lcid": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", - "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", - "requires": { - "invert-kv": "1.0.0" - } + "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=" }, "less": { "version": "2.7.3", "resolved": "https://registry.npmjs.org/less/-/less-2.7.3.tgz", "integrity": "sha1-zBJg9RyQCp7A2R+2mYE54CUHtjs=", "dev": true, - "requires": { - "errno": "0.1.4", - "graceful-fs": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", - "image-size": "0.5.5", - "mime": "1.4.1", - "mkdirp": "0.5.1", - "promise": "7.3.1", - "request": "2.81.0", - "source-map": "0.5.7" - }, "dependencies": { "ajv": { "version": "4.11.8", "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=", "dev": true, - "optional": true, - "requires": { - "co": "4.6.0", - "json-stable-stringify": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz" - } + "optional": true }, "assert-plus": { "version": "0.2.0", @@ -4145,20 +2499,14 @@ "version": "2.10.1", "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", - "dev": true, - "requires": { - "hoek": "2.16.3" - } + "dev": true }, "cryptiles": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=", "dev": true, - "optional": true, - "requires": { - "boom": "2.10.1" - } + "optional": true }, "form-data": { "version": "2.1.4", @@ -4166,21 +2514,13 @@ "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=", "dev": true, "optional": true, - "requires": { - "asynckit": "0.4.0", - "combined-stream": "1.0.6", - "mime-types": "2.1.16" - }, "dependencies": { "combined-stream": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", "dev": true, - "optional": true, - "requires": { - "delayed-stream": "1.0.0" - } + "optional": true } } }, @@ -4196,24 +2536,14 @@ "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-4.2.1.tgz", "integrity": "sha1-M0gdDxu/9gDdID11gSpqX7oALio=", "dev": true, - "optional": true, - "requires": { - "ajv": "4.11.8", - "har-schema": "1.0.5" - } + "optional": true }, "hawk": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=", "dev": true, - "optional": true, - "requires": { - "boom": "2.10.1", - "cryptiles": "2.0.5", - "hoek": "2.16.3", - "sntp": "1.0.9" - } + "optional": true }, "hoek": { "version": "2.16.3", @@ -4226,12 +2556,7 @@ "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=", "dev": true, - "optional": true, - "requires": { - "assert-plus": "0.2.0", - "jsprim": "1.4.1", - "sshpk": "1.13.1" - } + "optional": true }, "mime": { "version": "1.4.1", @@ -4260,30 +2585,6 @@ "integrity": "sha1-xpKJRqDgbF+Nb4qTM0af/aRimKA=", "dev": true, "optional": true, - "requires": { - "aws-sign2": "0.6.0", - "aws4": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", - "caseless": "0.12.0", - "combined-stream": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", - "extend": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", - "forever-agent": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "form-data": "2.1.4", - "har-validator": "4.2.1", - "hawk": "3.1.3", - "http-signature": "1.1.1", - "is-typedarray": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "isstream": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "json-stringify-safe": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "mime-types": "2.1.16", - "oauth-sign": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", - "performance-now": "0.2.0", - "qs": "6.4.0", - "safe-buffer": "5.1.1", - "stringstream": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", - "tough-cookie": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.2.tgz", - "tunnel-agent": "0.6.0", - "uuid": "3.1.0" - }, "dependencies": { "combined-stream": { "version": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", @@ -4310,10 +2611,7 @@ "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=", "dev": true, - "optional": true, - "requires": { - "hoek": "2.16.3" - } + "optional": true }, "source-map": { "version": "0.5.7", @@ -4324,15 +2622,15 @@ } } }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true + }, "load-json-file": { "version": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", - "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", - "requires": { - "graceful-fs": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", - "parse-json": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", - "pify": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "strip-bom": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz" - } + "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=" }, "lodash": { "version": "4.17.5", @@ -4342,11 +2640,7 @@ "lodash._baseassign": { "version": "https://registry.npmjs.org/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz", "integrity": "sha1-jDigmVAPIVrQnlnxci/QxSv+Ck4=", - "dev": true, - "requires": { - "lodash._basecopy": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz", - "lodash.keys": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz" - } + "dev": true }, "lodash._basecopy": { "version": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz", @@ -4388,12 +2682,7 @@ "lodash.create": { "version": "https://registry.npmjs.org/lodash.create/-/lodash.create-3.1.1.tgz", "integrity": "sha1-1/KEnw29p+BGgruM1yqwIkYd6+c=", - "dev": true, - "requires": { - "lodash._baseassign": "https://registry.npmjs.org/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz", - "lodash._basecreate": "https://registry.npmjs.org/lodash._basecreate/-/lodash._basecreate-3.0.3.tgz", - "lodash._isiterateecall": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz" - } + "dev": true }, "lodash.defaults": { "version": "4.2.0", @@ -4437,12 +2726,7 @@ "lodash.keys": { "version": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=", - "dev": true, - "requires": { - "lodash._getnative": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", - "lodash.isarguments": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", - "lodash.isarray": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz" - } + "dev": true }, "lodash.map": { "version": "4.6.0", @@ -4492,10 +2776,7 @@ }, "loose-envify": { "version": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz", - "integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=", - "requires": { - "js-tokens": "3.0.2" - } + "integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=" }, "lru-cache": { "version": "2.7.3", @@ -4511,55 +2792,27 @@ "map-visit": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", - "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", - "requires": { - "object-visit": "1.0.1" - } + "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=" }, "mem": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/mem/-/mem-1.1.0.tgz", - "integrity": "sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y=", - "requires": { - "mimic-fn": "1.2.0" - } + "integrity": "sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y=" }, "micromatch": { "version": "3.1.9", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.9.tgz", "integrity": "sha1-FdyTF1rjnlLpMIeEcJbv/HPvz4k=", - "requires": { - "arr-diff": "4.0.0", - "array-unique": "0.3.2", - "braces": "2.3.1", - "define-property": "2.0.2", - "extend-shallow": "3.0.2", - "extglob": "2.0.4", - "fragment-cache": "0.2.1", - "kind-of": "6.0.2", - "nanomatch": "1.2.9", - "object.pick": "1.3.0", - "regex-not": "1.0.2", - "snapdragon": "0.8.1", - "to-regex": "3.0.2" - }, "dependencies": { "extend-shallow": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "requires": { - "assign-symbols": "1.0.0", - "is-extendable": "1.0.1" - } + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=" }, "is-extendable": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha1-p0cPnkJnM9gb2B4RVSZOOjUHyrQ=", - "requires": { - "is-plain-object": "2.0.4" - } + "integrity": "sha1-p0cPnkJnM9gb2B4RVSZOOjUHyrQ=" } } }, @@ -4571,10 +2824,7 @@ "mime-types": { "version": "2.1.16", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.16.tgz", - "integrity": "sha1-K4WKUuXs1RbbiXrCvodIeDBpjiM=", - "requires": { - "mime-db": "1.29.0" - } + "integrity": "sha1-K4WKUuXs1RbbiXrCvodIeDBpjiM=" }, "mimic-fn": { "version": "1.2.0", @@ -4585,50 +2835,31 @@ "version": "2.19.0", "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz", "integrity": "sha1-e9KC4/WELtKVu3SM3Z8f+iyCRoU=", - "dev": true, - "requires": { - "dom-walk": "0.1.1" - } + "dev": true }, "minimatch": { "version": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=", - "requires": { - "brace-expansion": "1.1.8" - } + "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=" }, "minipass": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.2.1.tgz", - "integrity": "sha512-u1aUllxPJUI07cOqzR7reGmQxmCqlH88uIIsf6XZFEWgw7gXKpJdR+5R9Y3KEDmWYkdIz9wXZs3C0jOPxejk/Q==", - "requires": { - "yallist": "3.0.2" - } + "integrity": "sha512-u1aUllxPJUI07cOqzR7reGmQxmCqlH88uIIsf6XZFEWgw7gXKpJdR+5R9Y3KEDmWYkdIz9wXZs3C0jOPxejk/Q==" }, "minizlib": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.1.0.tgz", - "integrity": "sha512-4T6Ur/GctZ27nHfpt9THOdRZNgyJ9FZchYO1ceg5S8Q3DNLCKYy44nCZzgCJgcvx2UM8czmqak5BCxJMrq37lA==", - "requires": { - "minipass": "2.2.1" - } + "integrity": "sha512-4T6Ur/GctZ27nHfpt9THOdRZNgyJ9FZchYO1ceg5S8Q3DNLCKYy44nCZzgCJgcvx2UM8czmqak5BCxJMrq37lA==" }, "mixin-deep": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz", "integrity": "sha1-pJ5yaNzhoNlpjkUybFYm3zVD0P4=", - "requires": { - "for-in": "1.0.2", - "is-extendable": "1.0.1" - }, "dependencies": { "is-extendable": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha1-p0cPnkJnM9gb2B4RVSZOOjUHyrQ=", - "requires": { - "is-plain-object": "2.0.4" - } + "integrity": "sha1-p0cPnkJnM9gb2B4RVSZOOjUHyrQ=" } } }, @@ -4636,9 +2867,6 @@ "version": "0.5.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "requires": { - "minimist": "0.0.8" - }, "dependencies": { "minimist": { "version": "0.0.8", @@ -4652,42 +2880,18 @@ "resolved": "https://registry.npmjs.org/mocha/-/mocha-3.4.2.tgz", "integrity": "sha1-0O9NMyEm2/GNDWQMmzgt1IvpdZQ=", "dev": true, - "requires": { - "browser-stdout": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.0.tgz", - "commander": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", - "debug": "2.6.0", - "diff": "3.2.0", - "escape-string-regexp": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "glob": "7.1.1", - "growl": "https://registry.npmjs.org/growl/-/growl-1.9.2.tgz", - "json3": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz", - "lodash.create": "https://registry.npmjs.org/lodash.create/-/lodash.create-3.1.1.tgz", - "mkdirp": "0.5.1", - "supports-color": "3.1.2" - }, "dependencies": { "debug": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.0.tgz", "integrity": "sha1-vFlryr52F/Edn6FTYe3tVgi4SZs=", - "dev": true, - "requires": { - "ms": "0.7.2" - } + "dev": true }, "glob": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.1.tgz", "integrity": "sha1-gFIR3wT6rxxjo2ADBs31reULLsg=", - "dev": true, - "requires": { - "fs.realpath": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "inflight": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "inherits": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "minimatch": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "once": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "path-is-absolute": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" - } + "dev": true }, "ms": { "version": "0.7.2", @@ -4700,9 +2904,6 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.1.2.tgz", "integrity": "sha1-cqJiiU2dQIuVbKBf83su2KbiotU=", "dev": true, - "requires": { - "has-flag": "1.0.0" - }, "dependencies": { "has-flag": { "version": "1.0.0", @@ -4719,9 +2920,6 @@ "resolved": "https://registry.npmjs.org/mocha-appveyor-reporter/-/mocha-appveyor-reporter-0.4.0.tgz", "integrity": "sha1-gpOC/8Bla2Z+e+ZQoJSgba69Uk8=", "dev": true, - "requires": { - "request-json": "0.6.3" - }, "dependencies": { "depd": { "version": "1.1.2", @@ -4733,10 +2931,7 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/request-json/-/request-json-0.6.3.tgz", "integrity": "sha512-5TVnMD3LaeK0GRCyFlsNgJf5Fjg8J8j7VEfsoJESSWZlWRgPIf7IojsBLbTHcg2798JrrRkJ6L3k1+wj4sglgw==", - "dev": true, - "requires": { - "depd": "1.1.2" - } + "dev": true } } }, @@ -4745,12 +2940,6 @@ "resolved": "https://registry.npmjs.org/mocha-junit-and-console-reporter/-/mocha-junit-and-console-reporter-1.6.0.tgz", "integrity": "sha1-kBmEuev51g9xXvDo4EwG5KsSJFc=", "dev": true, - "requires": { - "debug": "2.6.8", - "mkdirp": "0.5.1", - "mocha": "2.5.3", - "xml": "1.0.1" - }, "dependencies": { "commander": { "version": "2.3.0", @@ -4762,10 +2951,7 @@ "version": "2.6.8", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz", "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=", - "dev": true, - "requires": { - "ms": "2.0.0" - } + "dev": true }, "diff": { "version": "1.4.0", @@ -4783,48 +2969,25 @@ "version": "3.2.11", "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz", "integrity": "sha1-Spc/Y1uRkPcV0QmH1cAP0oFevj0=", - "dev": true, - "requires": { - "inherits": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "minimatch": "0.3.0" - } + "dev": true }, "minimatch": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz", "integrity": "sha1-J12O2qxPG7MyZHIInnlJyDlGmd0=", - "dev": true, - "requires": { - "lru-cache": "2.7.3", - "sigmund": "1.0.1" - } + "dev": true }, "mocha": { "version": "2.5.3", "resolved": "https://registry.npmjs.org/mocha/-/mocha-2.5.3.tgz", "integrity": "sha1-FhvlvetJZ3HrmzV0UFC2IrWu/Fg=", "dev": true, - "requires": { - "commander": "2.3.0", - "debug": "2.2.0", - "diff": "1.4.0", - "escape-string-regexp": "1.0.2", - "glob": "3.2.11", - "growl": "https://registry.npmjs.org/growl/-/growl-1.9.2.tgz", - "jade": "0.26.3", - "mkdirp": "0.5.1", - "supports-color": "1.2.0", - "to-iso-string": "0.0.2" - }, "dependencies": { "debug": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", - "dev": true, - "requires": { - "ms": "0.7.1" - } + "dev": true }, "ms": { "version": "0.7.1", @@ -4869,6 +3032,12 @@ "resolved": "https://registry.npmjs.org/multi-list-selection/-/multi-list-selection-0.1.1.tgz", "integrity": "sha1-E8R3DJSkBd+ty8YCMvHAIixZ3EQ=" }, + "mute-stream": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.5.tgz", + "integrity": "sha1-j7+rsKmKJT0xhDMfno3rc3L6xsA=", + "dev": true + }, "nan": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/nan/-/nan-2.5.1.tgz", @@ -4878,37 +3047,16 @@ "version": "1.2.9", "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.9.tgz", "integrity": "sha1-h59xUMstq3pHElkGbBBO7m4Pp8I=", - "requires": { - "arr-diff": "4.0.0", - "array-unique": "0.3.2", - "define-property": "2.0.2", - "extend-shallow": "3.0.2", - "fragment-cache": "0.2.1", - "is-odd": "2.0.0", - "is-windows": "1.0.2", - "kind-of": "6.0.2", - "object.pick": "1.3.0", - "regex-not": "1.0.2", - "snapdragon": "0.8.1", - "to-regex": "3.0.2" - }, "dependencies": { "extend-shallow": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "requires": { - "assign-symbols": "1.0.0", - "is-extendable": "1.0.1" - } + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=" }, "is-extendable": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha1-p0cPnkJnM9gb2B4RVSZOOjUHyrQ=", - "requires": { - "is-plain-object": "2.0.4" - } + "integrity": "sha1-p0cPnkJnM9gb2B4RVSZOOjUHyrQ=" } } }, @@ -4917,6 +3065,12 @@ "integrity": "sha1-IKMYwwy0X3H+et+/eyHJnBRy7xE=", "dev": true }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, "next-tick": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-0.2.2.tgz", @@ -4926,11 +3080,7 @@ "node-fetch": { "version": "1.7.1", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.1.tgz", - "integrity": "sha512-j8XsFGCLw79vWXkZtMSmmLaOk9z5SQ9bV/tkbZVCqvgwzrjAGq66igobLofHtF63NvMTp2WjytpsNTGKa+XRIQ==", - "requires": { - "encoding": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", - "is-stream": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz" - } + "integrity": "sha512-j8XsFGCLw79vWXkZtMSmmLaOk9z5SQ9bV/tkbZVCqvgwzrjAGq66igobLofHtF63NvMTp2WjytpsNTGKa+XRIQ==" }, "node-int64": { "version": "0.4.0", @@ -4939,30 +3089,18 @@ }, "normalize-package-data": { "version": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", - "integrity": "sha1-EvlaMH1YNSB1oEkHuErIvpisAS8=", - "requires": { - "hosted-git-info": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.5.0.tgz", - "is-builtin-module": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", - "semver": "5.4.1", - "validate-npm-package-license": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz" - } + "integrity": "sha1-EvlaMH1YNSB1oEkHuErIvpisAS8=" }, "npm-run-path": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", - "requires": { - "path-key": "2.0.1" - } + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=" }, "nth-check": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.1.tgz", "integrity": "sha1-mSms32KPwsQQmN6rgqxYDPFJquQ=", - "dev": true, - "requires": { - "boolbase": "1.0.0" - } + "dev": true }, "number-is-nan": { "version": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", @@ -4980,45 +3118,26 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", - "requires": { - "copy-descriptor": "0.1.1", - "define-property": "0.2.5", - "kind-of": "3.2.2" - }, "dependencies": { "define-property": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "0.1.6" - } + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=" }, "is-accessor-descriptor": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "requires": { - "kind-of": "3.2.2" - } + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=" }, "is-data-descriptor": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "requires": { - "kind-of": "3.2.2" - } + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=" }, "is-descriptor": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", "integrity": "sha1-Nm2CQN3kh8pRgjsaufB6EKeCUco=", - "requires": { - "is-accessor-descriptor": "0.1.6", - "is-data-descriptor": "0.1.4", - "kind-of": "5.1.0" - }, "dependencies": { "kind-of": { "version": "5.1.0", @@ -5030,10 +3149,7 @@ "kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "1.1.6" - } + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=" } } }, @@ -5052,21 +3168,13 @@ "object-visit": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", - "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", - "requires": { - "isobject": "3.0.1" - } + "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=" }, "object.assign": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.0.4.tgz", "integrity": "sha1-scnMBE7xuf5jYG/BQau7MuFHMMw=", "dev": true, - "requires": { - "define-properties": "1.1.2", - "function-bind": "1.1.1", - "object-keys": "1.0.11" - }, "dependencies": { "function-bind": { "version": "1.1.1", @@ -5081,12 +3189,6 @@ "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.0.4.tgz", "integrity": "sha1-G/mk3SKI9bM/Opk9JXZh8F0WGl8=", "dev": true, - "requires": { - "define-properties": "1.1.2", - "es-abstract": "1.9.0", - "function-bind": "1.1.1", - "has": "https://registry.npmjs.org/has/-/has-1.0.1.tgz" - }, "dependencies": { "function-bind": { "version": "1.1.1", @@ -5099,22 +3201,13 @@ "object.pick": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", - "requires": { - "isobject": "3.0.1" - } + "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=" }, "object.values": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.0.4.tgz", "integrity": "sha1-5STaCbT2b/Bd9FdUbscqyZ8TBpo=", "dev": true, - "requires": { - "define-properties": "1.1.2", - "es-abstract": "1.9.0", - "function-bind": "1.1.1", - "has": "https://registry.npmjs.org/has/-/has-1.0.1.tgz" - }, "dependencies": { "function-bind": { "version": "1.1.1", @@ -5126,18 +3219,31 @@ }, "once": { "version": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "requires": { - "wrappy": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" - } + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=" + }, + "onetime": { + "version": "1.1.0", + "resolved": "http://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", + "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=", + "dev": true }, "optimist": { "version": "0.3.7", "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.3.7.tgz", - "integrity": "sha1-yQlBrVnkJzMokjB00s8ufLxuwNk=", - "requires": { - "wordwrap": "0.0.3" + "integrity": "sha1-yQlBrVnkJzMokjB00s8ufLxuwNk=" + }, + "optionator": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", + "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", + "dev": true, + "dependencies": { + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", + "dev": true + } } }, "os-homedir": { @@ -5147,12 +3253,7 @@ "os-locale": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-2.1.0.tgz", - "integrity": "sha1-QrwpAKa1uL0XN2yOiCtlr8zyS/I=", - "requires": { - "execa": "0.7.0", - "lcid": "1.0.0", - "mem": "1.1.0" - } + "integrity": "sha1-QrwpAKa1uL0XN2yOiCtlr8zyS/I=" }, "os-tmpdir": { "version": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", @@ -5165,10 +3266,7 @@ }, "parse-json": { "version": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", - "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", - "requires": { - "error-ex": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz" - } + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=" }, "pascalcase": { "version": "0.1.1", @@ -5190,6 +3288,12 @@ "version": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, + "path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", + "dev": true + }, "path-key": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", @@ -5204,9 +3308,6 @@ "version": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", "dev": true, - "requires": { - "isarray": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" - }, "dependencies": { "isarray": { "version": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", @@ -5217,10 +3318,7 @@ }, "path-type": { "version": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", - "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", - "requires": { - "pify": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz" - } + "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=" }, "performance-now": { "version": "2.1.0", @@ -5233,28 +3331,36 @@ }, "pinkie": { "version": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=" + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "dev": true }, "pinkie-promise": { "version": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", - "requires": { - "pinkie": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz" - } + "dev": true }, "pkg-dir": { "version": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-1.0.0.tgz", "integrity": "sha1-ektQio1bstYp1EcFb/TpyTFM89Q=", - "dev": true, - "requires": { - "find-up": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz" - } + "dev": true + }, + "pluralize": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-1.2.1.tgz", + "integrity": "sha1-0aIUg/0iu0HlihL6NCGCMUCJfEU=", + "dev": true }, "posix-character-classes": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=" }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true + }, "private": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", @@ -5279,20 +3385,12 @@ "promise": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", - "integrity": "sha1-BktyYCsY+Q8pGSuLG8QY/9Hr078=", - "requires": { - "asap": "2.0.6" - } + "integrity": "sha1-BktyYCsY+Q8pGSuLG8QY/9Hr078=" }, "prop-types": { "version": "15.6.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.1.tgz", "integrity": "sha1-NmREU1ZCVd3aORGR+zoSXL32VMo=", - "requires": { - "fbjs": "0.8.16", - "loose-envify": "1.3.1", - "object-assign": "4.1.1" - }, "dependencies": { "core-js": { "version": "1.2.7", @@ -5302,33 +3400,17 @@ "fbjs": { "version": "0.8.16", "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.16.tgz", - "integrity": "sha1-XmdDL1UNxBtXK/VYR7ispk5TN9s=", - "requires": { - "core-js": "1.2.7", - "isomorphic-fetch": "2.2.1", - "loose-envify": "1.3.1", - "object-assign": "4.1.1", - "promise": "7.3.1", - "setimmediate": "1.0.5", - "ua-parser-js": "0.7.14" - } + "integrity": "sha1-XmdDL1UNxBtXK/VYR7ispk5TN9s=" }, "isomorphic-fetch": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz", - "integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=", - "requires": { - "node-fetch": "1.7.1", - "whatwg-fetch": "2.0.3" - } + "integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=" }, "loose-envify": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz", - "integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=", - "requires": { - "js-tokens": "3.0.2" - } + "integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=" }, "object-assign": { "version": "4.1.1", @@ -5362,38 +3444,22 @@ "qs": { "version": "6.5.1", "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", - "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" + "integrity": "sha1-NJzfbu+J7EXBLX1es/wMhwNDptg=" }, "react": { "version": "15.6.2", "resolved": "https://registry.npmjs.org/react/-/react-15.6.2.tgz", - "integrity": "sha1-26BDSrQ5z+gvEI8PURZjkIF5qnI=", - "requires": { - "create-react-class": "15.6.2", - "fbjs": "0.8.14", - "loose-envify": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz", - "object-assign": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "prop-types": "15.6.1" - } + "integrity": "sha1-26BDSrQ5z+gvEI8PURZjkIF5qnI=" }, "react-dom": { "version": "15.6.2", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-15.6.2.tgz", - "integrity": "sha1-Qc+t9pO3V/rycIRDodH9WgK+9zA=", - "requires": { - "fbjs": "0.8.14", - "loose-envify": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz", - "object-assign": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "prop-types": "15.6.1" - } + "integrity": "sha1-Qc+t9pO3V/rycIRDodH9WgK+9zA=" }, "react-input-autosize": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/react-input-autosize/-/react-input-autosize-2.2.1.tgz", "integrity": "sha512-3+K4CD13iE4lQQ2WlF8PuV5htfmTRLH6MDnfndHM6LuBRszuXnuyIfE7nhSKt8AzRBZ50bu0sAhkNMeS5pxQQA==", - "requires": { - "prop-types": "15.6.1" - }, "dependencies": { "core-js": { "version": "1.2.7", @@ -5403,49 +3469,24 @@ "fbjs": { "version": "0.8.16", "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.16.tgz", - "integrity": "sha1-XmdDL1UNxBtXK/VYR7ispk5TN9s=", - "requires": { - "core-js": "1.2.7", - "isomorphic-fetch": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz", - "loose-envify": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz", - "object-assign": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "promise": "7.3.1", - "setimmediate": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "ua-parser-js": "0.7.14" - } + "integrity": "sha1-XmdDL1UNxBtXK/VYR7ispk5TN9s=" }, "prop-types": { "version": "15.6.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.1.tgz", - "integrity": "sha512-4ec7bY1Y66LymSUOH/zARVYObB23AT2h8cf6e/O6ZALB/N0sqZFEx7rq6EYPX2MkOdKORuooI/H5k9TlR4q7kQ==", - "requires": { - "fbjs": "0.8.16", - "loose-envify": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz", - "object-assign": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz" - } + "integrity": "sha512-4ec7bY1Y66LymSUOH/zARVYObB23AT2h8cf6e/O6ZALB/N0sqZFEx7rq6EYPX2MkOdKORuooI/H5k9TlR4q7kQ==" } } }, "react-relay": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/react-relay/-/react-relay-1.4.1.tgz", - "integrity": "sha1-Yfe1gC1wRG4VRJD50PzuyRUOvaU=", - "requires": { - "babel-runtime": "6.25.0", - "fbjs": "0.8.14", - "prop-types": "15.6.1", - "relay-runtime": "1.4.1" - } + "integrity": "sha1-Yfe1gC1wRG4VRJD50PzuyRUOvaU=" }, "react-select": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/react-select/-/react-select-1.2.1.tgz", "integrity": "sha512-vaCgT2bEl+uTyE/uKOEgzE5Dc/wLtzhnBvoHCeuLoJWc4WuadN6WQDhoL42DW+TziniZK2Gaqe/wUXydI3NSaQ==", - "requires": { - "classnames": "2.2.5", - "prop-types": "15.6.1", - "react-input-autosize": "2.2.1" - }, "dependencies": { "core-js": { "version": "1.2.7", @@ -5455,99 +3496,56 @@ "fbjs": { "version": "0.8.16", "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.16.tgz", - "integrity": "sha1-XmdDL1UNxBtXK/VYR7ispk5TN9s=", - "requires": { - "core-js": "1.2.7", - "isomorphic-fetch": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz", - "loose-envify": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz", - "object-assign": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "promise": "7.3.1", - "setimmediate": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "ua-parser-js": "0.7.14" - } + "integrity": "sha1-XmdDL1UNxBtXK/VYR7ispk5TN9s=" }, "prop-types": { "version": "15.6.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.1.tgz", - "integrity": "sha512-4ec7bY1Y66LymSUOH/zARVYObB23AT2h8cf6e/O6ZALB/N0sqZFEx7rq6EYPX2MkOdKORuooI/H5k9TlR4q7kQ==", - "requires": { - "fbjs": "0.8.16", - "loose-envify": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz", - "object-assign": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz" - } + "integrity": "sha512-4ec7bY1Y66LymSUOH/zARVYObB23AT2h8cf6e/O6ZALB/N0sqZFEx7rq6EYPX2MkOdKORuooI/H5k9TlR4q7kQ==" } } }, - "react-static-container": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/react-static-container/-/react-static-container-1.0.1.tgz", - "integrity": "sha1-aUwN1oqJa4eVGa+1SDmcwZicmrA=" - }, "react-test-renderer": { "version": "15.6.2", "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-15.6.2.tgz", "integrity": "sha1-0DM0NPwsQ4CSaWyncNpe1IA376g=", - "dev": true, - "requires": { - "fbjs": "0.8.14", - "object-assign": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz" - } + "dev": true }, "read-pkg": { "version": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", - "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", - "requires": { - "load-json-file": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", - "normalize-package-data": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", - "path-type": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz" - } + "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=" }, "read-pkg-up": { "version": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", - "requires": { - "find-up": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "read-pkg": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz" - }, "dependencies": { "find-up": { "version": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==" + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=" } } }, "readable-stream": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", - "integrity": "sha1-No8lEtefnUb9/HE0mueHi7weuVw=", - "dev": true, - "requires": { - "core-util-is": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "inherits": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "isarray": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "process-nextick-args": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", - "safe-buffer": "5.1.1", - "string_decoder": "1.0.3", - "util-deprecate": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" - } + "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", + "dev": true }, "readdir-enhanced": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/readdir-enhanced/-/readdir-enhanced-1.5.2.tgz", - "integrity": "sha1-YUYwSGkKxqRVt1ti+nioj43IXlM=", - "requires": { - "call-me-maybe": "1.0.1", - "es6-promise": "4.2.4", - "glob-to-regexp": "0.3.0" - } + "integrity": "sha1-YUYwSGkKxqRVt1ti+nioj43IXlM=" + }, + "readline2": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/readline2/-/readline2-1.0.1.tgz", + "integrity": "sha1-QQWWCP/BVHV7cV2ZidGZ/783LjU=", + "dev": true }, "rechoir": { "version": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", - "dev": true, - "requires": { - "resolve": "1.4.0" - } + "dev": true }, "regenerator-runtime": { "version": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz", @@ -5557,27 +3555,16 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", "integrity": "sha1-H07OJ+ALC2XgJHpoEOaoXYOldSw=", - "requires": { - "extend-shallow": "3.0.2", - "safe-regex": "1.1.0" - }, "dependencies": { "extend-shallow": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "requires": { - "assign-symbols": "1.0.0", - "is-extendable": "1.0.1" - } + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=" }, "is-extendable": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha1-p0cPnkJnM9gb2B4RVSZOOjUHyrQ=", - "requires": { - "is-plain-object": "2.0.4" - } + "integrity": "sha1-p0cPnkJnM9gb2B4RVSZOOjUHyrQ=" } } }, @@ -5585,98 +3572,48 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/relay-compiler/-/relay-compiler-1.4.1.tgz", "integrity": "sha1-EOg/D13o2z0ACFGkwOQ1590d65U=", - "requires": { - "babel-generator": "6.26.0", - "babel-polyfill": "6.26.0", - "babel-preset-fbjs": "2.1.4", - "babel-runtime": "6.25.0", - "babel-traverse": "6.26.0", - "babel-types": "6.25.0", - "babylon": "6.18.0", - "chalk": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "fast-glob": "1.0.1", - "fb-watchman": "2.0.0", - "fbjs": "0.8.14", - "graphql": "0.11.7", - "immutable": "3.7.6", - "relay-runtime": "1.4.1", - "signedsource": "1.0.0", - "yargs": "9.0.1" - }, "dependencies": { "babel-code-frame": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", - "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", - "requires": { - "chalk": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "esutils": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "js-tokens": "3.0.2" - } + "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=" }, "babel-traverse": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=", - "requires": { - "babel-code-frame": "6.26.0", - "babel-messages": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", - "babel-runtime": "6.26.0", - "babel-types": "6.26.0", - "babylon": "6.18.0", - "debug": "2.6.8", - "globals": "9.18.0", - "invariant": "2.2.3", - "lodash": "4.17.5" - }, "dependencies": { "babel-runtime": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", - "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", - "requires": { - "core-js": "2.5.0", - "regenerator-runtime": "0.11.1" - } + "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=" }, "babel-types": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", - "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", - "requires": { - "babel-runtime": "6.26.0", - "esutils": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "lodash": "4.17.5", - "to-fast-properties": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz" - } + "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=" } } }, "babylon": { "version": "6.18.0", "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", - "integrity": "sha1-ry87iPpvXB5MY00aD46sT1WzleM=" + "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==" }, "graphql": { "version": "0.11.7", "resolved": "https://registry.npmjs.org/graphql/-/graphql-0.11.7.tgz", - "integrity": "sha1-5auqnLe3zMuE6fCDa/Q3DSaHUMY=", - "requires": { - "iterall": "1.1.3" - } + "integrity": "sha1-5auqnLe3zMuE6fCDa/Q3DSaHUMY=" }, "invariant": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.3.tgz", - "integrity": "sha512-7Z5PPegwDTyjbaeCnV0efcyS6vdKAU51kpEmS7QFib3P4822l8ICYyMn7qvJnc+WzLoDsuI9gPMKbJ8pCu8XtA==", - "requires": { - "loose-envify": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz" - } + "integrity": "sha512-7Z5PPegwDTyjbaeCnV0efcyS6vdKAU51kpEmS7QFib3P4822l8ICYyMn7qvJnc+WzLoDsuI9gPMKbJ8pCu8XtA==" }, "regenerator-runtime": { "version": "0.11.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", - "integrity": "sha1-vgWtf5v30i4Fb5cmzuUBf78Z4uk=" + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" } } }, @@ -5688,12 +3625,7 @@ "relay-runtime": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/relay-runtime/-/relay-runtime-1.4.1.tgz", - "integrity": "sha1-+I3NCkInAKBFY/KR9XDkzjaONtA=", - "requires": { - "babel-runtime": "6.25.0", - "fbjs": "0.8.14", - "relay-debugger-react-native-runtime": "0.0.10" - } + "integrity": "sha1-+I3NCkInAKBFY/KR9XDkzjaONtA=" }, "repeat-element": { "version": "1.1.2", @@ -5707,39 +3639,12 @@ }, "repeating": { "version": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", - "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", - "requires": { - "is-finite": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz" - } + "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=" }, "request": { "version": "2.85.0", "resolved": "https://registry.npmjs.org/request/-/request-2.85.0.tgz", - "integrity": "sha512-8H7Ehijd4js+s6wuVPLjwORxD4zeuyjYugprdOXlPSqaApmL/QOy+EB/beICHVCHkGMKNh5rvihb5ov+IDw4mg==", - "requires": { - "aws-sign2": "0.7.0", - "aws4": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", - "caseless": "0.12.0", - "combined-stream": "1.0.6", - "extend": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", - "forever-agent": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "form-data": "2.3.2", - "har-validator": "5.0.3", - "hawk": "6.0.2", - "http-signature": "1.2.0", - "is-typedarray": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "isstream": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "json-stringify-safe": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "mime-types": "2.1.16", - "oauth-sign": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", - "performance-now": "2.1.0", - "qs": "6.5.1", - "safe-buffer": "5.1.1", - "stringstream": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", - "tough-cookie": "2.3.4", - "tunnel-agent": "0.6.0", - "uuid": "3.1.0" - } + "integrity": "sha1-WgNhWkfGFCCz65m326IE+DYD4fo=" }, "require-directory": { "version": "2.1.1", @@ -5751,20 +3656,35 @@ "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=" }, + "require-uncached": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz", + "integrity": "sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM=", + "dev": true + }, "resolve": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.4.0.tgz", - "integrity": "sha1-p1vgHFPaJdk0qY69DkxKcxL5KoY=", - "dev": true, - "requires": { - "path-parse": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz" - } + "integrity": "sha512-aW7sVKPufyHqOmyyLzg/J+8606v5nevBgaliIlV7nUpVMsDnoBGV/cbSLNjZAg9q0Cfd/+easKVKQ8vOu8fn1Q==", + "dev": true + }, + "resolve-from": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz", + "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=", + "dev": true }, "resolve-url": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=" }, + "restore-cursor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz", + "integrity": "sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=", + "dev": true + }, "ret": { "version": "0.1.15", "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", @@ -5772,7 +3692,18 @@ }, "rimraf": { "version": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.1.tgz", - "integrity": "sha1-wjOOxkPfeht/5cVPqG9XQopV8z0=", + "integrity": "sha1-wjOOxkPfeht/5cVPqG9XQopV8z0=" + }, + "run-async": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-0.1.0.tgz", + "integrity": "sha1-yK1KXhEGYeQCp9IbUw4AnyX444k=", + "dev": true + }, + "rx-lite": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-3.1.2.tgz", + "integrity": "sha1-Gc5QLKVyZl87ZHsQk5+X/RYV8QI=", "dev": true }, "safe-buffer": { @@ -5783,10 +3714,7 @@ "safe-regex": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", - "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", - "requires": { - "ret": "0.1.15" - } + "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=" }, "samsam": { "version": "https://registry.npmjs.org/samsam/-/samsam-1.2.1.tgz", @@ -5796,7 +3724,7 @@ "semver": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", - "integrity": "sha1-4FnAnYVx8FQII3M0M1BdOi8AsY4=" + "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==" }, "set-blocking": { "version": "2.0.0", @@ -5806,21 +3734,12 @@ "set-getter": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/set-getter/-/set-getter-0.1.0.tgz", - "integrity": "sha1-12nBgsnVpR9AkUXy+6guXoboA3Y=", - "requires": { - "to-object-path": "0.3.0" - } + "integrity": "sha1-12nBgsnVpR9AkUXy+6guXoboA3Y=" }, "set-value": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz", - "integrity": "sha1-ca5KiPD+77v1LR6mBPP7MV67YnQ=", - "requires": { - "extend-shallow": "2.0.1", - "is-extendable": "0.1.1", - "is-plain-object": "2.0.4", - "split-string": "3.1.0" - } + "integrity": "sha1-ca5KiPD+77v1LR6mBPP7MV67YnQ=" }, "setimmediate": { "version": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", @@ -5829,10 +3748,7 @@ "shebang-command": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "requires": { - "shebang-regex": "1.0.0" - } + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=" }, "shebang-regex": { "version": "1.0.0", @@ -5843,12 +3759,7 @@ "version": "0.7.8", "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.7.8.tgz", "integrity": "sha1-3svPh0sNHl+3LhSxZKloMEjprLM=", - "dev": true, - "requires": { - "glob": "7.1.2", - "interpret": "https://registry.npmjs.org/interpret/-/interpret-1.0.3.tgz", - "rechoir": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz" - } + "dev": true }, "sigmund": { "version": "1.0.1", @@ -5877,16 +3788,6 @@ "resolved": "https://registry.npmjs.org/sinon/-/sinon-2.3.6.tgz", "integrity": "sha1-lTeOfg+XapcS6bRZH/WznnPcPd4=", "dev": true, - "requires": { - "diff": "3.2.0", - "formatio": "https://registry.npmjs.org/formatio/-/formatio-1.2.0.tgz", - "lolex": "https://registry.npmjs.org/lolex/-/lolex-1.6.0.tgz", - "native-promise-only": "https://registry.npmjs.org/native-promise-only/-/native-promise-only-0.8.1.tgz", - "path-to-regexp": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", - "samsam": "https://registry.npmjs.org/samsam/-/samsam-1.2.1.tgz", - "text-encoding": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.6.4.tgz", - "type-detect": "4.0.3" - }, "dependencies": { "type-detect": { "version": "4.0.3", @@ -5901,44 +3802,31 @@ "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=" }, + "slice-ansi": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz", + "integrity": "sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=", + "dev": true + }, "snapdragon": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.1.tgz", "integrity": "sha1-4StUh/re0+PeoKyR6UAL91tAE3A=", - "requires": { - "base": "0.11.2", - "debug": "2.6.8", - "define-property": "0.2.5", - "extend-shallow": "2.0.1", - "map-cache": "0.2.2", - "source-map": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", - "source-map-resolve": "0.5.1", - "use": "2.0.2" - }, "dependencies": { "define-property": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "0.1.6" - } + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=" }, "is-accessor-descriptor": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "requires": { - "kind-of": "3.2.2" - }, "dependencies": { "kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "1.1.6" - } + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=" } } }, @@ -5946,29 +3834,18 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "requires": { - "kind-of": "3.2.2" - }, "dependencies": { "kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "1.1.6" - } + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=" } } }, "is-descriptor": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha1-Nm2CQN3kh8pRgjsaufB6EKeCUco=", - "requires": { - "is-accessor-descriptor": "0.1.6", - "is-data-descriptor": "0.1.4", - "kind-of": "5.1.0" - } + "integrity": "sha1-Nm2CQN3kh8pRgjsaufB6EKeCUco=" }, "kind-of": { "version": "5.1.0", @@ -5981,19 +3858,11 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", "integrity": "sha1-bBdfhv8UvbByRWPo88GwIaKGhTs=", - "requires": { - "define-property": "1.0.0", - "isobject": "3.0.1", - "snapdragon-util": "3.0.1" - }, "dependencies": { "define-property": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "requires": { - "is-descriptor": "1.0.2" - } + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=" } } }, @@ -6001,27 +3870,18 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", "integrity": "sha1-+VZHlIbyrNeXAGk/b3uAXkWrVuI=", - "requires": { - "kind-of": "3.2.2" - }, "dependencies": { "kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "1.1.6" - } + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=" } } }, "sntp": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/sntp/-/sntp-2.1.0.tgz", - "integrity": "sha512-FL1b58BDrqS3A11lJ0zEdnJ3UOKqVxawAkF3k7F0CVN7VQ34aZrV+G8BZ1WC9ZL7NyrwsW0oviwsWDgRuVYtJg==", - "requires": { - "hoek": "4.2.1" - } + "integrity": "sha1-LGzsFP7cIiJznK+bXD2F0cxaLMg=" }, "source-map": { "version": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", @@ -6030,22 +3890,12 @@ "source-map-resolve": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.1.tgz", - "integrity": "sha1-etD1k/IoFZjoVN+A8ZquS5LXoRo=", - "requires": { - "atob": "2.0.3", - "decode-uri-component": "0.2.0", - "resolve-url": "0.2.1", - "source-map-url": "0.4.0", - "urix": "0.1.0" - } + "integrity": "sha1-etD1k/IoFZjoVN+A8ZquS5LXoRo=" }, "source-map-support": { "version": "0.4.18", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", - "integrity": "sha1-Aoam3ovkJkEzhZTpfM6nXwosWF8=", - "requires": { - "source-map": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz" - } + "integrity": "sha1-Aoam3ovkJkEzhZTpfM6nXwosWF8=" }, "source-map-url": { "version": "0.4.0", @@ -6070,57 +3920,35 @@ "split": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", - "integrity": "sha1-YFvZvjA6pZ+zX5Ip++oN3snqB9k=", - "requires": { - "through": "https://registry.npmjs.org/through/-/through-2.3.8.tgz" - } + "integrity": "sha1-YFvZvjA6pZ+zX5Ip++oN3snqB9k=" }, "split-string": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", "integrity": "sha1-fLCd2jqGWFcFxks5pkZgOGguj+I=", - "requires": { - "extend-shallow": "3.0.2" - }, "dependencies": { "extend-shallow": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "requires": { - "assign-symbols": "1.0.0", - "is-extendable": "1.0.1" - } + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=" }, "is-extendable": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha1-p0cPnkJnM9gb2B4RVSZOOjUHyrQ=", - "requires": { - "is-plain-object": "2.0.4" - } + "integrity": "sha1-p0cPnkJnM9gb2B4RVSZOOjUHyrQ=" } } }, "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true }, "sshpk": { "version": "1.13.1", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz", "integrity": "sha1-US322mKHFEMW3EwY/hzx2UBzm+M=", - "requires": { - "asn1": "0.2.3", - "assert-plus": "1.0.0", - "bcrypt-pbkdf": "1.0.1", - "dashdash": "1.14.1", - "ecc-jsbn": "0.1.1", - "getpass": "0.1.7", - "jsbn": "0.1.1", - "tweetnacl": "0.14.5" - }, "dependencies": { "assert-plus": { "version": "1.0.0", @@ -6133,34 +3961,21 @@ "version": "0.1.2", "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", - "requires": { - "define-property": "0.2.5", - "object-copy": "0.1.0" - }, "dependencies": { "define-property": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "0.1.6" - } + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=" }, "is-accessor-descriptor": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "requires": { - "kind-of": "3.2.2" - }, "dependencies": { "kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "1.1.6" - } + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=" } } }, @@ -6168,29 +3983,18 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "requires": { - "kind-of": "3.2.2" - }, "dependencies": { "kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "1.1.6" - } + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=" } } }, "is-descriptor": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha1-Nm2CQN3kh8pRgjsaufB6EKeCUco=", - "requires": { - "is-accessor-descriptor": "0.1.6", - "is-data-descriptor": "0.1.4", - "kind-of": "5.1.0" - } + "integrity": "sha1-Nm2CQN3kh8pRgjsaufB6EKeCUco=" }, "kind-of": { "version": "5.1.0", @@ -6199,6 +4003,12 @@ } } }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha1-D8Z9fBQYJd6UKC3VNr7GubzoYKs=", + "dev": true + }, "string-template": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/string-template/-/string-template-0.2.1.tgz", @@ -6209,10 +4019,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "requires": { - "is-fullwidth-code-point": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "strip-ansi": "4.0.0" - }, "dependencies": { "ansi-regex": { "version": "3.0.0", @@ -6226,41 +4032,22 @@ "string-width": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha1-q5Pyeo3BPSjKyBXEYhQ6bZASrp4=", - "requires": { - "is-fullwidth-code-point": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "strip-ansi": "4.0.0" - } + "integrity": "sha1-q5Pyeo3BPSjKyBXEYhQ6bZASrp4=" }, "strip-ansi": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "requires": { - "ansi-regex": "3.0.0" - } + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=" } } }, - "string_decoder": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", - "integrity": "sha1-D8Z9fBQYJd6UKC3VNr7GubzoYKs=", - "dev": true, - "requires": { - "safe-buffer": "5.1.1" - } - }, "stringstream": { "version": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=" }, "strip-ansi": { "version": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "requires": { - "ansi-regex": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz" - } + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=" }, "strip-bom": { "version": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -6271,31 +4058,39 @@ "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true + }, "supports-color": { "version": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" }, + "table": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/table/-/table-3.8.3.tgz", + "integrity": "sha1-K7xULw/amGGnVdOUf+/Ys/UThV8=", + "dev": true, + "dependencies": { + "ajv": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", + "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=", + "dev": true + } + } + }, "tar": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.0.tgz", - "integrity": "sha512-gJlTiiErwo96K904FnoYWl+5+FBgS+FimU6GMh66XLdLa55al8+d4jeDfPoGwSNHdtWI5FJP6xurmVqhBuGJpQ==", - "requires": { - "chownr": "1.0.1", - "fs-minipass": "1.2.5", - "minipass": "2.2.1", - "minizlib": "1.1.0", - "mkdirp": "0.5.1", - "yallist": "3.0.2" - } + "integrity": "sha512-gJlTiiErwo96K904FnoYWl+5+FBgS+FimU6GMh66XLdLa55al8+d4jeDfPoGwSNHdtWI5FJP6xurmVqhBuGJpQ==" }, "temp": { "version": "0.8.3", "resolved": "https://registry.npmjs.org/temp/-/temp-0.8.3.tgz", "integrity": "sha1-4Ma8TSa5AxJEEOT+2BEDAU38H1k=", - "requires": { - "os-tmpdir": "1.0.2", - "rimraf": "2.2.8" - }, "dependencies": { "os-tmpdir": { "version": "1.0.2", @@ -6320,6 +4115,12 @@ "integrity": "sha1-45mpgiV6J22uQou5KEXLcb3CbRk=", "dev": true }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, "through": { "version": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" @@ -6333,10 +4134,7 @@ "version": "0.0.31", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.31.tgz", "integrity": "sha1-jzirlDjhcxXl29izZX6L+yd65Kc=", - "dev": true, - "requires": { - "os-tmpdir": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz" - } + "dev": true }, "to-fast-properties": { "version": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", @@ -6352,17 +4150,11 @@ "version": "0.3.0", "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", - "requires": { - "kind-of": "3.2.2" - }, "dependencies": { "kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "1.1.6" - } + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=" } } }, @@ -6370,48 +4162,28 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", "integrity": "sha1-E8/dmzNlUvMLUfM6iuG0Knp1mc4=", - "requires": { - "define-property": "2.0.2", - "extend-shallow": "3.0.2", - "regex-not": "1.0.2", - "safe-regex": "1.1.0" - }, "dependencies": { "extend-shallow": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "requires": { - "assign-symbols": "1.0.0", - "is-extendable": "1.0.1" - } + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=" }, "is-extendable": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha1-p0cPnkJnM9gb2B4RVSZOOjUHyrQ=", - "requires": { - "is-plain-object": "2.0.4" - } + "integrity": "sha1-p0cPnkJnM9gb2B4RVSZOOjUHyrQ=" } } }, "to-regex-range": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", - "requires": { - "is-number": "3.0.0", - "repeat-string": "1.6.1" - } + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=" }, "tough-cookie": { "version": "2.3.4", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", - "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==", - "requires": { - "punycode": "1.4.1" - } + "integrity": "sha1-7GDO44rGdQY//JelwYlwV47oNlU=" }, "tree-kill": { "version": "1.2.0", @@ -6425,10 +4197,7 @@ "tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "requires": { - "safe-buffer": "5.1.1" - } + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=" }, "tweetnacl": { "version": "0.14.5", @@ -6436,6 +4205,18 @@ "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", "optional": true }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", + "dev": true + }, "ua-parser-js": { "version": "0.7.14", "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.14.tgz", @@ -6445,23 +4226,11 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=", - "requires": { - "arr-union": "3.1.0", - "get-value": "2.0.6", - "is-extendable": "0.1.1", - "set-value": "0.4.3" - }, "dependencies": { "set-value": { "version": "0.4.3", "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz", - "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=", - "requires": { - "extend-shallow": "2.0.1", - "is-extendable": "0.1.1", - "is-plain-object": "2.0.4", - "to-object-path": "0.3.0" - } + "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=" } } }, @@ -6474,28 +4243,16 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", - "requires": { - "has-value": "0.3.1", - "isobject": "3.0.1" - }, "dependencies": { "has-value": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", - "requires": { - "get-value": "2.0.6", - "has-values": "0.1.4", - "isobject": "2.1.0" - }, "dependencies": { "isobject": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "requires": { - "isarray": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz" - } + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=" } } }, @@ -6515,35 +4272,21 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/use/-/use-2.0.2.tgz", "integrity": "sha1-riig1y+TvyJCKhii43mZMRLeyOg=", - "requires": { - "define-property": "0.2.5", - "isobject": "3.0.1", - "lazy-cache": "2.0.2" - }, "dependencies": { "define-property": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "0.1.6" - } + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=" }, "is-accessor-descriptor": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "requires": { - "kind-of": "3.2.2" - }, "dependencies": { "kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "1.1.6" - } + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=" } } }, @@ -6551,29 +4294,18 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "requires": { - "kind-of": "3.2.2" - }, "dependencies": { "kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "1.1.6" - } + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=" } } }, "is-descriptor": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha1-Nm2CQN3kh8pRgjsaufB6EKeCUco=", - "requires": { - "is-accessor-descriptor": "0.1.6", - "is-data-descriptor": "0.1.4", - "kind-of": "5.1.0" - } + "integrity": "sha1-Nm2CQN3kh8pRgjsaufB6EKeCUco=" }, "kind-of": { "version": "5.1.0", @@ -6582,6 +4314,12 @@ } } }, + "user-home": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/user-home/-/user-home-2.0.0.tgz", + "integrity": "sha1-nHC/2Babwdy/SGBODwS4tJzenp8=", + "dev": true + }, "util-deprecate": { "version": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", @@ -6590,7 +4328,7 @@ "uuid": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", - "integrity": "sha1-PdPT55Crwk17DToDT/q6vijrvAQ=" + "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==" }, "validate-npm-package-license": { "version": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz", @@ -6600,11 +4338,6 @@ "version": "1.10.0", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "requires": { - "assert-plus": "1.0.0", - "core-util-is": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "extsprintf": "1.3.0" - }, "dependencies": { "assert-plus": { "version": "1.0.0", @@ -6617,17 +4350,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/virtual-dom/-/virtual-dom-2.1.1.tgz", "integrity": "sha1-gO2i1IG57eDASRGM78tKBfIdE3U=", - "dev": true, - "requires": { - "browser-split": "0.0.1", - "error": "4.4.0", - "ev-store": "7.0.0", - "global": "4.3.2", - "is-object": "1.0.1", - "next-tick": "0.2.2", - "x-is-array": "0.1.0", - "x-is-string": "0.1.0" - } + "dev": true }, "what-the-diff": { "version": "0.4.0", @@ -6637,10 +4360,7 @@ "what-the-status": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/what-the-status/-/what-the-status-1.0.3.tgz", - "integrity": "sha1-lP3NAR/7U6Ijnnb6+NrL78mHdRA=", - "requires": { - "split": "1.0.1" - } + "integrity": "sha1-lP3NAR/7U6Ijnnb6+NrL78mHdRA=" }, "whatwg-fetch": { "version": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz", @@ -6649,10 +4369,7 @@ "which": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/which/-/which-1.3.0.tgz", - "integrity": "sha1-/wS9/AEO5UfXgL7DjhrBwnd9JTo=", - "requires": { - "isexe": "2.0.0" - } + "integrity": "sha1-/wS9/AEO5UfXgL7DjhrBwnd9JTo=" }, "which-module": { "version": "2.0.0", @@ -6668,24 +4385,22 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", - "requires": { - "string-width": "1.0.2", - "strip-ansi": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz" - }, "dependencies": { "string-width": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "requires": { - "strip-ansi": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz" - } + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=" } } }, "wrappy": { "version": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "write": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/write/-/write-0.2.1.tgz", + "integrity": "sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c=", "dev": true }, "x-is-array": { @@ -6725,30 +4440,12 @@ "yargs": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/yargs/-/yargs-9.0.1.tgz", - "integrity": "sha1-UqzCP+7Kw0BCB47njAwAf1CF20w=", - "requires": { - "camelcase": "4.1.0", - "cliui": "3.2.0", - "decamelize": "1.2.0", - "get-caller-file": "1.0.2", - "os-locale": "2.1.0", - "read-pkg-up": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", - "require-directory": "2.1.1", - "require-main-filename": "1.0.1", - "set-blocking": "2.0.0", - "string-width": "2.1.1", - "which-module": "2.0.0", - "y18n": "3.2.1", - "yargs-parser": "7.0.0" - } + "integrity": "sha1-UqzCP+7Kw0BCB47njAwAf1CF20w=" }, "yargs-parser": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-7.0.0.tgz", - "integrity": "sha1-jQrELxbqVd69MyyvTEA4s+P139k=", - "requires": { - "camelcase": "4.1.0" - } + "integrity": "sha1-jQrELxbqVd69MyyvTEA4s+P139k=" }, "yubikiri": { "version": "1.0.0", diff --git a/package.json b/package.json index cd9deac118..cf7857a8af 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,7 @@ "dedent-js": "^1.0.1", "electron-devtools-installer": "^2.2.3", "enzyme": "^2.9.1", - "eslint": "^3.0.0", + "eslint": "^3.19.0", "eslint-config-fbjs": "2.0.0-alpha.1", "eslint-config-standard": "^10.2.0", "eslint-plugin-babel": "^4.0.0", @@ -180,5 +180,6 @@ "GitDockItem": "createDockItemStub", "GithubDockItem": "createDockItemStub", "FilePatchControllerStub": "createFilePatchControllerStub" - } + }, + "false": {} } diff --git a/test/git-strategies.test.js b/test/git-strategies.test.js index 428f0f4c77..d1236dbe96 100644 --- a/test/git-strategies.test.js +++ b/test/git-strategies.test.js @@ -269,7 +269,7 @@ import {normalizeGitHelperPath, getTempDir} from '../lib/helpers'; const git = createTestStrategy(workingDirPath); const authors = await git.getAuthors({max: 1}); - assert.deepEqual(authors, []) + assert.deepEqual(authors, []); }); }); diff --git a/test/models/user-store.test.js b/test/models/user-store.test.js index 4d8be260ff..73159f7ad9 100644 --- a/test/models/user-store.test.js +++ b/test/models/user-store.test.js @@ -1,4 +1,4 @@ -import dedent from 'dedent-js' +import dedent from 'dedent-js'; import UserStore from '../../lib/models/user-store'; @@ -67,7 +67,7 @@ describe('UserStore', function() { }, ]); - sinon.spy(store, 'addUsers') + sinon.spy(store, 'addUsers'); // Head changes due to new commit await repository.commit(dedent` @@ -76,13 +76,13 @@ describe('UserStore', function() { Co-authored-by: New Author `, {allowEmpty: true}); - await assert.async.equal(store.addUsers.callCount, 1) + await assert.async.equal(store.addUsers.callCount, 1); assert.isOk(store.getUsers().find(user => { return user.name === 'New Author' && user.email === 'new-author@email.com'; })); // Change head due to branch checkout - await repository.checkout('new-branch') - await assert.async.equal(store.addUsers.callCount, 2) + await repository.checkout('new-branch'); + await assert.async.equal(store.addUsers.callCount, 2); }); }); From b9161a6487294d6af49ab12ce7b2a37dd5f27e72 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Wed, 21 Mar 2018 23:08:42 -0700 Subject: [PATCH 0550/5882] Iterate less --- lib/git-shell-out-strategy.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/git-shell-out-strategy.js b/lib/git-shell-out-strategy.js index 9c6770c1a6..0fd02553a1 100644 --- a/lib/git-shell-out-strategy.js +++ b/lib/git-shell-out-strategy.js @@ -622,9 +622,10 @@ export default class GitShellOutStrategy { ]); return output.split('\0') - .filter(l => l.length > 0) - .map(line => line.split(delimiterString)) - .reduce((acc, [an, ae, cn, ce, trailers]) => { + .reduce((acc, line) => { + if (line.length === 0) { return acc; } + + const [an, ae, cn, ce, trailers] = line.split(delimiterString); trailers .split('\n') .map(trailer => trailer.match(/^co-authored-by. (.+?) <(.+?)>$/i)) From d51cbbe3f8e65534e0de0553791ef004ca4e2987 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Wed, 21 Mar 2018 23:24:34 -0700 Subject: [PATCH 0551/5882] Describe shape of user in PropTypes --- lib/controllers/commit-controller.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/controllers/commit-controller.js b/lib/controllers/commit-controller.js index 176d9a89c8..78254d1c41 100644 --- a/lib/controllers/commit-controller.js +++ b/lib/controllers/commit-controller.js @@ -31,7 +31,10 @@ export default class CommitController extends React.Component { stagedChangesExist: PropTypes.bool.isRequired, lastCommit: PropTypes.object.isRequired, currentBranch: PropTypes.object.isRequired, - mentionableUsers: PropTypes.arrayOf(PropTypes.object).isRequired, + mentionableUsers: PropTypes.arrayOf(PropTypes.shape({ + email: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + })).isRequired, prepareToCommit: PropTypes.func.isRequired, commit: PropTypes.func.isRequired, From 6bf127c7f1717bf7a00c002b1adddde480683cbe Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Thu, 22 Mar 2018 13:21:55 -0700 Subject: [PATCH 0552/5882] :fire: package.json entry that eslint added --- package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/package.json b/package.json index cf7857a8af..06c1a480c9 100644 --- a/package.json +++ b/package.json @@ -180,6 +180,5 @@ "GitDockItem": "createDockItemStub", "GithubDockItem": "createDockItemStub", "FilePatchControllerStub": "createFilePatchControllerStub" - }, - "false": {} + } } From f019b05da23560e4ed7f53c52e3cabc53767e5d3 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Thu, 22 Mar 2018 13:50:15 -0700 Subject: [PATCH 0553/5882] Remove `body` from keymap selector --- keymaps/git.cson | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keymaps/git.cson b/keymaps/git.cson index ef078bcc60..17c774719d 100644 --- a/keymaps/git.cson +++ b/keymaps/git.cson @@ -55,7 +55,7 @@ '.github-Dialog input': 'enter': 'core:confirm' -'body .github-CommitView-coAuthorEditor': +'.github-CommitView-coAuthorEditor': 'enter': 'github:co-author:enter' 'down': 'github:co-author:down' 'up': 'github:co-author:up' From 3c8d2fd3e9384ba4a81ab5dbca1bf4b451226b1a Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Thu, 22 Mar 2018 15:35:03 -0700 Subject: [PATCH 0554/5882] Fix flakey GitTabController test - await operation promises Co-authored-by: Tilde Ann Thurium --- test/controllers/git-tab-controller.test.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/test/controllers/git-tab-controller.test.js b/test/controllers/git-tab-controller.test.js index 7d5b0f1d6f..f700406c6c 100644 --- a/test/controllers/git-tab-controller.test.js +++ b/test/controllers/git-tab-controller.test.js @@ -517,7 +517,9 @@ describe('GitTabController', function() { // click Cancel confirm.returns(1); - stagingView.dblclickOnItem({}, conflict1); + let result = stagingView.dblclickOnItem({}, conflict1); + await result.stageOperationPromise; + await result.selectionUpdatePromise; await assert.async.isTrue(confirm.calledOnce); assert.lengthOf(stagingView.props.mergeConflicts, 5); @@ -526,7 +528,9 @@ describe('GitTabController', function() { // click Stage confirm.reset(); confirm.returns(0); - await stagingView.dblclickOnItem({}, conflict1).selectionUpdatePromise; + result = stagingView.dblclickOnItem({}, conflict1); + await result.stageOperationPromise; + await result.selectionUpdatePromise; await assert.async.isTrue(confirm.calledOnce); await assert.async.lengthOf(stagingView.props.mergeConflicts, 4); From 328573407db7a00c383c440a1b13016ea031af16 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 22 Mar 2018 22:08:20 -0400 Subject: [PATCH 0555/5882] Set GIT_SSH_VARIANT to prevent git from doing a "dry run". See https://git-scm.com/docs/git-config#git-config-sshvariant for details. --- test/git-strategies.test.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/git-strategies.test.js b/test/git-strategies.test.js index d1236dbe96..b5ac05dac6 100644 --- a/test/git-strategies.test.js +++ b/test/git-strategies.test.js @@ -1340,6 +1340,7 @@ import {normalizeGitHelperPath, getTempDir} from '../lib/helpers'; // Append ' #' to ensure the script is run with sh on Windows. // https://github.com/git/git/blob/027a3b943b444a3e3a76f9a89803fc10245b858f/run-command.c#L196-L221 process.env.GIT_SSH_COMMAND = normalizeGitHelperPath(path.join(__dirname, 'scripts', 'ssh-remote.sh')) + ' #'; + process.env.GIT_SSH_VARIANT = 'simple'; return git; } From 6be2b051ae48af6e16f56ef3106f4e33ed1d10c0 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 22 Mar 2018 22:08:41 -0400 Subject: [PATCH 0556/5882] "git fetch" _does_ reject now. But only sometimes! --- test/git-strategies.test.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/git-strategies.test.js b/test/git-strategies.test.js index b5ac05dac6..5837a475df 100644 --- a/test/git-strategies.test.js +++ b/test/git-strategies.test.js @@ -1384,8 +1384,7 @@ import {normalizeGitHelperPath, getTempDir} from '../lib/helpers'; }, }); - // The git operation Promise does *not* reject if the git process is killed by a signal. - await git.fetch('mock', 'master'); + await git.fetch('mock', 'master').catch(() => {}); assert.equal(query.prompt, 'Speak friend and enter'); assert.isFalse(query.includeUsername); From 5d5c8fabba208c9d3fb5ae846c8260551913e984 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Mon, 26 Mar 2018 14:54:52 -0700 Subject: [PATCH 0557/5882] Revert "Remove `body` from keymap selector" This reverts commit f019b05da23560e4ed7f53c52e3cabc53767e5d3. --- keymaps/git.cson | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keymaps/git.cson b/keymaps/git.cson index 17c774719d..ef078bcc60 100644 --- a/keymaps/git.cson +++ b/keymaps/git.cson @@ -55,7 +55,7 @@ '.github-Dialog input': 'enter': 'core:confirm' -'.github-CommitView-coAuthorEditor': +'body .github-CommitView-coAuthorEditor': 'enter': 'github:co-author:enter' 'down': 'github:co-author:down' 'up': 'github:co-author:up' From 2223e0a121d8a04ec3439e665eb0fb6ba7ec9b4a Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Thu, 22 Mar 2018 16:46:09 -0700 Subject: [PATCH 0558/5882] Add 'Undo' button on most recent commit and 'amend' context menu item Co-authored-by: Tilde Ann Thurium --- lib/views/recent-commits-view.js | 18 +++++++++++++++--- menus/git.cson | 6 ++++++ styles/recent-commits.less | 8 ++++++++ 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/lib/views/recent-commits-view.js b/lib/views/recent-commits-view.js index 6c5681496d..21caf93702 100644 --- a/lib/views/recent-commits-view.js +++ b/lib/views/recent-commits-view.js @@ -1,6 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import moment from 'moment'; +import cx from 'classnames'; import Timeago from './timeago'; @@ -13,13 +14,20 @@ class RecentCommitView extends React.Component { const authorMoment = moment(this.props.commit.getAuthorDate() * 1000); return ( -
  • +
  • {this.renderAuthors()} {this.props.commit.getMessage()} + {this.props.isMostRecent && ( + + )} - {this.props.commits.map(commit => { + {this.props.commits.map((commit, i) => { return ( - + ); })} diff --git a/menus/git.cson b/menus/git.cson index 63be16c747..a584f6a706 100644 --- a/menus/git.cson +++ b/menus/git.cson @@ -115,3 +115,9 @@ 'command': 'github:view-staged-changes-for-current-file' } ] + '.most-recent': [ + { + 'label': 'Amend' + 'command': 'github:amend-last-commit' + } + ] diff --git a/styles/recent-commits.less b/styles/recent-commits.less index 74bb7535f1..0d4b7d4017 100644 --- a/styles/recent-commits.less +++ b/styles/recent-commits.less @@ -74,6 +74,14 @@ overflow: hidden; } + &-undoButton.btn { + padding: 0 @component-padding/1.5; + margin-right: @component-padding/2; + height: @size + @component-padding/2; + line-height: 0; + font-size: .9em; + } + &-time { color: @text-color-subtle; } From f6613bb7c8fb0e523683c63bf87ff088f70e06cc Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 23 Mar 2018 15:40:35 -0700 Subject: [PATCH 0559/5882] Add GSOS#reset method which does a soft reset to the parent of head Co-Authored-By: Tilde Ann Thurium --- lib/git-shell-out-strategy.js | 21 ++++++++++++++++----- test/git-strategies.test.js | 24 ++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/lib/git-shell-out-strategy.js b/lib/git-shell-out-strategy.js index 09c8414dae..86713a045b 100644 --- a/lib/git-shell-out-strategy.js +++ b/lib/git-shell-out-strategy.js @@ -723,6 +723,17 @@ export default class GitShellOutStrategy { return this.exec(args, {useGitPromptServer: true, writeOperation: true}); } + /** + * Undo Operations + */ + reset() { + // for now we'll just start with what we need and build out more options in the future + return this.exec(['reset', '--soft', 'HEAD~']); + } + + softResetToParentOfHead() { + + } /** * Branches @@ -733,6 +744,11 @@ export default class GitShellOutStrategy { return this.exec(args.concat(branchName), {writeOperation: true}); } + async getBranches() { + const output = await this.exec(['for-each-ref', '--format=%(refname:short)', 'refs/heads/**']); + return output.trim().split(LINE_ENDING_REGEX); + } + checkoutFiles(paths, revision) { if (paths.length === 0) { return null; } const args = ['checkout']; @@ -740,11 +756,6 @@ export default class GitShellOutStrategy { return this.exec(args.concat('--', paths.map(toGitPathSep)), {writeOperation: true}); } - async getBranches() { - const output = await this.exec(['for-each-ref', '--format=%(refname:short)', 'refs/heads/**']); - return output.trim().split(LINE_ENDING_REGEX); - } - async describeHead() { return (await this.exec(['describe', '--contains', '--all', '--always', 'HEAD'])).trim(); } diff --git a/test/git-strategies.test.js b/test/git-strategies.test.js index 5837a475df..eecf5344f4 100644 --- a/test/git-strategies.test.js +++ b/test/git-strategies.test.js @@ -619,6 +619,30 @@ import {normalizeGitHelperPath, getTempDir} from '../lib/helpers'; }); }); + describe('reset()', function() { + it('performs a soft reset to the parent of head', async function() { + const workingDirPath = await cloneRepository('three-files'); + const git = createTestStrategy(workingDirPath); + + fs.appendFileSync(path.join(workingDirPath, 'a.txt'), 'bar\n', 'utf8'); + await git.exec(['add', '.']); + await git.commit('add stuff') + + const parentCommit = await git.getCommit('HEAD~') + + await git.reset(); + + const commitAfterReset = await git.getCommit('HEAD') + assert.strictEqual(commitAfterReset.sha, parentCommit.sha) + + const stagedChanges = await git.getDiffsForFilePath('a.txt', {staged: true}); + assert.lengthOf(stagedChanges, 1); + const stagedChange = stagedChanges[0] + assert.strictEqual(stagedChange.newPath, 'a.txt'); + assert.deepEqual(stagedChange.hunks[0].lines, [' foo', '+bar']); + }); + }); + describe('getBranches()', function() { it('returns an array of all branches', async function() { const workingDirPath = await cloneRepository('three-files'); From 4a426e7498cc087b283e750570b96d083839d77a Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 23 Mar 2018 16:10:42 -0700 Subject: [PATCH 0560/5882] Add repository method `undoLastCommit` --- lib/git-shell-out-strategy.js | 13 +++++----- lib/models/repository-states/present.js | 16 ++++++++++++ lib/models/repository-states/state.js | 7 +++++ lib/models/repository.js | 2 ++ test/git-strategies.test.js | 34 +++++++++++++------------ 5 files changed, 49 insertions(+), 23 deletions(-) diff --git a/lib/git-shell-out-strategy.js b/lib/git-shell-out-strategy.js index 86713a045b..8d2d817dbe 100644 --- a/lib/git-shell-out-strategy.js +++ b/lib/git-shell-out-strategy.js @@ -726,13 +726,12 @@ export default class GitShellOutStrategy { /** * Undo Operations */ - reset() { - // for now we'll just start with what we need and build out more options in the future - return this.exec(['reset', '--soft', 'HEAD~']); - } - - softResetToParentOfHead() { - + reset(type, revision = 'HEAD') { + const validTypes = ['soft'] + if (!validTypes.includes(type)) { + throw new Error(`Invalid type ${type}. Must be one of: ${validTypes.join(', ')}`) + } + return this.exec(['reset', `--${type}`, revision]); } /** diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js index d1f871d31c..f7af981cdc 100644 --- a/lib/models/repository-states/present.js +++ b/lib/models/repository-states/present.js @@ -341,6 +341,22 @@ export default class Present extends State { return this.git().checkoutFiles(paths, revision); } + // Reset + + @invalidate(() => [ + Keys.stagedChangesSinceParentCommit, + Keys.lastCommit, + Keys.recentCommits, + Keys.authors, + Keys.statusBundle, + Keys.index.all, + ...Keys.filePatch.eachWithOpts({staged: true, amending: true}), + Keys.headDescription, + ]) + undoLastCommit() { + return this.git().reset('soft', 'HEAD~'); + } + // Remote interactions @invalidate(branchName => [ diff --git a/lib/models/repository-states/state.js b/lib/models/repository-states/state.js index 196dc0790e..1e48a2697a 100644 --- a/lib/models/repository-states/state.js +++ b/lib/models/repository-states/state.js @@ -222,6 +222,13 @@ export default class State { return unsupportedOperationPromise(this, 'checkoutPathsAtRevision'); } + // Reset + + @shouldDelegate + undoLastCommit() { + return unsupportedOperationPromise(this, 'undoLastCommit'); + } + // Remote interactions @shouldDelegate diff --git a/lib/models/repository.js b/lib/models/repository.js index 0e3f0dcc9a..c9779a682e 100644 --- a/lib/models/repository.js +++ b/lib/models/repository.js @@ -267,6 +267,8 @@ const delegates = [ 'checkout', 'checkoutPathsAtRevision', + 'undoLastCommit', + 'fetch', 'pull', 'push', diff --git a/test/git-strategies.test.js b/test/git-strategies.test.js index eecf5344f4..bc4c3426c2 100644 --- a/test/git-strategies.test.js +++ b/test/git-strategies.test.js @@ -620,27 +620,29 @@ import {normalizeGitHelperPath, getTempDir} from '../lib/helpers'; }); describe('reset()', function() { - it('performs a soft reset to the parent of head', async function() { - const workingDirPath = await cloneRepository('three-files'); - const git = createTestStrategy(workingDirPath); + describe('when soft and HEAD~ are passed as arguments', function() { + it('performs a soft reset to the parent of head', async function() { + const workingDirPath = await cloneRepository('three-files'); + const git = createTestStrategy(workingDirPath); - fs.appendFileSync(path.join(workingDirPath, 'a.txt'), 'bar\n', 'utf8'); - await git.exec(['add', '.']); - await git.commit('add stuff') + fs.appendFileSync(path.join(workingDirPath, 'a.txt'), 'bar\n', 'utf8'); + await git.exec(['add', '.']); + await git.commit('add stuff') - const parentCommit = await git.getCommit('HEAD~') + const parentCommit = await git.getCommit('HEAD~') - await git.reset(); + await git.reset('soft', 'HEAD~'); - const commitAfterReset = await git.getCommit('HEAD') - assert.strictEqual(commitAfterReset.sha, parentCommit.sha) + const commitAfterReset = await git.getCommit('HEAD') + assert.strictEqual(commitAfterReset.sha, parentCommit.sha) - const stagedChanges = await git.getDiffsForFilePath('a.txt', {staged: true}); - assert.lengthOf(stagedChanges, 1); - const stagedChange = stagedChanges[0] - assert.strictEqual(stagedChange.newPath, 'a.txt'); - assert.deepEqual(stagedChange.hunks[0].lines, [' foo', '+bar']); - }); + const stagedChanges = await git.getDiffsForFilePath('a.txt', {staged: true}); + assert.lengthOf(stagedChanges, 1); + const stagedChange = stagedChanges[0] + assert.strictEqual(stagedChange.newPath, 'a.txt'); + assert.deepEqual(stagedChange.hunks[0].lines, [' foo', '+bar']); + }); + }) }); describe('getBranches()', function() { From 70b8554cdf4e2bb0fe9b5c9d4f91c108bafe7525 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 23 Mar 2018 17:29:28 -0700 Subject: [PATCH 0561/5882] Implement undo last commit functionality. Restore message in commit box Co-authored-by: Tilde Ann Thurium --- lib/controllers/git-tab-controller.js | 11 +++++++ lib/controllers/recent-commits-controller.js | 1 + lib/views/git-tab-view.js | 1 + lib/views/recent-commits-view.js | 3 ++ test/controllers/git-tab-controller.test.js | 32 ++++++++++++++++++++ 5 files changed, 48 insertions(+) diff --git a/lib/controllers/git-tab-controller.js b/lib/controllers/git-tab-controller.js index 5394ffe97d..dc4835a710 100644 --- a/lib/controllers/git-tab-controller.js +++ b/lib/controllers/git-tab-controller.js @@ -165,6 +165,7 @@ export default class GitTabController extends React.Component { attemptStageAllOperation={this.attemptStageAllOperation} prepareToCommit={this.prepareToCommit} commit={this.commit} + undoLastCommit={this.undoLastCommit} push={this.push} pull={this.pull} fetch={this.fetch} @@ -332,6 +333,16 @@ export default class GitTabController extends React.Component { return this.props.repository.commit(message); } + @autobind + async undoLastCommit() { + const repo = this.props.repository; + const lastCommit = await repo.getLastCommit(); + if (lastCommit) { + repo.setRegularCommitMessage(lastCommit.message); + return repo.undoLastCommit(); + } + } + @autobind async abortMerge() { const choice = this.props.confirm({ diff --git a/lib/controllers/recent-commits-controller.js b/lib/controllers/recent-commits-controller.js index 1a375ad711..37930e2178 100644 --- a/lib/controllers/recent-commits-controller.js +++ b/lib/controllers/recent-commits-controller.js @@ -14,6 +14,7 @@ export default class RecentCommitsController extends React.Component { ); } diff --git a/lib/views/git-tab-view.js b/lib/views/git-tab-view.js index 864160e0c8..3eed26334c 100644 --- a/lib/views/git-tab-view.js +++ b/lib/views/git-tab-view.js @@ -190,6 +190,7 @@ export default class GitTabView extends React.Component { ref={c => { this.refRecentCommitController = c; }} commits={this.props.recentCommits} isLoading={this.props.isLoading} + undoLastCommit={this.props.undoLastCommit} />
  • ); diff --git a/lib/views/recent-commits-view.js b/lib/views/recent-commits-view.js index 21caf93702..b5e12c176d 100644 --- a/lib/views/recent-commits-view.js +++ b/lib/views/recent-commits-view.js @@ -8,6 +8,7 @@ import Timeago from './timeago'; class RecentCommitView extends React.Component { static propTypes = { commit: PropTypes.object.isRequired, + undoLastCommit: PropTypes.func.isRequired, }; render() { @@ -62,6 +63,7 @@ export default class RecentCommitsView extends React.Component { static propTypes = { commits: PropTypes.arrayOf(PropTypes.object).isRequired, isLoading: PropTypes.bool.isRequired, + undoLastCommit: PropTypes.func.isRequired, }; render() { @@ -96,6 +98,7 @@ export default class RecentCommitsView extends React.Component { key={commit.getSha()} isMostRecent={i === 0} commit={commit} + undoLastCommit={this.props.undoLastCommit} /> ); })} diff --git a/test/controllers/git-tab-controller.test.js b/test/controllers/git-tab-controller.test.js index f700406c6c..a19a590973 100644 --- a/test/controllers/git-tab-controller.test.js +++ b/test/controllers/git-tab-controller.test.js @@ -611,5 +611,37 @@ describe('GitTabController', function() { return modifiedFilePatch.status === 'modified' && modifiedFilePatch.filePath === 'new-file.txt'; }); }); + + describe('undoLastCommit()', function() { + it('restores to the state prior to committing', async function() { + const workdirPath = await cloneRepository('three-files'); + const repository = await buildRepository(workdirPath); + fs.writeFileSync(path.join(workdirPath, 'new-file.txt'), 'foo\nbar\nbaz\n'); + + await repository.stageFiles(['new-file.txt']); + const commitMessage = 'Commit some stuff'; + await repository.commit(commitMessage); + + app = React.cloneElement(app, {repository}); + const wrapper = mount(app); + + await assert.async.lengthOf(wrapper.find('.github-RecentCommit-undoButton'), 1); + wrapper.find('.github-RecentCommit-undoButton').simulate('click'); + + let commitMessages = wrapper.find('.github-RecentCommit-message').map(node => node.text()); + assert.deepEqual(commitMessages, [commitMessage, 'Initial commit']); + + await assert.async.lengthOf(wrapper.find('GitTabView').prop('stagedChanges'), 1); + assert.deepEqual(wrapper.find('GitTabView').prop('stagedChanges'), [{ + filePath: 'new-file.txt', + status: 'added', + }]); + + commitMessages = wrapper.find('.github-RecentCommit-message').map(node => node.text()); + assert.deepEqual(commitMessages, ['Initial commit']); + + assert.strictEqual(wrapper.find('CommitView').prop('message'), commitMessage); + }); + }); }); }); From 52797452015942fa83623baf271ac2b72ae2756b Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 23 Mar 2018 17:52:09 -0700 Subject: [PATCH 0562/5882] Test undo last commit when the repository has no commits --- lib/controllers/git-tab-controller.js | 7 +++---- test/controllers/git-tab-controller.test.js | 18 +++++++++++++++++- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/lib/controllers/git-tab-controller.js b/lib/controllers/git-tab-controller.js index dc4835a710..c71ed3446e 100644 --- a/lib/controllers/git-tab-controller.js +++ b/lib/controllers/git-tab-controller.js @@ -337,10 +337,9 @@ export default class GitTabController extends React.Component { async undoLastCommit() { const repo = this.props.repository; const lastCommit = await repo.getLastCommit(); - if (lastCommit) { - repo.setRegularCommitMessage(lastCommit.message); - return repo.undoLastCommit(); - } + if (lastCommit.isUnbornRef()) { return null; } + repo.setRegularCommitMessage(lastCommit.message); + return repo.undoLastCommit(); } @autobind diff --git a/test/controllers/git-tab-controller.test.js b/test/controllers/git-tab-controller.test.js index a19a590973..78a44d1a59 100644 --- a/test/controllers/git-tab-controller.test.js +++ b/test/controllers/git-tab-controller.test.js @@ -9,7 +9,7 @@ import until from 'test-until'; import GitTabController from '../../lib/controllers/git-tab-controller'; -import {cloneRepository, buildRepository, buildRepositoryWithPipeline} from '../helpers'; +import {cloneRepository, buildRepository, buildRepositoryWithPipeline, initRepository} from '../helpers'; import Repository from '../../lib/models/repository'; import {GitError} from '../../lib/git-shell-out-strategy'; @@ -613,6 +613,22 @@ describe('GitTabController', function() { }); describe('undoLastCommit()', function() { + it('does nothing when there are no commits', async function() { + const workdirPath = await initRepository(); + const repository = await buildRepository(workdirPath); + + app = React.cloneElement(app, {repository}); + const wrapper = mount(app); + + try { + await wrapper.instance().getWrappedComponentInstance().undoLastCommit(); + } catch (e) { + throw e; + } + + assert.doesNotThrow(async () => await wrapper.instance().getWrappedComponentInstance().undoLastCommit()); + }); + it('restores to the state prior to committing', async function() { const workdirPath = await cloneRepository('three-files'); const repository = await buildRepository(workdirPath); From b6b2e981c72ca716438ff6e1dab98f71ca70cf11 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 23 Mar 2018 17:56:45 -0700 Subject: [PATCH 0563/5882] :shirt: --- lib/controllers/recent-commits-controller.js | 1 + lib/git-shell-out-strategy.js | 4 ++-- lib/views/git-tab-view.js | 1 + lib/views/recent-commits-view.js | 1 + test/git-strategies.test.js | 12 ++++++------ 5 files changed, 11 insertions(+), 8 deletions(-) diff --git a/lib/controllers/recent-commits-controller.js b/lib/controllers/recent-commits-controller.js index 37930e2178..60ba927619 100644 --- a/lib/controllers/recent-commits-controller.js +++ b/lib/controllers/recent-commits-controller.js @@ -7,6 +7,7 @@ export default class RecentCommitsController extends React.Component { static propTypes = { commits: PropTypes.arrayOf(PropTypes.object).isRequired, isLoading: PropTypes.bool.isRequired, + undoLastCommit: PropTypes.func.isRequired, } render() { diff --git a/lib/git-shell-out-strategy.js b/lib/git-shell-out-strategy.js index 8d2d817dbe..dd3c452f66 100644 --- a/lib/git-shell-out-strategy.js +++ b/lib/git-shell-out-strategy.js @@ -727,9 +727,9 @@ export default class GitShellOutStrategy { * Undo Operations */ reset(type, revision = 'HEAD') { - const validTypes = ['soft'] + const validTypes = ['soft']; if (!validTypes.includes(type)) { - throw new Error(`Invalid type ${type}. Must be one of: ${validTypes.join(', ')}`) + throw new Error(`Invalid type ${type}. Must be one of: ${validTypes.join(', ')}`); } return this.exec(['reset', `--${type}`, revision]); } diff --git a/lib/views/git-tab-view.js b/lib/views/git-tab-view.js index 3eed26334c..20d7a32c96 100644 --- a/lib/views/git-tab-view.js +++ b/lib/views/git-tab-view.js @@ -48,6 +48,7 @@ export default class GitTabView extends React.Component { initializeRepo: PropTypes.func.isRequired, abortMerge: PropTypes.func.isRequired, commit: PropTypes.func.isRequired, + undoLastCommit: PropTypes.func.isRequired, prepareToCommit: PropTypes.func.isRequired, resolveAsOurs: PropTypes.func.isRequired, resolveAsTheirs: PropTypes.func.isRequired, diff --git a/lib/views/recent-commits-view.js b/lib/views/recent-commits-view.js index b5e12c176d..56e989e290 100644 --- a/lib/views/recent-commits-view.js +++ b/lib/views/recent-commits-view.js @@ -9,6 +9,7 @@ class RecentCommitView extends React.Component { static propTypes = { commit: PropTypes.object.isRequired, undoLastCommit: PropTypes.func.isRequired, + isMostRecent: PropTypes.bool.isRequired, }; render() { diff --git a/test/git-strategies.test.js b/test/git-strategies.test.js index bc4c3426c2..ea77b807a0 100644 --- a/test/git-strategies.test.js +++ b/test/git-strategies.test.js @@ -627,22 +627,22 @@ import {normalizeGitHelperPath, getTempDir} from '../lib/helpers'; fs.appendFileSync(path.join(workingDirPath, 'a.txt'), 'bar\n', 'utf8'); await git.exec(['add', '.']); - await git.commit('add stuff') + await git.commit('add stuff'); - const parentCommit = await git.getCommit('HEAD~') + const parentCommit = await git.getCommit('HEAD~'); await git.reset('soft', 'HEAD~'); - const commitAfterReset = await git.getCommit('HEAD') - assert.strictEqual(commitAfterReset.sha, parentCommit.sha) + const commitAfterReset = await git.getCommit('HEAD'); + assert.strictEqual(commitAfterReset.sha, parentCommit.sha); const stagedChanges = await git.getDiffsForFilePath('a.txt', {staged: true}); assert.lengthOf(stagedChanges, 1); - const stagedChange = stagedChanges[0] + const stagedChange = stagedChanges[0]; assert.strictEqual(stagedChange.newPath, 'a.txt'); assert.deepEqual(stagedChange.hunks[0].lines, [' foo', '+bar']); }); - }) + }); }); describe('getBranches()', function() { From c89a4bf82c2743491cc6125583131face5ad3f61 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Mon, 26 Mar 2018 15:45:19 -0700 Subject: [PATCH 0564/5882] Extract `extractCoAuthorsAndRawCommitMessage` method & LINE_ENDING_REGEX Co-authored-by: Tilde Ann Thurium --- lib/git-shell-out-strategy.js | 14 ++++---------- lib/helpers.js | 17 +++++++++++++++++ lib/views/commit-view.js | 4 +--- lib/views/recent-commits-view.js | 3 ++- 4 files changed, 24 insertions(+), 14 deletions(-) diff --git a/lib/git-shell-out-strategy.js b/lib/git-shell-out-strategy.js index dd3c452f66..1a2dda0908 100644 --- a/lib/git-shell-out-strategy.js +++ b/lib/git-shell-out-strategy.js @@ -14,13 +14,12 @@ import GitTempDir from './git-temp-dir'; import AsyncQueue from './async-queue'; import { getDugitePath, getSharedModulePath, getAtomHelperPath, - fileExists, isFileExecutable, isFileSymlink, isBinary, - normalizeGitHelperPath, toNativePathSep, toGitPathSep, + extractCoAuthorsAndRawCommitMessage, fileExists, isFileExecutable, isFileSymlink, isBinary, + normalizeGitHelperPath, toNativePathSep, toGitPathSep, LINE_ENDING_REGEX, } from './helpers'; import GitTimingsView from './views/git-timings-view'; import WorkerManager from './worker-manager'; -const LINE_ENDING_REGEX = /\r?\n/; const MAX_STATUS_OUTPUT_LENGTH = 1024 * 1024 * 10; let headless = null; @@ -579,19 +578,14 @@ export default class GitShellOutStrategy { for (let i = 0; i < fields.length; i += 5) { const body = fields[i + 4]; - // There's probably a better way. I tried finding a regex to do it in one fell swoop but had no luck - const coAuthors = body.split(LINE_ENDING_REGEX).reduce((emails, line) => { - const match = line.match(/^co-authored-by: .*<(.*)>\s*/i); - if (match && match[1]) { emails.push(match[1]); } - return emails; - }, []); + const {message, coAuthors} = extractCoAuthorsAndRawCommitMessage(body); commits.push({ sha: fields[i] && fields[i].trim(), authorEmail: fields[i + 1] && fields[i + 1].trim(), authorDate: parseInt(fields[i + 2], 10), message: fields[i + 3], - body: fields[i + 4], + body: message, coAuthors, }); } diff --git a/lib/helpers.js b/lib/helpers.js index 65ba5a3f7f..ce12b849eb 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -5,6 +5,8 @@ import temp from 'temp'; import FilePatchController from './controllers/file-patch-controller'; +export const LINE_ENDING_REGEX = /\r?\n/; + export function getPackageRoot() { const {resourcePath} = atom.getLoadSettings(); const currentFileWasRequiredFromSnapshot = !path.isAbsolute(__dirname); @@ -312,3 +314,18 @@ export function destroyEmptyFilePatchPaneItems(workspace) { const itemsToDestroy = getFilePatchPaneItems({empty: true}, workspace); itemsToDestroy.forEach(item => item.destroy()); } + +export function extractCoAuthorsAndRawCommitMessage(commitMessage) { + // There's probably a better way. I tried finding a regex to do it in one fell swoop but had no luck + let rawMessage = ''; + const coAuthors = commitMessage.split(LINE_ENDING_REGEX).reduce((emails, line) => { + const match = line.match(/^co-authored-by: .*<(.*)>\s*/i); + if (match && match[1]) { + emails.push(match[1]); + } else { + rawMessage += line; + } + return emails; + }, []); + return {message: rawMessage, coAuthors}; +} diff --git a/lib/views/commit-view.js b/lib/views/commit-view.js index 8fd762ebec..b3bd582c00 100644 --- a/lib/views/commit-view.js +++ b/lib/views/commit-view.js @@ -7,9 +7,7 @@ import Select from 'react-select'; import Tooltip from './tooltip'; import AtomTextEditor from './atom-text-editor'; -import {shortenSha} from '../helpers'; - -const LINE_ENDING_REGEX = /\r?\n/; +import {shortenSha, LINE_ENDING_REGEX} from '../helpers'; class FakeKeyDownEvent extends CustomEvent { constructor(keyCode) { diff --git a/lib/views/recent-commits-view.js b/lib/views/recent-commits-view.js index 56e989e290..690fb0668f 100644 --- a/lib/views/recent-commits-view.js +++ b/lib/views/recent-commits-view.js @@ -14,13 +14,14 @@ class RecentCommitView extends React.Component { render() { const authorMoment = moment(this.props.commit.getAuthorDate() * 1000); + const fullMessage = (this.props.commit.getMessage() + '\n\n' + this.props.commit.getBody()).trim(); return (
  • {this.renderAuthors()} + title={fullMessage}> {this.props.commit.getMessage()} {this.props.isMostRecent && ( From 62ea66ef0c065ff9c7e0de0ad3946bbca076090b Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Mon, 26 Mar 2018 16:38:58 -0700 Subject: [PATCH 0565/5882] Pull up selectedCoAuthors state to GitTabController Co-authored-by: Tilde Ann Thurium --- lib/controllers/commit-controller.js | 5 ++++- lib/controllers/git-tab-controller.js | 17 +++++++++++++++-- lib/views/commit-view.js | 18 ++++++++++-------- lib/views/git-tab-view.js | 4 ++++ 4 files changed, 33 insertions(+), 11 deletions(-) diff --git a/lib/controllers/commit-controller.js b/lib/controllers/commit-controller.js index 78254d1c41..6c003097f0 100644 --- a/lib/controllers/commit-controller.js +++ b/lib/controllers/commit-controller.js @@ -35,7 +35,8 @@ export default class CommitController extends React.Component { email: PropTypes.string.isRequired, name: PropTypes.string.isRequired, })).isRequired, - + selectedCoAuthors: PropTypes.arrayOf(PropTypes.string).isRequired, + updateSelectedCoAuthors: PropTypes.func.isRequired, prepareToCommit: PropTypes.func.isRequired, commit: PropTypes.func.isRequired, abortMerge: PropTypes.func.isRequired, @@ -115,6 +116,8 @@ export default class CommitController extends React.Component { toggleExpandedCommitMessageEditor={this.toggleExpandedCommitMessageEditor} deactivateCommitBox={!!this.getCommitMessageEditors().length > 0} mentionableUsers={this.props.mentionableUsers} + selectedCoAuthors={this.props.selectedCoAuthors} + updateSelectedCoAuthors={this.props.updateSelectedCoAuthors} /> ); } diff --git a/lib/controllers/git-tab-controller.js b/lib/controllers/git-tab-controller.js index c71ed3446e..799deb84c1 100644 --- a/lib/controllers/git-tab-controller.js +++ b/lib/controllers/git-tab-controller.js @@ -12,6 +12,7 @@ import ObserveModelDecorator from '../decorators/observe-model'; import UserStore from '../models/user-store'; import {nullBranch} from '../models/branch'; import {nullCommit} from '../models/commit'; +import {extractCoAuthorsAndRawCommitMessage} from '../helpers'; const DOMPurify = createDOMPurify(); @@ -105,7 +106,10 @@ export default class GitTabController extends React.Component { this.refView = null; - this.state = {mentionableUsers: []}; + this.state = { + mentionableUsers: [], + selectedCoAuthors: null, + }; this.userStore = new UserStore({ repository: this.props.repository, @@ -143,6 +147,8 @@ export default class GitTabController extends React.Component { workingDirectoryPath={this.props.workingDirectoryPath} mergeMessage={this.props.mergeMessage} mentionableUsers={this.state.mentionableUsers} + selectedCoAuthors={this.state.selectedCoAuthors} + updateSelectedCoAuthors={this.updateSelectedCoAuthors} resolutionProgress={this.props.resolutionProgress} workspace={this.props.workspace} @@ -333,12 +339,19 @@ export default class GitTabController extends React.Component { return this.props.repository.commit(message); } + @autobind + updateSelectedCoAuthors(selectedCoAuthors) { + this.setState({selectedCoAuthors}); + } + @autobind async undoLastCommit() { const repo = this.props.repository; const lastCommit = await repo.getLastCommit(); if (lastCommit.isUnbornRef()) { return null; } - repo.setRegularCommitMessage(lastCommit.message); + const {message, coAuthors} = extractCoAuthorsAndRawCommitMessage(lastCommit.message); + repo.setRegularCommitMessage(message); + this.updateSelectedCoAuthors(coAuthors); return repo.undoLastCommit(); } diff --git a/lib/views/commit-view.js b/lib/views/commit-view.js index b3bd582c00..df31ffacbe 100644 --- a/lib/views/commit-view.js +++ b/lib/views/commit-view.js @@ -40,7 +40,8 @@ export default class CommitView extends React.Component { maximumCharacterLimit: PropTypes.number.isRequired, message: PropTypes.string.isRequired, mentionableUsers: PropTypes.arrayOf(PropTypes.object).isRequired, - + selectedCoAuthors: PropTypes.arrayOf(PropTypes.string).isRequired, + updateSelectedCoAuthors: PropTypes.func.isRequired, commit: PropTypes.func.isRequired, abortMerge: PropTypes.func.isRequired, setAmending: PropTypes.func.isRequired, @@ -54,7 +55,6 @@ export default class CommitView extends React.Component { this.state = { showWorking: false, - selectedCoAuthors: [], showCoAuthorInput: false, }; @@ -237,7 +237,7 @@ export default class CommitView extends React.Component { optionRenderer={this.renderCoAuthorListItem} valueRenderer={this.renderCoAuthorValue} onChange={this.onSelectedCoAuthorsChanged} - value={this.state.selectedCoAuthors} + value={this.props.selectedCoAuthors} multi={true} /> ); @@ -286,9 +286,10 @@ export default class CommitView extends React.Component { } } - componentWillReceiveProps(nextProps) { - this.scheduleShowWorking(nextProps); - } + // componentWillReceiveProps(nextProps) { + // this.scheduleShowWorking(nextProps); + // this.state.selectedCoAuthors = nextProps.selectedCoAuthors; + // } componentWillUnmount() { this.subscriptions.dispose(); @@ -331,7 +332,7 @@ export default class CommitView extends React.Component { async commit() { if (await this.props.prepareToCommit() && this.isCommitButtonEnabled()) { try { - await this.props.commit(this.editor.getText(), this.state.selectedCoAuthors); + await this.props.commit(this.editor.getText(), this.props.selectedCoAuthors); } catch (e) { // do nothing } @@ -434,7 +435,8 @@ export default class CommitView extends React.Component { @autobind onSelectedCoAuthorsChanged(selectedCoAuthors) { - this.setState({selectedCoAuthors}); + this.props.updateSelectedCoAuthors(selectedCoAuthors); + // this.setState({selectedCoAuthors}); } rememberFocus(event) { diff --git a/lib/views/git-tab-view.js b/lib/views/git-tab-view.js index 20d7a32c96..f456847082 100644 --- a/lib/views/git-tab-view.js +++ b/lib/views/git-tab-view.js @@ -35,6 +35,8 @@ export default class GitTabView extends React.Component { workingDirectoryPath: PropTypes.string, mergeMessage: PropTypes.string, mentionableUsers: PropTypes.arrayOf(PropTypes.object).isRequired, + selectedCoAuthors: PropTypes.arrayOf(PropTypes.string).isRequired, + updateSelectedCoAuthors: PropTypes.func.isRequired, workspace: PropTypes.object.isRequired, commandRegistry: PropTypes.object.isRequired, @@ -186,6 +188,8 @@ export default class GitTabView extends React.Component { lastCommit={this.props.lastCommit} repository={this.props.repository} mentionableUsers={this.props.mentionableUsers} + selectedCoAuthors={this.props.selectedCoAuthors} + updateSelectedCoAuthors={this.props.updateSelectedCoAuthors} /> { this.refRecentCommitController = c; }} From dd872d0e84303298da32c98fbb5be6e7a5b17700 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Mon, 26 Mar 2018 17:03:31 -0700 Subject: [PATCH 0566/5882] Test that co author state populates after undoing last commit Co-authored-by: Tilde Ann Thurium --- test/controllers/git-tab-controller.test.js | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/test/controllers/git-tab-controller.test.js b/test/controllers/git-tab-controller.test.js index 78a44d1a59..c66e0158c9 100644 --- a/test/controllers/git-tab-controller.test.js +++ b/test/controllers/git-tab-controller.test.js @@ -635,17 +635,25 @@ describe('GitTabController', function() { fs.writeFileSync(path.join(workdirPath, 'new-file.txt'), 'foo\nbar\nbaz\n'); await repository.stageFiles(['new-file.txt']); - const commitMessage = 'Commit some stuff'; + const commitSubject = 'Commit some stuff'; + const commitMessage = dedent` + ${commitSubject} + + Co-authored-by: Foo Bar + `; await repository.commit(commitMessage); app = React.cloneElement(app, {repository}); const wrapper = mount(app); + assert.deepEqual(wrapper.find('CommitView').prop('selectedCoAuthors'), []); + await assert.async.lengthOf(wrapper.find('.github-RecentCommit-undoButton'), 1); wrapper.find('.github-RecentCommit-undoButton').simulate('click'); let commitMessages = wrapper.find('.github-RecentCommit-message').map(node => node.text()); - assert.deepEqual(commitMessages, [commitMessage, 'Initial commit']); + // ensure that the co author trailer is stripped from commit message + assert.deepEqual(commitMessages, [commitSubject, 'Initial commit']); await assert.async.lengthOf(wrapper.find('GitTabView').prop('stagedChanges'), 1); assert.deepEqual(wrapper.find('GitTabView').prop('stagedChanges'), [{ @@ -656,7 +664,9 @@ describe('GitTabController', function() { commitMessages = wrapper.find('.github-RecentCommit-message').map(node => node.text()); assert.deepEqual(commitMessages, ['Initial commit']); - assert.strictEqual(wrapper.find('CommitView').prop('message'), commitMessage); + assert.strictEqual(wrapper.find('CommitView').prop('message'), commitSubject); + + assert.deepEqual(wrapper.find('CommitView').prop('selectedCoAuthors'), ['foo@bar.com']); }); }); }); From 072bc28aa0944f18834ed503b942b885c732a389 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Mon, 26 Mar 2018 17:04:29 -0700 Subject: [PATCH 0567/5882] Initialize state of co authors to empty array Co-authored-by: Tilde Ann Thurium --- lib/controllers/git-tab-controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/controllers/git-tab-controller.js b/lib/controllers/git-tab-controller.js index 799deb84c1..e067c5ece0 100644 --- a/lib/controllers/git-tab-controller.js +++ b/lib/controllers/git-tab-controller.js @@ -108,7 +108,7 @@ export default class GitTabController extends React.Component { this.state = { mentionableUsers: [], - selectedCoAuthors: null, + selectedCoAuthors: [], }; this.userStore = new UserStore({ From 3d03b671d8644e6ff207ba0ef80fed038c0d56e4 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Mon, 26 Mar 2018 18:49:19 -0700 Subject: [PATCH 0568/5882] Implement `github:amend-last-commit` command Co-authored-by: Tilde Ann Thurium --- lib/controllers/commit-controller.js | 5 +++-- lib/controllers/git-tab-controller.js | 4 ++-- lib/git-shell-out-strategy.js | 9 ++++++++- lib/models/repository-states/present.js | 2 +- lib/views/commit-view.js | 19 +++++++++++++------ 5 files changed, 27 insertions(+), 12 deletions(-) diff --git a/lib/controllers/commit-controller.js b/lib/controllers/commit-controller.js index 6c003097f0..6f6db99eea 100644 --- a/lib/controllers/commit-controller.js +++ b/lib/controllers/commit-controller.js @@ -145,7 +145,7 @@ export default class CommitController extends React.Component { } @autobind - async commit(message, coAuthors = []) { + async commit(message, coAuthors = [], amend) { let msg; if (this.getCommitMessageEditors().length > 0) { @@ -165,7 +165,8 @@ export default class CommitController extends React.Component { }); msg = await this.props.repository.addTrailersToCommitMessage(msg, trailers); } - return this.props.commit(msg.trim()); + + return this.props.commit(msg.trim(), {amend}); } getCommitMessage() { diff --git a/lib/controllers/git-tab-controller.js b/lib/controllers/git-tab-controller.js index e067c5ece0..0858f34a97 100644 --- a/lib/controllers/git-tab-controller.js +++ b/lib/controllers/git-tab-controller.js @@ -335,8 +335,8 @@ export default class GitTabController extends React.Component { } @autobind - commit(message) { - return this.props.repository.commit(message); + commit(message, options) { + return this.props.repository.commit(message, options); } @autobind diff --git a/lib/git-shell-out-strategy.js b/lib/git-shell-out-strategy.js index 1a2dda0908..e5cc6195c8 100644 --- a/lib/git-shell-out-strategy.js +++ b/lib/git-shell-out-strategy.js @@ -400,13 +400,20 @@ export default class GitShellOutStrategy { commit(message, {allowEmpty, amend} = {}) { let args = ['commit', '--cleanup=strip']; + + // determine correct message arguments if (typeof message === 'object') { args = args.concat(['-F', message.filePath]); } else if (typeof message === 'string') { - args = args.concat(['-m', message]); + if (message.length === 0 && amend) { + args = args.concat(['--reuse-message=HEAD']); + } else { + args = args.concat(['-m', message]); + } } else { throw new Error(`Invalid message type ${typeof message}. Must be string or object with filePath property`); } + if (amend) { args.push('--amend'); } if (allowEmpty) { args.push('--allow-empty'); } return this.gpgExec(args, {writeOperation: true}); diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js index f7af981cdc..9b735aaf40 100644 --- a/lib/models/repository-states/present.js +++ b/lib/models/repository-states/present.js @@ -259,7 +259,7 @@ export default class Present extends State { commit(message, options) { // eslint-disable-next-line no-shadow return this.executePipelineAction('COMMIT', (message, options) => { - const opts = {...options, amend: this.isAmending()}; + const opts = {amend: this.isAmending(), ...options}; return this.git().commit(message, opts); }, message, options); } diff --git a/lib/views/commit-view.js b/lib/views/commit-view.js index df31ffacbe..b996ed0e45 100644 --- a/lib/views/commit-view.js +++ b/lib/views/commit-view.js @@ -90,6 +90,7 @@ export default class CommitView extends React.Component { this.subscriptions = new CompositeDisposable( this.props.commandRegistry.add('atom-workspace', { 'github:commit': this.commit, + 'github:amend-last-commit': this.amendLastCommit, 'github:toggle-expanded-commit-message-editor': this.toggleExpandedCommitMessageEditor, 'github:co-author:down': this.proxyKeyCode(40), @@ -329,18 +330,23 @@ export default class CommitView extends React.Component { } @autobind - async commit() { - if (await this.props.prepareToCommit() && this.isCommitButtonEnabled()) { + async commit(amend) { + if (await this.props.prepareToCommit() && this.isCommitButtonEnabled(amend)) { try { - await this.props.commit(this.editor.getText(), this.props.selectedCoAuthors); + await this.props.commit(this.editor.getText(), this.props.selectedCoAuthors, amend); } catch (e) { - // do nothing + // do nothing - error was taken care of in pipeline manager } } else { this.setFocus(CommitView.focus.EDITOR); } } + @autobind + amendLastCommit() { + this.commit(true); + } + getRemainingCharacters() { if (this.editor != null) { if (this.editor.getCursorBufferPosition().row === 0) { @@ -374,12 +380,13 @@ export default class CommitView extends React.Component { } } - isCommitButtonEnabled() { + isCommitButtonEnabled(amend) { + const messageExists = this.editor && this.editor.getText().length !== 0; return !this.props.isCommitting && this.props.stagedChangesExist && !this.props.mergeConflictsExist && this.props.lastCommit.isPresent() && - (this.props.deactivateCommitBox || (this.editor && this.editor.getText().length !== 0)); + (this.props.deactivateCommitBox || (amend || messageExists)); } commitButtonText() { From e14583762230db34ee7f431ef789b76ebbe60b7b Mon Sep 17 00:00:00 2001 From: Tilde Ann Thurium Date: Tue, 27 Mar 2018 11:57:50 -0700 Subject: [PATCH 0569/5882] Fix broken CommitView editor focus test Co-Authored-By: Katrina Uychaco --- 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 b996ed0e45..3a5d440ef7 100644 --- a/lib/views/commit-view.js +++ b/lib/views/commit-view.js @@ -330,7 +330,7 @@ export default class CommitView extends React.Component { } @autobind - async commit(amend) { + async commit(event, amend) { if (await this.props.prepareToCommit() && this.isCommitButtonEnabled(amend)) { try { await this.props.commit(this.editor.getText(), this.props.selectedCoAuthors, amend); @@ -344,7 +344,7 @@ export default class CommitView extends React.Component { @autobind amendLastCommit() { - this.commit(true); + this.commit(null, true); } getRemainingCharacters() { From f4081acff520dd90a9e2ef4c0828848aee496660 Mon Sep 17 00:00:00 2001 From: Tilde Ann Thurium Date: Tue, 27 Mar 2018 17:06:38 -0700 Subject: [PATCH 0570/5882] Add tests for amending with and without a new commit message Co-authored-by: Katrina Uychaco --- test/controllers/git-tab-controller.test.js | 55 +++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/test/controllers/git-tab-controller.test.js b/test/controllers/git-tab-controller.test.js index c66e0158c9..1728231329 100644 --- a/test/controllers/git-tab-controller.test.js +++ b/test/controllers/git-tab-controller.test.js @@ -612,6 +612,61 @@ describe('GitTabController', function() { }); }); + describe('amend', function() { + it('if commit message is not provided, default to using previous commit message', async function() { + const workdirPath = await cloneRepository('three-files'); + const repository = await buildRepository(workdirPath); + + app = React.cloneElement(app, {repository}); + const wrapper = mount(app); + + const commitMessage = 'most recent commit woohoo'; + await repository.commit(commitMessage, {allowEmpty: true}); + + // we have an empty commit editor with staged changes + fs.writeFileSync(path.join(workdirPath, 'new-file.txt'), 'oh\nem\ngee\n'); + await repository.stageFiles(['new-file.txt']); + await assert.async.lengthOf(wrapper.find('GitTabView').prop('stagedChanges'), 1); + + assert.strictEqual(wrapper.find('CommitView').getNode().editor.getText(), ''); + + sinon.spy(repository, 'commit'); + commandRegistry.dispatch(workspaceElement, 'github:amend-last-commit'); + + // amending should commit all unstaged changes + await assert.async.lengthOf(wrapper.find('GitTabView').prop('stagedChanges'), 0); + const lastCommit = wrapper.find('CommitView').prop('lastCommit'); + assert.equal(lastCommit.message, commitMessage); + assert.deepEqual(repository.commit.args[0][1], {amend: true}); + }); + + it('amend should use commit message if one is supplied', async function() { + const workdirPath = await cloneRepository('three-files'); + const repository = await buildRepository(workdirPath); + app = React.cloneElement(app, {repository}); + const wrapper = mount(app); + + const commitMessage = 'most recent commit woohoo'; + await repository.commit(commitMessage, {allowEmpty: true}); + + // st-st-staging changes with a shiny new commit message + fs.writeFileSync(path.join(workdirPath, 'new-file.txt'), 'oh\nem\ngee\n'); + await repository.stageFiles(['new-file.txt']); + await assert.async.lengthOf(wrapper.find('GitTabView').prop('stagedChanges'), 1); + const newMessage = 'such new very message'; + + const commitView = wrapper.find('CommitView'); + commitView.getNode().editor.setText(newMessage); + + sinon.spy(repository, 'commit'); + commandRegistry.dispatch(workspaceElement, 'github:amend-last-commit'); + await assert.async.lengthOf(wrapper.find('GitTabView').prop('stagedChanges'), 0); + const lastCommit = wrapper.find('CommitView').prop('lastCommit'); + assert.equal(lastCommit.message, newMessage); + assert.deepEqual(repository.commit.args[0][1], {amend: true}); + }); + }); + describe('undoLastCommit()', function() { it('does nothing when there are no commits', async function() { const workdirPath = await initRepository(); From c8c999dc04d3fbe5bb14a44d1929e30545c8779a Mon Sep 17 00:00:00 2001 From: Tilde Ann Thurium Date: Tue, 27 Mar 2018 17:16:05 -0700 Subject: [PATCH 0571/5882] Remove "amend" checkbox. Co-authored-by: Katrina Uychaco --- lib/views/commit-view.js | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/lib/views/commit-view.js b/lib/views/commit-view.js index 3a5d440ef7..416c810ad8 100644 --- a/lib/views/commit-view.js +++ b/lib/views/commit-view.js @@ -183,18 +183,6 @@ export default class CommitView extends React.Component { className="btn github-CommitView-button github-CommitView-abortMerge is-secondary" onClick={this.abortMerge}>Abort Merge } - {showAmendBox && - - } } + + disabled={!this.commitIsEnabled()}>{this.commitButtonText()}
    {this.getRemainingCharacters()}
    @@ -310,7 +310,7 @@ export default class CommitView extends React.Component { @autobind async commit(event, amend) { - if (await this.props.prepareToCommit() && this.isCommitButtonEnabled(amend)) { + if (await this.props.prepareToCommit() && this.commitIsEnabled(amend)) { try { await this.props.commit(this.editor.getText(), this.props.selectedCoAuthors, amend); } catch (e) { @@ -362,7 +362,7 @@ export default class CommitView extends React.Component { } } - isCommitButtonEnabled(amend) { + commitIsEnabled(amend) { const messageExists = this.editor && this.editor.getText().length !== 0; return !this.props.isCommitting && (amend || this.props.stagedChangesExist) && From dee8f79e2780fadef2c1a0c8d363c385901ce653 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 29 Mar 2018 11:19:32 -0700 Subject: [PATCH 0590/5882] PR feedback: fix whitespace, formatting --- lib/views/push-pull-view.js | 67 +++++++++++++++++++------------------ 1 file changed, 34 insertions(+), 33 deletions(-) diff --git a/lib/views/push-pull-view.js b/lib/views/push-pull-view.js index 4a398a256d..3be04dc863 100644 --- a/lib/views/push-pull-view.js +++ b/lib/views/push-pull-view.js @@ -4,7 +4,6 @@ import cx from 'classnames'; import {RemotePropType, BranchPropType} from '../prop-types'; - function getIconClass(icon, isSyncing = false) { return cx( 'github-PushPull-icon', @@ -105,33 +104,34 @@ export default class PushPullView extends React.Component { const isSyncing = this.props.isSyncing; const hasOrigin = !!this.props.originExists; - return (
    (this.tileNode = node)} - className={cx('github-PushPull', 'inline-block', {'github-branch-detached': isDetached})}> + return ( +
    (this.tileNode = node)} + className={cx('github-PushPull', 'inline-block', {'github-branch-detached': isDetached})}> - {isAhead && !isBehind && !isUnpublished && ( + {isAhead && !isBehind && !isUnpublished && ( (this.tooltipTarget = node)} onClick={this.onClickPush} className="push-pull-target" data-tooltip="Click to push
    Cmd-click to force push
    Right-click for more"> - Push {this.props.aheadCount} + Push {this.props.aheadCount}
    - )} + )} - {isBehind && !isAhead && !isUnpublished && ( + {isBehind && !isAhead && !isUnpublished && ( (this.tooltipTarget = node)} onClick={this.onClickPull} className="push-pull-target" data-tooltip="Click to pull
    Right-click for more"> - Pull {this.props.behindCount} + Pull {this.props.behindCount}
    - )} + )} - {isBehind && isAhead && !isUnpublished && !isSyncing && ( + {isBehind && isAhead && !isUnpublished && !isSyncing && ( (this.tooltipTarget = node)} onClick={this.onClickPushPull} @@ -142,56 +142,57 @@ export default class PushPullView extends React.Component { {this.props.aheadCount}{' '} - Pull {this.props.behindCount} + Pull {this.props.behindCount} - )} + )} - {isBehind && isAhead && !isUnpublished && isSyncing && ( + {isBehind && isAhead && !isUnpublished && isSyncing && ( - Pull {this.props.behindCount} + Pull {this.props.behindCount} - )} + )} - {!isBehind && !isAhead && !isUnpublished && !isDetached && ( + {!isBehind && !isAhead && !isUnpublished && !isDetached && ( (this.tooltipTarget = node)} onClick={this.onClickFetch} className="push-pull-target" data-tooltip="Click to fetch
    Right-click for more"> - Fetch -
    - )} + Fetch + + )} - {isUnpublished && !isDetached && hasOrigin && ( + {isUnpublished && !isDetached && hasOrigin && ( (this.tooltipTarget = node)} onClick={this.onClickPublish} className="push-pull-target" data-tooltip="Click to set up a remote tracking branch
    Right-click for more"> - Publish -
    - )} + Publish + + )} - {isUnpublished && !isDetached && !hasOrigin && ( + {isUnpublished && !isDetached && !hasOrigin && ( (this.tooltipTarget = node)} className="push-pull-target" data-tooltip={'There is no remote named "origin"'}> - No remote - - )} + No remote + + )} - {isDetached && ( + {isDetached && ( (this.tooltipTarget = node)} className="push-pull-target" data-tooltip={'Create a branch if you wish to push your work anywhere'}> - Not on branch - - )} -
    ); + Not on branch + + )} +
    + ); } } From d51a5a1cacb1448b57f8a88a5ada444d180d3f51 Mon Sep 17 00:00:00 2001 From: simurai Date: Fri, 30 Mar 2018 10:03:59 +0900 Subject: [PATCH 0591/5882] Align Undo button --- styles/recent-commits.less | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/styles/recent-commits.less b/styles/recent-commits.less index 0d4b7d4017..06a3bda3b5 100644 --- a/styles/recent-commits.less +++ b/styles/recent-commits.less @@ -29,8 +29,9 @@ .github-RecentCommit { position: relative; display: flex; - padding: @component-padding/2 @component-padding; - line-height: @size; + align-items: center; + padding: 0 @component-padding; + line-height: @size + @component-padding; &-authors { position: absolute; @@ -77,8 +78,8 @@ &-undoButton.btn { padding: 0 @component-padding/1.5; margin-right: @component-padding/2; - height: @size + @component-padding/2; - line-height: 0; + height: @size * 1.25; + line-height: 1; font-size: .9em; } From 28e1a10e68cf61a457094e9e715240e32f073013 Mon Sep 17 00:00:00 2001 From: Tilde Ann Thurium Date: Thu, 29 Mar 2018 16:30:09 -0700 Subject: [PATCH 0592/5882] refactor git-tab-controller tests to add beforeEach for amend methods. Co-authored-by: Katrina Uychaco --- test/controllers/git-tab-controller.test.js | 30 ++++++++++----------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/test/controllers/git-tab-controller.test.js b/test/controllers/git-tab-controller.test.js index 2fabfe6a5a..fe762a6861 100644 --- a/test/controllers/git-tab-controller.test.js +++ b/test/controllers/git-tab-controller.test.js @@ -573,16 +573,20 @@ describe('GitTabController', function() { }); describe('amend', function() { - it('if commit message is not provided, default to using previous commit message', async function() { - const workdirPath = await cloneRepository('three-files'); - const repository = await buildRepository(workdirPath); + let repository, commitMessage, workdirPath, wrapper; + beforeEach(async function() { + workdirPath = await cloneRepository('three-files'); + repository = await buildRepository(workdirPath); app = React.cloneElement(app, {repository}); - const wrapper = mount(app); + wrapper = mount(app); - const commitMessage = 'most recent commit woohoo'; + commitMessage = 'most recent commit woohoo'; await repository.commit(commitMessage, {allowEmpty: true}); + }); + + it('amends when there are staged changes and uses the last commit\'s message when there is no new message', async function() { // we have an empty commit editor with staged changes fs.writeFileSync(path.join(workdirPath, 'new-file.txt'), 'oh\nem\ngee\n'); await repository.stageFiles(['new-file.txt']); @@ -596,19 +600,12 @@ describe('GitTabController', function() { // amending should commit all unstaged changes await assert.async.lengthOf(wrapper.find('GitTabView').prop('stagedChanges'), 0); const lastCommit = wrapper.find('CommitView').prop('lastCommit'); + // commit message from previous commit should be used assert.equal(lastCommit.message, commitMessage); - assert.deepEqual(repository.commit.args[0][1], {amend: true}); + assert.deepEqual(repository.commit.args[0][1], {amend: true, coAuthors: []}); }); - it('amend should use commit message if one is supplied', async function() { - const workdirPath = await cloneRepository('three-files'); - const repository = await buildRepository(workdirPath); - app = React.cloneElement(app, {repository}); - const wrapper = mount(app); - - const commitMessage = 'most recent commit woohoo'; - await repository.commit(commitMessage, {allowEmpty: true}); - + it('amends when there are staged changes and a new commit message', async function() { // st-st-staging changes with a shiny new commit message fs.writeFileSync(path.join(workdirPath, 'new-file.txt'), 'oh\nem\ngee\n'); await repository.stageFiles(['new-file.txt']); @@ -622,8 +619,9 @@ describe('GitTabController', function() { commandRegistry.dispatch(workspaceElement, 'github:amend-last-commit'); await assert.async.lengthOf(wrapper.find('GitTabView').prop('stagedChanges'), 0); const lastCommit = wrapper.find('CommitView').prop('lastCommit'); + // new commit message should be used for last commit assert.equal(lastCommit.message, newMessage); - assert.deepEqual(repository.commit.args[0][1], {amend: true}); + assert.deepEqual(repository.commit.args[0][1], {amend: true, coAuthors: []}); }); }); From 217b834d0ea77c1abbc03689d9fb1919b4226d2f Mon Sep 17 00:00:00 2001 From: Tilde Ann Thurium Date: Thu, 29 Mar 2018 18:25:54 -0700 Subject: [PATCH 0593/5882] Refactor gsos#getHeadCommit to use gsos#getRecentCommits This reduces redundancy and prevents us from parsing the co author trailer twice. It also provides more information when you're asking for the last commit (such as co authors.) Co-authored-by: Katrina Uychaco --- lib/controllers/git-tab-controller.js | 2 +- lib/git-shell-out-strategy.js | 16 ++++------------ lib/models/repository-states/present.js | 4 ++-- 3 files changed, 7 insertions(+), 15 deletions(-) diff --git a/lib/controllers/git-tab-controller.js b/lib/controllers/git-tab-controller.js index bd0401795e..4d2eee2ab0 100644 --- a/lib/controllers/git-tab-controller.js +++ b/lib/controllers/git-tab-controller.js @@ -338,7 +338,7 @@ export default class GitTabController extends React.Component { const repo = this.props.repository; const lastCommit = await repo.getLastCommit(); if (lastCommit.isUnbornRef()) { return null; } - const {message, coAuthors} = extractCoAuthorsAndRawCommitMessage(lastCommit.message); + const {message, coAuthors} = lastCommit; repo.setCommitMessage(message); this.updateSelectedCoAuthors(coAuthors); return repo.undoLastCommit(); diff --git a/lib/git-shell-out-strategy.js b/lib/git-shell-out-strategy.js index 4ca602c981..c4a5cada91 100644 --- a/lib/git-shell-out-strategy.js +++ b/lib/git-shell-out-strategy.js @@ -566,17 +566,8 @@ export default class GitShellOutStrategy { } async getHeadCommit() { - try { - const commit = await this.getCommit('HEAD'); - commit.unbornRef = false; - return commit; - } catch (e) { - if (/unknown revision/.test(e.stdErr) || /bad revision 'HEAD'/.test(e.stdErr)) { - return {sha: '', message: '', unbornRef: true}; - } else { - throw e; - } - } + const [headCommit] = await this.getRecentCommits({max: 1, ref: 'HEAD'}); + return headCommit; } async getRecentCommits(options = {}) { @@ -599,7 +590,7 @@ export default class GitShellOutStrategy { } }); - if (output === '') { return []; } + if (output === '') { return [{sha: '', message: '', unbornRef: true}]; } const fields = output.trim().split('\0'); const commits = []; @@ -615,6 +606,7 @@ export default class GitShellOutStrategy { message: fields[i + 3], body: message, coAuthors, + unbornRef: false }); } return commits; diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js index 2a002582cb..fa752c188b 100644 --- a/lib/models/repository-states/present.js +++ b/lib/models/repository-states/present.js @@ -571,8 +571,8 @@ export default class Present extends State { getLastCommit() { return this.cache.getOrSet(Keys.lastCommit, async () => { - const {sha, message, unbornRef} = await this.git().getHeadCommit(); - return unbornRef ? Commit.createUnborn() : new Commit({sha, message}); + const headCommit = await this.git().getHeadCommit(); + return headCommit.unbornRef ? Commit.createUnborn() : new Commit(headCommit); }); } From b82b01b907502b69fbd021db2af86f8cd9e4b743 Mon Sep 17 00:00:00 2001 From: Tilde Ann Thurium Date: Thu, 29 Mar 2018 18:26:42 -0700 Subject: [PATCH 0594/5882] WIP - tests for git-tab-controller Co-authored-by: Katrina Uychaco --- test/controllers/git-tab-controller.test.js | 118 +++++++++++++++----- 1 file changed, 87 insertions(+), 31 deletions(-) diff --git a/test/controllers/git-tab-controller.test.js b/test/controllers/git-tab-controller.test.js index fe762a6861..29280c265e 100644 --- a/test/controllers/git-tab-controller.test.js +++ b/test/controllers/git-tab-controller.test.js @@ -572,7 +572,7 @@ describe('GitTabController', function() { }); }); - describe('amend', function() { + describe.only('amend', function() { let repository, commitMessage, workdirPath, wrapper; beforeEach(async function() { workdirPath = await cloneRepository('three-files'); @@ -585,43 +585,99 @@ describe('GitTabController', function() { await repository.commit(commitMessage, {allowEmpty: true}); }); + describe('when there are staged changes only', function() { + it('uses the last commit\'s message since there is no new message', async function() { + // stage some changes + fs.writeFileSync(path.join(workdirPath, 'new-file.txt'), 'oh\nem\ngee\n'); + await repository.stageFiles(['new-file.txt']); + await assert.async.lengthOf(wrapper.find('GitTabView').prop('stagedChanges'), 1); - it('amends when there are staged changes and uses the last commit\'s message when there is no new message', async function() { - // we have an empty commit editor with staged changes - fs.writeFileSync(path.join(workdirPath, 'new-file.txt'), 'oh\nem\ngee\n'); - await repository.stageFiles(['new-file.txt']); - await assert.async.lengthOf(wrapper.find('GitTabView').prop('stagedChanges'), 1); + // ensure that the commit editor is empty + assert.strictEqual(wrapper.find('CommitView').getNode().editor.getText(), ''); - assert.strictEqual(wrapper.find('CommitView').getNode().editor.getText(), ''); + sinon.spy(repository, 'commit'); + commandRegistry.dispatch(workspaceElement, 'github:amend-last-commit'); + assert.deepEqual(repository.commit.args[0][1], {amend: true, coAuthors: []}); - sinon.spy(repository, 'commit'); - commandRegistry.dispatch(workspaceElement, 'github:amend-last-commit'); - - // amending should commit all unstaged changes - await assert.async.lengthOf(wrapper.find('GitTabView').prop('stagedChanges'), 0); - const lastCommit = wrapper.find('CommitView').prop('lastCommit'); - // commit message from previous commit should be used - assert.equal(lastCommit.message, commitMessage); - assert.deepEqual(repository.commit.args[0][1], {amend: true, coAuthors: []}); + // amending should commit all unstaged changes + await assert.async.lengthOf(wrapper.find('GitTabView').prop('stagedChanges'), 0); + + // commit message from previous commit should be used + const lastCommit = wrapper.find('CommitView').prop('lastCommit'); + assert.equal(lastCommit.message, commitMessage); + }) }); - it('amends when there are staged changes and a new commit message', async function() { - // st-st-staging changes with a shiny new commit message - fs.writeFileSync(path.join(workdirPath, 'new-file.txt'), 'oh\nem\ngee\n'); - await repository.stageFiles(['new-file.txt']); - await assert.async.lengthOf(wrapper.find('GitTabView').prop('stagedChanges'), 1); - const newMessage = 'such new very message'; + describe('when there is a new commit message provided (and no staged changes)', function() { + it('discards the last commit\'s message and uses the new one', async function() { + // new commit message + const newMessage = 'such new very message'; + const commitView = wrapper.find('CommitView'); + commitView.getNode().editor.setText(newMessage); - const commitView = wrapper.find('CommitView'); - commitView.getNode().editor.setText(newMessage); + // no staged changes + await assert.async.lengthOf(wrapper.find('GitTabView').prop('stagedChanges'), 0); - sinon.spy(repository, 'commit'); - commandRegistry.dispatch(workspaceElement, 'github:amend-last-commit'); - await assert.async.lengthOf(wrapper.find('GitTabView').prop('stagedChanges'), 0); - const lastCommit = wrapper.find('CommitView').prop('lastCommit'); - // new commit message should be used for last commit - assert.equal(lastCommit.message, newMessage); - assert.deepEqual(repository.commit.args[0][1], {amend: true, coAuthors: []}); + sinon.spy(repository, 'commit'); + commandRegistry.dispatch(workspaceElement, 'github:amend-last-commit'); + assert.deepEqual(repository.commit.args[0][1], {amend: true, coAuthors: []}); + + // new commit message is used + const lastCommit = wrapper.find('CommitView').prop('lastCommit'); + assert.equal(lastCommit.message, newMessage); + }); + }); + + describe.only('when co-authors are changed', function() { + it('amends the last commit re-using the commit message and adding the co-author', async function() { + // stage some changes + fs.writeFileSync(path.join(workdirPath, 'new-file.txt'), 'oh\nem\ngee\n'); + await repository.stageFiles(['new-file.txt']); + + // verify that last commit has no co-author + let commitBeforeAmend = wrapper.find('CommitView').prop('lastCommit'); + console.log(commitBeforeAmend); + assert.deepEqual(commitBeforeAmend.coAuthors, []); + + // add co author + const author = {email: 'foo@bar.com', name: "foo bar"}; + const commitView = wrapper.find('CommitView').getNode(); + commitView.setState({showCoAuthorInput: true}); + commitView.onSelectedCoAuthorsChanged([author]); + + sinon.spy(repository, 'commit'); + commandRegistry.dispatch(workspaceElement, 'github:amend-last-commit'); + // verify that coAuthor was passed + await assert.async.deepEqual(repository.commit.args[0][1], {amend: true, coAuthors: [author]}); + + await new Promise(res => setTimeout(res, 1000)) + + // verify that commit message has coauthor (use helper) + const lastCommit = wrapper.find('CommitView').prop('lastCommit'); + assert.strictEqual(lastCommit.message, commitBeforeAmend.message); + assert.deepEqual(lastCommit.coAuthors, [author]); + }); + + it('uses a new commit message if provided', async function() { + + }); + + it('successfully removes a co-author', async function() { + const commitMessageWithCoAuthors = dedent` + We did this together! + + Co-authored-by: Mona Lisa + ` + await repository.commit(commitMessageWithCoAuthors, {allowEmpty: true}); + + // assert that last commit has co-author + const lastCommit = wrapper.find('CommitView').prop('lastCommit'); + + // commit with no co-authors set + commandRegistry.dispatch(workspaceElement, 'github:amend-last-commit'); + + + }); }); }); From 3283234dcbf5c2ebb82d199076e609e76884fbf0 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Thu, 29 Mar 2018 18:41:48 -0700 Subject: [PATCH 0595/5882] :fire: unnecessary test Rather than reading the file at ATOM_COMMIT_MSG_EDIT, we pull the text from the expanded commit message editor --- test/git-strategies.test.js | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/test/git-strategies.test.js b/test/git-strategies.test.js index d7695467e9..a79ab3e25e 100644 --- a/test/git-strategies.test.js +++ b/test/git-strategies.test.js @@ -742,19 +742,6 @@ import {normalizeGitHelperPath, getTempDir} from '../lib/helpers'; const lastCommit = await git.getHeadCommit(); assert.deepEqual(lastCommit.message, 'Make a commit\n\nother stuff'); }); - - it('strips out comments and whitespace from message at specified file path', async function() { - const workingDirPath = await cloneRepository('multiple-commits'); - const git = createTestStrategy(workingDirPath); - - const commitMessagePath = path.join(workingDirPath, 'commit-message.txt'); - fs.writeFileSync(commitMessagePath, message); - - await git.commit({filePath: commitMessagePath}, {allowEmpty: true}); - - const lastCommit = await git.getHeadCommit(); - assert.deepEqual(lastCommit.message, 'Make a commit\n\nother stuff'); - }); }); describe('when amend option is true', function() { From f9fc18611104354ab46273b347064c6cac6894a5 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Thu, 29 Mar 2018 18:48:00 -0700 Subject: [PATCH 0596/5882] Fix GSOS tests after refactoring `getHeadCommit` to use `getRecentCommits` --- lib/git-shell-out-strategy.js | 4 ++-- test/git-strategies.test.js | 22 +++++++++++++++------- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/lib/git-shell-out-strategy.js b/lib/git-shell-out-strategy.js index c4a5cada91..ea097711bf 100644 --- a/lib/git-shell-out-strategy.js +++ b/lib/git-shell-out-strategy.js @@ -420,7 +420,7 @@ export default class GitShellOutStrategy { msg = await this.addCoAuthorsToMessage(msg, coAuthors); } - args.push('-m', msg); + args.push('-m', msg.trim()); if (amend) { args.push('--amend'); } if (allowEmpty) { args.push('--allow-empty'); } @@ -606,7 +606,7 @@ export default class GitShellOutStrategy { message: fields[i + 3], body: message, coAuthors, - unbornRef: false + unbornRef: false, }); } return commits; diff --git a/test/git-strategies.test.js b/test/git-strategies.test.js index a79ab3e25e..331b293014 100644 --- a/test/git-strategies.test.js +++ b/test/git-strategies.test.js @@ -124,12 +124,15 @@ import {normalizeGitHelperPath, getTempDir} from '../lib/helpers'; }); describe('getRecentCommits()', function() { - it('returns an empty array if no commits exist yet', async function() { - const workingDirPath = await initRepository(); - const git = createTestStrategy(workingDirPath); + describe('when no commits exist in the repository', function() { + it('returns an array with an unborn ref commit', async function() { + const workingDirPath = await initRepository(); + const git = createTestStrategy(workingDirPath); - const commits = await git.getRecentCommits(); - assert.lengthOf(commits, 0); + const commits = await git.getRecentCommits(); + assert.lengthOf(commits, 1); + assert.isTrue(commits[0].unbornRef); + }); }); it('returns all commits if fewer than max commits exist', async function() { @@ -146,6 +149,7 @@ import {normalizeGitHelperPath, getTempDir} from '../lib/helpers'; message: 'third commit', body: '', coAuthors: [], + unbornRef: false, }); assert.deepEqual(commits[1], { sha: '18920c900bfa6e4844853e7e246607a31c3e2e8c', @@ -154,6 +158,7 @@ import {normalizeGitHelperPath, getTempDir} from '../lib/helpers'; message: 'second commit', body: '', coAuthors: [], + unbornRef: false, }); assert.deepEqual(commits[2], { sha: '46c0d7179fc4e348c3340ff5e7957b9c7d89c07f', @@ -162,6 +167,7 @@ import {normalizeGitHelperPath, getTempDir} from '../lib/helpers'; message: 'first commit', body: '', coAuthors: [], + unbornRef: false, }); }); @@ -181,12 +187,13 @@ import {normalizeGitHelperPath, getTempDir} from '../lib/helpers'; assert.strictEqual(commits[9].message, 'Commit 1'); }); - it.only('includes co-authors based on commit body trailers', async function() { + it('includes co-authors based on commit body trailers', async function() { const workingDirPath = await cloneRepository('multiple-commits'); const git = createTestStrategy(workingDirPath); await git.commit(dedent` Implemented feature collaboratively + Co-authored-by: name Co-authored-by: another-name Co-authored-by: yet-another @@ -740,7 +747,8 @@ import {normalizeGitHelperPath, getTempDir} from '../lib/helpers'; await git.commit(message, {allowEmpty: true}); const lastCommit = await git.getHeadCommit(); - assert.deepEqual(lastCommit.message, 'Make a commit\n\nother stuff'); + assert.deepEqual(lastCommit.message, 'Make a commit'); + assert.deepEqual(lastCommit.body, 'other stuff'); }); }); From 6b5ef1a9434a5c22a240a81ab05839643c483b5b Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Thu, 29 Mar 2018 18:49:24 -0700 Subject: [PATCH 0597/5882] Refactor GSOS#getCommit to use GSOS#getRecentCommits --- lib/git-shell-out-strategy.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/git-shell-out-strategy.js b/lib/git-shell-out-strategy.js index ea097711bf..cafc2b935b 100644 --- a/lib/git-shell-out-strategy.js +++ b/lib/git-shell-out-strategy.js @@ -560,9 +560,8 @@ export default class GitShellOutStrategy { * Miscellaneous getters */ async getCommit(ref) { - const output = await this.exec(['log', '--pretty=%H%x00%B%x00', '--no-abbrev-commit', '-1', ref, '--']); - const [sha, message] = (output).split('\0'); - return {sha, message: message.trim(), unbornRef: false}; + const [commit] = await this.getRecentCommits({max: 1, ref}); + return commit; } async getHeadCommit() { From 123a5fbe6a8fd909f779b106a03724705bb025b7 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Thu, 29 Mar 2018 18:53:52 -0700 Subject: [PATCH 0598/5882] Rename GSOS#getRecentCommits to GSOS#getCommits --- lib/git-shell-out-strategy.js | 6 +++--- lib/models/repository-states/present.js | 2 +- test/git-strategies.test.js | 10 +++++----- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/git-shell-out-strategy.js b/lib/git-shell-out-strategy.js index cafc2b935b..36c8c0491d 100644 --- a/lib/git-shell-out-strategy.js +++ b/lib/git-shell-out-strategy.js @@ -560,16 +560,16 @@ export default class GitShellOutStrategy { * Miscellaneous getters */ async getCommit(ref) { - const [commit] = await this.getRecentCommits({max: 1, ref}); + const [commit] = await this.getCommits({max: 1, ref}); return commit; } async getHeadCommit() { - const [headCommit] = await this.getRecentCommits({max: 1, ref: 'HEAD'}); + const [headCommit] = await this.getCommits({max: 1, ref: 'HEAD'}); return headCommit; } - async getRecentCommits(options = {}) { + async getCommits(options = {}) { const {max, ref} = {max: 1, ref: 'HEAD', ...options}; // https://git-scm.com/docs/git-log#_pretty_formats diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js index fa752c188b..5cb03644a5 100644 --- a/lib/models/repository-states/present.js +++ b/lib/models/repository-states/present.js @@ -578,7 +578,7 @@ export default class Present extends State { getRecentCommits(options) { return this.cache.getOrSet(Keys.recentCommits, async () => { - const commits = await this.git().getRecentCommits(options); + const commits = await this.git().getCommits({ref: 'HEAD', ...options}); return commits.map(commit => new Commit(commit)); }); } diff --git a/test/git-strategies.test.js b/test/git-strategies.test.js index 331b293014..c209a0cd37 100644 --- a/test/git-strategies.test.js +++ b/test/git-strategies.test.js @@ -123,13 +123,13 @@ import {normalizeGitHelperPath, getTempDir} from '../lib/helpers'; }); }); - describe('getRecentCommits()', function() { + describe('getCommits()', function() { describe('when no commits exist in the repository', function() { it('returns an array with an unborn ref commit', async function() { const workingDirPath = await initRepository(); const git = createTestStrategy(workingDirPath); - const commits = await git.getRecentCommits(); + const commits = await git.getCommits(); assert.lengthOf(commits, 1); assert.isTrue(commits[0].unbornRef); }); @@ -139,7 +139,7 @@ import {normalizeGitHelperPath, getTempDir} from '../lib/helpers'; const workingDirPath = await cloneRepository('multiple-commits'); const git = createTestStrategy(workingDirPath); - const commits = await git.getRecentCommits({max: 10}); + const commits = await git.getCommits({max: 10}); assert.lengthOf(commits, 3); assert.deepEqual(commits[0], { @@ -180,7 +180,7 @@ import {normalizeGitHelperPath, getTempDir} from '../lib/helpers'; await git.commit(`Commit ${i}`, {allowEmpty: true}); } - const commits = await git.getRecentCommits({max: 10}); + const commits = await git.getCommits({max: 10}); assert.lengthOf(commits, 10); assert.strictEqual(commits[0].message, 'Commit 10'); @@ -199,7 +199,7 @@ import {normalizeGitHelperPath, getTempDir} from '../lib/helpers'; Co-authored-by: yet-another `, {allowEmpty: true}); - const commits = await git.getRecentCommits({max: 1}); + const commits = await git.getCommits({max: 1}); assert.lengthOf(commits, 1); assert.deepEqual(commits[0].coAuthors, [ { From 7cc3b48567db732f04ac0e862d594ef08c948f4a Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Thu, 29 Mar 2018 19:22:34 -0700 Subject: [PATCH 0599/5882] Oops, we need a `this` when we call `isCommitMessageEditorExpanded` Co-authored-by: Tilde Ann Thurium --- 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 bd1a008a98..2996c89e20 100644 --- a/lib/controllers/commit-controller.js +++ b/lib/controllers/commit-controller.js @@ -167,7 +167,7 @@ export default class CommitController extends React.Component { @autobind async toggleExpandedCommitMessageEditor(messageFromBox) { - if (isCommitMessageEditorExpanded()) { + if (this.isCommitMessageEditorExpanded()) { if (this.commitMessageEditorIsInForeground()) { this.closeAllOpenCommitMessageEditors(); } else { From 3085646e897840e736e2d2000832215c4f0ba525 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Thu, 29 Mar 2018 19:25:33 -0700 Subject: [PATCH 0600/5882] :shirt: --- test/controllers/git-tab-controller.test.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/test/controllers/git-tab-controller.test.js b/test/controllers/git-tab-controller.test.js index 29280c265e..4c2b923cc8 100644 --- a/test/controllers/git-tab-controller.test.js +++ b/test/controllers/git-tab-controller.test.js @@ -605,7 +605,7 @@ describe('GitTabController', function() { // commit message from previous commit should be used const lastCommit = wrapper.find('CommitView').prop('lastCommit'); assert.equal(lastCommit.message, commitMessage); - }) + }); }); describe('when there is a new commit message provided (and no staged changes)', function() { @@ -635,12 +635,11 @@ describe('GitTabController', function() { await repository.stageFiles(['new-file.txt']); // verify that last commit has no co-author - let commitBeforeAmend = wrapper.find('CommitView').prop('lastCommit'); - console.log(commitBeforeAmend); + const commitBeforeAmend = wrapper.find('CommitView').prop('lastCommit'); assert.deepEqual(commitBeforeAmend.coAuthors, []); // add co author - const author = {email: 'foo@bar.com', name: "foo bar"}; + const author = {email: 'foo@bar.com', name: 'foo bar'}; const commitView = wrapper.find('CommitView').getNode(); commitView.setState({showCoAuthorInput: true}); commitView.onSelectedCoAuthorsChanged([author]); @@ -667,7 +666,7 @@ describe('GitTabController', function() { We did this together! Co-authored-by: Mona Lisa - ` + `; await repository.commit(commitMessageWithCoAuthors, {allowEmpty: true}); // assert that last commit has co-author From b3546379006a711ef9050bd7a58003275505145f Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Thu, 29 Mar 2018 20:00:27 -0700 Subject: [PATCH 0601/5882] WIP fix tests Co-authored-by: Tilde Ann Thurium --- test/controllers/git-tab-controller.test.js | 71 +++++++++++++-------- 1 file changed, 45 insertions(+), 26 deletions(-) diff --git a/test/controllers/git-tab-controller.test.js b/test/controllers/git-tab-controller.test.js index 4c2b923cc8..b02378028e 100644 --- a/test/controllers/git-tab-controller.test.js +++ b/test/controllers/git-tab-controller.test.js @@ -573,7 +573,7 @@ describe('GitTabController', function() { }); describe.only('amend', function() { - let repository, commitMessage, workdirPath, wrapper; + let repository, commitMessage, workdirPath, wrapper, getLastCommit; beforeEach(async function() { workdirPath = await cloneRepository('three-files'); repository = await buildRepository(workdirPath); @@ -582,7 +582,17 @@ describe('GitTabController', function() { wrapper = mount(app); commitMessage = 'most recent commit woohoo'; - await repository.commit(commitMessage, {allowEmpty: true}); + fs.writeFileSync(path.join(workdirPath, 'foo.txt'), 'oh\nem\ngee\n'); + await repository.stageFiles(['foo.txt']); + await repository.commit(commitMessage); + + getLastCommit = () => { + return wrapper.find('RecentCommitView').getNodes()[0].props.commit; + }; + + await assert.async.strictEqual(getLastCommit().message, commitMessage); + + sinon.spy(repository, 'commit'); }); describe('when there are staged changes only', function() { @@ -595,15 +605,14 @@ describe('GitTabController', function() { // ensure that the commit editor is empty assert.strictEqual(wrapper.find('CommitView').getNode().editor.getText(), ''); - sinon.spy(repository, 'commit'); commandRegistry.dispatch(workspaceElement, 'github:amend-last-commit'); - assert.deepEqual(repository.commit.args[0][1], {amend: true, coAuthors: []}); + await assert.async.deepEqual(repository.commit.args[0][1], {amend: true, coAuthors: []}); // amending should commit all unstaged changes await assert.async.lengthOf(wrapper.find('GitTabView').prop('stagedChanges'), 0); // commit message from previous commit should be used - const lastCommit = wrapper.find('CommitView').prop('lastCommit'); + const lastCommit = getLastCommit(); assert.equal(lastCommit.message, commitMessage); }); }); @@ -618,24 +627,18 @@ describe('GitTabController', function() { // no staged changes await assert.async.lengthOf(wrapper.find('GitTabView').prop('stagedChanges'), 0); - sinon.spy(repository, 'commit'); commandRegistry.dispatch(workspaceElement, 'github:amend-last-commit'); - assert.deepEqual(repository.commit.args[0][1], {amend: true, coAuthors: []}); + await assert.async.deepEqual(repository.commit.args[0][1], {amend: true, coAuthors: []}); // new commit message is used - const lastCommit = wrapper.find('CommitView').prop('lastCommit'); - assert.equal(lastCommit.message, newMessage); + await assert.async.equal(getLastCommit().message, newMessage); }); }); - describe.only('when co-authors are changed', function() { + describe('when co-authors are changed', function() { it('amends the last commit re-using the commit message and adding the co-author', async function() { - // stage some changes - fs.writeFileSync(path.join(workdirPath, 'new-file.txt'), 'oh\nem\ngee\n'); - await repository.stageFiles(['new-file.txt']); - // verify that last commit has no co-author - const commitBeforeAmend = wrapper.find('CommitView').prop('lastCommit'); + const commitBeforeAmend = getLastCommit(); assert.deepEqual(commitBeforeAmend.coAuthors, []); // add co author @@ -644,38 +647,54 @@ describe('GitTabController', function() { commitView.setState({showCoAuthorInput: true}); commitView.onSelectedCoAuthorsChanged([author]); - sinon.spy(repository, 'commit'); commandRegistry.dispatch(workspaceElement, 'github:amend-last-commit'); // verify that coAuthor was passed await assert.async.deepEqual(repository.commit.args[0][1], {amend: true, coAuthors: [author]}); - await new Promise(res => setTimeout(res, 1000)) - - // verify that commit message has coauthor (use helper) - const lastCommit = wrapper.find('CommitView').prop('lastCommit'); - assert.strictEqual(lastCommit.message, commitBeforeAmend.message); - assert.deepEqual(lastCommit.coAuthors, [author]); + await assert.async.deepEqual(getLastCommit().coAuthors, [author]); + assert.strictEqual(getLastCommit().message, commitBeforeAmend.message); }); it('uses a new commit message if provided', async function() { + // verify that last commit has no co-author + const commitBeforeAmend = getLastCommit(); + assert.deepEqual(commitBeforeAmend.coAuthors, []); + // add co author + const author = {email: 'foo@bar.com', name: 'foo bar'}; + const commitView = wrapper.find('CommitView').getNode(); + commitView.setState({showCoAuthorInput: true}); + commitView.onSelectedCoAuthorsChanged([author]); + const newMessage = 'Star Wars: A New Message'; + commitView.editor.setText(newMessage); + commandRegistry.dispatch(workspaceElement, 'github:amend-last-commit'); + + // verify that coAuthor was passed + await assert.async.deepEqual(repository.commit.args[0][1], {amend: true, coAuthors: [author]}); + + // verify that commit message has coauthor + await assert.async.deepEqual(getLastCommit().coAuthors, [author]); + assert.strictEqual(getLastCommit().message, newMessage); }); it('successfully removes a co-author', async function() { + // make commit with co-author const commitMessageWithCoAuthors = dedent` We did this together! Co-authored-by: Mona Lisa `; - await repository.commit(commitMessageWithCoAuthors, {allowEmpty: true}); + await repository.git.exec(['commit', '--amend', '-m', commitMessageWithCoAuthors]); // assert that last commit has co-author - const lastCommit = wrapper.find('CommitView').prop('lastCommit'); + await assert.async.deepEqual(getLastCommit().coAuthors, [{name: 'Mona Lisa', email: 'mona@lisa.com'}]); - // commit with no co-authors set commandRegistry.dispatch(workspaceElement, 'github:amend-last-commit'); + // verify that NO coAuthor was passed + await assert.async.deepEqual(repository.commit.args[0][1], {amend: true, coAuthors: []}); - + // assert that no co-authors are in last commit + await assert.async.deepEqual(getLastCommit().coAuthors, []); }); }); }); From 4e8b6adf1d6e9a50edbc77a11e6eee124270467a Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 30 Mar 2018 01:07:04 -0700 Subject: [PATCH 0602/5882] Ensure Tooltip component does not use undefined targets --- lib/views/tooltip.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/views/tooltip.js b/lib/views/tooltip.js index c4c3e85a19..ef4760b582 100644 --- a/lib/views/tooltip.js +++ b/lib/views/tooltip.js @@ -92,12 +92,14 @@ export default class Tooltip extends React.Component { } const target = this.getCurrentTarget(props); - this.disposable = props.manager.add(target, options); + if (target) { + this.disposable = props.manager.add(target, options); + } } getCurrentTarget(props) { const target = props.target(); - if (target !== null && target.element !== undefined) { + if (target && target.element !== undefined) { return target.element; } else { return target; From be08c40838a44992dead87fbc46eb185c2e69100 Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 30 Mar 2018 01:34:44 -0700 Subject: [PATCH 0603/5882] PR feedback: Use Tooltip, name action (push-/pull-/fetching), animations This commit refactors the component to not use atom.tooltips but the internal component instead. It displays "Pushing"/"Pulling"/"Fetching" in the status bar tile when such an action is in progress. It refactors the component to extract all possible tile states into a tileState object. It adds animations to the "Pushing" and "Pulling" status bar icons. --- lib/controllers/status-bar-tile-controller.js | 4 + lib/views/push-pull-view.js | 244 ++++++++++-------- styles/status-bar-tile-controller.less | 35 ++- 3 files changed, 179 insertions(+), 104 deletions(-) diff --git a/lib/controllers/status-bar-tile-controller.js b/lib/controllers/status-bar-tile-controller.js index 0e17a02957..2d44dda22e 100644 --- a/lib/controllers/status-bar-tile-controller.js +++ b/lib/controllers/status-bar-tile-controller.js @@ -149,9 +149,13 @@ export default class StatusBarTileController extends React.Component { diff --git a/lib/views/push-pull-view.js b/lib/views/push-pull-view.js index 3be04dc863..05358db086 100644 --- a/lib/views/push-pull-view.js +++ b/lib/views/push-pull-view.js @@ -4,12 +4,14 @@ import cx from 'classnames'; import {RemotePropType, BranchPropType} from '../prop-types'; -function getIconClass(icon, isSyncing = false) { +import Tooltip from './tooltip'; + +function getIconClass(icon, animation) { return cx( 'github-PushPull-icon', 'icon', - isSyncing ? 'icon-sync' : `icon-${icon}`, - {'animate-rotate': isSyncing}, + `icon-${icon}`, + {[`animate-${animation}`]: !!animation}, ); } @@ -18,36 +20,31 @@ export default class PushPullView extends React.Component { currentBranch: BranchPropType.isRequired, currentRemote: RemotePropType.isRequired, isSyncing: PropTypes.bool, + isFetching: PropTypes.bool, + isPulling: PropTypes.bool, + isPushing: PropTypes.bool, behindCount: PropTypes.number, aheadCount: PropTypes.number, push: PropTypes.func.isRequired, pull: PropTypes.func.isRequired, fetch: PropTypes.func.isRequired, originExists: PropTypes.bool, + tooltipManager: PropTypes.object.isRequired, } static defaultProps = { isSyncing: false, + isFetching: false, + isPulling: false, + isPushing: false, behindCount: 0, aheadCount: 0, } - componentDidMount() { - this.setTooltip(); - } - - componentDidUpdate() { - this.tooltip.dispose(); - this.setTooltip(); - } - - setTooltip = () => { - this.tooltip = atom.tooltips.add(this.tileNode, { - title: `
    ${this.tooltipTarget.dataset.tooltip}
    `, - delay: {show: 200, hide: 100}, - html: true, - }); + // unfortunately we need a forceUpdate here to ensure that the + // component has a valid ref to target + this.forceUpdate(); } onClickPush = clickEvent => { @@ -96,102 +93,147 @@ export default class PushPullView extends React.Component { this.props.fetch(); } + getTooltip(message) { + // cache the tileNode for when calls the "target" getter function + const target = this.tileNode; + // only set up a if we have a target ref to point it to + return target && ( + target} + title={`
    ${message}
    `} + showDelay={200} + hideDelay={100} + /> + ); + } + + getTileStates() { + return { + fetching: { + tooltip: 'Fetching from remote', + icon: 'sync', + text: 'Fetching', + iconAnimation: 'rotate', + }, + pulling: { + tooltip: 'Pulling from remote', + icon: 'arrow-down', + text: 'Pulling', + iconAnimation: 'down', + }, + pushing: { + tooltip: 'Pushing to remote', + icon: 'arrow-up', + text: 'Pushing', + iconAnimation: 'up', + }, + ahead: { + onClick: this.onClickPush, + tooltip: 'Click to push
    Cmd-click to force push
    Right-click for more', + icon: 'arrow-up', + text: `Push ${this.props.aheadCount}`, + }, + behind: { + onClick: this.onClickPull, + tooltip: 'Click to pull
    Right-click for more', + icon: 'arrow-down', + text: `Pull ${this.props.behindCount}`, + }, + aheadBehind: { + onClick: this.onClickPushPull, + tooltip: 'Click to pull
    Cmd-click to force push
    Right-click for more', + icon: 'arrow-down', + text: `Pull ${this.props.behindCount}`, + secondaryIcon: 'arrow-up', + secondaryText: `${this.props.aheadCount} `, + }, + published: { + onClick: this.onClickFetch, + tooltip: 'Click to fetch
    Right-click for more', + icon: 'sync', + text: 'Fetch', + }, + unpublished: { + onClick: this.onClickPublish, + tooltip: 'Click to set up a remote tracking branch
    Right-click for more', + icon: 'cloud-upload', + text: 'Publish', + }, + noRemote: { + tooltip: 'There is no remote named "origin"', + icon: 'stop', + text: 'No remote', + }, + detached: { + tooltip: 'Create a branch if you wish to push your work anywhere', + icon: 'stop', + text: 'Not on branch', + }, + }; + } + render() { const isAhead = this.props.aheadCount > 0; const isBehind = this.props.behindCount > 0; const isUnpublished = !this.props.currentRemote.isPresent(); const isDetached = !!this.props.currentBranch.detached; - const isSyncing = this.props.isSyncing; + const isFetching = this.props.isFetching; + const isPulling = this.props.isPulling; + const isPushing = this.props.isPushing; const hasOrigin = !!this.props.originExists; + const tileStates = this.getTileStates(); + + let tileState; + + if (isFetching) { + tileState = tileStates.fetching; + } else if (isPulling) { + tileState = tileStates.pulling; + } else if (isPushing) { + tileState = tileStates.pushing; + } else if (isAhead && !isBehind && !isUnpublished) { + tileState = tileStates.ahead; + } else if (isBehind && !isAhead && !isUnpublished) { + tileState = tileStates.behind; + } else if (isBehind && isAhead && !isUnpublished) { + tileState = tileStates.aheadBehind; + } else if (!isBehind && !isAhead && !isUnpublished && !isDetached) { + tileState = tileStates.published; + } else if (isUnpublished && !isDetached && hasOrigin) { + tileState = tileStates.unpublished; + } else if (isUnpublished && !isDetached && !hasOrigin) { + tileState = tileStates.noRemote; + } else if (isDetached) { + tileState = tileStates.detached; + } + + const Tag = tileState.onClick ? 'a' : 'span'; + return (
    (this.tileNode = node)} className={cx('github-PushPull', 'inline-block', {'github-branch-detached': isDetached})}> - - {isAhead && !isBehind && !isUnpublished && ( - (this.tooltipTarget = node)} - onClick={this.onClickPush} - className="push-pull-target" - data-tooltip="Click to push
    Cmd-click to force push
    Right-click for more"> - - Push {this.props.aheadCount} -
    - )} - - {isBehind && !isAhead && !isUnpublished && ( - (this.tooltipTarget = node)} - onClick={this.onClickPull} - className="push-pull-target" - data-tooltip="Click to pull
    Right-click for more"> - - Pull {this.props.behindCount} -
    - )} - - {isBehind && isAhead && !isUnpublished && !isSyncing && ( - (this.tooltipTarget = node)} - onClick={this.onClickPushPull} - className="push-pull-target" - data-tooltip="Click to push
    Cmd-click to force push
    Right-click for more"> - - - {this.props.aheadCount}{' '} - - - Pull {this.props.behindCount} -
    - )} - - {isBehind && isAhead && !isUnpublished && isSyncing && ( - - - Pull {this.props.behindCount} - - )} - - {!isBehind && !isAhead && !isUnpublished && !isDetached && ( - (this.tooltipTarget = node)} - onClick={this.onClickFetch} - className="push-pull-target" - data-tooltip="Click to fetch
    Right-click for more"> - - Fetch -
    - )} - - {isUnpublished && !isDetached && hasOrigin && ( - (this.tooltipTarget = node)} - onClick={this.onClickPublish} - className="push-pull-target" - data-tooltip="Click to set up a remote tracking branch
    Right-click for more"> - - Publish -
    - )} - - {isUnpublished && !isDetached && !hasOrigin && ( - (this.tooltipTarget = node)} - className="push-pull-target" - data-tooltip={'There is no remote named "origin"'}> - - No remote - - )} - - {isDetached && ( - (this.tooltipTarget = node)} - className="push-pull-target" - data-tooltip={'Create a branch if you wish to push your work anywhere'}> - - Not on branch + {tileState && ( + [ + + {tileState.secondaryText && ( + + + {tileState.secondaryText} )} + + {tileState.text} + , + this.getTooltip(tileState.tooltip), + ] + )}
    ); } diff --git a/styles/status-bar-tile-controller.less b/styles/status-bar-tile-controller.less index abf24ec2be..2cc0e640d4 100644 --- a/styles/status-bar-tile-controller.less +++ b/styles/status-bar-tile-controller.less @@ -33,12 +33,41 @@ cursor: default; } + @keyframes github-StatusBarAnimation-fade { + 0% { opacity: 0; } + 25% { opacity: 1; } + 85% { opacity: 1; } + 100% { opacity: 0; } + } + // Sync animation - .animate-rotate.icon-sync::before { - @keyframes github-StatusBarSync-animation { + .animate-rotate.github-PushPull-icon::before { + @keyframes github-StatusBarAnimation-rotate { 100% { transform: rotate(360deg); } } - animation: github-StatusBarSync-animation 1s linear infinite; + animation: github-StatusBarAnimation-rotate 1s linear infinite; + } + + // Push animation + .animate-up.github-PushPull-icon::before { + @keyframes github-StatusBarAnimation-up { + 0% { transform: translateY(40%); } + 100% { transform: translateY(-20%); } + } + animation: + github-StatusBarAnimation-up 800ms ease-out infinite, + github-StatusBarAnimation-fade 800ms ease-out infinite; + } + + // Pull animation + .animate-down.github-PushPull-icon::before { + @keyframes github-StatusBarAnimation-down { + 0% { transform: translateY(-20%); } + 100% { transform: translateY(40%); } + } + animation: + github-StatusBarAnimation-down 800ms ease-out infinite, + github-StatusBarAnimation-fade 800ms ease-out infinite; } // Merge conflict icon From 9a7d4040ad2b3eba1bdf7a55c220bc7b42197c09 Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 30 Mar 2018 01:35:36 -0700 Subject: [PATCH 0604/5882] Extend tests to ensure status bar tile is showing the right text --- test/controllers/status-bar-tile-controller.test.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/controllers/status-bar-tile-controller.test.js b/test/controllers/status-bar-tile-controller.test.js index 2b5ee8b441..37883e2c72 100644 --- a/test/controllers/status-bar-tile-controller.test.js +++ b/test/controllers/status-bar-tile-controller.test.js @@ -301,6 +301,7 @@ describe('StatusBarTileController', function() { statusBarTile = mount(React.cloneElement(component, {repository})); statusBarTile.find('.push-pull-target').simulate('click'); + assert.equal(statusBarTile.find('.github-PushPull').text().trim(), 'Pushing'); assert.isFalse(repository.fetch.called); assert.isFalse(repository.push.called); assert.isFalse(repository.pull.called); @@ -337,6 +338,7 @@ describe('StatusBarTileController', function() { statusBarTile = mount(React.cloneElement(component, {repository})); statusBarTile.find('.push-pull-target').simulate('click'); + assert.equal(statusBarTile.find('.github-PushPull').text().trim(), 'Fetching'); assert.isFalse(repository.fetch.called); assert.isFalse(repository.push.called); assert.isFalse(repository.pull.called); @@ -374,6 +376,7 @@ describe('StatusBarTileController', function() { statusBarTile = mount(React.cloneElement(component, {repository})); statusBarTile.find('.push-pull-target').simulate('click'); + assert.equal(statusBarTile.find('.github-PushPull').text().trim(), 'Pushing'); assert.isFalse(repository.fetch.called); assert.isFalse(repository.push.called); assert.isFalse(repository.pull.called); @@ -411,6 +414,7 @@ describe('StatusBarTileController', function() { statusBarTile = mount(React.cloneElement(component, {repository})); statusBarTile.find('.push-pull-target').simulate('click'); + assert.equal(statusBarTile.find('.github-PushPull').text().trim(), 'Pulling'); assert.isFalse(repository.fetch.called); assert.isFalse(repository.push.called); assert.isFalse(repository.pull.called); @@ -451,6 +455,7 @@ describe('StatusBarTileController', function() { statusBarTile = mount(React.cloneElement(component, {repository})); statusBarTile.find('.push-pull-target').simulate('click'); + assert.equal(statusBarTile.find('.github-PushPull').text().trim(), 'Pulling'); assert.isFalse(repository.fetch.called); assert.isFalse(repository.push.called); assert.isFalse(repository.pull.called); @@ -480,6 +485,7 @@ describe('StatusBarTileController', function() { it('does nothing when clicked', function() { statusBarTile.find('.push-pull-target').simulate('click'); + assert.equal(statusBarTile.find('.github-PushPull').text().trim(), 'Not on branch'); assert.isFalse(repository.fetch.called); assert.isFalse(repository.push.called); assert.isFalse(repository.pull.called); @@ -509,6 +515,7 @@ describe('StatusBarTileController', function() { it('does nothing when clicked', function() { statusBarTile.find('.push-pull-target').simulate('click'); + assert.equal(statusBarTile.find('.github-PushPull').text().trim(), 'No remote'); assert.isFalse(repository.fetch.called); assert.isFalse(repository.push.called); assert.isFalse(repository.pull.called); From 5380235c950b3a2fd2400c9e4caa81083d24b706 Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 30 Mar 2018 01:44:46 -0700 Subject: [PATCH 0605/5882] PR feedback: Show correct modifier key dependin on user platform --- lib/views/push-pull-view.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/views/push-pull-view.js b/lib/views/push-pull-view.js index 05358db086..c2399c3902 100644 --- a/lib/views/push-pull-view.js +++ b/lib/views/push-pull-view.js @@ -110,6 +110,7 @@ export default class PushPullView extends React.Component { } getTileStates() { + const modKey = process.platform === 'darwin' ? 'Cmd' : 'Ctrl'; return { fetching: { tooltip: 'Fetching from remote', @@ -131,7 +132,7 @@ export default class PushPullView extends React.Component { }, ahead: { onClick: this.onClickPush, - tooltip: 'Click to push
    Cmd-click to force push
    Right-click for more', + tooltip: `Click to push
    ${modKey}-click to force push
    Right-click for more`, icon: 'arrow-up', text: `Push ${this.props.aheadCount}`, }, @@ -143,7 +144,7 @@ export default class PushPullView extends React.Component { }, aheadBehind: { onClick: this.onClickPushPull, - tooltip: 'Click to pull
    Cmd-click to force push
    Right-click for more', + tooltip: `Click to pull
    ${modKey}-click to force push
    Right-click for more`, icon: 'arrow-down', text: `Pull ${this.props.behindCount}`, secondaryIcon: 'arrow-up', From cdb1227b359dfd9a1a58430fd37d6f1502d409e5 Mon Sep 17 00:00:00 2001 From: Tilde Ann Thurium Date: Fri, 30 Mar 2018 14:15:37 -0700 Subject: [PATCH 0606/5882] Finish test for adding a co author --- test/controllers/git-tab-controller.test.js | 36 +++++++++++++++------ 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/test/controllers/git-tab-controller.test.js b/test/controllers/git-tab-controller.test.js index b02378028e..75b6afc2e2 100644 --- a/test/controllers/git-tab-controller.test.js +++ b/test/controllers/git-tab-controller.test.js @@ -677,21 +677,37 @@ describe('GitTabController', function() { assert.strictEqual(getLastCommit().message, newMessage); }); - it('successfully removes a co-author', async function() { - // make commit with co-author - const commitMessageWithCoAuthors = dedent` - We did this together! + it.only('successfully removes a co-author', async function() { + // verify that last commit has no co-author + const commitBeforeAmend = getLastCommit(); + assert.deepEqual(commitBeforeAmend.coAuthors, []); + + // add co author + const author = {email: 'foo@bar.com', name: 'foo bar'}; + const commitView = wrapper.find('CommitView').getNode(); + commitView.setState({showCoAuthorInput: true}); + commitView.onSelectedCoAuthorsChanged([author]); + const newMessage = 'Star Wars: A New Message'; + commitView.editor.setText(newMessage); + commandRegistry.dispatch(workspaceElement, 'github:amend-last-commit'); + + // verify that coAuthor was passed + await assert.async.deepEqual(repository.commit.args[0][1], {amend: true, coAuthors: [author]}); - Co-authored-by: Mona Lisa - `; - await repository.git.exec(['commit', '--amend', '-m', commitMessageWithCoAuthors]); + // verify that commit message has coauthor + await assert.async.deepEqual(getLastCommit().coAuthors, [author]); + assert.strictEqual(getLastCommit().message, newMessage); - // assert that last commit has co-author - await assert.async.deepEqual(getLastCommit().coAuthors, [{name: 'Mona Lisa', email: 'mona@lisa.com'}]); + // buh bye co author and commit message + wrapper.find('CommitView').getNode().editor.setText(''); + assert.strictEqual(wrapper.find('CommitView').getNode().editor.getText(), ''); + commitView.onSelectedCoAuthorsChanged([]); + commitView.setState({showCoAuthorInput: false}); + // amend again commandRegistry.dispatch(workspaceElement, 'github:amend-last-commit'); // verify that NO coAuthor was passed - await assert.async.deepEqual(repository.commit.args[0][1], {amend: true, coAuthors: []}); + await assert.async.deepEqual(repository.commit.args[1][1], {amend: true, coAuthors: []}); // assert that no co-authors are in last commit await assert.async.deepEqual(getLastCommit().coAuthors, []); From d81d6546c6392986a5411f9672f3a02037f85396 Mon Sep 17 00:00:00 2001 From: Tilde Ann Thurium Date: Fri, 30 Mar 2018 15:13:24 -0700 Subject: [PATCH 0607/5882] Simplify test setup for successfully removing a co-author via amend Co-authored-by: Katrina Uychaco --- test/controllers/git-tab-controller.test.js | 33 +++++++++------------ 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/test/controllers/git-tab-controller.test.js b/test/controllers/git-tab-controller.test.js index 75b6afc2e2..e6b27a4b5b 100644 --- a/test/controllers/git-tab-controller.test.js +++ b/test/controllers/git-tab-controller.test.js @@ -678,39 +678,34 @@ describe('GitTabController', function() { }); it.only('successfully removes a co-author', async function() { - // verify that last commit has no co-author - const commitBeforeAmend = getLastCommit(); - assert.deepEqual(commitBeforeAmend.coAuthors, []); + const message = 'We did this together!'; + const author = {email: 'mona@lisa.com', name: 'Mona Lisa'}; + const commitMessageWithCoAuthors = dedent` + ${message} - // add co author - const author = {email: 'foo@bar.com', name: 'foo bar'}; - const commitView = wrapper.find('CommitView').getNode(); - commitView.setState({showCoAuthorInput: true}); - commitView.onSelectedCoAuthorsChanged([author]); - const newMessage = 'Star Wars: A New Message'; - commitView.editor.setText(newMessage); - commandRegistry.dispatch(workspaceElement, 'github:amend-last-commit'); + Co-authored-by: ${author.name} <${author.email}> + `; - // verify that coAuthor was passed - await assert.async.deepEqual(repository.commit.args[0][1], {amend: true, coAuthors: [author]}); + await repository.git.exec(['commit', '--amend', '-m', commitMessageWithCoAuthors]); + repository.refresh(); // clear the repository cache // verify that commit message has coauthor await assert.async.deepEqual(getLastCommit().coAuthors, [author]); - assert.strictEqual(getLastCommit().message, newMessage); + assert.strictEqual(getLastCommit().message, message); - // buh bye co author and commit message - wrapper.find('CommitView').getNode().editor.setText(''); - assert.strictEqual(wrapper.find('CommitView').getNode().editor.getText(), ''); + // buh bye co author + const commitView = wrapper.find('CommitView').getNode(); + assert.strictEqual(commitView.editor.getText(), ''); commitView.onSelectedCoAuthorsChanged([]); - commitView.setState({showCoAuthorInput: false}); // amend again commandRegistry.dispatch(workspaceElement, 'github:amend-last-commit'); // verify that NO coAuthor was passed - await assert.async.deepEqual(repository.commit.args[1][1], {amend: true, coAuthors: []}); + await assert.async.deepEqual(repository.commit.args[0][1], {amend: true, coAuthors: []}); // assert that no co-authors are in last commit await assert.async.deepEqual(getLastCommit().coAuthors, []); + assert.strictEqual(getLastCommit().message, message); }); }); }); From 5cfac03356ccde0e6142104bb4d1ed312963bd8f Mon Sep 17 00:00:00 2001 From: Tilde Ann Thurium Date: Fri, 30 Mar 2018 15:16:44 -0700 Subject: [PATCH 0608/5882] Add `includeUnborn` option to GSOS#getCommits This is to fix an issue where we were rending an unborn commit in the RecentCommitsView Co-authored-by: Katrina Uychaco --- lib/git-shell-out-strategy.js | 10 ++++++---- test/git-strategies.test.js | 11 +++++++++-- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/lib/git-shell-out-strategy.js b/lib/git-shell-out-strategy.js index 36c8c0491d..53a9f23e35 100644 --- a/lib/git-shell-out-strategy.js +++ b/lib/git-shell-out-strategy.js @@ -560,17 +560,17 @@ export default class GitShellOutStrategy { * Miscellaneous getters */ async getCommit(ref) { - const [commit] = await this.getCommits({max: 1, ref}); + const [commit] = await this.getCommits({max: 1, ref, includeUnborn: true}); return commit; } async getHeadCommit() { - const [headCommit] = await this.getCommits({max: 1, ref: 'HEAD'}); + const [headCommit] = await this.getCommits({max: 1, ref: 'HEAD', includeUnborn: true}); return headCommit; } async getCommits(options = {}) { - const {max, ref} = {max: 1, ref: 'HEAD', ...options}; + const {max, ref, includeUnborn} = {max: 1, ref: 'HEAD', includeUnborn: false, ...options}; // https://git-scm.com/docs/git-log#_pretty_formats // %x00 - null byte @@ -589,7 +589,9 @@ export default class GitShellOutStrategy { } }); - if (output === '') { return [{sha: '', message: '', unbornRef: true}]; } + if (output === '') { + return includeUnborn ? [{sha: '', message: '', unbornRef: true}] : []; + } const fields = output.trim().split('\0'); const commits = []; diff --git a/test/git-strategies.test.js b/test/git-strategies.test.js index c209a0cd37..b89f07d14b 100644 --- a/test/git-strategies.test.js +++ b/test/git-strategies.test.js @@ -125,14 +125,21 @@ import {normalizeGitHelperPath, getTempDir} from '../lib/helpers'; describe('getCommits()', function() { describe('when no commits exist in the repository', function() { - it('returns an array with an unborn ref commit', async function() { + it('returns an array with an unborn ref commit when the include unborn option is passed', async function() { const workingDirPath = await initRepository(); const git = createTestStrategy(workingDirPath); - const commits = await git.getCommits(); + const commits = await git.getCommits({includeUnborn: true}); assert.lengthOf(commits, 1); assert.isTrue(commits[0].unbornRef); }); + it('returns an empty array when the include unborn option is not passed', async function() { + const workingDirPath = await initRepository(); + const git = createTestStrategy(workingDirPath); + + const commits = await git.getCommits(); + assert.lengthOf(commits, 0); + }); }); it('returns all commits if fewer than max commits exist', async function() { From 35702ca80b7594ef46009a0f8d0622fcca062522 Mon Sep 17 00:00:00 2001 From: Tilde Ann Thurium Date: Fri, 30 Mar 2018 15:49:55 -0700 Subject: [PATCH 0609/5882] Move merge co author trailer tests into git-strategies. Co-authored-by: Katrina Uychaco --- lib/git-shell-out-strategy.js | 7 +- test/controllers/git-tab-controller.test.js | 4 +- test/git-strategies.test.js | 94 +++++++++++++++++++++ test/models/repository.test.js | 82 ------------------ 4 files changed, 100 insertions(+), 87 deletions(-) diff --git a/lib/git-shell-out-strategy.js b/lib/git-shell-out-strategy.js index 53a9f23e35..d67b153361 100644 --- a/lib/git-shell-out-strategy.js +++ b/lib/git-shell-out-strategy.js @@ -427,7 +427,7 @@ export default class GitShellOutStrategy { return this.gpgExec(args, {writeOperation: true}); } - addCoAuthorsToMessage(message, coAuthors) { + async addCoAuthorsToMessage(message, coAuthors = []) { const trailers = coAuthors.map(author => { return { token: 'Co-Authored-By', @@ -436,8 +436,9 @@ export default class GitShellOutStrategy { }); // Ensure that message ends with newline for git-interpret trailers to work - const msg = `${message}\n`.replace(/\s+$/, '\n'); - return this.mergeTrailers(msg, trailers); + const msg = `${message.trim()}\n`; + + return trailers.length ? this.mergeTrailers(msg, trailers) : msg; } /** diff --git a/test/controllers/git-tab-controller.test.js b/test/controllers/git-tab-controller.test.js index e6b27a4b5b..446a3c41bd 100644 --- a/test/controllers/git-tab-controller.test.js +++ b/test/controllers/git-tab-controller.test.js @@ -572,7 +572,7 @@ describe('GitTabController', function() { }); }); - describe.only('amend', function() { + describe('amend', function() { let repository, commitMessage, workdirPath, wrapper, getLastCommit; beforeEach(async function() { workdirPath = await cloneRepository('three-files'); @@ -677,7 +677,7 @@ describe('GitTabController', function() { assert.strictEqual(getLastCommit().message, newMessage); }); - it.only('successfully removes a co-author', async function() { + it('successfully removes a co-author', async function() { const message = 'We did this together!'; const author = {email: 'mona@lisa.com', name: 'Mona Lisa'}; const commitMessageWithCoAuthors = dedent` diff --git a/test/git-strategies.test.js b/test/git-strategies.test.js index b89f07d14b..85ccb940f4 100644 --- a/test/git-strategies.test.js +++ b/test/git-strategies.test.js @@ -774,6 +774,100 @@ import {normalizeGitHelperPath, getTempDir} from '../lib/helpers'; }); }); + describe('addCoAuthorsToMessage', function() { + it('always adds trailing newline', async () => { + const workingDirPath = await cloneRepository('multiple-commits'); + const git = createTestStrategy(workingDirPath); + + assert.equal(await git.addCoAuthorsToMessage('test'), 'test\n'); + }); + + it('appends trailers to a summary-only message', async () => { + const workingDirPath = await cloneRepository('three-files'); + const git = createTestStrategy(workingDirPath); + + const coAuthors = [ + { + name: 'Markus Olsson', + email: 'niik@github.com', + }, + { + name: 'Neha Batra', + email: 'nerdneha@github.com', + }, + ]; + + assert.equal(await git.addCoAuthorsToMessage('foo', coAuthors), + dedent` + foo + + Co-Authored-By: Markus Olsson + Co-Authored-By: Neha Batra + + `, + ); + }); + + // note, this relies on the default git config + it('merges duplicate trailers', async () => { + const workingDirPath = await cloneRepository('three-files'); + const git = createTestStrategy(workingDirPath); + + const coAuthors = [ + { + name: 'Markus Olsson', + email: 'niik@github.com', + }, + { + name: 'Neha Batra', + email: 'nerdneha@github.com', + }, + ]; + + assert.equal( + await git.addCoAuthorsToMessage( + 'foo\n\nCo-Authored-By: Markus Olsson ', + coAuthors, + ), + dedent` + foo + + Co-Authored-By: Markus Olsson + Co-Authored-By: Neha Batra + + `, + ); + }); + + // note, this relies on the default git config + it('fixes up malformed trailers when trailers are given', async () => { + const workingDirPath = await cloneRepository('three-files'); + const git = createTestStrategy(workingDirPath); + + const coAuthors = [ + { + name: 'Neha Batra', + email: 'nerdneha@github.com', + }, + ]; + + assert.equal( + await git.addCoAuthorsToMessage( + // note the lack of space after : + 'foo\n\nCo-Authored-By:Markus Olsson ', + coAuthors, + ), + dedent` + foo + + Co-Authored-By: Markus Olsson + Co-Authored-By: Neha Batra + + `, + ); + }); + }); + // Only needs to be tested on strategies that actually implement gpgExec describe('GPG signing', function() { let git; diff --git a/test/models/repository.test.js b/test/models/repository.test.js index d2d8a57bae..906851063e 100644 --- a/test/models/repository.test.js +++ b/test/models/repository.test.js @@ -508,88 +508,6 @@ describe('Repository', function() { }); }); - describe('addTrailersToCommitMessage', function() { - it('always adds trailing newline', async () => { - const workingDirPath = await cloneRepository('three-files'); - const repo = new Repository(workingDirPath); - await repo.getLoadPromise(); - - assert.equal(await repo.addTrailersToCommitMessage('test'), 'test\n'); - }); - - it('appends trailers to a summary-only message', async () => { - const workingDirPath = await cloneRepository('three-files'); - const repo = new Repository(workingDirPath); - await repo.getLoadPromise(); - - const trailers = [ - {token: 'Co-Authored-By', value: 'Markus Olsson '}, - {token: 'Signed-Off-By', value: 'nerdneha '}, - ]; - - assert.equal(await repo.addTrailersToCommitMessage('foo', trailers), - dedent` - foo - - Co-Authored-By: Markus Olsson - Signed-Off-By: nerdneha - - `, - ); - }); - - // note, this relies on the default git config - it('merges duplicate trailers', async () => { - const workingDirPath = await cloneRepository('three-files'); - const repo = new Repository(workingDirPath); - await repo.getLoadPromise(); - - const trailers = [ - {token: 'Co-Authored-By', value: 'Markus Olsson '}, - {token: 'Signed-Off-By', value: 'nerdneha '}, - ]; - assert.equal( - await repo.addTrailersToCommitMessage( - 'foo\n\nCo-Authored-By: Markus Olsson ', - trailers, - ), - dedent` - foo - - Co-Authored-By: Markus Olsson - Signed-Off-By: nerdneha - - `, - ); - }); - - // note, this relies on the default git config - it('fixes up malformed trailers when trailers are given', async () => { - const workingDirPath = await cloneRepository('three-files'); - const repo = new Repository(workingDirPath); - await repo.getLoadPromise(); - - const trailers = [ - {token: 'Signed-Off-By', value: 'nerdneha '}, - ]; - - assert.equal( - await repo.addTrailersToCommitMessage( - // note the lack of space after : - 'foo\n\nCo-Authored-By:Markus Olsson ', - trailers, - ), - dedent` - foo - - Co-Authored-By: Markus Olsson - Signed-Off-By: nerdneha - - `, - ); - }); - }); - describe('fetch(branchName)', function() { it('brings commits from the remote and updates remote branch, and does not update branch', async function() { const {localRepoPath} = await setUpLocalAndRemoteRepositories({remoteAhead: true}); From 970706d5c21ae0b2fcd7ef1b7b8629500e98850a Mon Sep 17 00:00:00 2001 From: Tilde Ann Thurium Date: Fri, 30 Mar 2018 16:13:02 -0700 Subject: [PATCH 0610/5882] :shirt: --- lib/controllers/git-tab-controller.js | 1 - lib/helpers.js | 1 + lib/views/recent-commits-view.js | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/controllers/git-tab-controller.js b/lib/controllers/git-tab-controller.js index 4d2eee2ab0..da09db5e26 100644 --- a/lib/controllers/git-tab-controller.js +++ b/lib/controllers/git-tab-controller.js @@ -12,7 +12,6 @@ import ObserveModelDecorator from '../decorators/observe-model'; import UserStore from '../models/user-store'; import {nullBranch} from '../models/branch'; import {nullCommit} from '../models/commit'; -import {extractCoAuthorsAndRawCommitMessage} from '../helpers'; const DOMPurify = createDOMPurify(); diff --git a/lib/helpers.js b/lib/helpers.js index 559a90eb35..a4a4aa43cc 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -321,6 +321,7 @@ export function extractCoAuthorsAndRawCommitMessage(commitMessage) { const coAuthors = commitMessage.split(LINE_ENDING_REGEX).reduce((authors, line) => { const match = line.match(CO_AUTHOR_REGEX); if (match) { + // eslint-disable-next-line no-unused-vars const [_, name, email] = match; authors.push({name, email}); } else { diff --git a/lib/views/recent-commits-view.js b/lib/views/recent-commits-view.js index a1f7b3ff06..edc373349a 100644 --- a/lib/views/recent-commits-view.js +++ b/lib/views/recent-commits-view.js @@ -43,7 +43,7 @@ class RecentCommitView extends React.Component { } renderAuthors() { - const coAuthorEmails = this.props.commit.getCoAuthors().map(author => author.email) + const coAuthorEmails = this.props.commit.getCoAuthors().map(author => author.email); const authorEmails = [this.props.commit.getAuthorEmail(), ...coAuthorEmails]; return ( From c0e30fda5dbca121662240c4b0394f7dc9761377 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 30 Mar 2018 22:49:02 -0700 Subject: [PATCH 0611/5882] Clarify commit message components - subject and body --- lib/controllers/commit-controller.js | 1 - lib/controllers/git-tab-controller.js | 5 ++-- lib/git-shell-out-strategy.js | 10 ++++---- lib/models/commit.js | 20 +++++++++------- lib/prop-types.js | 2 +- lib/views/recent-commits-view.js | 4 ++-- test/controllers/commit-controller.test.js | 2 +- test/controllers/git-tab-controller.test.js | 16 ++++++------- test/git-strategies.test.js | 22 ++++++++--------- test/models/repository.test.js | 26 ++++++++++----------- test/views/recent-commits-view.test.js | 4 ++-- 11 files changed, 57 insertions(+), 55 deletions(-) diff --git a/lib/controllers/commit-controller.js b/lib/controllers/commit-controller.js index 2996c89e20..6828e3fb94 100644 --- a/lib/controllers/commit-controller.js +++ b/lib/controllers/commit-controller.js @@ -123,7 +123,6 @@ export default class CommitController extends React.Component { @autobind commit(message, coAuthors = [], amend) { - let msg; if (this.isCommitMessageEditorExpanded()) { msg = this.getCommitMessageEditors()[0].getText(); diff --git a/lib/controllers/git-tab-controller.js b/lib/controllers/git-tab-controller.js index da09db5e26..e0be164267 100644 --- a/lib/controllers/git-tab-controller.js +++ b/lib/controllers/git-tab-controller.js @@ -337,9 +337,8 @@ export default class GitTabController extends React.Component { const repo = this.props.repository; const lastCommit = await repo.getLastCommit(); if (lastCommit.isUnbornRef()) { return null; } - const {message, coAuthors} = lastCommit; - repo.setCommitMessage(message); - this.updateSelectedCoAuthors(coAuthors); + repo.setCommitMessage(lastCommit.getFullMessage()); + this.updateSelectedCoAuthors(lastCommit.getCoAuthors()); return repo.undoLastCommit(); } diff --git a/lib/git-shell-out-strategy.js b/lib/git-shell-out-strategy.js index d67b153361..a12522fc99 100644 --- a/lib/git-shell-out-strategy.js +++ b/lib/git-shell-out-strategy.js @@ -409,8 +409,8 @@ export default class GitShellOutStrategy { // if amending and no new message is passed, use last commit's message if (amend && rawMessage.length === 0) { - const {unbornRef, message} = await this.getHeadCommit(); - msg = unbornRef ? rawMessage : message; + const {unbornRef, messageBody, messageSubject} = await this.getHeadCommit(); + msg = unbornRef ? rawMessage : `${messageSubject}\n\n${messageBody}`.trim(); } else { msg = rawMessage; } @@ -599,14 +599,14 @@ export default class GitShellOutStrategy { for (let i = 0; i < fields.length; i += 5) { const body = fields[i + 4]; - const {message, coAuthors} = extractCoAuthorsAndRawCommitMessage(body); + const {message: messageBody, coAuthors} = extractCoAuthorsAndRawCommitMessage(body); commits.push({ sha: fields[i] && fields[i].trim(), authorEmail: fields[i + 1] && fields[i + 1].trim(), authorDate: parseInt(fields[i + 2], 10), - message: fields[i + 3], - body: message, + messageSubject: fields[i + 3], + messageBody, coAuthors, unbornRef: false, }); diff --git a/lib/models/commit.js b/lib/models/commit.js index 08e5511ca3..6bfa738b8d 100644 --- a/lib/models/commit.js +++ b/lib/models/commit.js @@ -5,13 +5,13 @@ export default class Commit { return new Commit({unbornRef: UNBORN}); } - constructor({sha, authorEmail, coAuthors, authorDate, message, body, unbornRef}) { + constructor({sha, authorEmail, coAuthors, authorDate, messageSubject, messageBody, unbornRef}) { this.sha = sha; this.authorEmail = authorEmail; this.coAuthors = coAuthors || []; this.authorDate = authorDate; - this.message = message; - this.body = body; + this.messageSubject = messageSubject; + this.messageBody = messageBody; this.unbornRef = unbornRef === UNBORN; } @@ -31,12 +31,16 @@ export default class Commit { return this.coAuthors; } - getMessage() { - return this.message; + getMessageSubject() { + return this.messageSubject; } - getBody() { - return this.body; + getMessageBody() { + return this.messageBody; + } + + getFullMessage() { + return `${this.getMessageSubject()}\n\n${this.getMessageBody()}`.trim(); } isUnbornRef() { @@ -53,7 +57,7 @@ export const nullCommit = { return ''; }, - getMessage() { + getMessageSubject() { return ''; }, diff --git a/lib/prop-types.js b/lib/prop-types.js index 0e47a3bd48..56eb105de9 100644 --- a/lib/prop-types.js +++ b/lib/prop-types.js @@ -26,7 +26,7 @@ export const BranchPropType = PropTypes.shape({ export const CommitPropType = PropTypes.shape({ getSha: PropTypes.func.isRequired, - getMessage: PropTypes.func.isRequired, + getMessageSubject: PropTypes.func.isRequired, isUnbornRef: PropTypes.func.isRequired, isPresent: PropTypes.func.isRequired, }); diff --git a/lib/views/recent-commits-view.js b/lib/views/recent-commits-view.js index edc373349a..0458b6fb4a 100644 --- a/lib/views/recent-commits-view.js +++ b/lib/views/recent-commits-view.js @@ -14,7 +14,7 @@ class RecentCommitView extends React.Component { render() { const authorMoment = moment(this.props.commit.getAuthorDate() * 1000); - const fullMessage = (this.props.commit.getMessage() + '\n\n' + this.props.commit.getBody()).trim(); + const fullMessage = this.props.commit.getFullMessage(); return (
  • @@ -22,7 +22,7 @@ class RecentCommitView extends React.Component { - {this.props.commit.getMessage()} + {this.props.commit.getMessageSubject()} {this.props.isMostRecent && ( + + +
  • + ); + } + + @autobind + confirm() { + this.props.onSubmit({name: this.state.name, email: this.state.email}); + } + + @autobind + cancel() { + this.props.onCancel(); + } + + @autobind + onNameChange(e) { + this.setState({name: e.target.value}); + } + + @autobind + onEmailChange(e) { + this.setState({email: e.target.value}); + } + + @autobind + focusFirstInput() { + this.nameInput.focus(); + } +} diff --git a/lib/views/commit-view.js b/lib/views/commit-view.js index 9943a729cf..b327d137d6 100644 --- a/lib/views/commit-view.js +++ b/lib/views/commit-view.js @@ -4,6 +4,7 @@ import {CompositeDisposable} from 'event-kit'; import {autobind} from 'core-decorators'; import cx from 'classnames'; import Select from 'react-select'; +import CoAuthorDialog from './co-author-dialog'; import Tooltip from './tooltip'; import AtomTextEditor from './atom-text-editor'; @@ -56,6 +57,7 @@ export default class CommitView extends React.Component { this.state = { showWorking: false, showCoAuthorInput: false, + showCoAuthorDialog: false, }; this.timeoutHandle = null; @@ -171,6 +173,7 @@ export default class CommitView extends React.Component { />
    + {this.renderCoAuthorDialog()} {this.renderCoAuthorInput()}